Repository navigation
Conversation
resolve_proc_dirfd_path, resolve_proc_cwd_path and path_rebase_hostdirfd each seed an absolute guest path from their base directory and apply the relative name's components to it. path_rebuild_under now does it for all three, so a change to how the join treats /dev/bus is made once. What any of them returns is unchanged.
A descriptor or cwd on a synthetic /dev/bus directory takes the guest's
spelling as its name, so chdir("/dev/bus/usb/001/..") leaves getcwd
reporting /dev/bus/usb/001/.., and /proc/self/fd/N repeats any '..' or
trailing slash the open was spelled with. Linux names the directory the
walk reached. usb_sysfs_guest_path_for_fd already asks a /sys
descriptor what it holds, for the open stamp and for chdir; it now
answers for the /dev/bus scratch tree the same way.
/dev/bus/../null answers ENOENT where Linux resolves /dev/null: the name leaves the layer, so it goes to the backing as written, and the backing cannot apply the '..'. The directories it climbs are the layer's, and the macOS host has no /dev/bus at all. path_translate_at now rewrites a /dev/bus name that climbs out over directories the layer serves, /dev/bus/usb and the bus directories that exist, to /dev followed by what comes after the climbing '..'. A '..' after any other name leaves the walk to the backing, which answers ENOTDIR after a file as Linux does. A climb that ends the name becomes /dev/., so rmdir and rename fail rather than act on /dev, with EINVAL on a sysroot where Linux gives ENOTEMPTY and EBUSY for a final '..'. The dev-climb and dev-climb-f matrix columns are recorded on Linux 7.0, where every existing cell records unchanged. Without the rewrite dev-climb fails on the 16 rows that spell the name in full, and without the usb_dev_names_dir check dev-climb-f fails on 16. test-usb-sysfs-sysroot gives its sysroot an empty /dev and checks that rmdir and rename of /dev/bus/.. leave it there.
classify_and_normalize folds a /dev/bus name lexically before deciding whose it is, so /dev/bus/usb/001/001/.. becomes /dev/bus/usb/001 and every entry point answers with the bus directory. Linux applies '..' only to a directory, and the node is a character device, so the walk fails there with ENOTDIR. The fold now stops at a '..' after a usbfs node and keeps the rest as written, which classify_path answers as USB_PATH_DEV_NODE_SUB. path_rebuild_under stops the same way for a name relative to a synthetic cwd or descriptor, or retried by openat from a host directory, and folds every other '..' as before, so a node opened as ../001/001 still gets a usbfs descriptor. statfs asks the /dev/bus layer before its lexical /sys fold, which would otherwise carry /dev/bus/usb/001/001/../../../../../sys to sysfs. The node-dotdot matrix column is recorded on Linux 7.0; without the stop it fails on all 16 cells that can name the path. The classify_and_normalize comment and the vectors header justified the lexical fold by every /dev/bus component being a plain directory; a node is not one, and the claim is gone from both. The vectors header paragraphs around it drop a measurement against an old build as well.
There was a problem hiding this comment.
3 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/runtime/usb-sysfs.c">
<violation number="1" location="src/runtime/usb-sysfs.c:1285">
P2: An absent bus number makes this helper leave the climb to the backing, even though `/dev/bus/usb` is synthetic and hides backing entries. With a backing `/dev/bus/usb/099`, `/dev/bus/usb/099/../../../null` can resolve to `/dev/null` instead of failing `ENOENT` at `099`; preserve the missing-path result rather than falling through.</violation>
</file>
<file name="tests/test-usb-sysfs-matrix.c">
<violation number="1" location="tests/test-usb-sysfs-matrix.c:179">
P2: `COL_CLIMB` never verifies that the path reaches `/dev`: its rows assert only success or directory type, and `r_chdir` immediately resets cwd to `/`. Compare the resolved inode with `/dev` and check `getcwd()` before resetting cwd.</violation>
</file>
<file name="src/syscall/path.c">
<violation number="1" location="src/syscall/path.c:681">
P2: The loop stops after the first climb when the remaining `..` components spell the path as `/dev/../dev/bus`. That path resolves back to the synthetic `/dev/bus` directory, but the USB intercept misses it and host fallback can return `ENOENT`; normalize the rewritten remainder before deciding whether to repeat the climb.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| int bus = 0; | ||
| usb_path_kind_t kind = usb_dev_kind(folded, &bus); | ||
| return kind == USB_PATH_DEV_USB || | ||
| (kind == USB_PATH_DEV_BUSNUM && bus_exists(bus)); |
There was a problem hiding this comment.
P2: An absent bus number makes this helper leave the climb to the backing, even though /dev/bus/usb is synthetic and hides backing entries. With a backing /dev/bus/usb/099, /dev/bus/usb/099/../../../null can resolve to /dev/null instead of failing ENOENT at 099; preserve the missing-path result rather than falling through.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/runtime/usb-sysfs.c, line 1285:
<comment>An absent bus number makes this helper leave the climb to the backing, even though `/dev/bus/usb` is synthetic and hides backing entries. With a backing `/dev/bus/usb/099`, `/dev/bus/usb/099/../../../null` can resolve to `/dev/null` instead of failing `ENOENT` at `099`; preserve the missing-path result rather than falling through.</comment>
<file context>
@@ -1244,28 +1254,55 @@ static bool bus_exists(int busnum)
+ int bus = 0;
+ usb_path_kind_t kind = usb_dev_kind(folded, &bus);
+ return kind == USB_PATH_DEV_USB ||
+ (kind == USB_PATH_DEV_BUSNUM && bus_exists(bus));
+}
+
</file context>
| case COL_SYS_FOLD_IN: | ||
| return "/sys/class/../bus/usb/devices"; | ||
| case COL_CLIMB: | ||
| return "/dev/bus/usb/001/../../.."; |
There was a problem hiding this comment.
P2: COL_CLIMB never verifies that the path reaches /dev: its rows assert only success or directory type, and r_chdir immediately resets cwd to /. Compare the resolved inode with /dev and check getcwd() before resetting cwd.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test-usb-sysfs-matrix.c, line 179:
<comment>`COL_CLIMB` never verifies that the path reaches `/dev`: its rows assert only success or directory type, and `r_chdir` immediately resets cwd to `/`. Compare the resolved inode with `/dev` and check `getcwd()` before resetting cwd.</comment>
<file context>
@@ -171,6 +175,12 @@ static const char *col_path(int c)
case COL_SYS_FOLD_IN:
return "/sys/class/../bus/usb/devices";
+ case COL_CLIMB:
+ return "/dev/bus/usb/001/../../..";
+ case COL_CLIMB_FILE:
+ return "/dev/bus/other/f/../../../null";
</file context>
| * out of it. What follows can climb out again, as in | ||
| * /dev/bus/../bus/../null, and each pass shortens the name. | ||
| */ | ||
| while (path_prefix_match(tx->guest_path, "/dev/bus", 8)) { |
There was a problem hiding this comment.
P2: The loop stops after the first climb when the remaining .. components spell the path as /dev/../dev/bus. That path resolves back to the synthetic /dev/bus directory, but the USB intercept misses it and host fallback can return ENOENT; normalize the rewritten remainder before deciding whether to repeat the climb.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/syscall/path.c, line 681:
<comment>The loop stops after the first climb when the remaining `..` components spell the path as `/dev/../dev/bus`. That path resolves back to the synthetic `/dev/bus` directory, but the USB intercept misses it and host fallback can return `ENOENT`; normalize the rewritten remainder before deciding whether to repeat the climb.</comment>
<file context>
@@ -674,6 +674,20 @@ int path_translate_at(guest_fd_t dirfd,
+ * out of it. What follows can climb out again, as in
+ * /dev/bus/../bus/../null, and each pass shortens the name.
+ */
+ while (path_prefix_match(tx->guest_path, "/dev/bus", 8)) {
+ char resolved[LINUX_PATH_MAX];
+ if (!usb_sysfs_resolve_dev_climb(tx->guest_path, resolved,
</file context>
| * rather than act on /dev itself, with EINVAL on a sysroot where Linux | ||
| * gives ENOTEMPTY and EBUSY for a final '..'. | ||
| */ | ||
| int n = snprintf(out, outsz, "/dev/%s", *rest ? rest : "."); |
There was a problem hiding this comment.
When the name climbs more than once, the rest still starts with .., and the rewrite hands the host /dev/../... without folding it. /dev/bus/usb/001/../../../../sys/bus/usb/devices becomes /dev/../sys/bus/usb/devices, and /dev/bus/../../proc/self/status becomes /dev/../proc/self/status. No /sys or /proc intercept matches those spellings, and the /sys/bus/usb/devices/ subsystem rewrite in path_translate_at runs before this loop, so both return ENOENT where Linux resolves them. The relative form of the same walk (openat(dfd, "../../../../sys/bus/usb/devices"), which the new test covers) works, because path_rebuild_under folds it all. A follow-up could have this function also consume each leading ../ of rest against /dev (clamping at /), and move the climb loop above the subsystem rewrite so a climb that lands in /sys still gets resolved.
| * usbdev_open_path, which takes only the canonical spelling, gets it. | ||
| */ | ||
| TEST("\"../001/001\" against a /dev/bus/usb/001 dirfd opens read-write"); | ||
| int fd = openat(dfd, "../001/001", O_RDWR); |
There was a problem hiding this comment.
This open works only because the name is relative and path_rebuild_under folds it. The absolute spelling of the same node, open("/dev/bus/usb/001/../001/001", O_RDWR), is not folded by path_translate_at (the fold returns 1, so the climb rewrite does not fire). usbdev_parse_node rejects anything but the exact canonical spelling, so the open falls through to proc_intercept_open, whose USB_PATH_DEV_NODE arm returns EACCES from the path its comment calls unreachable. /dev/bus/other/../usb/001/001 (the dev-fold-in column) fails the same way. Linux opens the node in both cases. A follow-up could pass usbdev_open_path the dev-mode folded spelling from usb_suffix_normalize, and add the absolute twin of this case next to it.
/dev/bus/usb/001/001/..answers as the bus directory where Linux answersENOTDIR, because a..after a usbfs node is folded away, and/dev/bus/../nullanswersENOENT, because a..that climbs out of/dev/busreaches a backing that cannot apply it. The first commit gives the relative-name join one home without changing behavior, and the second makes a/dev/busdescriptor and cwd report the directory reached rather than a spelling with..in it.Reproduce with the fixture devices and a static aarch64 busybox:
On main these print
directoryandNo such file or directory. With this series they printNot a directoryandcharacter special file, as Linux does.Tested on macOS 15.6.1 (24G90) with the 15.5 SDK on an Apple M1 (MacBookAir10,1).
make checkpasses 100 of 101 andmake test-matrix-elfuse-aarch64gives 298 passed, 1 failed and 10 skipped. Main gives the same on this machine: the one failure in both istest-thread-churntiming out, and it passes when run alone.Summary by cubic
Stops folding
..after a usbfs node and rewrites..climbs out of/dev/bus, so names like/dev/bus/usb/001/001/..and/dev/bus/../nullanswer as Linux does (ENOTDIRand/dev/null, respectively) instead of resolving to the wrong directory orENOENT...after a usbfs node now stops the fold, keeping the rest as written, so every entry point answersENOTDIRas Linux does...that climbs out of/dev/busover the layer's directories is rewritten inpath_translate_atto/devplus what follows, so/dev/bus/../nullresolves; a climb that ends the name becomes/dev/., keepingrmdirandrenamefrom acting on/dev./dev/busdirectory now report the directory reached, not the guest's spelling with..or a trailing slash./dev/buscwd or dirfd fold components up to a usbfs node, which the earlier relative-name join refactor (path_rebuild_under) makes a single code path.statfsasks the/dev/buslayer before the/sysfold so a..through a node answersENOTDIR.dev-climb,dev-climb-f, andnode-dotdotcolumns, recorded against Linux 7.0;test-usb-sysfs-sysrootpins thatrmdirandrenameof a final..leave the sysroot's/devin place.Written for commit b0887e3. Summary will update on new commits.