Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1683,12 +1683,8 @@ path is built by `usb_sys_resolve_suffix`, which resolves symlinks and applies
each `..` to what the previous component resolved to, as Linux does, so
`<dev>/subsystem/..` names `/sys/bus`. On `/dev/bus` a `..` after a usbfs node
is not applied, since Linux applies `..` only to a directory, and the name
answers `ENOTDIR` once the node exists. A `..` after any other name is applied
without checking that the name exists, so `/dev/bus/usb/099/../001/001` is
served where Linux answers `ENOENT`. A relative name is folded the same way up
to a usbfs node, so `099/../../../null` from a `/dev/bus/usb` cwd skips the
check the climb rewrite makes for the absolute spelling. Deciding ownership on
the unfolded spelling splits the two halves apart in both directions:
answers `ENOTDIR` once the node exists. Deciding ownership on the unfolded
spelling splits the two halves apart in both directions:
`/dev/bus/usb/../other/f` reads as a malformed device number and is claimed,
and `/dev/bus/other/../usb/001/002` reads as a foreign bus and is disowned.
A suffix that folds away above its own root leaves as `USB_PATH_NONE`.
Expand All @@ -1703,6 +1699,20 @@ 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 `..`.

Every name under `/dev/bus/usb` is the layer's, so a `..` after a bus no device
has, or after a malformed bus or device number, fails the walk with `ENOENT`, as
Linux fails it at the missing name. The path layer refuses
`/dev/bus/usb/099/../001/001` in the same step that rewrites a climb, before any
intercept sees it, and a relative name is folded only up to a usbfs node or such
a name, so `099/../001/001` from a `/dev/bus/usb` cwd is refused the same way. A
`..` after a name shaped like a node for a device that does not exist is left to
the intercepts, which answer `ENOENT`. A `..` after a `USB_PATH_DEV_FOREIGN`
name is applied without checking that the backing has it, so
`/dev/bus/nosuch/../usb/001/001` is served where Linux answers `ENOENT`. In an
absolute name the rest is then folded unchecked as well, so
`/dev/bus/other/../usb/099/../001/001` is served too, while the same name
relative to a `/dev/bus` cwd is `ENOENT`.

### Filesystem Identity Of A Descriptor

Two questions have one answer here: what `statfs` reports for a name, and what
Expand Down
50 changes: 34 additions & 16 deletions src/runtime/usb-sysfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1295,8 +1295,9 @@ static bool usb_dev_names_dir(const char *folded)
* Returns 1 with @out filled, 0 when the walk climbs above the root, -1 when
* the result does not fit. With @rest, under usb_lock after ensure_usb_tree(),
* a '..' pops only a directory usb_dev_names_dir accepts, and *@rest is left on
* what follows a climbing '..', or NULL when a '..' meets any other name.
* Without @rest no lock is held, and any other name is popped unchecked.
* what follows a climbing '..'; a '..' that meets any other name returns 0 with
* *@rest NULL and that name in @out. Without @rest no lock is held, and any
* other name is popped unchecked.
*/
static int usb_suffix_normalize(const char *suffix,
bool dev,
Expand Down Expand Up @@ -2327,12 +2328,21 @@ int usb_sysfs_resolve_guest_path(const char *guest_path,
return rc;
}

bool usb_sysfs_names_node(const char *guest_path)
bool usb_sysfs_stops_fold(const char *guest_path)
{
if (!path_prefix_match(guest_path, "/dev/bus/usb", 12))
return false;
int bus = 0, dev = 0;
const char *sfx = "";
return path_prefix_match(guest_path, "/dev/bus/usb", 12) &&
classify_path(guest_path, &bus, &dev, &sfx) == USB_PATH_DEV_NODE;
usb_path_kind_t kind = classify_path(guest_path, &bus, &dev, &sfx);
if (kind == USB_PATH_DEV_USB)
return false;
if (kind != USB_PATH_DEV_BUSNUM)
return true;
pthread_mutex_lock(&usb_lock);
bool served = ensure_usb_tree() == 0 && bus_exists(bus);
pthread_mutex_unlock(&usb_lock);
return !served;
}

bool usb_sysfs_node_path(const char *guest_path, char *out, size_t outsz)
Expand All @@ -2345,23 +2355,31 @@ bool usb_sysfs_node_path(const char *guest_path, char *out, size_t outsz)
&cerr) == USB_PATH_DEV_NODE;
}

bool usb_sysfs_resolve_dev_climb(const char *guest_path,
char *out,
size_t outsz)
int usb_sysfs_resolve_dev_climb(const char *guest_path, char *out, size_t outsz)
{
if (!path_prefix_match(guest_path, "/dev/bus", 8) ||
!strstr(guest_path, ".."))
return false;
return 0;
char folded[LINUX_PATH_MAX];
const char *rest = NULL;
int frc = -1;
pthread_mutex_lock(&usb_lock);
bool climbs = ensure_usb_tree() == 0 &&
usb_suffix_normalize(skip_slashes(guest_path + 8), true,
folded, sizeof(folded), &rest) == 0 &&
rest;
if (ensure_usb_tree() == 0)
frc = usb_suffix_normalize(skip_slashes(guest_path + 8), true, folded,
sizeof(folded), &rest);
pthread_mutex_unlock(&usb_lock);
if (!climbs)
return false;
if (frc != 0)
return 0;

/* The '..' met a name that is not a directory the layer serves. */
if (!rest) {
int bus = 0;
usb_path_kind_t kind = usb_dev_kind(folded, &bus);
if (kind != USB_PATH_DEV_BUSNUM && kind != USB_PATH_DEV_ABSENT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: /dev/bus/other/../usb/099/../001/001 hits this FOREIGN early return, so 099/.. is later folded unchecked and a valid 001/001 node is served instead of ENOENT. Continue validating the walk after the foreign climb so missing names reached under /dev/bus/usb still fail.

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 2378:

<comment>`/dev/bus/other/../usb/099/../001/001` hits this `FOREIGN` early return, so `099/..` is later folded unchecked and a valid `001/001` node is served instead of `ENOENT`. Continue validating the walk after the foreign climb so missing names reached under `/dev/bus/usb` still fail.</comment>

<file context>
@@ -2345,23 +2355,31 @@ bool usb_sysfs_node_path(const char *guest_path, char *out, size_t outsz)
+    if (!rest) {
+        int bus = 0;
+        usb_path_kind_t kind = usb_dev_kind(folded, &bus);
+        if (kind != USB_PATH_DEV_BUSNUM && kind != USB_PATH_DEV_ABSENT)
+            return 0;
+        errno = ENOENT;
</file context>

return 0;
errno = ENOENT;
return -1;
}

/* A further '..' climbs /dev, and one past it stays at the root as on
* Linux; left in the name, it reaches the backing as /dev/.., where no
Expand All @@ -2379,7 +2397,7 @@ bool usb_sysfs_resolve_dev_climb(const char *guest_path,
* sysroot where Linux gives ENOTEMPTY and EBUSY for a final '..'.
*/
int n = snprintf(out, outsz, "%s/%s", dir, *rest ? rest : ".");
return n > 0 && (size_t) n < outsz;
return n > 0 && (size_t) n < outsz ? 1 : 0;
}

bool usb_sysfs_dir_unions_backing(const char *guest_path)
Expand Down
23 changes: 14 additions & 9 deletions src/runtime/usb-sysfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ int usb_sysfs_resolve_guest_path(const char *guest_path,
char *out,
size_t outsz);

/* Whether the folded guest name @guest_path is a usbfs node, by its shape
* alone; it takes no lock.
/* Whether a fold of a relative name stops at the folded guest name @guest_path:
* every name under /dev/bus/usb except /dev/bus/usb itself and a bus directory
* the layer has, since Linux applies '..' only to a directory that exists. It
* takes usb_lock to look a bus directory up.
*/
bool usb_sysfs_names_node(const char *guest_path);
bool usb_sysfs_stops_fold(const char *guest_path);

/* The canonical /dev/bus/usb/BBB/DDD spelling of @guest_path when it names a
* usbfs node once its '.' and '..' are folded as the stat intercept folds them.
Expand All @@ -95,14 +97,17 @@ bool usb_sysfs_node_path(const char *guest_path, char *out, size_t outsz);
* directories the layer serves, to /dev followed by what comes after the '..'
* that climbed, or to /dev/. when nothing does. Those directories are the
* layer's, and the macOS host has no /dev/bus at all, so the '..' cannot be
* left to the backing; a walk through any other name is. A further '..' is
* applied to /dev and stops at the root.
* left to the backing. A further '..' is applied to /dev and stops at the root.
* A '..' after a bus no device has, or after a name under /dev/bus/usb that is
* not a bus or device number, fails the walk, as on Linux; one after a node, a
* name below a node or a name outside /dev/bus/usb is left as written.
*
* Returns true with @out filled, or false when nothing is rewritten.
* Returns 1 with @out filled, 0 when nothing is rewritten, or -1 with errno set
* to ENOENT for a '..' that fails the walk.
*/
bool usb_sysfs_resolve_dev_climb(const char *guest_path,
char *out,
size_t outsz);
int usb_sysfs_resolve_dev_climb(const char *guest_path,
char *out,
size_t outsz);

/* Malloc'd copy of the usbfs descriptors blob (18-byte little-endian device
* descriptor followed by every raw configuration descriptor in index order) for
Expand Down
43 changes: 26 additions & 17 deletions src/syscall/path.c
Original file line number Diff line number Diff line change
Expand Up @@ -647,14 +647,19 @@ int path_translate_at(guest_fd_t dirfd,
}

/* usb_sysfs_resolve_dev_climb rewrites a /dev/bus name whose '..' climbs
* out of it. What follows can climb out again, as in
* /dev/bus/../bus/../null, and each pass shortens the name. A climb can
* land in /sys, so this runs before the /sys rewrite below.
* out of it, and fails one whose '..' follows a bus no device has or a name
* under /dev/bus/usb that is not a bus or device number. What follows a
* climb can climb out again, as in /dev/bus/../bus/../null, and each pass
* shortens the name. A climb can land in /sys, so this runs before the /sys
* rewrite below.
*/
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,
sizeof(resolved)))
int crc = usb_sysfs_resolve_dev_climb(tx->guest_path, resolved,
sizeof(resolved));
if (crc < 0)
return -1;
if (crc == 0)
break;
str_copy_trunc(tx->guest_buf, resolved, sizeof(tx->guest_buf));
tx->guest_path = tx->guest_buf;
Expand Down Expand Up @@ -1368,10 +1373,11 @@ static bool proc_path_fd_is_dir(const fd_entry_t *snap)
}

/* Rebuild @path against the guest directory @base as an absolute guest path,
* folding its components. Once the walk reaches a usbfs node the rest is joined
* as written from the slash after it: Linux applies '..' only to a directory,
* and the USB layer answers ENOTDIR for a '..', a name or a trailing slash
* after a node.
* folding its components. Once the walk reaches a usbfs node, or a name under
* /dev/bus/usb that does not exist, the rest is joined as written from the
* slash after it: Linux applies '..' only to a directory that exists, and the
* USB layer answers ENOTDIR for a '..', a name or a trailing slash after a
* node, and ENOENT for a '..' after a missing name.
*
* Returns 0, or -1 with errno set to ENAMETOOLONG.
*/
Expand All @@ -1388,7 +1394,7 @@ static int path_rebuild_under(const char *base,
if (proc_seed_absolute_path(base, out, outsz, marks, ARRAY_SIZE(marks),
&depth) < 0)
goto toolong;
while (*p && !usb_sysfs_names_node(out)) {
while (*p && !usb_sysfs_stops_fold(out)) {
p += strspn(p, "/");
size_t n = strcspn(p, "/");
if (n == 0)
Expand Down Expand Up @@ -1461,9 +1467,15 @@ static int resolve_proc_cwd_path(const char *path, char *out, size_t outsz)
if (!path || path[0] == '\0' || path[0] == '/')
return 0;

/* The cwd is copied out of the view: path_rebuild_under can take usb_lock,
* and cwd_lock holds only fuse_lock beneath it.
*/
proc_cwd_view_t view;
if (proc_acquire_cwd_view(&view) < 0)
return 0;
char base[LINUX_PATH_MAX];
str_copy_trunc(base, view.path, sizeof(base));
proc_release_cwd_view(&view);

/* /dev/pts and the synthetic USB trees join /proc here: all are served from
* host directories whose contents are not what the guest names, so a
Expand All @@ -1472,13 +1484,10 @@ static int resolve_proc_cwd_path(const char *path, char *out, size_t outsz)
* set by fchdir() onto a synthetic USB directory would resolve relative
* names straight against the scratch tree.
*/
int rc = 0;
if (!strncmp(view.path, "/proc", 5) || !strncmp(view.path, "/dev/pts", 8) ||
!strncmp(view.path, "/sys", 4) || !strncmp(view.path, "/dev/bus", 8))
rc = path_rebuild_under(view.path, path, out, outsz) < 0 ? -1 : 1;

proc_release_cwd_view(&view);
return rc;
if (!strncmp(base, "/proc", 5) || !strncmp(base, "/dev/pts", 8) ||
!strncmp(base, "/sys", 4) || !strncmp(base, "/dev/bus", 8))
return path_rebuild_under(base, path, out, outsz) < 0 ? -1 : 1;
return 0;
}

int resolve_proc_at_path(guest_fd_t dirfd,
Expand Down
2 changes: 1 addition & 1 deletion src/syscall/path.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ int path_translate_dirent_name(bool dir_holds_escapes,

/* Rebase a relative path against a host directory fd into the guest-visible
* absolute spelling (F_GETPATH and sysroot strip, with the components folded up
* to a usbfs node).
* to a usbfs node or a /dev/bus/usb name that does not exist).
*
* Returns 1 with out filled, 0 when no mapping exists. See path.c for the
* chase() rationale.
Expand Down
10 changes: 9 additions & 1 deletion tests/test-usb-sysfs-matrix.c
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,15 @@ enum {
COL_CLIMB, /* a '..' out of /dev/bus over its own directories */
COL_CLIMB_FILE, /* a '..' out of /dev/bus after the backing's file */
COL_NODE_DOTDOT, /* the usb node followed by '..' */
COL_SHADOW_DOTDOT, /* a '..' out of /dev/bus/usb after the backing's bus */
COL_COUNT,
};

static const char *col_name[COL_COUNT] = {
"synth-dir", "back-sys", "back-dev", "subsys", "escape",
"escape-syn", "usb-node", "absent", "long-sys", "sys-root",
"dev-bus", "shadow", "subsys-out", "dev-fold-out", "dev-fold-in",
"sys-fold-in", "dev-climb", "dev-climb-f", "node-dotdot",
"sys-fold-in", "dev-climb", "dev-climb-f", "node-dotdot", "shadow-dotdot",
};

/* COL_SUBSYS is the one spelling that cannot be shared: the recording host's
Expand Down Expand Up @@ -181,6 +182,8 @@ static const char *col_path(int c)
return "/dev/bus/other/f/../../../null";
case COL_NODE_DOTDOT:
return "/dev/bus/usb/001/001/..";
case COL_SHADOW_DOTDOT:
return "/dev/bus/usb/099/../../other/f";
default:
return "/dev/bus";
}
Expand Down Expand Up @@ -234,6 +237,11 @@ static const char *col_path(int c)
* answers ENOTDIR for and a lexical fold would turn into the bus directory.
*/

/* COL_SHADOW_DOTDOT walks through COL_SHADOW's bus directory and out to
* COL_BACK_DEV's file. Linux fails the walk at 099, a bus no device has; the
* directory the sysroot fixture plants there must not let it through.
*/

/* Names that must be listed by the union directories, one comma-free name per
* entry, NULL-terminated. Empty for the columns where enumeration is not the
* property under test.
Expand Down
Loading
Loading