Skip to content

scryptenc: free the expanded AES key when a write fails - #430

Open
woahwhattheheck wants to merge 3 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-free-key-on-write-error
Open

woahwhattheheck wants to merge 3 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-free-key-on-write-error

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Sep 6, 2026

Copy link
Copy Markdown

Fixes #429.

scryptenc_file() and scryptdec_file_copy() free the crypto_aesctr stream
when a write to the output file fails partway through, but not the
struct crypto_aes_key * it was built from:

		if (fwrite(buf, 1, readlen, outfile) < readlen) {
			crypto_aesctr_free(AES);
			rc = SCRYPT_EWRFILE;
			goto err1;
		}

err1 in scryptenc_file() only zeroes dk, and err0 in
scryptdec_file_copy() only returns, so key_enc_exp never reaches
crypto_aes_key_free(). The crypto_aesctr_init() failure path a few lines
above each of these already frees it; this just makes the write-error path
match. The _buf variants have no fwrite() and are unaffected.

The change

Two lines, one in each function. After it, every exit from the scope of a
crypto_aes_key_expand() in this file frees the result.

Test

tests/13-write-error.sh is new. The write-error path in these two loops has
no coverage today, which is why make test USE_VALGRIND=1 does not catch this.
The test builds an input larger than stdio's buffer — so the failure lands
inside the loop rather than at the final flush — and points the output at
/dev/full, checking exit status 1 and the Error writing file message for
both enc and dec. It also confirms that encrypting the same large input to
a normal file still succeeds.

/dev/full is not portable, so the test does that part only if
[ -c /dev/full ] and [ -w /dev/full ]; elsewhere it runs the plain
large-file encryption check and returns. Explicit parameters plus -f keep
the key derivation cheap so the test does not measure CPU speed on every run.

Note on the test number

Numbered 13 so it does not collide with tests/11-info.sh from #426 or
tests/12-same-file.sh from #428. The ordering works whichever of the three
lands first; the only overlap between them is the adjacent line each adds to
EXTRA_DIST in Makefile.am. Happy to renumber.

scryptenc_file() and scryptdec_file_copy() both free the crypto_aesctr
stream when a write to the output file fails partway through the data, but
not the struct crypto_aes_key it was built from.  err1 in scryptenc_file()
only zeroes dk, and err0 in scryptdec_file_copy() only returns, so
key_enc_exp never reaches crypto_aes_key_free().

The crypto_aesctr_init() failure path a few lines above each of these
already frees it; make the write-error path match.  scryptenc_buf() and
scryptdec_buf() have no fwrite() and are unaffected.

Add tests/13-write-error.sh, which drives both loops into a failing write
via /dev/full -- these paths had no coverage, which is why "make test
USE_VALGRIND=1" did not notice.  The test uses an input larger than stdio's
buffer so the failure lands inside the loop rather than at the final flush,
and skips the /dev/full part on platforms which do not have it.
The previous commit was uploaded through the GitHub API by a helper
which dropped the final newline of every text file it sent.  Rewrite
those files with their trailing newline intact; no other change.
@woahwhattheheck
woahwhattheheck force-pushed the scrypt-free-key-on-write-error branch from 5b2623f to b73e2d8 Compare September 20, 2026 02:11
@tokenjunkielabs

Copy link
Copy Markdown

Scope update for this carrier: the current head b73e2d8b6bdf7a15013aa06439c437194478eaed fixes both first-reporter issues #429 and #438. In addition to freeing the expanded AES key on the streaming write-error paths (#429), the current diff wipes the complete stack buf in both scryptenc_file() and scryptdec_file_copy() with insecure_memzero(buf, sizeof(buf)) on success and on their common failure returns (#438). The /dev/full regression directly exercises the write-error paths; I am not claiming that shell test inspects post-return stack residue. I attempted to update the PR title/body to list both issues, but the provider rejected metadata edits from the available publisher credential, so this comment records the current combined scope without opening a duplicate conflicting PR.

@tokenjunkielabs

Copy link
Copy Markdown

Carrier linkage update: current head b73e2d8b6bdf7a15013aa06439c437194478eaed also fixes #438. In both scryptenc_file() and scryptdec_file_copy(), the complete stack buf is wiped with insecure_memzero(buf, sizeof(buf)) on the success and common failure return paths, alongside the #429 expanded-key cleanup. Please review this PR as the shared carrier for #429 and first-reporter issue #438; keeping both fixes here avoids a conflicting second PR against the same functions. The existing write-error regression exercises the streaming failure exits; it does not claim to inspect post-return stack residue directly.

@tokenjunkielabs

Copy link
Copy Markdown

Maintainer review / bounty-classification follow-up for shared carrier #430. Current head fixes both first-reporter reports #429 (expanded AES key lifetime on streaming write failure) and #438 (plaintext stack buffers left unwiped). Please classify each accepted report separately under the published Tarsnap bug-bounty schedule and confirm the applicable payment route for reporter woahwhattheheck. I am not presuming a tier; sponsor classification controls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[bug bounty] The expanded AES key is leaked when a write to the output file fails

2 participants