Repository navigation
Stop folding '..' across a usbfs node - #439
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.
| * 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.
There was a problem hiding this comment.
In this PR as well, in c9ab320: usb_sysfs_node_path folds the name with classify_and_normalize, and sys_openat_path carries on with the canonical spelling, which usbdev_open_path accepts and which names the descriptor, O_PATH included. open("/dev/bus/usb/001/../001/001", O_RDWR) returns a usbfs descriptor that /proc/self/fd names /dev/bus/usb/001/001; the absolute twin sits next to the relative case, with the O_PATH name and ENOTDIR for /dev/bus/usb/001/../001/001/. beside it. /dev/bus/other/../usb/001/001 takes the same fold. The EACCES arm in usb_sysfs_intercept_open is then unreachable from sys_openat_path, as its comment says.
/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 further '..' is applied to /dev and stops at /, so /dev/bus/../../proc/self/status reaches the /proc intercept. A '..' after any other name leaves the walk to the backing, which answers ENOTDIR after a file as Linux does. The rewrite runs before the subsystem link rewrite, which a climb that lands in /sys still needs. When the climb ends the name, the rewrite ends in '.', as in /dev/., so rmdir and rename fail rather than act on the directory reached, 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, and that a climb through a subsystem link reads a file under the sysroot's /sys.
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.
usbdev_open_path takes only the canonical /dev/bus/usb/BBB/DDD
spelling, and an absolute name keeps its '..' through path
translation. open("/dev/bus/usb/001/../001/001", O_RDWR) therefore
fell through to the intercepts, where usb_sysfs_intercept_open refuses
a writable open of a node with EACCES, and
/dev/bus/other/../usb/001/001 did the same on a sysroot with a
/dev/bus/other. Linux opens the node in both cases, and stat already
reports it as a character device.
usb_sysfs_node_path folds the name with classify_and_normalize, as the
stat intercept does, and sys_openat_path carries on with the canonical
spelling: usbdev_open_path accepts it, and it names the descriptor, an
O_PATH one included. A read-only open of such a name gets the
FD_USBDEV descriptor a canonical open gets, where it got the
descriptor blob usb_blob_fd serves, named by the spelling with '..' in
it. classify_and_normalize drops a final '.', so usb_sysfs_node_path
tests path_dir_required on the name as given, and one that ends in '.'
or a slash is left to the intercepts, which answer ENOTDIR for a node.
b0887e3 to
c9ab320
Compare
|
Thank @jotpalch for contributing! |
/dev/bus/usb/001/001/..answers as the bus directory where Linux answersENOTDIR, because a..after a usbfs node is folded away;/dev/bus/../nullanswersENOENT, because a..that climbs out of/dev/busreaches a backing that cannot apply it; and opening/dev/bus/usb/001/../001/001for writing fails withEACCESwhilestatreports a character device, because the usbfs open takes only the canonical spelling. 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
mainthese printdirectory,No such file or directoryandPermission denied. With this series the first two printNot a directoryandcharacter special file, as Linux does, and the third printsopened.Tested on macOS 15.6.1 (24G90) with the 15.5 SDK on an Apple M1 (MacBookAir10,1).
make checkpasses all 101 tests,test-thread-churnon the runner's re-run after a timeout, andmake test-matrix-elfuse-aarch64gives 298 passed, 1 failed and 10 skipped. The one failure istest-thread-churntiming out, whichmainshows on this machine too, inmake checkand in the matrix, and it passes when run alone.