From 4a74e1d9b2cf86ecd53592242d7f8df7a4733ab7 Mon Sep 17 00:00:00 2001 From: Trung Date: Fri, 17 Jul 2026 10:42:17 +0700 Subject: [PATCH] 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. Fixes #187 --- src/syscall/proc-state.c | 1 - tests/test-sysroot-create-paths.c | 78 ++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/syscall/proc-state.c b/src/syscall/proc-state.c index 859a07c4..a1472a9a 100644 --- a/src/syscall/proc-state.c +++ b/src/syscall/proc-state.c @@ -441,7 +441,6 @@ bool proc_sysroot_casefold_enabled(void) pthread_mutex_unlock(&sysroot_lock); return enabled; } - /* Confirm @resolved_path canonicalizes inside @sysroot. This is a * check-then-use sequence: callers issue the actual syscall after this returns, * so a symlink swapped in between will not be re-validated. openat2 diff --git a/tests/test-sysroot-create-paths.c b/tests/test-sysroot-create-paths.c index 6aecb6ad..4a386a12 100644 --- a/tests/test-sysroot-create-paths.c +++ b/tests/test-sysroot-create-paths.c @@ -7,10 +7,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -293,7 +295,81 @@ int main(int argc, char **argv) PASS(); } } - unlink_symlink_escape_done:; + unlink_symlink_escape_done: + unlink(sysroot_tmp); + mkdir(sysroot_tmp, 0755); + } + + TEST("open file in non-existent parent returns ENOENT, not ELOOP"); + { + int fd = open("/etc/nonexistent_parent_dir_123/sub/file.txt", + O_CREAT | O_WRONLY, 0644); + EXPECT_ERRNO(fd, ENOENT, + "open on non-existent parent failed to return ENOENT"); + if (fd >= 0) + close(fd); + } + + TEST("open file in non-existent parent TOCTOU race stress pass"); + { + char race_dir[512]; + char race_file[512]; + snprintf(race_dir, sizeof(race_dir), "%s/tmp/race_dir", + mounted_sysroot_root); + snprintf(race_file, sizeof(race_file), "/tmp/race_dir/file.txt"); + + pid_t pid = fork(); + if (pid < 0) + FAIL("fork failed"); + else if (pid == 0) { + /* Child process: continuously create and delete the directory */ + while (1) { + mkdir(race_dir, 0755); + rmdir(race_dir); + } + _exit(0); + } + + int hit_eloop = 0; + int iterations = 0; + while (iterations < 10000) { + iterations++; + int fd = open("/tmp/race_dir/file.txt", O_CREAT | O_WRONLY, 0644); + if (fd >= 0) { + close(fd); + unlink("/tmp/race_dir/file.txt"); + } else if (errno == ELOOP) { + hit_eloop = 1; + break; + } + } + + kill(pid, SIGKILL); + int status; + waitpid(pid, &status, 0); + + /* Cleanup guest paths */ + unlink("/tmp/race_dir/file.txt"); + rmdir("/tmp/race_dir"); + + /* Cleanup host paths inside sysroot */ + char host_race_file[512], host_race_dir[512]; + snprintf(host_race_file, sizeof(host_race_file), + "%s/tmp/race_dir/file.txt", mounted_sysroot_root); + snprintf(host_race_dir, sizeof(host_race_dir), "%s/tmp/race_dir", + mounted_sysroot_root); + unlink(host_race_file); + rmdir(host_race_dir); + + /* Assert that no artifact exists outside mounted_sysroot_root on the + * host */ + struct stat host_st; + if (stat("/tmp/race_dir", &host_st) == 0) + FAIL("TOCTOU race created directory outside of sysroot"); + else if (hit_eloop) + FAIL("TOCTOU race returned ELOOP"); + else + PASS(); } SUMMARY("test-sysroot-create-paths");