Skip to content

test(sandbox): ratchet-aware NoNewPrivs child matrices - #98

Merged
asto18089 merged 2 commits into
pinvou3-cleanfrom
fix/sandbox-nnp-pinned-ratchet
Oct 12, 2026
Merged

asto18089 merged 2 commits into
pinvou3-cleanfrom
fix/sandbox-nnp-pinned-ratchet

Conversation

@asto18089

@asto18089 asto18089 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Hardened containers pin NoNewPrivs=1 across the whole process tree. The two child-matrix tests in sandbox::process_hardening::linux_flag_tests include rows that expect the child to report NNP=0 (the danger-full-access posture row, and the falsey CODEWHALE_NO_NEW_PRIVS=0 override row). Under a pinned kernel flag those rows are unsatisfiable — the child inherits the flag and the kernel forbids clearing it — so both tests failed deterministically in such environments (observed in containerized dev environments; CI and developer machines are unaffected).

Fix

Probe the parent process's kernel flag (no_new_privs_active()) before running each matrix. Where the flag is already pinned:

  • the "expected 0" rows assert the ratchet instead (the flag stays 1 — the downgrade is refused), and
  • each substituted row prints a disclosure line naming the mode/override it could not observe.

Unpinned environments run the full original matrix unchanged; no row is skipped silently.

Verification

  • Pinned container (this environment, where both tests previously failed): full no_new_privs filter set 14/14 green, disclosure lines emitted for exactly the two unsatisfiable rows.
  • cargo fmt -p codewhale-tui -- --check clean.

No-Issue: test-only fix observed in pinned-NoNewPrivs containers; no tracked issue

Hardened containers pin NoNewPrivs=1 across the whole process tree, so
the child-matrix rows that expect NNP=0 (the danger-full-access posture
and the falsey env override) are unsatisfiable there: the child
inherits the pinned flag and the kernel forbids clearing it, and the
two matrices failed deterministically in such environments.

Probe the parent process kernel flag first; where it is already
pinned, assert the ratchet instead (the flag stays 1) and print a
disclosure for each substituted row. Unpinned environments (developer
machines, CI) still run the full original matrix unchanged.

Signed-off-by: asto18089 <asto18089@126.com>
@asto18089

Copy link
Copy Markdown
Collaborator Author

Fresh review (from scratch), performed on the branch rebased onto latest pinvou3-clean 0bc868ff0 (head was 7 commits behind; none of those commits touch this file, so the rebase is content-trivial — the applied file blob 6266812595 is byte-identical to the head's). Verdict: the fix is correct, complete, and minimal — approve after the link check (I appended the missing No-Issue: line to the body; everything else was already green).

Claim verification — first-hand in a pinned-NNP container

This review environment runs NoNewPrivs: 1 (hardened container), i.e. exactly the environment the PR targets.

  • Pre-fix, on base 0bc868ff0: both matrix tests fail deterministically, exactly as claimed — mode Some("danger-full-access"): expected NoNewPrivs=0, got: NNP=1 and mode "workspace-write" with CODEWHALE_NO_NEW_PRIVS=0: expected NoNewPrivs=0, got: NNP=1.
  • Post-fix: cargo test no_new_privs → 14 passed, 0 failed, with disclosure lines emitted for exactly the two unsatisfiable rows and no others.
  • Kernel premises verified empirically: prctl(PR_SET_NO_NEW_PRIVS, 0) on a pinned process fails (EINVAL — one-way ratchet confirmed); the flag survives fork+exec (children inherit 1); the parent-side prctl(PR_GET_NO_NEW_PRIVS) probe agrees with what children report.

Completeness (related cases)

Repo-wide sweep: these two child-matrix tests are the only tests whose assertion outcome flips under NNP pinning. Every other NNP-adjacent test either injects synthetic Option<bool> values (policy.rs, status.rs:775, engine/tests.rs:9161) or asserts strings that are invariant across flag states (status.rs:717/:846, engine/tests.rs:9097). The no_new_privs filter count is exactly 14 — the "14/14" claim is accurate. No test spawns sudo/setuid helpers or asserts dumpable/RLIMIT_CORE values, so nothing else breaks in hardened containers.

Smuggling

Clean. +37/−0, all inside mod linux_flag_tests, additions only, no renames or formatting churn; a full tree diff of the rebased branch against base shows only this one file.

Elegance / correctness notes (no action required)

  • Reuses the existing production probe no_new_privs_active() rather than re-probing /proc — the right call; nothing reinvented.
  • Substitution only ever tightens an expectation ("0" → the ratchet-guaranteed "1"), never relaxes a "1" row, and is gated on the live read being Some(true). Unpinned environments run the original matrix unchanged.
  • Known-by-design tradeoff, honestly disclosed in the new doc comment: in a pinned environment the matrices degrade to a smoke test — no row can detect a regression that removes the PR_SET_NO_NEW_PRIVS application, because the child reports 1 regardless. That is physically unobservable from inside the pinned tree, so accepting it with disclosure is correct.
  • Nits: kernel_pinned_no_new_privs() is a pure forward whose name slightly overclaims (it reads the current task's flag — it cannot distinguish "container pinned it" from "this process set it"; outcome-neutral here); the 12-line substitution block is duplicated across the two tests (acceptable at two occurrences with context-specific messages); the disclosure println! is only visible with --nocapture or on failure, which matches existing repo practice.
  • PR-body wording nit: "the downgrade is refused" — nothing in production ever attempts a downgrade; the test proves inheritance plus the kernel's refusal rule, not a production refusal path. The in-code comment ("inherits 1 and cannot drop it") is the accurate phrasing.

Environment notes (pre-existing, not this PR)

  • In this sandbox a plain cargo test no_new_privs aborts on an unrelated stack overflow in status_safety_row_discloses_no_new_privs_flag_state_for_full_access (i18n formatting path; reproduces identically on base; none of the 7 base commits touch that file). With RUST_MIN_STACK=33554432 the full 14/14 is green. CI is unaffected.
  • A local cargo clippy -p codewhale-tui --lib --tests trips 7 pre-existing -D warnings lints (shell_dispatcher.rs:762, engine/tests.rs:20141/:25056, js_execution.rs:386/:428, web_search.rs:4652) — byte-identical on base, none in this file; the repo's own Analyze (rust) gate is green on this head.

Gates

All checks were green except the required link check (body had no closing keyword and no opt-out; no tracking issue exists to close). Fixed by appending No-Issue: test-only fix observed in pinned-NoNewPrivs containers; no tracked issue to the body. Commit format (test(sandbox): …) and DCO verified.

@asto18089 asto18089 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: APPROVE (self-review; GitHub blocks formal approval from the author's account).

Fresh review, from scratch, on the branch rebased onto latest pinvou3-clean 0bc868f — content-trivial rebase (applied file blob 6266812595 byte-identical to head). Evidence (full details in the review comment below):

  • Claim: reproduced first-hand in this pinned-NoNewPrivs container — pre-fix both child-matrix tests fail deterministically exactly as claimed; post-fix the full no_new_privs set is 14/14 green with disclosure lines for exactly the two unsatisfiable rows. Kernel premises verified empirically (PR_SET_NO_NEW_PRIVS,0 fails EINVAL when set; flag survives fork+exec).
  • Completeness: repo-wide sweep — these two matrices are the only tests whose outcome flips under NNP pinning; 14/14 filter count exact; every other NNP-adjacent test injects synthetic values or asserts flag-invariant strings.
  • Smuggling: none (+37/−0, single test module, additions only).
  • Gates: all 13 checks green (link check fixed by the body No-Issue line); fmt clean; the local clippy hits are pre-existing on base and CI's Analyze (rust) is green.

Non-blocking notes: forwarding-wrapper name slightly overclaims 'pinned'; substitution block duplicated ×2; PR-body 'the downgrade is refused' is shorthand — nothing in production attempts a downgrade. Residual-by-design (disclosed in code): pinned environments cannot detect removal of the PR_SET_NO_NEW_PRIVS application.

@JensenChen28 JensenChen28 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed current head 28c37d304386bfcbba438632d934871873a0d152. The expectation is adjusted only when the parent process already reports NoNewPrivs=1, where the kernel's one-way ratchet makes an expected-zero child row unobservable; unpinned environments still assert the original zero outcomes. Both child-process matrices pass locally (2/2) in this pinned environment. Current pinvou3-clean does not touch this file, the PR remains mergeable, and the earlier link failure was superseded by the later successful rerun; check and gate are green. Approved.

@JensenChen28 JensenChen28 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed 28c37d3. No blocking findings. The diff changes only the Linux child-process test expectations when the inherited kernel NoNewPrivs flag is already set. Normal unpinned opt-out expectations remain intact; the pinned substitution is explicitly disclosed, and the production hardening path is unchanged.

Independent Linux validation: compiled the entire current process_hardening.rs unchanged in an isolated Rust crate with libc/tracing (source hashes matched). Ran all module tests in both environments:

  • Parent NoNewPrivs=0: test result: 11 passed; 0 failed.
  • A separate process explicitly set NoNewPrivs=1 before executing the same test binary: test result: 11 passed; 0 failed. Both changed matrix tests emitted their pinned-state disclosure and passed.

The flag was set only in the dedicated probe process; the review parent remained at 0. This validates the actual kernel behavior and child inheritance on Linux, not just a mocked predicate. Full workspace and non-Linux suites were not rerun. Required checks were rechecked before submission. This approval concerns the CodeWhale PR, not a parent gitlink update.

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.

2 participants