Resolve /proc/self/fd/<n> for path-based syscalls - #289
Conversation
a6696d8 to
8cbe7b6
Compare
8cbe7b6 to
e6973f5
Compare
There was a problem hiding this comment.
2 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/syscall/path.c">
<violation number="1" location="src/syscall/path.c:258">
P1: When the fd's file is renamed, unlinked, or replaced after `F_GETPATH` returns, the later path-based syscall uses a stale pathname and can fail with `ENOENT` or modify a different inode instead of the file held by the fd. Keep the duplicated descriptor alive through the operation or use an identity-preserving fd-backed operation.</violation>
<violation number="2" location="src/syscall/path.c:348">
P2: This early return rewrites host_path for every follow-style absolute /proc/self/fd/<n> translation, not just the chmod/chown/utimensat family the PR targets. When the /proc open intercept does not serve the path (sys_openat_path falls through to open(tx.host_path) at fs.c:573), open/stat behavior changes from ENOENT to acting on the resolved file, contradicting the stated invariant that open/stat/readlink stay unchanged. Verify the /proc intercept always shadows these paths before relying on host_path-only scoping.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| char resolved[MAXPATHLEN]; | ||
| int rc = fcntl(host_fd, F_GETPATH, resolved); | ||
| close(host_fd); |
There was a problem hiding this comment.
P1: When the fd's file is renamed, unlinked, or replaced after F_GETPATH returns, the later path-based syscall uses a stale pathname and can fail with ENOENT or modify a different inode instead of the file held by the fd. Keep the duplicated descriptor alive through the operation or use an identity-preserving fd-backed operation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/path.c, line 258:
<comment>When the fd's file is renamed, unlinked, or replaced after `F_GETPATH` returns, the later path-based syscall uses a stale pathname and can fail with `ENOENT` or modify a different inode instead of the file held by the fd. Keep the duplicated descriptor alive through the operation or use an identity-preserving fd-backed operation.</comment>
<file context>
@@ -200,6 +201,71 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd,
+
+ char resolved[MAXPATHLEN];
+ int rc = fcntl(host_fd, F_GETPATH, resolved);
+ close(host_fd);
+ if (rc < 0)
+ return 0;
</file context>
| * file, or unlinkat("/proc/self/fd/<n>") would delete it instead of failing | ||
| * on the /proc entry. | ||
| */ | ||
| if (tx->guest_path[0] == '/' && |
There was a problem hiding this comment.
P2: This early return rewrites host_path for every follow-style absolute /proc/self/fd/ translation, not just the chmod/chown/utimensat family the PR targets. When the /proc open intercept does not serve the path (sys_openat_path falls through to open(tx.host_path) at fs.c:573), open/stat behavior changes from ENOENT to acting on the resolved file, contradicting the stated invariant that open/stat/readlink stay unchanged. Verify the /proc intercept always shadows these paths before relying on host_path-only scoping.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/path.c, line 348:
<comment>This early return rewrites host_path for every follow-style absolute /proc/self/fd/<n> translation, not just the chmod/chown/utimensat family the PR targets. When the /proc open intercept does not serve the path (sys_openat_path falls through to open(tx.host_path) at fs.c:573), open/stat behavior changes from ENOENT to acting on the resolved file, contradicting the stated invariant that open/stat/readlink stay unchanged. Verify the /proc intercept always shadows these paths before relying on host_path-only scoping.</comment>
<file context>
@@ -265,6 +331,28 @@ int path_translate_at(guest_fd_t dirfd,
+ * file, or unlinkat("/proc/self/fd/<n>") would delete it instead of failing
+ * on the /proc entry.
+ */
+ if (tx->guest_path[0] == '/' &&
+ !(flags & (PATH_TR_NOFOLLOW | PATH_TR_CREATE)) &&
+ resolve_proc_fd_host_path(tx->guest_path, tx->host_buf,
</file context>
1f96445 to
2349355
Compare
jserv
left a comment
There was a problem hiding this comment.
Rebase latest main branch and resolve conflicts.
2349355 to
dacd796
Compare
dacd796 to
76783f1
Compare
|
You're right and the sentence was simply false. I'd also been using it as an implied containment argument, which it can't carry. The real reasons are different, and I've split them out: The early return is right for a mechanical reason (the string is already a host path, so guest→host mapping doesn't apply to it), and containment rests on the descriptor, not the path.
|
| name | open | stat | chmod | /dev/fd |
|---|---|---|---|---|
| 9 | reads file | ok | ok | ok |
| +9 | rejected | rejected | rejected | — |
| 09 | rejected | rejected | rejected | rejected |
| -9, " 9", 9x | rejected | — | — | — |
One divergence I did not fix
Rejected names now report EBADF where Linux reports ENOENT — on Linux these are dcache lookups, so a malformed or closed fd name is always ENOENT, never EBADF. That comes from the errno_on_invalid argument at the four proc_parse_fd_index() call sites, two of which already pass ENOENT and two EBADF:
procemu.c:924 EBADF procemu.c:2768 ENOENT
procemu.c:3212 ENOENT procemu.c:3265 EBADF
Unifying those on ENOENT would match Linux for both the malformed-name and closed-slot cases, but it changes behaviour for callers beyond this PR's subject, so I left it. Say the word and I'll do it here or file it separately.
There was a problem hiding this comment.
1 existing issue remains and 1 new issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/syscall/path.c">
<violation number="1" location="src/syscall/path.c:246">
P3: The /proc magic-link shape is matched with exact strncmp against fixed prefixes, so non-canonical spellings that Linux normalizes — /proc/self//fd/3, /proc//self/fd/3, /proc/self/fd//3 — are not recognized and fall through to generic resolution, which fails on the host with ENOENT. This only affects redundant-separator spellings (glibc usually normalizes before syscalls, but raw syscalls can pass them), so it is low impact, but the fix leaves those spellings broken while the canonical form works.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Re-trigger cubic
| { | ||
| const char *rest = NULL; | ||
|
|
||
| if (strncmp(path, "/proc/", 6) == 0) { |
There was a problem hiding this comment.
P3: The /proc magic-link shape is matched with exact strncmp against fixed prefixes, so non-canonical spellings that Linux normalizes — /proc/self//fd/3, /proc//self/fd/3, /proc/self/fd//3 — are not recognized and fall through to generic resolution, which fails on the host with ENOENT. This only affects redundant-separator spellings (glibc usually normalizes before syscalls, but raw syscalls can pass them), so it is low impact, but the fix leaves those spellings broken while the canonical form works.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/path.c, line 246:
<comment>The /proc magic-link shape is matched with exact strncmp against fixed prefixes, so non-canonical spellings that Linux normalizes — /proc/self//fd/3, /proc//self/fd/3, /proc/self/fd//3 — are not recognized and fall through to generic resolution, which fails on the host with ENOENT. This only affects redundant-separator spellings (glibc usually normalizes before syscalls, but raw syscalls can pass them), so it is low impact, but the fix leaves those spellings broken while the canonical form works.</comment>
<file context>
@@ -200,6 +201,106 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd,
+{
+ const char *rest = NULL;
+
+ if (strncmp(path, "/proc/", 6) == 0) {
+ rest = path + 6;
+ if (!strncmp(rest, "self/", 5)) {
</file context>
resolve_proc_at_path() returns early for absolute paths, so path_translate_at never resolved /proc/self/fd/<n> and handed the sysroot spelling to the host. open(), stat() and readlink() were unaffected -- each has its own /proc intercept -- but chmod, chown and utimensat got ENOENT for a descriptor that was perfectly valid. On Linux that path is a magic symlink, and it is the standard way to reach a file through an fd when no f*() variant applies. systemd's fchmod_opath() uses it because fchmod() rejects O_PATH descriptors, and reads ENOENT there as "this fd is not valid", reporting EBADF: Failed to copy permissions from /etc/group to /etc/.#group...: Bad file descriptor which stopped systemd-sysusers from writing /etc/group and failed `dpkg --configure systemd`. Map the path to the host path the descriptor holds. Only host_path is rewritten; guest_path and intercept_path keep the /proc spelling so the existing intercepts still match. A descriptor with no host path (pipe, socket, anonymous fd) and a path walking through the number are both left alone.
76783f1 to
fa662bf
Compare
resolve_proc_at_path() returns early for absolute paths, so path_translate_at never resolved /proc/self/fd/ and handed the sysroot spelling to the host. open(), stat() and readlink() were unaffected -- each has its own /proc intercept -- but chmod, chown and utimensat got ENOENT for a descriptor that was perfectly valid.
On Linux that path is a magic symlink, and it is the standard way to reach a file through an fd when no f*() variant applies. systemd's fchmod_opath() uses it because fchmod() rejects O_PATH descriptors, and reads ENOENT there as "this fd is not valid", reporting EBADF:
Failed to copy permissions from /etc/group to /etc/.#group...:
Bad file descriptor
which stopped systemd-sysusers from writing /etc/group and failed
dpkg --configure systemd.Map the path to the host path the descriptor holds. Only host_path is rewritten; guest_path and intercept_path keep the /proc spelling so the existing intercepts still match. A descriptor with no host path (pipe, socket, anonymous fd) and a path walking through the number are both left alone.
Summary by cubic
Resolve absolute fd magic links like /proc/self/fd/, /proc//fd/, /dev/fd/, and /dev/std{in,out,err} to the underlying host file for follow-style path-based syscalls, matching Linux. Previously we forwarded the sysroot spelling, so chmod/chown/utimensat/truncate/access on valid O_PATH fds returned ENOENT and surfaced as EBADF (e.g., systemd’s fchmod_opath); now those calls act on the descriptor’s file.
Written for commit fa662bf. Summary will update on new commits.