镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Fix getrandom() retry loop resuming from the wrong buffer position - #106

Open
Uyuz-ux wants to merge 1 commit into
cheatfate:masterfrom
Uyuz-ux:fix/getrandom-partial-read
Open

Uyuz-ux wants to merge 1 commit into
cheatfate:masterfrom
Uyuz-ux:fix/getrandom-partial-read

Conversation

@Uyuz-ux

@Uyuz-ux Uyuz-ux commented Nov 2, 2026

Copy link
Copy Markdown

Fixes #103.

The bug

In the Linux implementation of randomBytes() the retry loop for partial getrandom(2) reads computed the resume pointer p, but then passed the original buffer start pbytes to the syscall:

while res < nbytes:
  p = cast[pointer](cast[uint](pbytes) + uint(res))
  let bytesRead = syscall(SYS_getrandom, pbytes, nbytes - res, 0)  # should be `p`

After a partial read of k bytes, every retry restarted at the buffer start while res kept accumulating, so the region [k, nbytes) was never written and the function still returned full success. For a CSPRNG API that means keys/nonces/salts can silently contain uninitialized memory — exactly as described in #103.

The fix

One line: pass p instead of pbytes, matching how the other implementations in this file (urandomRead, the OpenBSD path) already handle the resume pointer.

Verification

I could not force a partial read deterministically from userspace (this kernel fills large requests in a single call), so I verified the resume path with a small LD_PRELOAD harness that intercepts libc syscall() and makes the first getrandom call with len > 8 return a partial read of 8 bytes, passing later calls through unchanged. A checker program fills a 256-byte buffer with the 0xAB marker, calls randomBytes(), and reports how many stale marker bytes remain after the first 8 positions:

=== upstream master (before fix) ===
without harness: returned=256 staleTailBytes=0 (no partial read -> passes)
with harness:    returned=256 staleTailBytes=248
                 RESULT: FAIL - buffer tail still holds stale memory

=== with this fix ===
with harness:    returned=256 staleTailBytes=0
                 RESULT: PASS - whole buffer filled with random data

So with the old code the tail of the buffer (248 of 256 bytes) kept its pre-call contents while the function reported success; with the fix the whole buffer is filled.

tests/testsysrand.nim passes in all four compile modes used by nimble test (default, -d:danger, --threads:on, --mm:orc --threads:on) on Nim 2.2.4 / Linux amd64.

I did not add a regression test because a partial getrandom(2) read can't be triggered deterministically from a normal unit test (it depends on signal timing / kernel limits), and mocking would require refactoring the direct syscall import. Happy to add one if you'd prefer a refactor that injects the syscall — happy to follow up in a separate PR.

The Linux randomBytes() loop computed the resume pointer p for each
iteration but then passed the original buffer start (pbytes) to the
getrandom(2) syscall. After a partial read, every retry overwrote the
beginning of the buffer while res kept accumulating, so the tail of
the buffer was never written and randomBytes() still reported full
success. Fixes cheatfate#103.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] randomBytes on Linux can return non-random, uninitialized memory

1 participant