Skip to content

test: run permission-denied tests as an unprivileged user when root#10633

Merged
basvandijk merged 7 commits into
masterfrom
basvandijk/run-permission-denied-tests-as-nobody
Jul 3, 2026
Merged

test: run permission-denied tests as an unprivileged user when root#10633
basvandijk merged 7 commits into
masterfrom
basvandijk/run-permission-denied-tests-as-nobody

Conversation

@basvandijk

@basvandijk basvandijk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

Supports running rust_tests, that assume being run as an unprivileged user, as root.

Spin off from the RBE @ Namespace experiment.

Why

In certain environments, like on a Remote Build Execution cluster @ Namespace, bazel runs as root. This breaks several tests that assert PermissionDenied on files whose permission bits have been restricted. Root holds CAP_DAC_OVERRIDE and bypasses filesystem permission bits, so when run as root the operations succeed and the tests fail.

How

This adds the ic-test-utilities-privileges crate exposing run_as_nobody_if_root, which runs a test closure in a forked child that drops to the nobody user when the process is root (redirecting TMPDIR/TEST_TMPDIR to a nobody-owned base and mirroring the child's outcome — including panic messages — so #[should_panic] keeps working), and runs in-process otherwise. The helper is gated to Linux since the semantics (uid 65534, CAP_DAC_OVERRIDE) are Linux-specific and nix 0.24.3 lacks setgroups on Apple targets.

Since wrapping a whole test body in a closure pollutes it with extra code and indentation, the companion proc-macro crate ic-test-utilities-privileges-macros provides the #[as_nobody_when_root] attribute, which performs that wrapping at compile time. Proc-macros must live in their own crate, so it is re-exported through ic-test-utilities-privileges (the serde/serde_derive facade pattern, like canlog re-exports canlog_derive) and consumers keep a single dependency. The attribute composes with #[test] and #[should_panic], and rejects attribute arguments, async fns, and non-unit return types at compile time.

The attribute is applied to the affected permission tests in rs/sys, the crypto service provider, rs/state_manager, and rs/sns/cli.

Tested on a RBE cluster running as root @ Namespace in #10579 and manually verified by running sudo bazel test.

Add the ic-test-utilities-privileges crate exposing run_as_nobody_if_root,
which runs a test closure in a forked child that drops to the nobody user when
the process is root, and in-process otherwise.

Root holds CAP_DAC_OVERRIDE and therefore bypasses filesystem permission bits,
so tests asserting PermissionDenied instead observe the operation succeeding and
fail when run as root (e.g. on Bazel remote-execution workers). The helper forks,
redirects TMPDIR/TEST_TMPDIR to a nobody-owned base, drops supplementary groups,
gid, and uid to nobody, and runs the whole test body there; the child's outcome
(including any panic message) is mirrored to the parent so #[should_panic] and
its expected message keep working.

Use it in the affected permission tests in rs/sys, the crypto service provider,
and rs/state_manager.
nix 0.24.3 configures `setgroups` out on Apple targets, so the `imp`
module failed to compile on darwin. The helper's semantics (the Linux
`nobody` uid 65534 and CAP_DAC_OVERRIDE) are Linux-specific anyway, so
gate `imp` on `target_os = "linux"` and fall through to the in-process
no-op on the other platforms.
The tests test_ensure_file_exists_and_is_writeable_fails_if_non_writeable and
test_save_proposal_id_to_file_fails_if_write_fails assert PermissionDenied on a
read-only file. Root holds CAP_DAC_OVERRIDE and bypasses filesystem permission
bits, so under Bazel remote-execution (running as root) the operations succeed
and the assertions fail.

Wrap both test bodies in ic_test_utilities_privileges::run_as_nobody_if_root so
they drop to the nobody user when the process is root, mirroring the treatment
applied in 380255a.

Copilot AI 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.

Pull request overview

Adds a small Rust test-utility crate to make filesystem permission-denied assertions reliable when tests execute as root (e.g., on remote-execution workers), and updates affected tests across several crates to run those assertions as an unprivileged user.

Changes:

  • Introduces ic-test-utilities-privileges with run_as_nobody_if_root (Linux: fork + drop to uid/gid 65534; non-Linux: no-op).
  • Wraps permission-sensitive tests in rs/sys, rs/state_manager, rs/crypto/internal/crypto_service_provider, and rs/sns/cli with run_as_nobody_if_root.
  • Wires the new crate into Cargo workspace and Bazel targets (plus a small formatting-only change in root Cargo.toml).

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
rs/test_utilities/privileges/src/lib.rs New helper that forks and drops privileges to nobody when running as root.
rs/test_utilities/privileges/Cargo.toml Defines new test-utility crate and its dependency on nix.
rs/test_utilities/privileges/BUILD.bazel Bazel rust_library target for the new helper crate.
rs/sys/src/fs.rs Wraps permission-denied assertions to run unprivileged when root.
rs/sys/Cargo.toml Adds new helper crate as a dev-dependency.
rs/sys/BUILD.bazel Adds Bazel dependency on the new helper crate for tests.
rs/state_manager/src/checkpoint/tests.rs Wraps permission-sensitive checkpoint tests with the unprivileged runner.
rs/state_manager/Cargo.toml Adds new helper crate dependency.
rs/state_manager/BUILD.bazel Adds Bazel dependency on the new helper crate for tests.
rs/sns/cli/src/propose/propose_tests.rs Wraps permission-denied tests for file writability with the unprivileged runner.
rs/sns/cli/Cargo.toml Adds new helper crate as a dev-dependency.
rs/sns/cli/BUILD.bazel Adds Bazel dependency on the new helper crate for tests.
rs/crypto/internal/crypto_service_provider/src/secret_key_store/proto_store/tests.rs Wraps SKS permission-denied tests so they behave correctly under root.
rs/crypto/internal/crypto_service_provider/src/public_key_store/proto_pubkey_store/tests.rs Wraps pubkey store permission tests so they behave correctly under root.
rs/crypto/internal/crypto_service_provider/Cargo.toml Adds new helper crate dependency.
rs/crypto/internal/crypto_service_provider/BUILD.bazel Adds Bazel dependency on the new helper crate for tests.
Cargo.toml Adds the new crate to the workspace members; formats opentelemetry-otlp entry.
Cargo.lock Locks the new crate into the dependency graph.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/test_utilities/privileges/src/lib.rs Outdated
Comment thread rs/test_utilities/privileges/src/lib.rs Outdated
Comment thread rs/test_utilities/privileges/src/lib.rs Outdated
Comment thread rs/test_utilities/privileges/src/lib.rs Outdated
Comment thread rs/sys/BUILD.bazel
Comment thread rs/state_manager/BUILD.bazel
Comment thread rs/crypto/internal/crypto_service_provider/BUILD.bazel
basvandijk and others added 2 commits July 2, 2026 10:45
* Terminate the forked child with _exit(2) instead of std::process::exit,
  which would run atexit(3) handlers and stdio flushing inherited from the
  (potentially multi-threaded) parent test process.

* Make the child's temp base nobody-owned with mode 0700 instead of
  root-owned world-writable 0777, matching the documentation and avoiding a
  predictable world-writable directory created as root. Create it with
  create_dir rather than create_dir_all so a pre-existing directory of
  unknown ownership is rejected.

* Make the fork path safe in a multi-threaded test process without forcing
  --test-threads=1 on whole test suites: the temp base and putenv(3)
  NAME=VALUE strings are prepared in the parent before forking, and the
  child no longer calls std::env::set_var (whose global environment lock
  another parent thread could have held at fork time), using putenv with
  the pre-allocated strings instead.

* Add smoke tests for the helper, including a concurrency regression test
  that forks from multiple threads; verified as root, where they exercise
  the fork + privilege-drop path.
@basvandijk basvandijk marked this pull request as ready for review July 2, 2026 12:10
@basvandijk basvandijk requested review from a team as code owners July 2, 2026 12:10

@github-actions github-actions Bot 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.

This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):

  1. Update unreleased_changelog.md (if there are behavior changes, even if they are
    non-breaking).

  2. Are there BREAKING changes?

  3. Is a data migration needed?

  4. Security review?

How to Satisfy This Automatic Review

  1. Go to the bottom of the pull request page.

  2. Look for where it says this bot is requesting changes.

  3. Click the three dots to the right.

  4. Select "Dismiss review".

  5. In the text entry box, respond to each of the numbered items in the previous
    section, declare one of the following:

  • Done.

  • $REASON_WHY_NO_NEED. E.g. for unreleased_changelog.md, "No
    canister behavior changes.", or for item 2, "Existing APIs
    behave as before.".

Brief Guide to "Externally Visible" Changes

"Externally visible behavior change" is very often due to some NEW canister API.

Changes to EXISTING APIs are more likely to be "breaking".

If these changes are breaking, make sure that clients know how to migrate, how to
maintain their continuity of operations.

If your changes are behind a feature flag, then, do NOT add entrie(s) to
unreleased_changelog.md in this PR! But rather, add entrie(s) later, in the PR
that enables these changes in production.

Reference(s)

For a more comprehensive checklist, see here.

GOVERNANCE_CHECKLIST_REMINDER_DEDUP

@zeropath-ai

zeropath-ai Bot commented Jul 2, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to e224f9f.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/test_utilities/privileges/src/lib.rs
    Implement privilege-dropping test utility and run_as_nobody_if_root functionality
► rs/test_utilities/privileges/tests/smoke.rs
    Add smoke tests for privilege utility
► rs/test_utilities/privileges/Cargo.toml
    Add package for privileges utilities
► rs/test_utilities/privileges/BUILD.bazel
    Add Bazel build for privileges utilities
► rs/test_utilities/privileges/macros/src/lib.rs
    Add proc-macro for as_nobody_when_root attribute
► rs/test_utilities/privileges/macros/Cargo.toml
    Add macros crate for privileges
► rs/test_utilities/privileges/macros/BUILD.bazel
    Add Bazel build for privilege macros
► rs/test_utilities/privileges/macros/src/lib.rs
    Provide as_nobody_when_root attribute macro
► rs/test_utilities/privileges/macros/src/lib.rs
    Provide implementation of attribute to wrap tests in privilege drop
► rs/ sns/cli/BUILD.bazel
    Include privileges test utilities in test targets
Enhancement ► rs/ s sys/src/fs.rs
    Add test attribute to should_return_error_if_permission_is_denied using privileges
Enhancement ► rs/crypto/internal/crypto_service_provider/BUILD.bazel
    Add test dependency on privileges utilities
► rs/crypto/internal/crypto_service_provider/Cargo.toml
    Add ic-test-utilities-privileges dependency
► a number of test files updated to use ic_test_utilities_privileges attribute (e.g., should_panic_on_opening_corrupt_pubkey_store, should_fail_to_read_without_read_permissions, etc.)
Enhancement ► rs/state_manager/BUILD.bazel
    Add test utilities privileges to test deps
► rs/state_manager/Cargo.toml
    Add ic-test-utilities-privileges as dependency
► rs/state_manager/src/checkpoint/tests.rs
    Annotate with as_nobody_when_root for certain tests
Enhancement ► rs/ sns/cli/BUILD.bazel
    Add privileges test utility in test suite
► rs/ sns/cli/Cargo.toml
    Add ic-test-utilities-privileges dev-dependency
► rs/sns/cli/src/propose/propose_tests.rs
    Annotate with as_nobody_when_root for a test
Enhancement ► rs/sys/BUILD.bazel
    Add privileges test utility as dependency in sys tests
► rs/sys/Cargo.toml
    Add ic-test-utilities-privileges dev-dependency
► rs/sys/src/fs.rs
    Annotate tests with as_nobody_when_root to test permission-denied scenarios
Enhancement ► Cargo.lock
    Add ic-test-utilities-privileges and related crates as dependencies across multiple packages
► Cargo.toml updates across multiple crates to include privileges utilities
► ic-test-utilities-privileges and ic-test-utilities-privileges-macros crates added

@basvandijk basvandijk dismissed github-actions[bot]’s stale review July 2, 2026 12:11

No canister changes.

@zeropath-ai

zeropath-ai Bot commented Jul 2, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to e224f9f.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► Cargo.lock
    Add ic-test-utilities-privileges dependency to several crates
Enhancement ► Cargo.toml
    Include ic-test-utilities-privileges and related privileges paths in workspace members and dev-dependencies
Enhancement ► rs/crypto/internal/crypto_service_provider/BUILD.bazel
    Add ic-test-utilities-privileges as test dependency for crypto service provider tests
Enhancement ► rs/crypto/internal/crypto_service_provider/Cargo.toml
    Add ic-test-utilities-privileges as a dependency
Enhancement ► rs/crypto/internal/crypto_service_provider/src/public_key_store/proto_pubkey_store/tests.rs
    Annotate tests with ic_test_utilities_privileges::as_nobody_when_root attribute
Enhancement ► rs/crypto/internal/crypto_service_provider/src/secret_key_store/proto_store/tests.rs
    Annotate tests with ic_test_utilities_privileges::as_nobody_when_root attribute
Enhancement ► rs/state_manager/BUILD.bazel
    Add //rs/test_utilities/privileges as a test dependency
Enhancement ► rs/state_manager/Cargo.toml
    Add ic-test-utilities-privileges as a dependency
Enhancement ► rs/state_manager/src/checkpoint/tests.rs
    Annotate test with ic_test_utilities_privileges::as_nobody_when_root attribute
Enhancement ► rs/sys/BUILD.bazel
    Add privileges test utilities as a dependency for sys tests
Enhancement ► rs/sys/Cargo.toml
    Add ic-test-utilities-privileges as dev-dependency
Enhancement ► rs/sys/src/fs.rs
    Annotate test with ic_test_utilities_privileges::as_nobody_when_root attribute
Enhancement ► rs/sns/cli/BUILD.bazel
    Add privileges test utilities to dev dependencies for SNS CLI
Enhancement ► rs/sns/cli/Cargo.toml
    Add ic-test-utilities-privileges as dev-dependency
Enhancement ► rs/sns/cli/src/propose/propose_tests.rs
    Annotate tests with ic_test_utilities_privileges::as_nobody_when_root attribute
Enhancement ► rs/state_manager/src/checkpoint/tests.rs (duplicate entry shown as separate patch)
Enhancement ► rs/test_utilities/privileges/BUILD.bazel
    Add new privileges build for tests (new file)
Enhancement ► rs/test_utilities/privileges/Cargo.toml
    Add new ic-test-utilities-privileges package (new)
Enhancement ► rs/test_utilities/privileges/macros/BUILD.bazel
    Add macros for privileges (new)
Enhancement ► rs/test_utilities/privileges/macros/Cargo.toml
    Add macros crate (new)
Enhancement ► rs/test_utilities/privileges/macros/src/lib.rs
    Add proc-macro for as_nobody_when_root (new)
Enhancement ► rs/test_utilities/privileges/src/lib.rs
    Add privileges utilities and run_as_nobody_if_root implementation (new)
Enhancement ► rs/test_utilities/privileges/tests/smoke.rs
    Add smoke tests for privileges utilities (new)
Enhancement ► rs/test_utilities/privileges/tests/smoke.rs (attribute_form module updates)
    Add tests using as_nobody_when_root under attribute form (new)

Comment thread rs/sns/cli/src/propose/propose_tests.rs Outdated
Comment thread rs/sns/cli/src/propose/propose_tests.rs Outdated
Requested in review: each of the 12 wrapped tests now carries a short
comment explaining that root bypasses file permission bits
(CAP_DAC_OVERRIDE), so the permission denial the test expects only
happens when dropping to nobody on root-run workers (e.g. Bazel remote
execution).
Requested in review: wrapping whole test bodies in a closure pollutes them
with extra code and indentation. Add an attribute that does the wrapping
instead.

Proc-macros must live in their own crate, so the attribute is defined in the
new ic-test-utilities-privileges-macros crate and re-exported from
ic-test-utilities-privileges (the serde/serde_derive facade pattern, like
canlog re-exports canlog_derive); consumers keep a single dependency. The
attribute composes with #[test] and #[should_panic] (the wrapper re-raises
the child's panic message) and rejects async fns and non-unit return types
at compile time.

Convert all 12 call sites to the attribute form and extend the smoke tests
with attribute-form cases; verified as root, where the permission-denied
tests only pass because the attribute drops privileges to nobody.

@alin-at-dfinity alin-at-dfinity 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, thank you.

Comment thread rs/crypto/internal/crypto_service_provider/Cargo.toml
Comment thread rs/test_utilities/privileges/tests/smoke.rs
@basvandijk basvandijk added this pull request to the merge queue Jul 3, 2026
Merged via the queue into master with commit 01256d8 Jul 3, 2026
67 of 68 checks passed
@basvandijk basvandijk deleted the basvandijk/run-permission-denied-tests-as-nobody branch July 3, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants