Skip to content

No verification that setuid/setgid actually succeeded #32

Closed

Description

@pimlock

When the sandbox drops privileges before executing the child process, it calls setgid(group.gid) and then setuid(user.uid). The code propagates errors from these syscalls via .into_diagnostic()?, so if the kernel returns an error, the process will fail. However, there is no post-condition check that the effective uid and gid actually changed. Verifying the effective ids immediately after setuid/setgid would ensure that privileges were actually dropped before the child continues.

Source code:

  • In crates/navigator-sandbox/src/process.rs, drop_privileges (lines 302-365) calls nix::unistd::setgid(group.gid).into_diagnostic()? (line 358) and nix::unistd::setuid(user.uid).into_diagnostic()? (line 362). The function then returns Ok(()) without calling nix::unistd::geteuid() or nix::unistd::getegid() to confirm the effective ids match the intended values.
  • The same function is used from the process spawn path (e.g. lines 150-152 and 229-231 in the same file) and from the SSH shell handler in crates/navigator-sandbox/src/ssh.rs (line 554). None of the call sites perform a subsequent check that privileges were actually dropped.

Originally by @igurel on 2026-02-20T05:45:16.932-08:00

Activity

  1. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🔒 security-review-agent

    Security Review

    Determination: Legitimate concern

    Summary

    The drop_privileges function in crates/navigator-sandbox/src/process.rs (lines 352-416) calls setgid() and setuid() and propagates errors via ?, but does not verify the effective UID/GID after the calls. While the Linux kernel guarantees that a successful setuid()/setgid() changes the effective IDs, adding a post-condition check is a well-established defense-in-depth best practice (CWE-250) for security-critical privilege-dropping code.

    Severity Assessment

    • Impact: High — if privileges are not actually dropped, the child process runs as root inside the sandbox, enabling sandbox escape.
    • Exploitability: Very low under normal conditions. The kernel guarantees setuid()/setgid() success means the IDs changed. A failure would require a kernel bug. However, the cost of the check is negligible (two syscalls), making this a worthwhile hardening measure.
    • Affected components:
      • crates/navigator-sandbox/src/process.rs — drop_privileges() (line 352), called from pre_exec closures at lines 180 and 269
      • crates/navigator-sandbox/src/ssh.rs — line 1017, SSH shell handler

    Attack Scenario

    1. A hypothetical kernel bug or unusual security module interaction causes setuid()/setgid() to return success (0) without actually changing the effective UID/GID.
    2. The sandbox child process continues execution as root.
    3. The attacker's code running inside the sandbox operates with full root privileges, bypassing all privilege-based restrictions.

    Note: This scenario requires a kernel-level defect. No known current Linux kernel version exhibits this behavior. The value is purely defense-in-depth.

    Remediation Plan

    1. After the setgid() call on line 409, add a check: verify nix::unistd::getegid() == group.gid, and return an error if they don't match.
    2. After the setuid() call on line 412, add a check: verify nix::unistd::geteuid() == user.uid, and return an error if they don't match.
    3. Optionally, also verify that setuid(0) fails after dropping privileges (a common hardening pattern to confirm root cannot be re-acquired).

    Additional Notes

    • This is a low-risk hardening issue, not an active vulnerability. The existing error propagation via ? handles the primary failure mode (syscall returning an error).
    • The fix is minimal (~6 lines of code) with zero performance cost, making it a clear net-positive for security posture.
    • References: CWE-250: Execution with Unnecessary Privileges, CERT POS37-C
  2. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🏗️ build-plan

    Implementation Plan

    Issue type: fix
    Complexity: Low
    Confidence: High

    Summary

    The drop_privileges() function in the sandbox process module calls setgid()/setuid() and propagates kernel errors, but never verifies the effective UID/GID actually changed. This is a defense-in-depth hardening fix: after each syscall, verify the effective IDs match the intended values, and optionally confirm that root cannot be re-acquired. The change is confined to a single function (~10 lines added).

    Scope

    • crates/navigator-sandbox/src/process.rs: Add post-condition checks in drop_privileges() after setgid() and setuid() calls. Verify getegid() == group.gid and geteuid() == user.uid. Optionally verify setuid(Uid::from_raw(0)) fails when user privileges were dropped (confirms root cannot be re-acquired).

    Implementation Steps

    1. Add effective GID verification after setgid() (after line 409): Call nix::unistd::getegid() and compare against group.gid. Return a descriptive error if they don't match.
    2. Add effective UID verification after setuid() (after line 412): Call nix::unistd::geteuid() and compare against user.uid. Return a descriptive error if they don't match.
    3. Add root re-acquisition guard (after step 2, when user_name.is_some()): Attempt nix::unistd::setuid(Uid::from_raw(0)) and return an error if it succeeds — this confirms the process cannot escalate back to root.
    4. Add unit tests for drop_privileges(): Test the happy path (current user/group passes verification), and test that the function returns Ok(()) when no run_as_user/run_as_group is configured (early return path). Note: full privilege-drop testing requires root, so the verification logic can be tested via the error-path structure.
    5. Update architecture docs: Add a note in architecture/security-policy.md (around line 369) and architecture/sandbox.md (around line 956-959) mentioning the post-condition verification.

    Test Plan

    • Unit tests: Add tests in crates/navigator-sandbox/src/process.rs (inline #[cfg(test)] module) verifying: (1) drop_privileges returns Ok(()) when policy has no run_as_user/run_as_group (no-op path), (2) drop_privileges with current user/group succeeds and effective IDs match (non-root test environment). If running as non-root, test that specifying a different user returns the expected lookup error.
    • Integration tests: N/A — the function runs inside pre_exec (post-fork, pre-exec) which is inherently difficult to integration-test without root. The existing sandbox E2E tests exercise this path implicitly.
    • E2E tests: N/A — covered by existing sandbox spawn tests that exercise the full pre_exec chain.

    Risks & Open Questions

    • Async-signal-safety: geteuid(), getegid(), and setuid() are all async-signal-safe per POSIX, so they are safe to call in pre_exec context. No risk here.
    • Root re-acquisition check: The setuid(0) guard is a well-known hardening pattern (CERT POS37-C). If the process was not running as root to begin with, setuid(0) will fail regardless — the check is meaningful only when dropping from root, which is the primary sandbox use case.
    • Non-Linux platforms: The function is gated with #[cfg(unix)], and geteuid/getegid/setuid are available on all Unix platforms via nix. No platform risk.

    Documentation Impact

    • Update architecture/security-policy.md (~line 369) to mention post-condition verification of effective UID/GID.
    • Update architecture/sandbox.md (~line 956-959) in the drop_privileges() description to note the verification steps.

    Revision 1 — initial plan

  3. added a commit that references this issue on Mar 5, 2026
    6d25a5d
  4. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🏗️ build-from-issue-agent

    Implementation Complete

    PR: #132

    What was built

    Added defense-in-depth post-condition checks in drop_privileges() to verify effective UID/GID after setgid()/setuid(), and a root re-acquisition guard (setuid(0) must fail). This hardens the sandbox privilege-drop path per CWE-250 and CERT POS37-C.

    Tests

    • Unit: 5 tests added (drop_privileges_noop_when_no_user_or_group, drop_privileges_noop_when_empty_strings, drop_privileges_succeeds_for_current_user, drop_privileges_fails_for_nonexistent_user, drop_privileges_fails_for_nonexistent_group)

    Docs updated

    • architecture/security-policy.md — enforcement sequence updated with verification steps
    • architecture/sandbox.md — drop_privileges() description updated

    The issue will auto-close when the PR is merged.

  5. added a commit that references this issue on Mar 5, 2026
    29876c0
  6. added a commit that references this issue on Mar 16, 2026
    5e7a2f2
  7. added a commit that references this issue on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions