Repository navigation
Conversation
|
nihalpasham
left a comment
There was a problem hiding this comment.
- Blocker: broad prefix capture persists unrelated secrets such as
CUDA_API_TOKEN, while omitted variables are inherited from the replay shell. The recorded environment therefore is neither minimal nor reproducible. - Replay scripts are also created permissively and changed to
0755afterward; that leaves a disclosure window and makes captured values readable to other users. - A complete fix needs one typed replay-environment schema used by both campaign and replay,
env_clear, explicit captured/unset/required-external fields, and atomic creation with restrictive permissions. - Add GPU-independent tests for secret exclusion, absent-variable replay, non-Unicode values, inherited-variable clearing, and file/directory modes. The current tests instead pin the unsafe prefix behavior.
…oducible Rewritten from the review on NVIDIA#1137, which blocked the first attempt for two reasons, both correct. **Secrets.** Carrying variables by prefix cannot tell `CUDA_VISIBLE_DEVICES` from `CUDA_API_TOKEN`. Both matched `CUDA_`, so both were written into `replay.sh` on disk, as was `CUDA_OXIDE_LICENSE_KEY`. Worse, the test that was supposed to prove exclusion used `AWS_SECRET_ACCESS_KEY` -- a name outside every captured prefix -- so it asserted safety the rule did not provide and pinned the unsafe behaviour as correct. **Reproducibility.** The run inherited the campaign's whole environment while the script re-exported a subset, so the two never described the same thing. A variable the script omitted was silently supplied by whatever the replay shell happened to hold. One value now decides both, and every name in it is explicit: pub struct ReplayEnvironment { captured: Vec<(String, String)>, // set on the child, exported by the script unset: Vec<String>, // `unset NAME`, so no ambient value leaks in required_external: Vec<String>, // `${NAME:?}`, so a wrong environment refuses } `CAPTURED_ENV` is seven names, each read at run time by something in this tree: the two driver knobs, `CUDA_OXIDE_TARGET` (the three FFI examples), the three `GEMM_SOL_*` knobs (`gemm_sol`, `gemm_sol_final`), and `MATHDX_ROOT`. A token is not on the list, so it cannot be captured whatever it is called. `REQUIRED_EXTERNAL_ENV` is `HOME`, `LD_LIBRARY_PATH`, `PATH`: needed to execute, machine-specific, and never recorded. The script guards each with `: "${NAME:?required by this run and not recorded here}"`, so replaying without one stops instead of quietly running in a different environment. A captured value that is not valid UTF-8 joins them -- the child still receives it, but no POSIX script can carry it, so the script states the requirement rather than inventing a lossy value. `run_binary` now calls `env_clear` and sets exactly this. That is the half no script could fix: measured here, a child without it inherits 96 variables from the campaign, `SSH_AUTH_SOCK` among them. The script and the seed directory are created owner-only. `fs::write` followed by `set_permissions` created the file at the process umask -- world-readable by default -- and only narrowed it afterwards, leaving a window in which captured values were readable by other users. `OpenOptions::mode` applies at creation, and an explicit remove before `create_new` keeps a stale file from surviving with its old mode. Two refactors the change forced, both improvements: `RunEnvironment` bundles the captured environment with the process environment it came from, since they were always used together, and `SeedContext` collects the six per-campaign constants that were threaded through `run_seed` one parameter at a time. Seven tests, and seven mutations to show each one bites: | mutation | caught by | |---|---| | drop `env_clear` | the child inherits 96 parent variables | | add `CUDA_API_TOKEN` to `CAPTURED_ENV` | secret exclusion, and the clear test | | lossy-convert a non-UTF-8 value | the non-Unicode requirement test | | `fs::write` then chmod | the file/directory mode test | | a comment instead of `${NAME:?}` | the refusal test, run through `/bin/sh` | | stop emitting `unset` | the absent-variable test | | seed directory at 0755 | the file/directory mode test | The refusal test executes the generated script under `/bin/sh` with a cleared environment and asserts it fails naming the missing variable, then passes once it is supplied. The clear test runs a real child through `run_binary` and asserts no name from this process's own environment reached it. Verified: 21 tests pass, fmt clean, `clippy --all-targets -- -D warnings` clean, `cargo doc` clean with and without `--document-private-items`, and cargo-oxide's 177 tests still pass. Not verified: `env_clear` changes how every variant is launched, and I have no hardware evidence that a real CUDA example still runs under the reduced environment. That wants one campaign on a box before this merges. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
465edee to
b7c512a
Compare
|
Rewritten against both blockers. Force-pushed as a single commit; the prefix rule and the tests that pinned it are gone. Secrets. Carrying by prefix could not tell Reproducibility. One
Modes. Script and seed directory are created owner-only. Two refactors the change forced: Seven tests, each shown to bite by reverting the thing it covers:
The refusal test executes the generated script under 21 tests, fmt, strict clippy and rustdoc pass; cargo-oxide's 177 still pass. Merges clean over your commits on #1134 and #1135 in either order. One thing I have not verified, and would not want merged without it: |
Hardware evidence for
|
…oducible Rewritten from the review on NVIDIA#1137, which blocked the first attempt for two reasons, both correct. **Secrets.** Carrying variables by prefix cannot tell `CUDA_VISIBLE_DEVICES` from `CUDA_API_TOKEN`. Both matched `CUDA_`, so both were written into `replay.sh` on disk, as was `CUDA_OXIDE_LICENSE_KEY`. Worse, the test that was supposed to prove exclusion used `AWS_SECRET_ACCESS_KEY` -- a name outside every captured prefix -- so it asserted safety the rule did not provide and pinned the unsafe behaviour as correct. **Reproducibility.** The run inherited the campaign's whole environment while the script re-exported a subset, so the two never described the same thing. A variable the script omitted was silently supplied by whatever the replay shell happened to hold. One value now decides both, and every name in it is explicit: pub struct ReplayEnvironment { captured: Vec<(String, String)>, // set on the child, exported by the script unset: Vec<String>, // `unset NAME`, so no ambient value leaks in required_external: Vec<String>, // `${NAME:?}`, so a wrong environment refuses } `CAPTURED_ENV` is seven names, each read at run time by something in this tree: the two driver knobs, `CUDA_OXIDE_TARGET` (the three FFI examples), the three `GEMM_SOL_*` knobs (`gemm_sol`, `gemm_sol_final`), and `MATHDX_ROOT`. A token is not on the list, so it cannot be captured whatever it is called. `REQUIRED_EXTERNAL_ENV` is `HOME`, `LD_LIBRARY_PATH`, `PATH`: needed to execute, machine-specific, and never recorded. The script guards each with `: "${NAME:?required by this run and not recorded here}"`, so replaying without one stops instead of quietly running in a different environment. A captured value that is not valid UTF-8 joins them -- the child still receives it, but no POSIX script can carry it, so the script states the requirement rather than inventing a lossy value. `run_binary` now calls `env_clear` and sets exactly this. That is the half no script could fix: measured here, a child without it inherits 96 variables from the campaign, `SSH_AUTH_SOCK` among them. The script and the seed directory are created owner-only. `fs::write` followed by `set_permissions` created the file at the process umask -- world-readable by default -- and only narrowed it afterwards, leaving a window in which captured values were readable by other users. `OpenOptions::mode` applies at creation, and an explicit remove before `create_new` keeps a stale file from surviving with its old mode. Two refactors the change forced, both improvements: `RunEnvironment` bundles the captured environment with the process environment it came from, since they were always used together, and `SeedContext` collects the six per-campaign constants that were threaded through `run_seed` one parameter at a time. Seven tests, and seven mutations to show each one bites: | mutation | caught by | |---|---| | drop `env_clear` | the child inherits 96 parent variables | | add `CUDA_API_TOKEN` to `CAPTURED_ENV` | secret exclusion, and the clear test | | lossy-convert a non-UTF-8 value | the non-Unicode requirement test | | `fs::write` then chmod | the file/directory mode test | | a comment instead of `${NAME:?}` | the refusal test, run through `/bin/sh` | | stop emitting `unset` | the absent-variable test | | seed directory at 0755 | the file/directory mode test | The refusal test executes the generated script under `/bin/sh` with a cleared environment and asserts it fails naming the missing variable, then passes once it is supplied. The clear test runs a real child through `run_binary` and asserts no name from this process's own environment reached it. Verified: 21 tests pass, fmt clean, `clippy --all-targets -- -D warnings` clean, `cargo doc` clean with and without `--document-private-items`, and cargo-oxide's 177 tests still pass. Verified on hardware, since `env_clear` changes how every variant is launched and no local test can show that a real CUDA example still runs under the reduced environment. Two campaigns on an A10G (compute capability 8.6, driver 595.71.05), with `CUDA_API_TOKEN`, `CUDA_OXIDE_LICENSE_KEY` and an unrelated sentinel exported alongside `CUDA_VISIBLE_DEVICES` and `GEMM_SOL_MODE`: * `vecadd` has 0 schedule sites, which isolates the launch path: baseline and three seeds all Pass. * `redux_sum` has 2 `WarpCollective` sites, so variants are genuinely perturbed: baseline and five seeds all Pass, with injections from 91 ns to 48 us and matching `nanosleep.u32` in each patched module. Nine launches, every one through `run_binary` with the environment cleared. `HOME`, `LD_LIBRARY_PATH` and `PATH` proved sufficient; nothing had to be added to `REQUIRED_EXTERNAL_ENV`. Grepping every byte of both artifact trees for the three sentinel values and their variable names found none of the six. Replaying a perturbed seed under `env -i` plus the three required names reproduced the run and exited 0; with a required name withheld the script refused, naming it, and exited 2. All five seed directories and scripts came out 0700. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
b7c512a to
5059bb4
Compare
|
Amended the commit message only — no code change. It still carried "Not verified: … I have no hardware evidence", which stopped being true once the A10G run in the comment above landed. Leaving a false statement in the permanent record seemed worse than a force-push, so the paragraph now states what was actually measured: two campaigns, nine launches through Code is byte-identical to what you last saw. Ready for another look whenever it suits. |
|
Closing this PR because campaign execution and generated replay are still not faithful to the same environment and artifact contract, and the remaining repair is a redesign rather than a small follow-up.
The stock 21 tests pass because they test the pieces separately; the executable contract counterexamples fail. A replacement needs one process specification shared by campaign and replay, exact environment-state semantics, ambient clearing, normalized/raw path policy, and exclusive atomic artifact creation with stale-path and concurrency tests. Please align on that design first under the acceptance criteria shared in Discord. |
What is wrong
Every seed that produces a finding gets a
replay.shnext to its artifacts. Its whole purpose is to re-run that variant, and it re-exported three environment variables by name:The variant itself runs with the campaign's whole environment —
run_binarybuilds aCommandand never callsenv_clear— so the script reproduces strictly less than the run had. The tell is that one member of a family was carried and its siblings were not:GEMM_SOL_PHASEgemm_solGEMM_SOL_MODEgemm_sol_finalGEMM_SOL_VARIANTgemm_sol_finalCUDA_OXIDE_TARGETdevice_ffi_test,mathdx_ffi_test,small_type_ffi_testMATHDX_ROOTmathdx_ffi_testGEMM_SOL_MODEandGEMM_SOL_VARIANTare documented, user-facing knobs —gemm_sol_final/README.mdshowsGEMM_SOL_MODE=plan GEMM_SOL_VARIANT=m512xn256 cargo oxide run gemm_sol_final, and the book's kernel-families page documents the variant override. They select which mode runs and which kernel variant is chosen.So
GEMM_SOL_MODE=bench GEMM_SOL_VARIANT=m512xn256 cargo oxide fuzz-schedule gemm_sol_finalwrites replay scripts that runmode=both, variant=auto. The script reproduces a different execution than the one that found the bug — the worst failure available to a reproduction artifact, because it does not error, it just quietly disagrees.The change
Carry by prefix instead of by name, so a new knob in a family that already matters needs no edit here:
CUDA_,GEMM_SOL_,MATHDX_. Deliberately not the whole environment — this text is written to a file in the artifact directory, and only the CUDA knobs and the example-suite families belong there. (mathdx_ffi_testalso readsHOME, which is not carried and does not need to be: any shell running the script already has it.)Two details the rewrite fixes on the way:
std::env::varshas no defined order and the script is an artifact, so the same environment now always produces the same file.export BAD-NAME=...would fail under the script's ownset -euand take every later export and theexecwith it.The selection is split into
replay_exports, a pure function over (key, value) pairs, so all of this is testable without mutating the process environment — whichset_varisunsafefor in edition 2024, and which races across test threads.Verification
Five tests: every variable an in-tree example reads is carried; nothing outside those families is (
PATH,HOME,LD_LIBRARY_PATH,RUSTFLAGS,AWS_SECRET_ACCESS_KEY, and a near-missMYCUDA_THING); the order is stable under a shuffled input; an unassignable name is dropped; and a value containing a single quote still round-trips throughshell_quote_value.Reverting to the three hard-coded names fails three of them, dropping the name filter fails one, dropping the sort fails two.
19 tests pass, fmt clean,
clippy --all-targets -- -D warningsclean,cargo doc --document-private-itemsclean.One file. No open PR touches it; it shares
campaign.rswith my #1134 and #1135, in a disjoint region, and all six of this batch merge to a byte-identical tree in either order.