Skip to content

Fix /tmp cleanup and verify absent parent ENOENT#212

Merged
jserv merged 1 commit into
sysprog21:mainfrom
open-sources-port:eloop
Jul 19, 2026
Merged

Fix /tmp cleanup and verify absent parent ENOENT#212
jserv merged 1 commit into
sysprog21:mainfrom
open-sources-port:eloop

Conversation

@doanbaotrung

@doanbaotrung doanbaotrung commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Fix /tmp cleanup and verify absent parent ENOENT

A preceding test left ${sysroot}/tmp as a symlink to host-out, causing
downstream creation tests to hit the symlink-rejecting directory
creation path and fail with ELOOP. We now restore the /tmp directory
at the end of the test.

Since containment checks are only run on paths verified to exist,
checks on absent intermediate directories only occur via a TOCTOU race.
The recursive parent resolution walk-up in sysroot_path_is_contained
has been reverted to a secure, fail-closed behavior to prevent any
fail-open vulnerabilities.

A deterministic test is added to verify that opening a path with a
non-existent parent returns ENOENT (and not ELOOP). The TOCTOU racer
is kept as a non-fatal stress check, asserting that no artifacts are
written outside of the sysroot mount.

Fix #187


Summary by cubic

Return ENOENT/ENOTDIR for paths with missing or non-directory components instead of ELOOP, and harden tests to prevent false ELOOPs. Fixes #187.

  • Bug Fixes
    • Fail-closed: only validate containment on paths that exist; remove parent-walk fallback to avoid fail-open behavior.
    • Tests: added deterministic ENOENT check for a missing parent; TOCTOU stress ensures no ELOOP and no artifacts outside the sysroot.
    • Cleanup: restore /tmp inside the sysroot after tests to remove a leftover symlink that caused spurious ELOOPs.

Written for commit 3066dd4. Summary will update on new commits.

Review in cubic

@doanbaotrung
doanbaotrung force-pushed the eloop branch 3 times, most recently from 860a5a9 to 33fc1df Compare July 17, 2026 04:22
cubic-dev-ai[bot]

This comment was marked as resolved.

Comment thread tests/test-sysroot-create-paths.c
@Max042004

Copy link
Copy Markdown
Collaborator

Looking at the callers, containment is invoked only after sysroot_path_exists(), after
access(parent), or after sysroot_ensure_dir_exists(parent). In a stable filesystem each guarantees
that the subsequent realpath() has an existing path to resolve. The new fallback appears reachable
only through a TOCTOU change between those operations and realpath().

The issue’s /etc/sysusers.d/basic.conf example also does not take this path today: when the
sysroot parent is absent and the path is outside the forced /tmp, /var/tmp, and .ccache routing
set, proc_resolve_sysroot_create_path() returns the literal host path before containment is
checked.

Please provide a concrete reproducer that reaches this fallback, or clarify that the change only
addresses a TOCTOU case and add a deterministic test for it. The current regression test should
also restore the /tmp fixture state.

@doanbaotrung

Copy link
Copy Markdown
Collaborator Author

Actions Completed

  1. Restored /tmp Fixture State: Cleaned up the leftover ${sysroot}/tmp symlink in test-sysroot-create-paths.c by unlinking it and recreating /tmp as a normal directory at the end of the symlink escape test (unlink_symlink_escape_done). This prevents downstream /tmp operations from hitting the symlink-rejecting creation code path and triggering unexpected ELOOP failures.
  2. Clarified and Tested TOCTOU Mitigation: As you correctly noted, under stable filesystem conditions the resolver callers guarantee existence before calling sysroot_path_is_contained, making realpath_existing_parent only reachable via a TOCTOU race (where a directory is removed/replaced in between the check and realpath).
    To verify this mitigation deterministically, we implemented a concurrent TOCTOU race test in test-sysroot-create-paths.c. The test spawns a child process that loops creating and deleting /tmp/race_dir while the parent attempts open(O_CREAT) on /tmp/race_dir/file.txt. The test asserts that the race eventually triggers ENOENT (due to the recursive parent resolution fallback) and never returns ELOOP (which would signify a containment check violation).

@doanbaotrung
doanbaotrung requested a review from Max042004 July 18, 2026 17:23

@jserv jserv 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.

The new walk-up in sysroot_path_is_contained fires only under the single-window TOCTOU that the module comment (proc-state.c:469) already declares out of scope, so the actionable issue is the fail-open regression noted inline, not the race itself. Two lower-priority items without their own anchor: str_copy_trunc in realpath_existing_parent (:448) silently truncates an over-long path and could authorize a different, shorter contained path — security code should fail closed with ENAMETOOLONG; and the test's rmdir(race_dir) cleanup can leave residue (ENOTEMPTY) if a race iteration created file.txt inside.

Comment thread src/syscall/proc-state.c Outdated
mounted_sysroot_root);
snprintf(race_file, sizeof(race_file), "/tmp/race_dir/file.txt");

pid_t pid = fork();

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.

fork()'s return isn't checked. On -1 the child branch is skipped and kill(pid, SIGKILL) becomes kill(-1, SIGKILL) — SIGKILL to every process the user can signal, then waitpid(-1, ...) reaps an arbitrary child. Add if (pid < 0) FAIL(...) before the split.

Comment thread tests/test-sysroot-create-paths.c Outdated

@jserv jserv 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.

Enforce rules specified in https://cbea.ms/git-commit/ .

@doanbaotrung
doanbaotrung force-pushed the eloop branch 2 times, most recently from c43e5ac to 3066dd4 Compare July 19, 2026 14:26
@doanbaotrung doanbaotrung changed the title sysroot permit ENOENT and ENOTDIR for containment checks on absent paths Fix /tmp cleanup and verify absent parent ENOENT Jul 19, 2026
A preceding test left ${sysroot}/tmp as a symlink to host-out, causing
downstream creation tests to hit the symlink-rejecting directory
creation path and fail with ELOOP. We now restore the /tmp directory
at the end of the test.

Since containment checks are only run on paths verified to exist,
checks on absent intermediate directories only occur via a TOCTOU race.
The recursive parent resolution walk-up in sysroot_path_is_contained
has been reverted to a secure, fail-closed behavior to prevent any
fail-open vulnerabilities.

A deterministic test is added to verify that opening a path with a
non-existent parent returns ENOENT (and not ELOOP). The TOCTOU racer
is kept as a non-fatal stress check, asserting that no artifacts are
written outside of the sysroot mount.

Fixes sysprog21#187
@doanbaotrung

Copy link
Copy Markdown
Collaborator Author

Updated commit message

@doanbaotrung

Copy link
Copy Markdown
Collaborator Author

1. Reverted to Fail-Closed containment behavior in proc-state.c

  • Completely removed the realpath_existing_parent helper function and its usage inside sysroot_path_is_contained.
  • Reverted the sysroot_path_is_contained implementation to standard fail-closed behavior (returning false immediately if any realpath call fails). This eliminates the vulnerability where intermediate path components could resolve incorrectly, potentially creating a fail-open escape if they were later replaced by symlinks pointing outside the sysroot.

2. Robust Test Process Fork Handling in test-sysroot-create-paths.c

  • Checked the return value of fork(). If pid < 0, we immediately fail the test with a clean error message rather than calling kill(-1, SIGKILL).

3. Deterministic ENOENT and Leak checks in test-sysroot-create-paths.c

  • Deterministic check: Added a test checking that opening /etc/nonexistent_parent_dir_123/sub/file.txt with O_CREAT returns ENOENT and not ELOOP deterministically. Since this path is outside of /tmp, no directory auto-creation is attempted, ensuring stable and reliable testing.
  • Non-fatal stress racer: The TOCTOU racer now runs up to 10,000 iterations without failing if it does not win the timing window to observe ENOENT, removing flakiness in CI.
  • Robust cleanup: Cleaned up guest and host paths inside the sysroot for race_file and race_dir to ensure the directory is empty and rmdir succeeds without ENOTEMPTY residues.
  • Leak verification: Added an assertion checking that no directories/files were created in /tmp/race_dir on the host outside of the sysroot mount.

@jserv
jserv merged commit a8a4c32 into sysprog21:main Jul 19, 2026
10 checks passed
@doanbaotrung
doanbaotrung deleted the eloop branch July 19, 2026 14:42
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.

Path containment checks fail with ELOOP on non-existent parent directories

3 participants