Skip to content

feat: add --deterministic-rng to make client randomness reproducible - #54

Open
not-matthias wants to merge 6 commits into
masterfrom
cod-3957-determinstic-rng
Open

not-matthias wants to merge 6 commits into
masterfrom
cod-3957-determinstic-rng

Conversation

@not-matthias

@not-matthias not-matthias commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

TLDR: Hash-seed randomness (Rust RandomState, CPython hash seed, glibc canary) changes bucket collisions and probe lengths between runs, which shows up as instruction-count noise. --deterministic-rng=yes serves every randomness source a client can observe from a fixed-seed PRNG (Linux only; time, pid and tid are deliberately untouched).

Core

  • AT_RANDOM auxv bytes come from a dedicated PRNG stream.
  • Each client thread gets its own splitmix64 stream, keyed by a monotonic creation counter (not the ThreadState slot), so the N-th created thread draws the same bytes whichever slot it reuses.

Syswrap

  • getrandom(2) completes in the PRE wrapper and never enters the kernel.
  • fds on /dev/random and /dev/urandom (resolved through /proc/self/fd) are tracked across open*, dup*, fcntl F_DUPFD*, close and close_range; read, pread64, readv, preadv, preadv2 on them are served from the PRNG.
  • Served paths keep kernel errors: a non-writable client buffer fails with EFAULT, and a device fd is only tracked if a zero-length read succeeds, so O_WRONLY and O_PATH fds keep the kernel's EBADF.
  • Served paths clear SfMayBlock; a PRE that completes a syscall must not keep it (syswrap-main.c:2440).

VEX (amd64)

  • When the host supports RDRAND/RDSEED, they draw from the calling thread's stream through a new LibVEX_SetRandom32 callback and always report success.
  • CPUID and decoding still follow the host: on hosts without them, they are neither advertised nor decoded, exactly as without the option. With the option off, the helpers run the host instructions as before.

Review notes

  • Option is parsed in the early pass because VG_(ii_create_image) writes AT_RANDOM before the main pass.
  • A forked child continues its parent's streams; under --trace-children=yes each exec'd program restarts from the seed.
  • Bytes depend on which thread makes the call, so runtimes that move work between threads based on timing (thread pools, work stealing, Go goroutines) can still vary.
  • PRNG output uses host byte order; only little-endian targets are supported for now.
  • Regtests: callgrind/tests/deterministic_rng pins the bytes per source, each draw on a fresh thread so libc's startup getrandom calls cannot shift the expectation, and checks the EFAULT/EBADF cases. none/tests/amd64/deterministic_rng covers RDRAND/RDSEED for every operand width, flags, stream advance and fresh threads; it only runs on hosts with both instructions.

@codspeed

codspeed Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 84 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing cod-3957-determinstic-rng (7a783cb) with master (908f432)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@not-matthias
not-matthias force-pushed the cod-3957-determinstic-rng branch 2 times, most recently from a3e75ea to 0687003 Compare October 21, 2026 10:32
@not-matthias
not-matthias marked this pull request as ready for review October 21, 2026 10:33
@not-matthias
not-matthias force-pushed the cod-3957-determinstic-rng branch from 0687003 to 612a8c8 Compare October 21, 2026 10:35
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge with the stated platform limits.

Summary

Adds opt-in reproducible randomness through --deterministic-rng=yes.

  • Linux clients can draw covered random values from fixed-seed streams.
  • On amd64, RDRAND and RDSEED use the caller's deterministic stream.

Previously accepted limits remain unchanged: not-matthias deferred portability guards because their execution targets have the required APIs, excluded big-endian and non-Linux targets, accepted private descriptor-table limits because Valgrind already shares them, and set Ubuntu 24.04+ as the toolchain baseline.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["--deterministic-rng=yes"] --> B["Dedicated startup stream"]
  A --> C["Per-thread stream"]
  A --> D["Fixed boot UUID stream"]
  B --> E["AT_RANDOM"]
  C --> F["getrandom and random-device reads"]
  C --> G["RDRAND and RDSEED"]
  C --> H["Proc UUID text"]
  D --> I["Proc boot_id text"]
  J["Kernel preserves proc read results"] --> H
  J --> I
Loading

Reviews (5) · Last reviewed commit: "feat(callgrind): make proc UUID reads de..." · Reviewed by Greptile

Comment thread coregrind/m_syswrap/syswrap-linux.c Outdated
Comment thread callgrind/tests/Makefile.am
Comment thread coregrind/m_syswrap/syswrap-generic.c Outdated
Comment thread callgrind/tests/deterministic_rng.stdout.exp
@not-matthias
not-matthias removed the request for review from GuillaumeLagrange October 21, 2026 13:10
Add a fixed-seed splitmix64 PRNG and the --deterministic-rng=no|yes
option (parsed in the early pass, since VG_(ii_create_image) runs before
the main option pass). When on:

- AT_RANDOM is filled from a dedicated stream instead of the kernel's
  bytes, so glibc's canary/pointer guard and Go's hash seed are stable.
- Every client thread gets its own stream in VG_(alloc_ThreadState),
  keyed by a monotonic creation counter rather than the ThreadState slot,
  so the N-th created thread draws the same bytes whichever slot it
  reuses.

Also update the --help snapshots.
@not-matthias
not-matthias force-pushed the cod-3957-determinstic-rng branch from 612a8c8 to fd10c47 Compare October 21, 2026 13:30
Comment thread coregrind/m_syswrap/syswrap-generic.c
Comment thread coregrind/m_syswrap/syswrap-generic.c Outdated
Comment thread coregrind/m_syswrap/syswrap-generic.c Outdated
Comment thread coregrind/m_syswrap/syswrap-linux.c Outdated
@not-matthias
not-matthias force-pushed the cod-3957-determinstic-rng branch from fd10c47 to 5f43405 Compare October 21, 2026 14:20
Comment thread coregrind/m_syswrap/syswrap-generic.c
Comment thread none/tests/amd64/Makefile.am
Comment thread coregrind/m_syswrap/syswrap-linux.c Outdated
@not-matthias
not-matthias force-pushed the cod-3957-determinstic-rng branch from 5f43405 to 76c6a38 Compare October 21, 2026 14:41
Comment thread coregrind/m_syswrap/syswrap-linux.c Outdated
…c PRNG

With --deterministic-rng=yes, getrandom(2) is completed in the PRE
wrapper from the calling thread's stream and never reaches the kernel.
fds opened on /dev/random or /dev/urandom (resolved via /proc/self/fd)
are tracked across open/openat/openat2, dup/dup2/dup3, fcntl F_DUPFD*,
close and close_range; read, pread64, readv, preadv and preadv2 on a
tracked fd are served the same way. Served paths clear SfMayBlock, as a
PRE that completes the syscall must not keep it.

All served paths go through ML_(rng_serve_read), which fails with
EFAULT when the client buffer is not writable instead of faulting
inside Valgrind. A newly opened device fd is only tracked if a
zero-length read succeeds, so O_WRONLY and O_PATH fds keep the
kernel's EBADF.
Route amd64 RDRAND and RDSEED helpers through a VEX callback backed by
the calling thread's PRNG stream when --deterministic-rng is enabled,
so both instructions always report success with reproducible values.

CPUID and instruction decoding still follow the host: the instructions
are only advertised and decoded where the host CPU supports them. With
the option off, the helpers execute the host instructions as before.
Test all operand widths, status flags, advancing streams and fresh
threads on hosts with RDRAND and RDSEED.
Under --deterministic-rng=yes, print AT_RANDOM and bytes from
getrandom, /dev/urandom via read, a dup'd fd and readv. Each source is
drawn on a fresh, sequentially joined thread so the expected bytes do
not depend on how much libc pulls from getrandom(2) before main().

Also check that the served paths keep the kernel's errors: EFAULT for a
read-only buffer and EBADF for O_WRONLY and O_PATH device fds.
Describe the option in the core manual and the CodSpeed changelog: the
covered randomness sources, per-thread streams, fork and exec
behavior, and known limits (thread affinity of draws, Go map
iteration order).

@GuillaumeLagrange GuillaumeLagrange left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, although thorough testing will be required

@art049 art049 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me on the approach although as mentionned in the changelog, we still miss some non deterministic randomness.

Would it be hard to cover /proc/sys/kernel/random/* devices?
For example /proc/sys/kernel/random/uuid might be widely used, pretty sure there are others

For clock_gettime definitely this will be a mess with certificates and a lot of other things so let's not deal with it now.

I think we need to cover arm64 for cpu level randomness.

Comment thread VEX/priv/guest_amd64_helpers.c
Cover /proc/sys/kernel/random/uuid and boot_id with typed RNG descriptor
tracking. Draw UUIDv4 text from the calling thread stream and derive
boot_id independently from the fixed seed.

Run proc reads under the big lock and replace only returned bytes,
retaining kernel offsets, short reads, EOF and errors. Snapshot vector
reads before output can overwrite their iovecs. Add regression coverage
and document the supported sources.
@not-matthias

Copy link
Copy Markdown
Member Author

Looks good to me on the approach although as mentionned in the changelog, we still miss some non deterministic randomness.

Would it be hard to cover /proc/sys/kernel/random/* devices? For example /proc/sys/kernel/random/uuid might be widely used, pretty sure there are others

For clock_gettime definitely this will be a mess with certificates and a lot of other things so let's not deal with it now.

I think we need to cover arm64 for cpu level randomness.

Implemented support for some /proc/sys/kernel/random/* variables that can be spoofed. See:
7a783cb (this PR)

@not-matthias
not-matthias requested a review from art049 October 27, 2026 18:56
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.

3 participants