Skip to content

[bug bounty] The plaintext buffers in scryptenc_file and scryptdec_file_copy are never wiped #438

Open

Description

@woahwhattheheck

Summary

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_t buf[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_t buf[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:

	if ((readlen = fread(buf, 1, ENCBLOCK, infile)) == 0)
		break;
	crypto_aesctr_stream(AES, buf, buf, readlen);

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.

About this report

Activity

  1. woahwhattheheck commented on Sep 7, 2026

    @woahwhattheheck
    Author

    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.

  2. added 2 commits that reference this issue on Sep 20, 2026
    30f52e7
    b73e2d8
  3. tokenjunkielabs commented on Sep 20, 2026

    @tokenjunkielabs

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions