diff --git a/docs/internals.md b/docs/internals.md index e73de956..0f86ef9d 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -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 `/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`. @@ -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 diff --git a/src/runtime/usb-sysfs.c b/src/runtime/usb-sysfs.c index 2a992219..300092db 100644 --- a/src/runtime/usb-sysfs.c +++ b/src/runtime/usb-sysfs.c @@ -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, @@ -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) @@ -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) + 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 @@ -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) diff --git a/src/runtime/usb-sysfs.h b/src/runtime/usb-sysfs.h index 0827b1b5..d2df85d0 100644 --- a/src/runtime/usb-sysfs.h +++ b/src/runtime/usb-sysfs.h @@ -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. @@ -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 diff --git a/src/syscall/path.c b/src/syscall/path.c index e619555a..88573782 100644 --- a/src/syscall/path.c +++ b/src/syscall/path.c @@ -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; @@ -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. */ @@ -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) @@ -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 @@ -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, diff --git a/src/syscall/path.h b/src/syscall/path.h index 1a95f1fd..3c3af1a7 100644 --- a/src/syscall/path.h +++ b/src/syscall/path.h @@ -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. diff --git a/tests/test-usb-sysfs-matrix.c b/tests/test-usb-sysfs-matrix.c index 3db37c3a..12739a45 100644 --- a/tests/test-usb-sysfs-matrix.c +++ b/tests/test-usb-sysfs-matrix.c @@ -100,6 +100,7 @@ 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, }; @@ -107,7 +108,7 @@ 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 @@ -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"; } @@ -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. diff --git a/tests/test-usb-sysfs.c b/tests/test-usb-sysfs.c index e0d00a34..6da69f98 100644 --- a/tests/test-usb-sysfs.c +++ b/tests/test-usb-sysfs.c @@ -1185,8 +1185,8 @@ static void check_dev_bus_names(void) close(fd); /* path_rebuild_under folds a relative name against a /dev/bus cwd or dirfd - * up to a usbfs node, so every other '..' is applied before any entry point - * or stamp sees the name. + * up to a usbfs node or a missing /dev/bus/usb name, so every other '..' is + * applied before any entry point or stamp sees the name. */ TEST("getcwd after chdir(\"..\") from /dev/bus/usb/001 is /dev/bus/usb"); EXPECT_TRUE(chdir("/dev/bus/usb/001") == 0 && chdir("..") == 0 && @@ -1312,14 +1312,17 @@ static void check_dotdot_leaves_bus(void) } /* openat of @rel from the host directory @dir, which the host refuses and - * openat retries through the guest spelling: ENOTDIR when @enotdir, else a + * openat retries through the guest spelling: @err when it is not 0, else a * descriptor. */ -static void check_hostdir_openat(const char *dir, const char *rel, bool enotdir) +static void check_hostdir_openat(const char *dir, const char *rel, int err) { char what[96]; snprintf(what, sizeof(what), "openat(%s, \"%s\") %s", dir, rel, - enotdir ? "is ENOTDIR" : "opens"); + !err ? "opens" + : err == ENOTDIR ? "is ENOTDIR" + : err == ENOENT ? "is ENOENT" + : "fails"); TEST(what); int hfd = open(dir, O_RDONLY | O_DIRECTORY); if (hfd < 0) { @@ -1328,7 +1331,7 @@ static void check_hostdir_openat(const char *dir, const char *rel, bool enotdir) } errno = 0; int fd = openat(hfd, rel, O_RDONLY | O_DIRECTORY); - EXPECT_TRUE(enotdir ? fd < 0 && errno == ENOTDIR : fd >= 0, what); + EXPECT_TRUE(err ? fd < 0 && errno == err : fd >= 0, what); if (fd >= 0) close(fd); close(hfd); @@ -1419,10 +1422,89 @@ static void check_node_dotdot_relative(void) /* The retry folds the name up to the node, so the node is not folded away * and a walk from / or out of /dev still reaches the layer. */ - check_hostdir_openat("/dev", "bus/usb/001/001/..", true); - check_hostdir_openat("/", "dev/bus/usb/001/001/..", true); - check_hostdir_openat("/dev", "../dev/bus/usb/001/001/..", true); - check_hostdir_openat("/", "dev/bus/usb/001", false); + check_hostdir_openat("/dev", "bus/usb/001/001/..", ENOTDIR); + check_hostdir_openat("/", "dev/bus/usb/001/001/..", ENOTDIR); + check_hostdir_openat("/dev", "../dev/bus/usb/001/001/..", ENOTDIR); + check_hostdir_openat("/", "dev/bus/usb/001", 0); +} + +/* A '..' after a missing /dev/bus/usb name is ENOENT, absolute or relative to a + * cwd, a dirfd or a host directory, as Linux fails the walk at the missing + * name. + */ +static void check_dotdot_after_missing(void) +{ + static const char *const names[] = { + "/dev/bus/usb/099/../001/001", + "/dev/bus/usb/foo/../001/001", + "/dev/bus/usb/001/foo/../001", + "/dev/bus/usb/../usb/099/../001/001", + "/dev/bus/../bus/usb/099/../001/001", + }; + static const char *const relative[] = { + "099/../001/001", + "099/../../../null", + "foo/..", + }; + struct stat st; + char what[96]; + + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + snprintf(what, sizeof(what), "%s is ENOENT", names[i]); + TEST(what); + EXPECT_ERRNO(stat(names[i], &st), ENOENT, names[i]); + } + + TEST("a read-write open of /dev/bus/usb/099/../001/001 is ENOENT"); + EXPECT_ERRNO(open("/dev/bus/usb/099/../001/001", O_RDWR), ENOENT, + "open(\"/dev/bus/usb/099/../001/001\", O_RDWR)"); + + for (size_t i = 0; i < sizeof(relative) / sizeof(relative[0]); i++) { + snprintf(what, sizeof(what), "\"%s\" in a /dev/bus/usb cwd is ENOENT", + relative[i]); + TEST(what); + if (chdir("/dev/bus/usb") != 0) + FAIL("chdir /dev/bus/usb"); + else + EXPECT_ERRNO(stat(relative[i], &st), ENOENT, relative[i]); + if (chdir("/") != 0) + FAIL("chdir back to /"); + } + + TEST("chdir(\"099/..\") in a /dev/bus/usb cwd is ENOENT"); + if (chdir("/dev/bus/usb") != 0) + FAIL("chdir /dev/bus/usb"); + else + EXPECT_ERRNO(chdir("099/.."), ENOENT, "chdir(\"099/..\")"); + if (chdir("/") != 0) + FAIL("chdir back to /"); + + check_hostdir_openat("/", "dev/bus/usb/099/../001", ENOENT); + + int dfd = open("/dev/bus/usb/001", O_RDONLY | O_DIRECTORY); + TEST("\"../099/../001/001\" against a /dev/bus/usb/001 dirfd is ENOENT"); + if (dfd < 0) { + FAIL("open /dev/bus/usb/001"); + return; + } + EXPECT_ERRNO(fstatat(dfd, "../099/../001/001", &st, 0), ENOENT, + "fstatat(/dev/bus/usb/001, \"../099/../001/001\")"); + + TEST("\"../001/001\" against a /dev/bus/usb/001 dirfd is the node"); + EXPECT_TRUE(fstatat(dfd, "../001/001", &st, 0) == 0 && S_ISCHR(st.st_mode), + "fstatat(/dev/bus/usb/001, \"../001/001\")"); + + TEST( + "\"../099/../001/001\" in a /dev/bus/usb/001 cwd from fchdir is " + "ENOENT"); + if (fchdir(dfd) != 0) + FAIL("fchdir /dev/bus/usb/001"); + else + EXPECT_ERRNO(stat("../099/../001/001", &st), ENOENT, + "stat(\"../099/../001/001\")"); + if (chdir("/") != 0) + FAIL("chdir back to /"); + close(dfd); } int main(void) @@ -1435,6 +1517,7 @@ int main(void) check_dev_bus_names(); check_dotdot_leaves_bus(); check_node_dotdot_relative(); + check_dotdot_after_missing(); /* Stated, not implied: the second half is only as strong as the bus it ran * against, and a zero here means those assertions did not execute. diff --git a/tests/usb-sysfs-matrix-vectors.h b/tests/usb-sysfs-matrix-vectors.h index 252e0707..c8b966a6 100644 --- a/tests/usb-sysfs-matrix-vectors.h +++ b/tests/usb-sysfs-matrix-vectors.h @@ -52,6 +52,9 @@ * the same, after back-dev's file * node-dotdot /dev/bus/usb/001/001/.. * usb-node's node followed by '..' + * shadow-dotdot /dev/bus/usb/099/../../other/f + * back-dev's file, spelled through shadow's bus directory, + * which only the backing has * * Two markers appear in the table. * @@ -97,25 +100,25 @@ */ /* clang-format off */ -/* 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 */ -/* open */ {"ok", "ok", "ok", "ok", "ok", "?ok", "-", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "-", "?ok", "ok", "E20", "E20"}, -/* open_nofollow */ {"ok", "ok", "ok", "E40", "ok", "?ok", "-", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "-", "?ok", "ok", "E20", "E20"}, -/* openat_dirfd */ {"ok", "ok", "ok", "ok", "ok", "?ok", "-", "E2", "ok", "skip", "ok", "skip", "ok", "ok", "-", "?ok", "ok", "skip", "skip"}, -/* stat */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20"}, -/* lstat */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20"}, -/* fstatat_nofollow */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20"}, -/* fstatat_dirfd */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "skip", "ok:d", "skip", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "skip", "skip"}, -/* statx */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20"}, -/* access */ {"ok", "ok", "ok", "ok", "ok", "?ok", "ok", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "ok", "?ok", "ok", "E20", "E20"}, -/* faccessat_nofollow */ {"ok", "ok", "ok", "ok", "ok", "?ok", "ok", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "ok", "?ok", "ok", "E20", "E20"}, -/* readlink */ {"E22", "E22", "E22", "ok", "E22", "?E22", "E22", "E2", "E22", "E22", "E22", "E2", "E22", "E22", "E22", "?E22", "E22", "E20", "E20"}, -/* readlinkat_dirfd */ {"E22", "E22", "E22", "ok", "E22", "?E22", "E22", "E2", "E22", "skip", "E22", "skip", "E22", "E22", "E22", "?E22", "E22", "skip", "skip"}, -/* getdents64 */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20"}, -/* statfs */ {"sysfs", "sysfs", "other", "sysfs","other","?other", "other", "E2", "sysfs", "sysfs", "other", "E2", "sysfs", "other", "other", "sysfs", "other", "E20", "E20"}, -/* fstatfs */ {"sysfs", "sysfs", "other", "sysfs","other","?other", "other", "E2", "sysfs", "sysfs", "other", "E2", "sysfs", "other", "other", "?sysfs", "other", "E20", "E20"}, -/* fstat_type */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20"}, -/* chdir */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20"}, -/* fchdir */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20"}, -/* epoll_ctl */ {"E1", "E1", "E1", "E1", "E1", "?E1", "-", "E2", "ok", "E1", "E1", "E2", "E1", "E1", "-", "?E1", "E1", "E20", "E20"}, -/* union_listing */ {"n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "all", "all", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a"}, +/* 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 shadow-dotdot */ +/* open */ {"ok", "ok", "ok", "ok", "ok", "?ok", "-", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "-", "?ok", "ok", "E20", "E20", "E2"}, +/* open_nofollow */ {"ok", "ok", "ok", "E40", "ok", "?ok", "-", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "-", "?ok", "ok", "E20", "E20", "E2"}, +/* openat_dirfd */ {"ok", "ok", "ok", "ok", "ok", "?ok", "-", "E2", "ok", "skip", "ok", "skip", "ok", "ok", "-", "?ok", "ok", "skip", "skip", "skip"}, +/* stat */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20", "E2"}, +/* lstat */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20", "E2"}, +/* fstatat_nofollow */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20", "E2"}, +/* fstatat_dirfd */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "skip", "ok:d", "skip", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "skip", "skip", "skip"}, +/* statx */ {"ok:d", "ok:d", "ok:f", "ok:d", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20", "E2"}, +/* access */ {"ok", "ok", "ok", "ok", "ok", "?ok", "ok", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "ok", "?ok", "ok", "E20", "E20", "E2"}, +/* faccessat_nofollow */ {"ok", "ok", "ok", "ok", "ok", "?ok", "ok", "E2", "ok", "ok", "ok", "E2", "ok", "ok", "ok", "?ok", "ok", "E20", "E20", "E2"}, +/* readlink */ {"E22", "E22", "E22", "ok", "E22", "?E22", "E22", "E2", "E22", "E22", "E22", "E2", "E22", "E22", "E22", "?E22", "E22", "E20", "E20", "E2"}, +/* readlinkat_dirfd */ {"E22", "E22", "E22", "ok", "E22", "?E22", "E22", "E2", "E22", "skip", "E22", "skip", "E22", "E22", "E22", "?E22", "E22", "skip", "skip", "skip"}, +/* getdents64 */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20", "E2"}, +/* statfs */ {"sysfs", "sysfs", "other", "sysfs","other","?other", "other", "E2", "sysfs", "sysfs", "other", "E2", "sysfs", "other", "other", "sysfs", "other", "E20", "E20", "E2"}, +/* fstatfs */ {"sysfs", "sysfs", "other", "sysfs","other","?other", "other", "E2", "sysfs", "sysfs", "other", "E2", "sysfs", "other", "other", "?sysfs", "other", "E20", "E20", "E2"}, +/* fstat_type */ {"ok:d", "ok:d", "ok:f", "ok:l", "ok:f", "?ok:f", "ok:c", "E2", "ok:f", "ok:d", "ok:d", "E2", "ok:d", "ok:f", "ok:c", "?ok:d", "ok:d", "E20", "E20", "E2"}, +/* chdir */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20", "E2"}, +/* fchdir */ {"ok", "ok", "E20", "ok", "E20", "?E20", "E20", "E2", "E20", "ok", "ok", "E2", "ok", "E20", "E20", "?ok", "ok", "E20", "E20", "E2"}, +/* epoll_ctl */ {"E1", "E1", "E1", "E1", "E1", "?E1", "-", "E2", "ok", "E1", "E1", "E2", "E1", "E1", "-", "?E1", "E1", "E20", "E20", "E2"}, +/* union_listing */ {"n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "all", "all", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a"}, /* clang-format on */