diff --git a/tools/linuxfs/preload/descriptor.c b/tools/linuxfs/preload/descriptor.c new file mode 100644 index 000000000..681dce999 --- /dev/null +++ b/tools/linuxfs/preload/descriptor.c @@ -0,0 +1,291 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DESCRIPTOR "user.wine.sd" +#define STORE "/.droiddeck-descriptors" +#define ENTRY_MAX (PATH_MAX * 2) + +int bl_is_wineserver(void) __attribute__((visibility("hidden"))); + +static int store_seen; + +static int kept(const char *name) { + return name != NULL && strcmp(name, DESCRIPTOR) == 0 && bl_is_wineserver(); +} + +static int store_root(char *out, size_t size) { + const char *prefix = getenv("WINEPREFIX"); + int n; + + if (prefix == NULL || prefix[0] != '/') return -1; + n = snprintf(out, size, "%s" STORE, prefix); + return n > 0 && (size_t)n < size ? n : -1; +} + +static int store_exists(void) { + char root[PATH_MAX]; + + if (store_seen) return 1; + if (store_root(root, sizeof(root)) < 0) return 0; + store_seen = access(root, F_OK) == 0; + return store_seen; +} + +static int entry_for(const char *path, char *out, size_t size, const char *suffix, int create) { + int used = store_root(out, size); + const char *part = path, *end; + + if (used < 0 || path[0] != '/') return -1; + if (create && mkdir(out, 0700) != 0 && errno != EEXIST) return -1; + while (*part == '/') part++; + if (*part == '\0') return -1; + for (;;) { + size_t length; + const char *tail; + + end = strchr(part, '/'); + length = end ? (size_t)(end - part) : strlen(part); + tail = end ? ".d" : suffix; + if (length + strlen(tail) > NAME_MAX || used + length + strlen(tail) + 2 > size) return -1; + out[used++] = '/'; + memcpy(out + used, part, length); + used += (int)length; + strcpy(out + used, tail); + used += (int)strlen(tail); + if (!end) break; + if (create && mkdir(out, 0700) != 0 && errno != EEXIST) return -1; + part = end + 1; + } + if (create) store_seen = 1; + return 0; +} + +static int path_of(int fd, char *out, size_t size) { + char link[40]; + ssize_t n; + + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + n = readlink(link, out, size - 1); + if (n <= 0 || out[0] != '/') return -1; + out[n] = '\0'; + return 0; +} + +static int resolved(const char *path, char *out, size_t size) { + int fd, result; + + if (path == NULL) return -1; + fd = open(path, O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return -1; + result = path_of(fd, out, size); + close(fd); + return result; +} + +static int entry_of(int fd, char *out, size_t size, int create) { + char path[PATH_MAX]; + + if (path_of(fd, path, sizeof(path)) != 0) return -1; + return entry_for(path, out, size, ".sd", create); +} + +static ssize_t stored_read(int fd, void *value, size_t size) { + char entry[ENTRY_MAX]; + struct stat st; + ssize_t got = 0; + int file; + + if (entry_of(fd, entry, sizeof(entry), 0) != 0) return -2; + file = open(entry, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (file < 0) { + if (errno != ENOENT) return -2; + errno = ENODATA; + return -1; + } + if (fstat(file, &st) != 0) { + close(file); + return -2; + } + if (size == 0) { + close(file); + return (ssize_t)st.st_size; + } + if ((size_t)st.st_size > size) { + close(file); + errno = ERANGE; + return -1; + } + while (got < st.st_size) { + ssize_t n = read(file, (char *)value + got, (size_t)(st.st_size - got)); + if (n <= 0) { + close(file); + return -2; + } + got += n; + } + close(file); + return got; +} + +static int stored_write(int fd, const void *value, size_t size) { + char entry[ENTRY_MAX], fresh[ENTRY_MAX + 8]; + size_t put = 0; + int file; + + if (entry_of(fd, entry, sizeof(entry), 1) != 0) return -1; + snprintf(fresh, sizeof(fresh), "%s.new", entry); + file = open(fresh, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600); + if (file < 0) return -1; + while (put < size) { + ssize_t n = write(file, (const char *)value + put, size - put); + if (n <= 0) { + close(file); + unlinkat(AT_FDCWD, fresh, 0); + return -1; + } + put += (size_t)n; + } + if (close(file) != 0 || renameat(AT_FDCWD, fresh, AT_FDCWD, entry) != 0) { + unlinkat(AT_FDCWD, fresh, 0); + return -1; + } + return 0; +} + +ssize_t fgetxattr(int fd, const char *name, void *value, size_t size) { + static ssize_t (*next)(int, const char *, void *, size_t); + ssize_t result; + + if (next == NULL) next = dlsym(RTLD_NEXT, "fgetxattr"); + if (next == NULL) { + errno = ENOSYS; + return -1; + } + result = next(fd, name, value, size); + if (result >= 0 || errno != ENOTSUP || !kept(name)) return result; + result = stored_read(fd, value, size); + if (result == -2) { + errno = ENOTSUP; + return -1; + } + return result; +} + +int fsetxattr(int fd, const char *name, const void *value, size_t size, int flags) { + static int (*next)(int, const char *, const void *, size_t, int); + int saved = errno, result; + + if (next == NULL) next = dlsym(RTLD_NEXT, "fsetxattr"); + if (next == NULL) { + errno = ENOSYS; + return -1; + } + result = next(fd, name, value, size, flags); + if (result == 0 || errno != ENOTSUP || !kept(name)) return result; + if (stored_write(fd, value, size) != 0) { + errno = ENOTSUP; + return -1; + } + errno = saved; + return 0; +} + +int fremovexattr(int fd, const char *name) { + static int (*next)(int, const char *); + char entry[ENTRY_MAX]; + int saved = errno, result; + + if (next == NULL) next = dlsym(RTLD_NEXT, "fremovexattr"); + if (next == NULL) { + errno = ENOSYS; + return -1; + } + result = next(fd, name); + if (result == 0 || errno != ENOTSUP || !kept(name)) return result; + if (entry_of(fd, entry, sizeof(entry), 0) != 0) { + errno = ENOTSUP; + return -1; + } + if (unlinkat(AT_FDCWD, entry, 0) != 0) { + errno = errno == ENOENT ? ENODATA : ENOTSUP; + return -1; + } + errno = saved; + return 0; +} + +static int tracked(const char *path, char *out, size_t size) { + int saved = errno, found; + + found = bl_is_wineserver() && store_exists() && resolved(path, out, size) == 0; + errno = saved; + return found; +} + +static void forget(const char *path, int folder) { + char entry[ENTRY_MAX]; + int saved = errno; + + if (entry_for(path, entry, sizeof(entry), ".sd", 0) == 0) unlinkat(AT_FDCWD, entry, 0); + if (folder && entry_for(path, entry, sizeof(entry), ".d", 0) == 0) unlinkat(AT_FDCWD, entry, AT_REMOVEDIR); + errno = saved; +} + +static void follow(const char *from, const char *to) { + static const char *const kinds[] = {".sd", ".d"}; + char old[ENTRY_MAX], new[ENTRY_MAX]; + int saved = errno; + + for (size_t i = 0; i < 2; i++) { + if (entry_for(from, old, sizeof(old), kinds[i], 0) != 0) continue; + if (access(old, F_OK) != 0) { + if (i == 0 && entry_for(to, new, sizeof(new), kinds[i], 0) == 0) unlinkat(AT_FDCWD, new, 0); + continue; + } + if (entry_for(to, new, sizeof(new), kinds[i], 1) != 0 || renameat(AT_FDCWD, old, AT_FDCWD, new) != 0) { + if (i == 0) unlinkat(AT_FDCWD, old, 0); + } + } + errno = saved; +} + +__attribute__((visibility("hidden"))) +int bl_descriptor_unlink(const char *path, int (*real)(const char *)) { + char target[PATH_MAX]; + int known = tracked(path, target, sizeof(target)), result = real(path); + + if (result == 0 && known) forget(target, 0); + return result; +} + +__attribute__((visibility("hidden"))) +int bl_descriptor_rmdir(const char *path, int (*real)(const char *)) { + char target[PATH_MAX]; + int known = tracked(path, target, sizeof(target)), result = real(path); + + if (result == 0 && known) forget(target, 1); + return result; +} + +__attribute__((visibility("hidden"))) +int bl_descriptor_remove(const char *path, int (*real)(const char *)) { + return bl_descriptor_rmdir(path, real); +} + +__attribute__((visibility("hidden"))) +int bl_descriptor_rename(const char *from, const char *to, int (*real)(const char *, const char *)) { + char old[PATH_MAX], new[PATH_MAX]; + int known = tracked(from, old, sizeof(old)), result = real(from, to); + + if (result == 0 && known && tracked(to, new, sizeof(new))) follow(old, new); + return result; +} diff --git a/tools/linuxfs/preload/dircache.c b/tools/linuxfs/preload/dircache.c index 296faa616..f6951497f 100644 --- a/tools/linuxfs/preload/dircache.c +++ b/tools/linuxfs/preload/dircache.c @@ -846,9 +846,17 @@ int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode, } ONE_PATH(mkdir, ADDED, IN_CREATE, next_mkdir(path, mode), mode_t mode) -ONE_PATH(rmdir, REMOVED, IN_DELETE, next_rmdir(path)) -ONE_PATH(unlink, REMOVED, IN_DELETE, next_unlink(path)) -ONE_PATH(remove, REMOVED, IN_DELETE, next_remove(path)) +int bl_descriptor_unlink(const char *path, int (*real)(const char *)) __attribute__((visibility("hidden"))); +int bl_descriptor_rmdir(const char *path, int (*real)(const char *)) __attribute__((visibility("hidden"))); +int bl_descriptor_remove(const char *path, int (*real)(const char *)) __attribute__((visibility("hidden"))); +int bl_descriptor_rename(const char *from, const char *to, int (*real)(const char *, const char *)) + __attribute__((visibility("hidden"))); + +static int moved(const char *from, const char *to) { return bl_descriptor_rename(from, to, next_rename); } + +ONE_PATH(rmdir, REMOVED, IN_DELETE, bl_descriptor_rmdir(path, next_rmdir)) +ONE_PATH(unlink, REMOVED, IN_DELETE, bl_descriptor_unlink(path, next_unlink)) +ONE_PATH(remove, REMOVED, IN_DELETE, bl_descriptor_remove(path, next_remove)) #define TWO_PATHS(name, gone, event, call) \ int name(const char *from, const char *to) { \ @@ -881,7 +889,7 @@ ONE_PATH(remove, REMOVED, IN_DELETE, next_remove(path)) int bl_link_lock(const char *oldpath, const char *newpath, int result) __attribute__((visibility("hidden"))); static int link_or_lock(const char *from, const char *to) { return bl_link_lock(from, to, next_link(from, to)); } -TWO_PATHS(rename, 1, IN_MOVED_TO, next_rename) +TWO_PATHS(rename, 1, IN_MOVED_TO, moved) TWO_PATHS(link, 0, IN_CREATE, link_or_lock) TWO_PATHS(symlink, 0, IN_CREATE, next_symlink) diff --git a/tools/linuxfs/preload/owner.c b/tools/linuxfs/preload/owner.c index 78ada3bf0..1e493f8a8 100644 --- a/tools/linuxfs/preload/owner.c +++ b/tools/linuxfs/preload/owner.c @@ -12,7 +12,8 @@ #define FAT_MAGIC 0x4d44 #define EXFAT_MAGIC 0x2011bab0 -static int is_wineserver(void) { +__attribute__((visibility("hidden"))) +int bl_is_wineserver(void) { static int cached = -1; char exe[PATH_MAX]; const char *base; @@ -43,7 +44,7 @@ void bl_storage_owner(int fd, struct stat *st) { int saved = errno; uid_t uid; - if (st != NULL && is_wineserver()) { + if (st != NULL && bl_is_wineserver()) { uid = getuid(); if (st->st_uid != uid && has_no_unix_owner(fd)) st->st_uid = uid; } @@ -60,7 +61,7 @@ int fchmod(int fd, mode_t mode) { return -1; } result = next_fchmod(fd, mode); - if (result != 0 && errno == EPERM && is_wineserver()) { + if (result != 0 && errno == EPERM && bl_is_wineserver()) { if (has_no_unix_owner(fd)) { errno = saved; return 0; diff --git a/tools/tests/test_descriptor_preload.py b/tools/tests/test_descriptor_preload.py new file mode 100644 index 000000000..f83df2e9b --- /dev/null +++ b/tools/tests/test_descriptor_preload.py @@ -0,0 +1,245 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" + +FAKE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int answer(int fd) { + char link[32], path[4096]; + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + long n = syscall(SYS_readlinkat, AT_FDCWD, link, path, sizeof(path) - 1); + path[n > 0 ? n : 0] = '\0'; + if (strstr(path, "/card/")) return ENOTSUP; + if (strstr(path, "/denied/")) return EACCES; + return ENODATA; +} + +ssize_t fgetxattr(int fd, const char *name, void *value, size_t size) { + (void)name; (void)value; (void)size; + errno = answer(fd); + return -1; +} + +int fsetxattr(int fd, const char *name, const void *value, size_t size, int flags) { + (void)name; (void)value; (void)size; (void)flags; + int error = answer(fd); + if (error == ENODATA) return 0; + errno = error; + return -1; +} + +int fremovexattr(int fd, const char *name) { + (void)name; + errno = answer(fd); + return -1; +} +""" + +PROGRAM = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +static const char *error(void) { + return errno == ENOTSUP ? "unsupported" : errno == ENODATA ? "none" : errno == ERANGE ? "range" : + errno == EACCES ? "denied" : "other"; +} + +int main(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + const char *op = argv[i]; + if (!strcmp(op, "unlink") || !strcmp(op, "rmdir")) { + const char *path = argv[++i]; + printf("%s\n", (op[0] == 'u' ? unlink(path) : rmdir(path)) == 0 ? "ok" : "failed"); + continue; + } + if (!strcmp(op, "remove-path")) { + printf("%s\n", remove(argv[++i]) == 0 ? "ok" : "failed"); + continue; + } + if (!strcmp(op, "rename")) { + const char *from = argv[++i], *to = argv[++i]; + printf("%s\n", rename(from, to) == 0 ? "ok" : "failed"); + continue; + } + int fd = open(argv[++i], O_RDONLY); + char value[64]; + if (!strcmp(op, "set")) { + const char *text = argv[++i]; + errno = 1234; + int result = fsetxattr(fd, "user.wine.sd", text, strlen(text), 0); + printf("%s\n", result == 0 ? errno == 1234 ? "ok" : "errno" : error()); + } else if (!strcmp(op, "get")) { + ssize_t n = fgetxattr(fd, "user.wine.sd", value, sizeof(value)); + if (n < 0) printf("%s\n", error()); + else printf("=%.*s\n", (int)n, value); + } else if (!strcmp(op, "size")) { + ssize_t n = fgetxattr(fd, "user.wine.sd", NULL, 0); + if (n < 0) printf("%s\n", error()); + else printf("%d\n", (int)n); + } else if (!strcmp(op, "short")) { + ssize_t n = fgetxattr(fd, "user.wine.sd", value, 2); + printf("%s\n", n < 0 ? error() : "read"); + } else if (!strcmp(op, "remove")) { + errno = 1234; + int result = fremovexattr(fd, "user.wine.sd"); + printf("%s\n", result == 0 ? errno == 1234 ? "ok" : "errno" : error()); + } else if (!strcmp(op, "other")) { + ssize_t n = fgetxattr(fd, "user.other", value, sizeof(value)); + printf("%s\n", n < 0 ? error() : "read"); + } + close(fd); + } + return 0; +} +""" + + +@unittest.skipUnless(shutil.which("cc"), "needs a C compiler") +class DescriptorPreloadTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.build_dir = tempfile.TemporaryDirectory() + build = Path(cls.build_dir.name) + (build / "fake.c").write_text(FAKE) + (build / "program.c").write_text(PROGRAM) + cls.preload = build / "libblsession.so" + cls.fake = build / "fake.so" + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-o", str(cls.preload), + *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True) + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(cls.fake), str(build / "fake.c")], check=True) + cls.wineserver = build / "wineserver" + subprocess.run(["cc", "-O2", "-o", str(cls.wineserver), str(build / "program.c")], check=True) + cls.other = build / "wine64-preloader" + shutil.copy(cls.wineserver, cls.other) + + @classmethod + def tearDownClass(cls): + cls.build_dir.cleanup() + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.dir = Path(os.path.realpath(self.temp.name)) + self.prefix = self.dir / "prefix" + self.prefix.mkdir() + self.store = self.prefix / ".droiddeck-descriptors" + for kind in ("card", "disk", "denied"): + (self.dir / kind / "game").mkdir(parents=True) + (self.dir / kind / "game" / "file").write_text("x") + + def tearDown(self): + self.temp.cleanup() + + def path(self, *parts): + return str(self.dir.joinpath(*parts)) + + def run_as(self, program, *args, prefix=True): + env = dict(os.environ, LD_PRELOAD="%s:%s" % (self.preload, self.fake)) + env.pop("WINEPREFIX", None) + if prefix: + env["WINEPREFIX"] = str(self.prefix) + result = subprocess.run([str(program), *args], env=env, capture_output=True, text=True, check=True) + return result.stdout.split() + + def server(self, *args, **kwargs): + return self.run_as(self.wineserver, *args, **kwargs) + + def test_a_descriptor_is_kept_where_the_volume_cannot_store_it(self): + file, folder = self.path("card", "game", "file"), self.path("card", "game") + self.assertEqual(["none", "ok", "ok"], self.server("get", file, "set", file, "one", "set", folder, "two")) + self.assertEqual(["=one", "3", "=two"], self.server("get", file, "size", file, "get", folder)) + + def test_a_descriptor_is_replaced_and_removed(self): + file = self.path("card", "game", "file") + self.assertEqual(["ok", "ok", "=longer"], self.server("set", file, "one", "set", file, "longer", "get", file)) + self.assertEqual(["range", "ok", "none", "none"], self.server("short", file, "remove", file, "get", file, "remove", file)) + + def test_a_volume_that_stores_descriptors_is_left_alone(self): + file = self.path("disk", "game", "file") + self.assertEqual(["ok", "none", "none"], self.server("set", file, "one", "get", file, "remove", file)) + self.assertFalse(self.store.exists()) + + def test_other_failures_keep_their_error(self): + file = self.path("denied", "game", "file") + self.assertEqual(["denied", "denied", "denied"], self.server("set", file, "one", "get", file, "remove", file)) + self.assertFalse(self.store.exists()) + + def test_other_programs_and_other_attributes_are_unchanged(self): + file = self.path("card", "game", "file") + self.assertEqual(["unsupported", "unsupported", "unsupported"], + self.run_as(self.other, "set", file, "one", "get", file, "remove", file)) + self.assertEqual(["ok", "unsupported"], self.server("set", file, "one", "other", file)) + self.assertEqual(["unsupported"], self.run_as(self.other, "get", file)) + + def test_nothing_is_kept_without_a_prefix(self): + file = self.path("card", "game", "file") + self.assertEqual(["unsupported", "unsupported", "unsupported"], + self.server("set", file, "one", "get", file, "remove", file, prefix=False)) + + def test_a_deleted_file_loses_its_descriptor(self): + file, folder = self.path("card", "game", "file"), self.path("card", "game") + self.assertEqual(["ok", "ok", "ok"], self.server("set", file, "one", "set", folder, "two", "unlink", file)) + Path(file).write_text("x") + self.assertEqual(["none", "ok", "ok"], self.server("get", file, "unlink", file, "rmdir", folder)) + Path(folder).mkdir() + self.assertEqual(["none"], self.server("get", folder)) + + def test_a_removed_file_or_folder_loses_its_descriptor(self): + file, folder = self.path("card", "game", "file"), self.path("card", "game") + self.assertEqual(["ok", "ok", "ok", "ok"], + self.server("set", file, "one", "set", folder, "two", "remove-path", file, "remove-path", folder)) + Path(folder).mkdir() + Path(file).write_text("x") + self.assertEqual(["none", "none"], self.server("get", folder, "get", file)) + + def test_a_file_deleted_through_a_link_loses_its_descriptor(self): + file = self.path("card", "game", "file") + os.symlink(self.path("card"), self.path("drive")) + self.assertEqual(["ok", "ok"], self.server("set", file, "one", "unlink", self.path("drive", "game", "file"))) + Path(file).write_text("x") + self.assertEqual(["none"], self.server("get", file)) + + def test_a_descriptor_follows_a_rename(self): + file, moved = self.path("card", "game", "file"), self.path("card", "game", "moved") + self.assertEqual(["ok", "ok", "=one"], self.server("set", file, "one", "rename", file, moved, "get", moved)) + Path(file).write_text("x") + self.assertEqual(["none", "ok", "none"], self.server("get", file, "rename", file, moved, "get", moved)) + + def test_descriptors_follow_a_renamed_folder(self): + file, folder, moved = self.path("card", "game", "file"), self.path("card", "game"), self.path("card", "moved") + self.assertEqual(["ok", "ok", "ok"], self.server("set", file, "one", "set", folder, "two", "rename", folder, moved)) + self.assertEqual(["=two", "=one"], self.server("get", moved, "get", self.path("card", "moved", "file"))) + Path(folder).mkdir() + (Path(folder) / "file").write_text("x") + self.assertEqual(["none", "none"], self.server("get", folder, "get", file)) + + def test_deletes_outside_the_store_cost_nothing_before_it_exists(self): + file = self.path("disk", "game", "file") + self.assertEqual(["ok"], self.server("unlink", file)) + self.assertFalse(self.store.exists()) + + def test_a_name_too_long_to_keep_reports_no_support(self): + long_file = self.path("card", "game", "n" * 254) + Path(long_file).write_text("x") + self.assertEqual(["unsupported", "unsupported"], self.server("set", long_file, "one", "get", long_file)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_fastpath_fuse_dirs.py b/tools/tests/test_fastpath_fuse_dirs.py index d8c52e99d..4347e0aed 100644 --- a/tools/tests/test_fastpath_fuse_dirs.py +++ b/tools/tests/test_fastpath_fuse_dirs.py @@ -13,8 +13,18 @@ PROGRAM = r""" #include "FASTPATH" #include +static const char *race_from, *race_to; +static dev_t race_dev; +static ino_t race_ino; + static long host_sys(long nr, long a, long b, long c, long d, long e, long f) { long r = syscall(nr, a, b, c, d, e, f); + struct stat fs; + if (r >= 0 && nr == SYS_fcntl && b == F_DUPFD_CLOEXEC && c == DIR_FD_FLOOR && race_from + && syscall(SYS_fstat, a, &fs) == 0 && fs.st_dev == race_dev && fs.st_ino == race_ino) { + syscall(SYS_renameat, AT_FDCWD, race_from, AT_FDCWD, race_to); + race_from = NULL; + } return r < 0 ? -errno : r; } @@ -72,6 +82,14 @@ int main(int argc, char **argv) { } else if (!strcmp(op, "host-mv")) { show(host_sys(SYS_renameat, AT_FDCWD, (long)argv[i + 1], AT_FDCWD, (long)argv[i + 2], 0, 0)); i += 2; + } else if (!strcmp(op, "race-mv")) { + struct stat rs; + race_from = argv[i + 1]; + race_to = argv[i + 2]; + i += 2; + show(host_sys(SYS_newfstatat, AT_FDCWD, (long)race_from, (long)&rs, 0, 0, 0)); + race_dev = rs.st_dev; + race_ino = rs.st_ino; } else if (!strcmp(op, "host-rmdir")) { show(host_sys(SYS_unlinkat, AT_FDCWD, (long)argv[++i], AT_REMOVEDIR, 0, 0, 0)); } else if (!strcmp(op, "host-mkdir")) { @@ -169,6 +187,16 @@ class FastPathFuseDirsTest(unittest.TestCase): "stat", "/mnt/card/Games/steamapps/common/Moved/Data/keep") self.assertEqual(["ok", "ok", "E2", "ok"], out) + def test_a_folder_renamed_while_it_is_opened_is_not_used_under_its_old_name(self): + game = self.card / "Games/steamapps/common/Game" + (game / "Data/keep").write_text("x") + out = self.ask("fuse", "race-mv", str(game / "Data"), str(game / "Moved"), + "stat", "/mnt/card/Games/steamapps/common/Game/Data/keep", + "touch", "/mnt/card/Games/steamapps/common/Game/Data/new", "fds") + self.assertEqual(["ok", "E2", "E2"], out[:3]) + self.assertEqual(["keep"], [p.name for p in (game / "Moved").iterdir()]) + self.assertLessEqual(int(out[-1]), 64) + def test_a_folder_deleted_and_made_again_is_looked_up_fresh(self): data = self.card / "Games/steamapps/common/Game/Data" out = self.ask("fuse", "touch", "/mnt/card/Games/steamapps/common/Game/Data/old",