You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug bounty] The plaintext buffers in scryptenc_file and scryptdec_file_copy are never wiped #438
scryptenc_file() and scryptdec_file_copy() each keep a 64 KiB stack buffer holding
user data, and neither is wiped on any path — including the success path. Both functions
already zero dk on every exit, so the buffer looks like an omission rather than a
decision.
This is the same class as d718f28 ("Zero scrypt scratch buffers before freeing"), which
zeroed crypto_scrypt()'s heap scratch buffers V, XY and B. That commit did not
reach scryptenc.c, and these two buffers hold the user's plaintext rather than KDF
scratch.
scryptdec_file_copy() — decrypted plaintext, on every path
lib/scryptenc/scryptenc.c:818:
uint8_tbuf[ENCBLOCK+32]; /* 65568 bytes */
The stream is decrypted in place:
crypto_aesctr_stream(AES, buf, buf, buflen-32);
The function contains no insecure_memzero at all. On return — success included — buf
still holds the decrypted plaintext of the last block processed. (The cookie's dk is
zeroed separately in scryptdec_file_cookie_free(); the buffer is not.)
scryptenc_file() — plaintext in the tail of the buffer
lib/scryptenc/scryptenc.c:605:
uint8_tbuf[ENCBLOCK]; /* 65536 bytes */
Here the plaintext is encrypted in place, so most of the buffer ends up holding
ciphertext. But the final fread() is usually short, and only readlen bytes are
encrypted:
so buf[readlen .. ENCBLOCK) still holds unencrypted plaintext carried over from the
previous iteration. That is the case for every input whose length is not an exact multiple
of 65536.
Suggested fix
Alongside the existing insecure_memzero(dk, 64) calls in scryptenc_file() (lines 675
and 681):
insecure_memzero(buf, ENCBLOCK);
and in scryptdec_file_copy(), before both the success return and the err0 return:
insecure_memzero(buf, ENCBLOCK+32);
Note on overlap
I am filing this as an issue only, without a PR. #429 / #430 are open against these exact
two functions (the expanded AES key leaked on the write-error path), and a second patch
touching the same lines would just create conflicts. If you would like this folded into
that branch or sent separately once it lands, say which and I will do it.
Adding a limitation I should have stated in the report itself.
What I did not exercise. This is a code-reading finding. I did not demonstrate it at
runtime — I have not inspected the stack after either function returns to confirm the
residue empirically, and there is no test or instrumented run behind it. What I verified
is static: scryptdec_file_copy() contains no insecure_memzero call at all, and scryptenc_file() zeroes only dk on both of its exits, while both functions decrypt or
encrypt in place into a buffer that outlives the last operation on it.
The scryptenc_file() half also depends on a claim I reasoned about rather than measured:
that buf[readlen .. ENCBLOCK) still holds plaintext from the previous iteration after a
short final fread(). That follows from the buffer being reused without clearing and only readlen bytes being encrypted in place, but I have not observed it.
I mention it because the fix is cheap and the reasoning is checkable by inspection, so
runtime evidence seemed like the wrong thing to spend your review time on — but you should
know it is absent rather than assume I ran something.
Carrier update: the current head of #430 (b73e2d8b6bdf7a15013aa06439c437194478eaed) now contains the #438 cleanup as well as the #429 write-error fix. In both scryptenc_file() and scryptdec_file_copy(), the complete stack buf is wiped with insecure_memzero(buf, sizeof(buf)) on success and on the common failure return path. I am updating #430's description to say Fixes #438 explicitly rather than opening a second conflicting PR against the same functions. The existing shell regression exercises the streaming write-error exits; I am not claiming it directly inspects post-return stack residue.
Activity
woahwhattheheck commentedon Sep 7, 2026
Adding a limitation I should have stated in the report itself.
What I did not exercise. This is a code-reading finding. I did not demonstrate it at
runtime — I have not inspected the stack after either function returns to confirm the
residue empirically, and there is no test or instrumented run behind it. What I verified
is static:
scryptdec_file_copy()contains noinsecure_memzerocall at all, andscryptenc_file()zeroes onlydkon both of its exits, while both functions decrypt orencrypt in place into a buffer that outlives the last operation on it.
The
scryptenc_file()half also depends on a claim I reasoned about rather than measured:that
buf[readlen .. ENCBLOCK)still holds plaintext from the previous iteration after ashort final
fread(). That follows from the buffer being reused without clearing and onlyreadlenbytes being encrypted in place, but I have not observed it.I mention it because the fix is cheap and the reasoning is checkable by inspection, so
runtime evidence seemed like the wrong thing to spend your review time on — but you should
know it is absent rather than assume I ran something.
scryptenc: wipe stream buffers before returning
scryptenc: wipe file stream buffers before returning
tokenjunkielabs commentedon Sep 20, 2026
Carrier update: the current head of #430 (
b73e2d8b6bdf7a15013aa06439c437194478eaed) now contains the #438 cleanup as well as the #429 write-error fix. In bothscryptenc_file()andscryptdec_file_copy(), the complete stackbufis wiped withinsecure_memzero(buf, sizeof(buf))on success and on the common failure return path. I am updating #430's description to sayFixes #438explicitly rather than opening a second conflicting PR against the same functions. The existing shell regression exercises the streaming write-error exits; I am not claiming it directly inspects post-return stack residue.