-
Notifications
You must be signed in to change notification settings - Fork 18
Resolve /proc/self/fd/<n> for path-based syscalls #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| #include "syscall/fuse.h" | ||
| #include "proved/pathdepth.h" | ||
|
|
||
| #include "syscall/internal.h" /* fd_to_host_dup */ | ||
| #include "syscall/path.h" | ||
| #include "syscall/proc.h" | ||
|
|
||
|
|
@@ -200,6 +201,116 @@ static int path_check_relative_sysroot_containment(guest_fd_t dirfd, | |
| char *host_out, | ||
| size_t host_outsz); | ||
|
|
||
| int path_parse_proc_name(const char *name) | ||
| { | ||
| if (!name || !*name) | ||
| return -1; | ||
| /* Linux rejects a leading zero on any name longer than one character, so | ||
| * "0" names descriptor 0 but "00" and "03" name nothing. | ||
| */ | ||
| if (name[0] == '0' && name[1] != '\0') | ||
| return -1; | ||
|
|
||
| long n = 0; | ||
| for (const char *p = name; *p; p++) { | ||
| if (*p < '0' || *p > '9') | ||
| return -1; | ||
| n = n * 10 + (*p - '0'); | ||
| if (n > INT_MAX) | ||
| return -1; | ||
| } | ||
| return (int) n; | ||
| } | ||
|
|
||
| /* Resolve an absolute fd magic link to the host path guest fd <n> is open on. | ||
| * This accepts "/proc/self/fd/<n>", the equivalent spelling with this process's | ||
| * own pid, and the /dev aliases Linux exposes as symlinks to procfs. | ||
| * | ||
| * Linux makes that a magic symlink, so a path-based syscall against it acts on | ||
| * the file the descriptor holds. It is the standard way to reach a file through | ||
| * an fd when no f*() variant applies -- systemd's fchmod_opath() chmods | ||
| * /proc/self/fd/<n> precisely because fchmod() rejects O_PATH descriptors, and | ||
| * reads ENOENT there as "this fd is not valid" (reporting EBADF) rather than as | ||
| * a missing file. | ||
| * | ||
| * Returns 1 and fills out on success, 0 when the path is not that shape or the | ||
| * descriptor has no host path (a pipe, socket, or anonymous fd, where F_GETPATH | ||
| * fails and the caller's own /proc intercepts remain the right answer). | ||
| */ | ||
| static int resolve_fd_magiclink_host_path(const char *path, | ||
| char *out, | ||
| size_t outsz) | ||
| { | ||
| const char *rest = NULL; | ||
|
|
||
| if (strncmp(path, "/proc/", 6) == 0) { | ||
| rest = path + 6; | ||
| if (!strncmp(rest, "self/", 5)) { | ||
| rest += 5; | ||
| } else { | ||
| /* The pid component gets the same strict rules as the fd leaf: | ||
| * Linux resolves /proc/<pid> through name_to_int as well, so | ||
| * "/proc/+1234/fd/3" names nothing there even when 1234 is this | ||
| * process. A component too long for the buffer is not a pid either. | ||
| */ | ||
| const char *slash = strchr(rest, '/'); | ||
| if (!slash) | ||
| return 0; | ||
| char pid_name[16]; | ||
| if (path_component_copy(pid_name, sizeof(pid_name), rest, | ||
| (size_t) (slash - rest)) < 0) | ||
| return 0; | ||
| if (path_parse_proc_name(pid_name) != (int) proc_get_pid()) | ||
| return 0; | ||
| rest = slash + 1; | ||
| } | ||
|
|
||
| if (strncmp(rest, "fd/", 3) != 0) | ||
| return 0; | ||
| rest += 3; | ||
| } else if (strncmp(path, "/dev/fd/", 8) == 0) { | ||
| rest = path + 8; | ||
| } else if (!strcmp(path, "/dev/stdin")) { | ||
| rest = "0"; | ||
| } else if (!strcmp(path, "/dev/stdout")) { | ||
| rest = "1"; | ||
| } else if (!strcmp(path, "/dev/stderr")) { | ||
| rest = "2"; | ||
| } else { | ||
| return 0; | ||
| } | ||
|
|
||
| /* Only a bare descriptor number names the file itself. Anything trailing | ||
| * ("/proc/self/fd/3/x" or "/dev/fd/3/x") walks through it, which the host | ||
| * path cannot express here, and a leaf Linux would not accept as a procfs | ||
| * fd name is not this shape at all. | ||
| */ | ||
| int fd = path_parse_proc_name(rest); | ||
| if (fd < 0) | ||
| return 0; | ||
|
|
||
| /* Take a dup under fd_lock rather than the bare host fd: a sibling vCPU | ||
| * closing this slot between the lookup and F_GETPATH would leave the number | ||
| * free for the next open to claim, and the caller would then chmod or chown | ||
| * whatever file landed there. | ||
| */ | ||
| int host_fd = fd_to_host_dup(fd); | ||
| if (host_fd < 0) | ||
| return 0; | ||
|
|
||
| char resolved[MAXPATHLEN]; | ||
| int rc = fcntl(host_fd, F_GETPATH, resolved); | ||
| close(host_fd); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When the fd's file is renamed, unlinked, or replaced after Prompt for AI agentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please address this issue. |
||
| if (rc < 0) | ||
| return 0; | ||
|
|
||
| size_t len = strlen(resolved); | ||
| if (len >= outsz) | ||
| return 0; | ||
| memcpy(out, resolved, len + 1); | ||
| return 1; | ||
| } | ||
|
|
||
| int path_translate_at(guest_fd_t dirfd, | ||
| const char *path, | ||
| unsigned int flags, | ||
|
|
@@ -265,6 +376,39 @@ int path_translate_at(guest_fd_t dirfd, | |
| return 0; | ||
| } | ||
|
|
||
| /* Only host_path moves; guest_path and intercept_path keep the /proc | ||
| * spelling. open, stat and readlink never reach host_path for these paths: | ||
| * proc_intercept_open dups the descriptor, proc_intercept_stat fstats it, | ||
| * and proc_intercept_readlink reports its path, and none of the three fall | ||
| * through to the host on a fd magic link that names an open slot (a | ||
| * closed one fails as EBADF rather than falling through). What this changes | ||
| * is every other follow-style operation -- chmod, chown, utimensat, | ||
| * truncate, access -- which now acts on the file the descriptor holds, the | ||
| * way Linux does when it resolves the magic link. | ||
| * | ||
| * Returning before sysroot resolution is not a containment claim about the | ||
| * path: F_GETPATH reports where the descriptor's file actually lives, which | ||
| * is regularly outside the sysroot -- an emulated character device, a | ||
| * /dev/shm backing file, inherited stdio. Re-resolving one of those as a | ||
| * guest path would be wrong, since it is already a host path. Nothing is | ||
| * widened by it either: the guest holds the descriptor, so this reaches | ||
| * only what it could already reach through it. | ||
| * | ||
| * Follow-style only. Linux resolves the link for an operation that follows | ||
| * the final component and acts on the link itself otherwise, so a no-follow | ||
| * or create-style caller -- unlinkat, renameat, chmod with | ||
| * AT_SYMLINK_NOFOLLOW -- must not be handed the descriptor's 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| !(flags & (PATH_TR_NOFOLLOW | PATH_TR_CREATE)) && | ||
| resolve_fd_magiclink_host_path(tx->guest_path, tx->host_buf, | ||
| sizeof(tx->host_buf))) { | ||
| tx->host_path = tx->host_buf; | ||
| return 0; | ||
| } | ||
|
|
||
| unsigned int lookup_flags = flags; | ||
| if (path_has_trailing_slash(tx->guest_path)) | ||
| lookup_flags &= ~PATH_TR_NOFOLLOW; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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