about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/Cargo.lock171
-rw-r--r--tvix/Cargo.nix499
-rw-r--r--tvix/crate-hashes.json1
-rw-r--r--tvix/default.nix6
-rw-r--r--tvix/store/Cargo.toml10
-rw-r--r--tvix/store/src/bin/tvix-store.rs32
-rw-r--r--tvix/store/src/fuse/file_attr.rs55
-rw-r--r--tvix/store/src/fuse/inodes.rs10
-rw-r--r--tvix/store/src/fuse/mod.rs640
-rw-r--r--tvix/store/src/fuse/tests.rs92
-rw-r--r--tvix/store/src/lib.rs2
11 files changed, 934 insertions, 584 deletions
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock
index f0a73f3a3f51..02726751705c 100644
--- a/tvix/Cargo.lock
+++ b/tvix/Cargo.lock
@@ -88,6 +88,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 
 [[package]]
+name = "arc-swap"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
+
+[[package]]
 name = "arrayref"
 version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -332,6 +338,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
 [[package]]
+name = "caps"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b"
+dependencies = [
+ "libc",
+ "thiserror",
+]
+
+[[package]]
 name = "cast"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -477,12 +493,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
 [[package]]
+name = "const-zero"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3c6565524986fe3225da0beb9b4aa55ebc73cd57ff8cb4ccf016ca4c8d006af"
+
+[[package]]
 name = "constant_time_eq"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
 
 [[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
 name = "count-write"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -578,7 +606,7 @@ dependencies = [
  "autocfg",
  "cfg-if",
  "crossbeam-utils",
- "memoffset",
+ "memoffset 0.8.0",
  "scopeguard",
 ]
 
@@ -791,19 +819,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 
 [[package]]
-name = "fuser"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5910691a0ececcc6eba8bb14029025c2d123e96a53db1533f6a4602861a5aaf7"
+name = "fuse-backend-rs"
+version = "0.10.5"
+source = "git+https://github.com/cbrewster/fuse-backend-rs.git?branch=optional-allow_other#b553ce526a7267578ae5af4f4cbba717932518ac"
 dependencies = [
+ "arc-swap",
+ "bitflags",
+ "caps",
+ "core-foundation-sys",
+ "lazy_static",
  "libc",
  "log",
- "memchr",
- "page_size",
- "pkg-config",
- "smallvec",
- "users",
- "zerocopy",
+ "mio",
+ "nix 0.24.3",
+ "vm-memory",
+ "vmm-sys-util",
 ]
 
 [[package]]
@@ -1328,6 +1358,15 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "memoffset"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
@@ -1379,6 +1418,18 @@ dependencies = [
 
 [[package]]
 name = "nix"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+ "memoffset 0.6.5",
+]
+
+[[package]]
+name = "nix"
 version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
@@ -1501,24 +1552,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
 [[package]]
-name = "page_size"
-version = "0.4.2"
+name = "parking_lot"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
 dependencies = [
- "libc",
- "winapi",
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
 ]
 
 [[package]]
 name = "parking_lot"
-version = "0.11.2"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
- "instant",
  "lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.9.8",
 ]
 
 [[package]]
@@ -1536,6 +1587,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets 0.48.0",
+]
+
+[[package]]
 name = "path-clean"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1590,12 +1654,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
-name = "pkg-config"
-version = "0.3.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
-
-[[package]]
 name = "plotters"
 version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1999,7 +2057,7 @@ checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf"
 dependencies = [
  "countme",
  "hashbrown",
- "memoffset",
+ "memoffset 0.8.0",
  "rustc-hash",
  "text-size",
 ]
@@ -2062,7 +2120,7 @@ dependencies = [
  "libc",
  "log",
  "memchr",
- "nix",
+ "nix 0.25.1",
  "radix_trie",
  "scopeguard",
  "unicode-segmentation",
@@ -2207,7 +2265,7 @@ dependencies = [
  "fxhash",
  "libc",
  "log",
- "parking_lot",
+ "parking_lot 0.11.2",
  "zstd",
 ]
 
@@ -2869,13 +2927,15 @@ dependencies = [
  "blake3",
  "bytes",
  "clap 4.2.7",
+ "const-zero",
  "count-write",
  "data-encoding",
- "fuser",
+ "fuse-backend-rs",
  "futures",
  "lazy_static",
  "libc",
  "nix-compat",
+ "parking_lot 0.12.1",
  "pin-project-lite",
  "prost",
  "prost-build",
@@ -2964,16 +3024,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "users"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
-dependencies = [
- "libc",
- "log",
-]
-
-[[package]]
 name = "utf8parse"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2992,6 +3042,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
+name = "vm-memory"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688a70366615b45575a424d9c665561c1b5ab2224d494f706b6a6812911a827c"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "vmm-sys-util"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd64fe09d8e880e600c324e7d664760a17f56e9672b7495a86381b49e4f72f46"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
 name = "wait-timeout"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3282,27 +3352,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
-name = "zerocopy"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236"
-dependencies = [
- "byteorder",
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6505e6815af7de1746a08f69c69606bb45695a17149517680f3b2149713b19a3"
-dependencies = [
- "proc-macro2 1.0.56",
- "quote 1.0.26",
- "syn 1.0.109",
-]
-
-[[package]]
 name = "zstd"
 version = "0.9.2+zstd.1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix
index e967b345e26f..cd9570c48ac5 100644
--- a/tvix/Cargo.nix
+++ b/tvix/Cargo.nix
@@ -336,6 +336,18 @@ rec {
         };
         resolvedDefaultFeatures = [ "default" "std" ];
       };
+      "arc-swap" = rec {
+        crateName = "arc-swap";
+        version = "1.6.0";
+        edition = "2018";
+        sha256 = "19n9j146bpxs9phyh48gmlh9jjsdijr9p9br04qms0g9ypfsvp5x";
+        authors = [
+          "Michal 'vorner' Vaner <vorner@vorner.cz>"
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+      };
       "arrayref" = rec {
         crateName = "arrayref";
         version = "0.3.7";
@@ -1020,6 +1032,29 @@ rec {
         };
         resolvedDefaultFeatures = [ "default" "std" ];
       };
+      "caps" = rec {
+        crateName = "caps";
+        version = "0.5.5";
+        edition = "2018";
+        sha256 = "02vk0w48rncgvfmj2mz2kpzvdgc14z225451w7lvvkwvaansl2qr";
+        authors = [
+          "Luca Bruno <lucab@lucabruno.net>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde_support" = [ "serde" ];
+        };
+      };
       "cast" = rec {
         crateName = "cast";
         version = "0.3.0";
@@ -1399,6 +1434,16 @@ rec {
         sha256 = "1ix7w85kwvyybwi2jdkl3yva2r2bvdcc3ka2grjfzfgrapqimgxc";
 
       };
+      "const-zero" = rec {
+        crateName = "const-zero";
+        version = "0.1.1";
+        edition = "2018";
+        sha256 = "1bq6s34a8v01rx6cpy3zslycgssmmasbkgm0blif6vwq4iamdim3";
+        authors = [
+          "Max Blachman <blachmanmax@gmail.com>"
+        ];
+
+      };
       "constant_time_eq" = rec {
         crateName = "constant_time_eq";
         version = "0.2.5";
@@ -1409,6 +1454,16 @@ rec {
         ];
 
       };
+      "core-foundation-sys" = rec {
+        crateName = "core-foundation-sys";
+        version = "0.8.4";
+        edition = "2015";
+        sha256 = "1yhf471qj6snnm2mcswai47vsbc9w30y4abmdp4crb4av87sb5p4";
+        authors = [
+          "The Servo Project Developers"
+        ];
+        features = { };
+      };
       "count-write" = rec {
         crateName = "count-write";
         version = "0.1.0";
@@ -1692,7 +1747,7 @@ rec {
           }
           {
             name = "memoffset";
-            packageId = "memoffset";
+            packageId = "memoffset 0.8.0";
           }
           {
             name = "scopeguard";
@@ -2211,16 +2266,47 @@ rec {
         ];
 
       };
-      "fuser" = rec {
-        crateName = "fuser";
-        version = "0.12.0";
+      "fuse-backend-rs" = rec {
+        crateName = "fuse-backend-rs";
+        version = "0.10.5";
         edition = "2018";
-        sha256 = "1xxalmhjhq54yqribnskdblj7lf24n80455vm3mwdk6f1qd6j42r";
+        workspace_member = null;
+        src = pkgs.fetchgit {
+          url = "https://github.com/cbrewster/fuse-backend-rs.git";
+          rev = "b553ce526a7267578ae5af4f4cbba717932518ac";
+          sha256 = "0165g94bcwyg03yi4kbr6yw75p42kiz2vd1d7s8f3fj4injy49vx";
+        };
         authors = [
-          "Christopher Berner <christopherberner@gmail.com>"
+          "Liu Bo <bo.liu@linux.alibaba.com>"
+          "Liu Jiang <gerry@linux.alibaba.com>"
+          "Peng Tao <bergwolf@hyper.sh>"
         ];
         dependencies = [
           {
+            name = "arc-swap";
+            packageId = "arc-swap";
+          }
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "caps";
+            packageId = "caps";
+            optional = true;
+            target = { target, features }: ("linux" == target."os");
+          }
+          {
+            name = "core-foundation-sys";
+            packageId = "core-foundation-sys";
+            optional = true;
+            target = { target, features }: ("macos" == target."os");
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
             name = "libc";
             packageId = "libc";
           }
@@ -2229,63 +2315,53 @@ rec {
             packageId = "log";
           }
           {
-            name = "memchr";
-            packageId = "memchr";
+            name = "mio";
+            packageId = "mio";
+            features = [ "os-poll" "os-ext" ];
           }
           {
-            name = "page_size";
-            packageId = "page_size";
+            name = "nix";
+            packageId = "nix 0.24.3";
           }
           {
-            name = "smallvec";
-            packageId = "smallvec";
+            name = "vm-memory";
+            packageId = "vm-memory";
+            features = [ "backend-mmap" ];
           }
           {
-            name = "users";
-            packageId = "users";
+            name = "vmm-sys-util";
+            packageId = "vmm-sys-util";
+            optional = true;
           }
+        ];
+        devDependencies = [
           {
-            name = "zerocopy";
-            packageId = "zerocopy";
+            name = "vm-memory";
+            packageId = "vm-memory";
+            features = [ "backend-mmap" "backend-bitmap" ];
           }
-        ];
-        buildDependencies = [
           {
-            name = "pkg-config";
-            packageId = "pkg-config";
-            optional = true;
-          }
-        ];
-        features = {
-          "abi-7-10" = [ "abi-7-9" ];
-          "abi-7-11" = [ "abi-7-10" ];
-          "abi-7-12" = [ "abi-7-11" ];
-          "abi-7-13" = [ "abi-7-12" ];
-          "abi-7-14" = [ "abi-7-13" ];
-          "abi-7-15" = [ "abi-7-14" ];
-          "abi-7-16" = [ "abi-7-15" ];
-          "abi-7-17" = [ "abi-7-16" ];
-          "abi-7-18" = [ "abi-7-17" ];
-          "abi-7-19" = [ "abi-7-18" ];
-          "abi-7-20" = [ "abi-7-19" ];
-          "abi-7-21" = [ "abi-7-20" ];
-          "abi-7-22" = [ "abi-7-21" ];
-          "abi-7-23" = [ "abi-7-22" ];
-          "abi-7-24" = [ "abi-7-23" ];
-          "abi-7-25" = [ "abi-7-24" ];
-          "abi-7-26" = [ "abi-7-25" ];
-          "abi-7-27" = [ "abi-7-26" ];
-          "abi-7-28" = [ "abi-7-27" ];
-          "abi-7-29" = [ "abi-7-28" ];
-          "abi-7-30" = [ "abi-7-29" ];
-          "abi-7-31" = [ "abi-7-30" ];
-          "default" = [ "libfuse" ];
-          "libfuse" = [ "pkg-config" ];
-          "pkg-config" = [ "dep:pkg-config" ];
-          "serde" = [ "dep:serde" ];
-          "serializable" = [ "serde" ];
+            name = "vmm-sys-util";
+            packageId = "vmm-sys-util";
+          }
+        ];
+        features = {
+          "async-io" = [ "async-trait" "tokio-uring" "tokio/fs" "tokio/net" "tokio/sync" "tokio/rt" "tokio/macros" "io-uring" ];
+          "async-trait" = [ "dep:async-trait" ];
+          "caps" = [ "dep:caps" ];
+          "core-foundation-sys" = [ "dep:core-foundation-sys" ];
+          "default" = [ "fusedev" ];
+          "fusedev" = [ "vmm-sys-util" "caps" "core-foundation-sys" ];
+          "io-uring" = [ "dep:io-uring" ];
+          "tokio" = [ "dep:tokio" ];
+          "tokio-uring" = [ "dep:tokio-uring" ];
+          "vhost" = [ "dep:vhost" ];
+          "vhost-user-fs" = [ "virtiofs" "vhost" "caps" ];
+          "virtio-queue" = [ "dep:virtio-queue" ];
+          "virtiofs" = [ "virtio-queue" "caps" "vmm-sys-util" ];
+          "vmm-sys-util" = [ "dep:vmm-sys-util" ];
         };
-        resolvedDefaultFeatures = [ "default" "libfuse" "pkg-config" ];
+        resolvedDefaultFeatures = [ "caps" "core-foundation-sys" "default" "fusedev" "vmm-sys-util" ];
       };
       "futures" = rec {
         crateName = "futures";
@@ -3817,7 +3893,24 @@ rec {
         };
         resolvedDefaultFeatures = [ "default" "std" ];
       };
-      "memoffset" = rec {
+      "memoffset 0.6.5" = rec {
+        crateName = "memoffset";
+        version = "0.6.5";
+        edition = "2015";
+        sha256 = "1kkrzll58a3ayn5zdyy9i1f1v3mx0xgl29x0chq614zazba638ss";
+        authors = [
+          "Gilad Naaman <gilad.naaman@gmail.com>"
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "memoffset 0.8.0" = rec {
         crateName = "memoffset";
         version = "0.8.0";
         edition = "2015";
@@ -3910,7 +4003,7 @@ rec {
         features = {
           "os-ext" = [ "os-poll" "windows-sys/Win32_System_Pipes" "windows-sys/Win32_Security" ];
         };
-        resolvedDefaultFeatures = [ "net" "os-ext" "os-poll" ];
+        resolvedDefaultFeatures = [ "default" "net" "os-ext" "os-poll" ];
       };
       "multimap" = rec {
         crateName = "multimap";
@@ -3942,7 +4035,53 @@ rec {
         ];
 
       };
-      "nix" = rec {
+      "nix 0.24.3" = rec {
+        crateName = "nix";
+        version = "0.24.3";
+        edition = "2018";
+        sha256 = "0sc0yzdl51b49bqd9l9cmimp1sw1hxb8iyv4d35ww6d7m5rfjlps";
+        authors = [
+          "The nix-rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            features = [ "extra_traits" ];
+          }
+          {
+            name = "memoffset";
+            packageId = "memoffset 0.6.5";
+            optional = true;
+            target = { target, features }: (!("redox" == target."os"));
+          }
+        ];
+        features = {
+          "default" = [ "acct" "aio" "dir" "env" "event" "feature" "fs" "hostname" "inotify" "ioctl" "kmod" "mman" "mount" "mqueue" "net" "personality" "poll" "process" "pthread" "ptrace" "quota" "reboot" "resource" "sched" "signal" "socket" "term" "time" "ucontext" "uio" "user" "zerocopy" ];
+          "dir" = [ "fs" ];
+          "memoffset" = [ "dep:memoffset" ];
+          "mount" = [ "uio" ];
+          "mqueue" = [ "fs" ];
+          "net" = [ "socket" ];
+          "ptrace" = [ "process" ];
+          "sched" = [ "process" ];
+          "signal" = [ "process" ];
+          "socket" = [ "memoffset" ];
+          "ucontext" = [ "signal" ];
+          "user" = [ "feature" ];
+          "zerocopy" = [ "fs" "uio" ];
+        };
+        resolvedDefaultFeatures = [ "acct" "aio" "default" "dir" "env" "event" "feature" "fs" "hostname" "inotify" "ioctl" "kmod" "memoffset" "mman" "mount" "mqueue" "net" "personality" "poll" "process" "pthread" "ptrace" "quota" "reboot" "resource" "sched" "signal" "socket" "term" "time" "ucontext" "uio" "user" "zerocopy" ];
+      };
+      "nix 0.25.1" = rec {
         crateName = "nix";
         version = "0.25.1";
         edition = "2018";
@@ -4299,52 +4438,55 @@ rec {
         ];
 
       };
-      "page_size" = rec {
-        crateName = "page_size";
-        version = "0.4.2";
-        edition = "2015";
-        sha256 = "1kgdv7f626jy4i2pq8czp4ppady4g4kqfa5ik4dah7mzzd4fbggf";
+      "parking_lot 0.11.2" = rec {
+        crateName = "parking_lot";
+        version = "0.11.2";
+        edition = "2018";
+        sha256 = "16gzf41bxmm10x82bla8d6wfppy9ym3fxsmdjyvn61m66s0bf5vx";
         authors = [
-          "Philip Woods <elzairthesorcerer@gmail.com>"
+          "Amanieu d'Antras <amanieu@gmail.com>"
         ];
         dependencies = [
           {
-            name = "libc";
-            packageId = "libc";
-            target = { target, features }: (target."unix" or false);
+            name = "instant";
+            packageId = "instant";
           }
           {
-            name = "winapi";
-            packageId = "winapi";
-            target = { target, features }: (target."windows" or false);
-            features = [ "sysinfoapi" ];
+            name = "lock_api";
+            packageId = "lock_api";
+          }
+          {
+            name = "parking_lot_core";
+            packageId = "parking_lot_core 0.8.6";
           }
         ];
         features = {
-          "no_std" = [ "spin" ];
-          "spin" = [ "dep:spin" ];
+          "arc_lock" = [ "lock_api/arc_lock" ];
+          "deadlock_detection" = [ "parking_lot_core/deadlock_detection" ];
+          "nightly" = [ "parking_lot_core/nightly" "lock_api/nightly" ];
+          "owning_ref" = [ "lock_api/owning_ref" ];
+          "serde" = [ "lock_api/serde" ];
+          "stdweb" = [ "instant/stdweb" ];
+          "wasm-bindgen" = [ "instant/wasm-bindgen" ];
         };
+        resolvedDefaultFeatures = [ "default" ];
       };
-      "parking_lot" = rec {
+      "parking_lot 0.12.1" = rec {
         crateName = "parking_lot";
-        version = "0.11.2";
+        version = "0.12.1";
         edition = "2018";
-        sha256 = "16gzf41bxmm10x82bla8d6wfppy9ym3fxsmdjyvn61m66s0bf5vx";
+        sha256 = "13r2xk7mnxfc5g0g6dkdxqdqad99j7s7z8zhzz4npw5r0g0v4hip";
         authors = [
           "Amanieu d'Antras <amanieu@gmail.com>"
         ];
         dependencies = [
           {
-            name = "instant";
-            packageId = "instant";
-          }
-          {
             name = "lock_api";
             packageId = "lock_api";
           }
           {
             name = "parking_lot_core";
-            packageId = "parking_lot_core";
+            packageId = "parking_lot_core 0.9.8";
           }
         ];
         features = {
@@ -4353,12 +4495,10 @@ rec {
           "nightly" = [ "parking_lot_core/nightly" "lock_api/nightly" ];
           "owning_ref" = [ "lock_api/owning_ref" ];
           "serde" = [ "lock_api/serde" ];
-          "stdweb" = [ "instant/stdweb" ];
-          "wasm-bindgen" = [ "instant/wasm-bindgen" ];
         };
         resolvedDefaultFeatures = [ "default" ];
       };
-      "parking_lot_core" = rec {
+      "parking_lot_core 0.8.6" = rec {
         crateName = "parking_lot_core";
         version = "0.8.6";
         edition = "2018";
@@ -4403,6 +4543,46 @@ rec {
           "thread-id" = [ "dep:thread-id" ];
         };
       };
+      "parking_lot_core 0.9.8" = rec {
+        crateName = "parking_lot_core";
+        version = "0.9.8";
+        edition = "2018";
+        sha256 = "0ixlak319bpzldq20yvyfqk0y1vi736zxbw101jvzjp7by30rw4k";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "redox_syscall";
+            packageId = "redox_syscall 0.3.5";
+            target = { target, features }: ("redox" == target."os");
+          }
+          {
+            name = "smallvec";
+            packageId = "smallvec";
+          }
+          {
+            name = "windows-targets";
+            packageId = "windows-targets 0.48.0";
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+        features = {
+          "backtrace" = [ "dep:backtrace" ];
+          "deadlock_detection" = [ "petgraph" "thread-id" "backtrace" ];
+          "petgraph" = [ "dep:petgraph" ];
+          "thread-id" = [ "dep:thread-id" ];
+        };
+      };
       "path-clean" = rec {
         crateName = "path-clean";
         version = "0.1.0";
@@ -4511,16 +4691,6 @@ rec {
         ];
 
       };
-      "pkg-config" = rec {
-        crateName = "pkg-config";
-        version = "0.3.27";
-        edition = "2015";
-        sha256 = "0r39ryh1magcq4cz5g9x88jllsnxnhcqr753islvyk4jp9h2h1r6";
-        authors = [
-          "Alex Crichton <alex@alexcrichton.com>"
-        ];
-
-      };
       "plotters" = rec {
         crateName = "plotters";
         version = "0.3.4";
@@ -5682,7 +5852,7 @@ rec {
           }
           {
             name = "memoffset";
-            packageId = "memoffset";
+            packageId = "memoffset 0.8.0";
           }
           {
             name = "rustc-hash";
@@ -5937,7 +6107,7 @@ rec {
           }
           {
             name = "nix";
-            packageId = "nix";
+            packageId = "nix 0.25.1";
             usesDefaultFeatures = false;
             target = { target, features }: (target."unix" or false);
             features = [ "fs" "ioctl" "poll" "signal" "term" ];
@@ -6363,7 +6533,7 @@ rec {
           }
           {
             name = "parking_lot";
-            packageId = "parking_lot";
+            packageId = "parking_lot 0.11.2";
           }
           {
             name = "zstd";
@@ -8548,6 +8718,10 @@ rec {
             features = [ "derive" "env" ];
           }
           {
+            name = "const-zero";
+            packageId = "const-zero";
+          }
+          {
             name = "count-write";
             packageId = "count-write";
           }
@@ -8556,8 +8730,8 @@ rec {
             packageId = "data-encoding";
           }
           {
-            name = "fuser";
-            packageId = "fuser";
+            name = "fuse-backend-rs";
+            packageId = "fuse-backend-rs";
             optional = true;
           }
           {
@@ -8578,6 +8752,10 @@ rec {
             packageId = "nix-compat";
           }
           {
+            name = "parking_lot";
+            packageId = "parking_lot 0.12.1";
+          }
+          {
             name = "pin-project-lite";
             packageId = "pin-project-lite";
           }
@@ -8681,7 +8859,7 @@ rec {
         ];
         features = {
           "default" = [ "fuse" "reflection" ];
-          "fuse" = [ "dep:fuser" "dep:libc" ];
+          "fuse" = [ "dep:libc" "dep:fuse-backend-rs" ];
           "reflection" = [ "tonic-reflection" ];
           "tonic-reflection" = [ "dep:tonic-reflection" ];
         };
@@ -8826,33 +9004,6 @@ rec {
         };
         resolvedDefaultFeatures = [ "default" ];
       };
-      "users" = rec {
-        crateName = "users";
-        version = "0.11.0";
-        edition = "2015";
-        sha256 = "0cmhafhhka2yya66yrprlv33kg7rm1xh1pyalbjp6yr6dxnhzk14";
-        authors = [
-          "Benjamin Sago <ogham@bsago.me>"
-        ];
-        dependencies = [
-          {
-            name = "libc";
-            packageId = "libc";
-          }
-          {
-            name = "log";
-            packageId = "log";
-            optional = true;
-            usesDefaultFeatures = false;
-          }
-        ];
-        features = {
-          "default" = [ "cache" "mock" "logging" ];
-          "log" = [ "dep:log" ];
-          "logging" = [ "log" ];
-        };
-        resolvedDefaultFeatures = [ "cache" "default" "log" "logging" "mock" ];
-      };
       "utf8parse" = rec {
         crateName = "utf8parse";
         version = "0.2.1";
@@ -8888,6 +9039,57 @@ rec {
         ];
 
       };
+      "vm-memory" = rec {
+        crateName = "vm-memory";
+        version = "0.10.0";
+        edition = "2021";
+        sha256 = "0z423a8i4s3addq4yjad4ar5l6qwarjwdn94lismbd0mcqv712k8";
+        authors = [
+          "Liu Jiang <gerry@linux.alibaba.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "errhandlingapi" "sysinfoapi" ];
+          }
+        ];
+        features = {
+          "arc-swap" = [ "dep:arc-swap" ];
+          "backend-atomic" = [ "arc-swap" ];
+        };
+        resolvedDefaultFeatures = [ "backend-mmap" "default" ];
+      };
+      "vmm-sys-util" = rec {
+        crateName = "vmm-sys-util";
+        version = "0.11.1";
+        edition = "2021";
+        sha256 = "0iigyzj4j6rqhrd4kdvjjrpga5qafrjddrr4qc0fd078v04zwr6x";
+        authors = [
+          "Intel Virtualization Team <vmm-maintainers@intel.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+            target = { target, features }: (("linux" == target."os") || ("android" == target."os"));
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde_derive" = [ "dep:serde_derive" ];
+          "with-serde" = [ "serde" "serde_derive" ];
+        };
+      };
       "wait-timeout" = rec {
         crateName = "wait-timeout";
         version = "0.2.0";
@@ -10532,55 +10734,6 @@ rec {
         ];
 
       };
-      "zerocopy" = rec {
-        crateName = "zerocopy";
-        version = "0.6.1";
-        edition = "2018";
-        sha256 = "0dpj4nd9v56wy93ahjkp95znjzj91waqvidqch8gxwdwq661hbrk";
-        authors = [
-          "Joshua Liebow-Feeser <joshlf@google.com>"
-        ];
-        dependencies = [
-          {
-            name = "byteorder";
-            packageId = "byteorder";
-            usesDefaultFeatures = false;
-          }
-          {
-            name = "zerocopy-derive";
-            packageId = "zerocopy-derive";
-          }
-        ];
-        features = {
-          "simd-nightly" = [ "simd" ];
-        };
-      };
-      "zerocopy-derive" = rec {
-        crateName = "zerocopy-derive";
-        version = "0.3.2";
-        edition = "2018";
-        sha256 = "18qr7dqlj89v1xl1g58l2xd6jidv0sbccscgl131gpppba0yc1b5";
-        procMacro = true;
-        authors = [
-          "Joshua Liebow-Feeser <joshlf@google.com>"
-        ];
-        dependencies = [
-          {
-            name = "proc-macro2";
-            packageId = "proc-macro2 1.0.56";
-          }
-          {
-            name = "quote";
-            packageId = "quote 1.0.26";
-          }
-          {
-            name = "syn";
-            packageId = "syn 1.0.109";
-            features = [ "visit" ];
-          }
-        ];
-
-      };
       "zstd" = rec {
         crateName = "zstd";
         version = "0.9.2+zstd.1.5.1";
diff --git a/tvix/crate-hashes.json b/tvix/crate-hashes.json
index 3e5b692e0b4b..360ab6937fef 100644
--- a/tvix/crate-hashes.json
+++ b/tvix/crate-hashes.json
@@ -1,4 +1,5 @@
 {
+  "fuse-backend-rs 0.10.5 (git+https://github.com/cbrewster/fuse-backend-rs.git?branch=optional-allow_other#b553ce526a7267578ae5af4f4cbba717932518ac)": "0165g94bcwyg03yi4kbr6yw75p42kiz2vd1d7s8f3fj4injy49vx",
   "test-generator 0.3.0 (git+https://github.com/JamesGuthrie/test-generator.git?rev=82e799979980962aec1aa324ec6e0e4cad781f41#82e799979980962aec1aa324ec6e0e4cad781f41)": "08brp3qqa55hijc7xby3lam2cc84hvx1zzfqv6lj7smlczh8k32y",
   "tonic-mock 0.1.0 (git+https://github.com/brainrake/tonic-mock?branch=bump-dependencies#ec1a15510875de99d709d684190db5d9beab175e)": "0lwa03hpp0mxa6aa1zv5w68k61y4hccfm0q2ykyq392fwal8vb50",
   "wu-manber 0.1.0 (git+https://github.com/tvlfyi/wu-manber.git#0d5b22bea136659f7de60b102a7030e0daaa503d)": "1zhk83lbq99xzyjwphv2qrb8f8qgfqwa5bbbvyzm0z0bljsjv0pd"
diff --git a/tvix/default.nix b/tvix/default.nix
index f5f1ca47cbc9..e12ff9522f01 100644
--- a/tvix/default.nix
+++ b/tvix/default.nix
@@ -28,11 +28,6 @@ let
         nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ iconvDarwinDep;
       };
 
-      fuser = prev: {
-        buildInputs = prev.buildInputs or [ ] ++ [ pkgs.fuse ];
-        nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [ pkgs.pkg-config ];
-      };
-
       prost-build = prev: {
         nativeBuildInputs = protobufDep prev;
       };
@@ -59,6 +54,7 @@ let
         (crateName:
           (lib.nameValuePair "${crateName}-${crates.internal.crates.${crateName}.version}" crates.internal.crates.${crateName}.src.outputHash)
         ) [
+        "fuse-backend-rs"
         "test-generator"
         "tonic-mock"
         "wu-manber"
diff --git a/tvix/store/Cargo.toml b/tvix/store/Cargo.toml
index 3fabf4f84bd5..cd71555851ff 100644
--- a/tvix/store/Cargo.toml
+++ b/tvix/store/Cargo.toml
@@ -30,10 +30,14 @@ smol_str = "0.2.0"
 serde_json = "1.0"
 url = "2.4.0"
 pin-project-lite = "0.2.13"
+const-zero = "0.1.1"
+parking_lot = "0.12.1"
 
-[dependencies.fuser]
+[dependencies.fuse-backend-rs]
 optional = true
-version = "0.12.0"
+# TODO: Switch back to upstream version once https://github.com/cloud-hypervisor/fuse-backend-rs/pull/153 lands.
+git = "https://github.com/cbrewster/fuse-backend-rs.git"
+branch = "optional-allow_other"
 
 [dependencies.tonic-reflection]
 optional = true
@@ -54,5 +58,5 @@ tonic-mock = { git = "https://github.com/brainrake/tonic-mock", branch = "bump-d
 
 [features]
 default = ["fuse", "reflection"]
-fuse = ["dep:fuser", "dep:libc"]
+fuse = ["dep:libc", "dep:fuse-backend-rs"]
 reflection = ["tonic-reflection"]
diff --git a/tvix/store/src/bin/tvix-store.rs b/tvix/store/src/bin/tvix-store.rs
index 0da264808efb..f707e3850d4f 100644
--- a/tvix/store/src/bin/tvix-store.rs
+++ b/tvix/store/src/bin/tvix-store.rs
@@ -22,7 +22,7 @@ use tvix_store::proto::GRPCPathInfoServiceWrapper;
 use tvix_store::proto::NamedNode;
 use tvix_store::proto::NarInfo;
 use tvix_store::proto::PathInfo;
-use tvix_store::FUSE;
+use tvix_store::{FuseDaemon, FUSE};
 
 #[cfg(feature = "reflection")]
 use tvix_store::proto::FILE_DESCRIPTOR_SET;
@@ -94,6 +94,10 @@ enum Commands {
         #[arg(long, env, default_value = "grpc+http://[::1]:8000")]
         path_info_service_addr: String,
 
+        /// Number of FUSE threads to spawn.
+        #[arg(long, env, default_value_t = default_threads())]
+        threads: usize,
+
         /// Whether to list elements at the root of the mount point.
         /// This is useful if your PathInfoService doesn't provide an
         /// (exhaustive) listing.
@@ -102,6 +106,13 @@ enum Commands {
     },
 }
 
+#[cfg(feature = "fuse")]
+fn default_threads() -> usize {
+    std::thread::available_parallelism()
+        .map(|threads| threads.into())
+        .unwrap_or(4)
+}
+
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let cli = Cli::parse();
@@ -280,6 +291,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
             directory_service_addr,
             path_info_service_addr,
             list_root,
+            threads,
         } => {
             let blob_service = blobservice::from_addr(&blob_service_addr)?;
             let directory_service = directoryservice::from_addr(&directory_service_addr)?;
@@ -289,35 +301,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
                 directory_service.clone(),
             )?;
 
-            let mut fuse_session = tokio::task::spawn_blocking(move || {
+            let mut fuse_daemon = tokio::task::spawn_blocking(move || {
                 let f = FUSE::new(
                     blob_service,
                     directory_service,
                     path_info_service,
                     list_root,
                 );
+                info!("mounting tvix-store on {:?}", &dest);
 
-                fuser::Session::new(f, &dest, &[])
+                FuseDaemon::new(f, &dest, threads)
             })
             .await??;
 
             // grab a handle to unmount the file system, and register a signal
             // handler.
-            let mut fuse_unmounter = fuse_session.unmount_callable();
             tokio::spawn(async move {
                 tokio::signal::ctrl_c().await.unwrap();
                 info!("interrupt received, unmountingโ€ฆ");
-                fuse_unmounter.unmount().unwrap();
-            });
-
-            // Start the fuse filesystem and wait for its completion, which
-            // happens when it's unmounted externally, or via the signal handler
-            // task.
-            tokio::task::spawn_blocking(move || -> io::Result<()> {
-                info!("mounting tvix-store on {:?}", fuse_session.mountpoint());
-                fuse_session.run()?;
+                fuse_daemon.unmount()?;
                 info!("unmount occured, terminatingโ€ฆ");
-                Ok(())
+                Ok::<_, io::Error>(())
             })
             .await??;
         }
diff --git a/tvix/store/src/fuse/file_attr.rs b/tvix/store/src/fuse/file_attr.rs
index 25cfd28dd1f9..b946aa977a0a 100644
--- a/tvix/store/src/fuse/file_attr.rs
+++ b/tvix/store/src/fuse/file_attr.rs
@@ -1,20 +1,19 @@
-use std::time::SystemTime;
-
 use super::inodes::{DirectoryInodeData, InodeData};
-use fuser::FileAttr;
+use fuse_backend_rs::abi::fuse_abi::Attr;
 
-/// The [FileAttr] describing the root
-pub const ROOT_FILE_ATTR: FileAttr = FileAttr {
-    ino: fuser::FUSE_ROOT_ID,
+/// The [Attr] describing the root
+pub const ROOT_FILE_ATTR: Attr = Attr {
+    ino: fuse_backend_rs::api::filesystem::ROOT_ID,
     size: 0,
     blksize: 1024,
     blocks: 0,
-    atime: SystemTime::UNIX_EPOCH,
-    mtime: SystemTime::UNIX_EPOCH,
-    ctime: SystemTime::UNIX_EPOCH,
-    crtime: SystemTime::UNIX_EPOCH,
-    kind: fuser::FileType::Directory,
-    perm: 0o555,
+    mode: libc::S_IFDIR | 0o555,
+    atime: 0,
+    mtime: 0,
+    ctime: 0,
+    atimensec: 0,
+    mtimensec: 0,
+    ctimensec: 0,
     nlink: 0,
     uid: 0,
     gid: 0,
@@ -22,10 +21,12 @@ pub const ROOT_FILE_ATTR: FileAttr = FileAttr {
     flags: 0,
 };
 
-/// for given &Node and inode, construct a [FileAttr]
-pub fn gen_file_attr(inode_data: &InodeData, inode: u64) -> FileAttr {
-    FileAttr {
+/// for given &Node and inode, construct an [Attr]
+pub fn gen_file_attr(inode_data: &InodeData, inode: u64) -> Attr {
+    Attr {
         ino: inode,
+        // FUTUREWORK: play with this numbers, as it affects read sizes for client applications.
+        blocks: 1024,
         size: match inode_data {
             InodeData::Regular(_, size, _) => *size as u64,
             InodeData::Symlink(target) => target.len() as u64,
@@ -34,24 +35,12 @@ pub fn gen_file_attr(inode_data: &InodeData, inode: u64) -> FileAttr {
                 children.len() as u64
             }
         },
-        // FUTUREWORK: play with this numbers, as it affects read sizes for client applications.
-        blksize: 1024,
-        blocks: 0,
-        atime: SystemTime::UNIX_EPOCH,
-        mtime: SystemTime::UNIX_EPOCH,
-        ctime: SystemTime::UNIX_EPOCH,
-        crtime: SystemTime::UNIX_EPOCH,
-        kind: inode_data.into(),
-        perm: match inode_data {
-            InodeData::Regular(_, _, false) => 0o444, // no-executable files
-            InodeData::Regular(_, _, true) => 0o555,  // executable files
-            InodeData::Symlink(_) => 0o444,
-            InodeData::Directory(..) => 0o555,
+        mode: match inode_data {
+            InodeData::Regular(_, _, false) => libc::S_IFREG | 0o444, // no-executable files
+            InodeData::Regular(_, _, true) => libc::S_IFREG | 0o555,  // executable files
+            InodeData::Symlink(_) => libc::S_IFLNK | 0o444,
+            InodeData::Directory(_) => libc::S_IFDIR | 0o555,
         },
-        nlink: 0,
-        uid: 0,
-        gid: 0,
-        rdev: 0,
-        flags: 0,
+        ..Default::default()
     }
 }
diff --git a/tvix/store/src/fuse/inodes.rs b/tvix/store/src/fuse/inodes.rs
index f44dde7b804f..e8959ce3629b 100644
--- a/tvix/store/src/fuse/inodes.rs
+++ b/tvix/store/src/fuse/inodes.rs
@@ -66,13 +66,3 @@ impl From<proto::Directory> for InodeData {
         InodeData::Directory(DirectoryInodeData::Populated(digest, children))
     }
 }
-
-impl From<&InodeData> for fuser::FileType {
-    fn from(val: &InodeData) -> Self {
-        match val {
-            InodeData::Regular(..) => fuser::FileType::RegularFile,
-            InodeData::Symlink(_) => fuser::FileType::Symlink,
-            InodeData::Directory(..) => fuser::FileType::Directory,
-        }
-    }
-}
diff --git a/tvix/store/src/fuse/mod.rs b/tvix/store/src/fuse/mod.rs
index 978fd50e2634..1a5d884ef7a9 100644
--- a/tvix/store/src/fuse/mod.rs
+++ b/tvix/store/src/fuse/mod.rs
@@ -8,25 +8,34 @@ mod tests;
 use crate::{
     blobservice::{BlobReader, BlobService},
     directoryservice::DirectoryService,
-    fuse::{
-        file_attr::gen_file_attr,
-        inodes::{DirectoryInodeData, InodeData},
-    },
+    fuse::inodes::{DirectoryInodeData, InodeData},
     pathinfoservice::PathInfoService,
     proto::{node::Node, NamedNode},
     B3Digest, Error,
 };
-use fuser::{FileAttr, ReplyAttr, Request};
+use fuse_backend_rs::{
+    api::filesystem::{Context, FileSystem, FsOptions, ROOT_ID},
+    transport::FuseSession,
+};
 use nix_compat::store_path::StorePath;
-use std::io;
-use std::os::unix::ffi::OsStrExt;
-use std::str::FromStr;
-use std::sync::Arc;
-use std::{collections::HashMap, time::Duration};
+use parking_lot::RwLock;
+use std::{
+    collections::HashMap,
+    io,
+    path::Path,
+    str::FromStr,
+    sync::atomic::AtomicU64,
+    sync::{atomic::Ordering, Arc},
+    thread,
+    time::Duration,
+};
 use tokio::io::{AsyncBufReadExt, AsyncSeekExt};
-use tracing::{debug, info_span, warn};
+use tracing::{debug, error, info_span, warn};
 
-use self::inode_tracker::InodeTracker;
+use self::{
+    file_attr::{gen_file_attr, ROOT_FILE_ATTR},
+    inode_tracker::InodeTracker,
+};
 
 /// This implements a read-only FUSE filesystem for a tvix-store
 /// with the passed [BlobService], [DirectoryService] and [PathInfoService].
@@ -71,15 +80,15 @@ pub struct FUSE {
     list_root: bool,
 
     /// This maps a given StorePath to the inode we allocated for the root inode.
-    store_paths: HashMap<StorePath, u64>,
+    store_paths: RwLock<HashMap<StorePath, u64>>,
 
     /// This keeps track of inodes and data alongside them.
-    inode_tracker: InodeTracker,
+    inode_tracker: RwLock<InodeTracker>,
 
     /// This holds all open file handles
-    file_handles: HashMap<u64, Box<dyn BlobReader>>,
+    file_handles: RwLock<HashMap<u64, Arc<tokio::sync::Mutex<Box<dyn BlobReader>>>>>,
 
-    next_file_handle: u64,
+    next_file_handle: AtomicU64,
 
     tokio_handle: tokio::runtime::Handle,
 }
@@ -98,11 +107,11 @@ impl FUSE {
 
             list_root,
 
-            store_paths: HashMap::default(),
-            inode_tracker: Default::default(),
+            store_paths: RwLock::new(HashMap::default()),
+            inode_tracker: RwLock::new(Default::default()),
 
-            file_handles: Default::default(),
-            next_file_handle: 1,
+            file_handles: RwLock::new(Default::default()),
+            next_file_handle: AtomicU64::new(1),
             tokio_handle: tokio::runtime::Handle::current(),
         }
     }
@@ -114,11 +123,11 @@ impl FUSE {
     /// or otherwise fetch from [self.path_info_service], and then insert into
     /// [self.inode_tracker].
     fn name_in_root_to_ino_and_data(
-        &mut self,
-        name: &std::ffi::OsStr,
+        &self,
+        name: &std::ffi::CStr,
     ) -> Result<Option<(u64, Arc<InodeData>)>, Error> {
         // parse the name into a [StorePath].
-        let store_path = if let Some(name) = name.to_str() {
+        let store_path = if let Some(name) = name.to_str().ok() {
             match StorePath::from_str(name) {
                 Ok(store_path) => store_path,
                 Err(e) => {
@@ -129,19 +138,26 @@ impl FUSE {
                 }
             }
         } else {
-            debug!("{name:?} is no string");
+            debug!("{name:?} is not a valid utf-8 string");
             // same here.
             return Ok(None);
         };
 
-        if let Some(ino) = self.store_paths.get(&store_path) {
+        let ino = {
+            // This extra scope makes sure we drop the read lock
+            // immediately after reading, to prevent deadlocks.
+            let store_paths = self.store_paths.read();
+            store_paths.get(&store_path).cloned()
+        };
+
+        if let Some(ino) = ino {
             // If we already have that store path, lookup the inode from
             // self.store_paths and then get the data from [self.inode_tracker],
             // which in the case of a [InodeData::Directory] will be fully
             // populated.
             Ok(Some((
-                *ino,
-                self.inode_tracker.get(*ino).expect("must exist"),
+                ino,
+                self.inode_tracker.read().get(ino).expect("must exist"),
             )))
         } else {
             // If we don't have it, look it up in PathInfoService.
@@ -157,15 +173,29 @@ impl FUSE {
                         return Ok(None);
                     }
 
+                    // Let's check if someone else beat us to updating the inode tracker and
+                    // store_paths map.
+                    let mut store_paths = self.store_paths.write();
+                    if let Some(ino) = store_paths.get(&store_path).cloned() {
+                        return Ok(Some((
+                            ino,
+                            self.inode_tracker.read().get(ino).expect("must exist"),
+                        )));
+                    }
+
                     // insert the (sparse) inode data and register in
                     // self.store_paths.
                     // FUTUREWORK: change put to return the data after
                     // inserting, so we don't need to lookup a second
                     // time?
-                    let ino = self.inode_tracker.put((&root_node).into());
-                    self.store_paths.insert(store_path, ino);
+                    let (ino, inode) = {
+                        let mut inode_tracker = self.inode_tracker.write();
+                        let ino = inode_tracker.put((&root_node).into());
+                        (ino, inode_tracker.get(ino).unwrap())
+                    };
+                    store_paths.insert(store_path, ino);
 
-                    Ok(Some((ino, self.inode_tracker.get(ino).unwrap())))
+                    Ok(Some((ino, inode)))
                 }
             }
         }
@@ -194,136 +224,152 @@ impl FUSE {
     }
 }
 
-impl fuser::Filesystem for FUSE {
-    #[tracing::instrument(skip_all, fields(rq.inode = ino))]
-    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
-        debug!("getattr");
+impl FileSystem for FUSE {
+    type Inode = u64;
+    type Handle = u64;
+
+    fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
+        Ok(FsOptions::empty())
+    }
 
-        if ino == fuser::FUSE_ROOT_ID {
-            reply.attr(&Duration::MAX, &file_attr::ROOT_FILE_ATTR);
-            return;
+    #[tracing::instrument(skip_all, fields(rq.inode = inode))]
+    fn getattr(
+        &self,
+        _ctx: &Context,
+        inode: Self::Inode,
+        _handle: Option<Self::Handle>,
+    ) -> io::Result<(libc::stat64, Duration)> {
+        if inode == ROOT_ID {
+            return Ok((ROOT_FILE_ATTR.into(), Duration::MAX));
         }
 
-        match self.inode_tracker.get(ino) {
-            None => reply.error(libc::ENOENT),
+        match self.inode_tracker.read().get(inode) {
+            None => return Err(io::Error::from_raw_os_error(libc::ENOENT)),
             Some(node) => {
                 debug!(node = ?node, "found node");
-                reply.attr(&Duration::MAX, &file_attr::gen_file_attr(&node, ino));
+                Ok((gen_file_attr(&node, inode).into(), Duration::MAX))
             }
         }
     }
 
-    #[tracing::instrument(skip_all, fields(rq.parent_inode = parent_ino, rq.name = ?name))]
+    #[tracing::instrument(skip_all, fields(rq.parent_inode = parent, rq.name = ?name))]
     fn lookup(
-        &mut self,
-        _req: &Request,
-        parent_ino: u64,
-        name: &std::ffi::OsStr,
-        reply: fuser::ReplyEntry,
-    ) {
+        &self,
+        _ctx: &Context,
+        parent: Self::Inode,
+        name: &std::ffi::CStr,
+    ) -> io::Result<fuse_backend_rs::api::filesystem::Entry> {
         debug!("lookup");
 
         // This goes from a parent inode to a node.
-        // - If the parent is [fuser::FUSE_ROOT_ID], we need to check
+        // - If the parent is [ROOT_ID], we need to check
         //   [self.store_paths] (fetching from PathInfoService if needed)
         // - Otherwise, lookup the parent in [self.inode_tracker] (which must be
         //   a [InodeData::Directory]), and find the child with that name.
-        if parent_ino == fuser::FUSE_ROOT_ID {
-            match self.name_in_root_to_ino_and_data(name) {
+        if parent == ROOT_ID {
+            return match self.name_in_root_to_ino_and_data(name) {
                 Err(e) => {
                     warn!("{}", e);
-                    reply.error(libc::EIO);
-                }
-                Ok(None) => {
-                    reply.error(libc::ENOENT);
+                    Err(io::Error::from_raw_os_error(libc::ENOENT))
                 }
+                Ok(None) => Err(io::Error::from_raw_os_error(libc::ENOENT)),
                 Ok(Some((ino, inode_data))) => {
                     debug!(inode_data=?&inode_data, ino=ino, "Some");
-                    reply_with_entry(reply, &gen_file_attr(&inode_data, ino));
+                    Ok(fuse_backend_rs::api::filesystem::Entry {
+                        inode: ino,
+                        attr: gen_file_attr(&inode_data, ino).into(),
+                        attr_timeout: Duration::MAX,
+                        entry_timeout: Duration::MAX,
+                        ..Default::default()
+                    })
                 }
+            };
+        }
+
+        // This is the "lookup for "a" inside inode 42.
+        // We already know that inode 42 must be a directory.
+        // It might not be populated yet, so if it isn't, we do (by
+        // fetching from [self.directory_service]), and save the result in
+        // [self.inode_tracker].
+        // Now it for sure is populated, so we search for that name in the
+        // list of children and return the FileAttrs.
+
+        // TODO: Reduce the critical section of this write lock.
+        let mut inode_tracker = self.inode_tracker.write();
+        let parent_data = inode_tracker.get(parent).unwrap();
+        let parent_data = match *parent_data {
+            InodeData::Regular(..) | InodeData::Symlink(_) => {
+                // if the parent inode was not a directory, this doesn't make sense
+                return Err(io::Error::from_raw_os_error(libc::ENOTDIR));
             }
-        } else {
-            // This is the "lookup for "a" inside inode 42.
-            // We already know that inode 42 must be a directory.
-            // It might not be populated yet, so if it isn't, we do (by
-            // fetching from [self.directory_service]), and save the result in
-            // [self.inode_tracker].
-            // Now it for sure is populated, so we search for that name in the
-            // list of children and return the FileAttrs.
-
-            let parent_data = self.inode_tracker.get(parent_ino).unwrap();
-            let parent_data = match *parent_data {
-                InodeData::Regular(..) | InodeData::Symlink(_) => {
-                    // if the parent inode was not a directory, this doesn't make sense
-                    reply.error(libc::ENOTDIR);
-                    return;
-                }
-                InodeData::Directory(DirectoryInodeData::Sparse(ref parent_digest, _)) => {
-                    match self.fetch_directory_inode_data(parent_digest) {
-                        Ok(new_data) => {
-                            // update data in [self.inode_tracker] with populated variant.
-                            // FUTUREWORK: change put to return the data after
-                            // inserting, so we don't need to lookup a second
-                            // time?
-                            let ino = self.inode_tracker.put(new_data);
-                            self.inode_tracker.get(ino).unwrap()
-                        }
-                        Err(_e) => {
-                            reply.error(libc::EIO);
-                            return;
-                        }
+            InodeData::Directory(DirectoryInodeData::Sparse(ref parent_digest, _)) => {
+                match self.fetch_directory_inode_data(parent_digest) {
+                    Ok(new_data) => {
+                        // update data in [self.inode_tracker] with populated variant.
+                        // FUTUREWORK: change put to return the data after
+                        // inserting, so we don't need to lookup a second
+                        // time?
+                        let ino = inode_tracker.put(new_data);
+                        inode_tracker.get(ino).unwrap()
+                    }
+                    Err(_e) => {
+                        return Err(io::Error::from_raw_os_error(libc::EIO));
                     }
                 }
-                InodeData::Directory(DirectoryInodeData::Populated(..)) => parent_data,
-            };
-
-            // now parent_data can only be a [InodeData::Directory(DirectoryInodeData::Populated(..))].
-            let (parent_digest, children) = if let InodeData::Directory(
-                DirectoryInodeData::Populated(ref parent_digest, ref children),
-            ) = *parent_data
-            {
-                (parent_digest, children)
-            } else {
-                panic!("unexpected type")
-            };
-            let span = info_span!("lookup", directory.digest = %parent_digest);
-            let _enter = span.enter();
-
-            // in the children, find the one with the desired name.
-            if let Some((child_ino, _)) =
-                children.iter().find(|e| e.1.get_name() == name.as_bytes())
-            {
-                // lookup the child [InodeData] in [self.inode_tracker].
-                // We know the inodes for children have already been allocated.
-                let child_inode_data = self.inode_tracker.get(*child_ino).unwrap();
-
-                // Reply with the file attributes for the child.
-                // For child directories, we still have all data we need to reply.
-                reply_with_entry(reply, &gen_file_attr(&child_inode_data, *child_ino));
-            } else {
-                // Child not found, return ENOENT.
-                reply.error(libc::ENOENT);
             }
+            InodeData::Directory(DirectoryInodeData::Populated(..)) => parent_data,
+        };
+
+        // now parent_data can only be a [InodeData::Directory(DirectoryInodeData::Populated(..))].
+        let (parent_digest, children) = if let InodeData::Directory(
+            DirectoryInodeData::Populated(ref parent_digest, ref children),
+        ) = *parent_data
+        {
+            (parent_digest, children)
+        } else {
+            panic!("unexpected type")
+        };
+        let span = info_span!("lookup", directory.digest = %parent_digest);
+        let _enter = span.enter();
+
+        // in the children, find the one with the desired name.
+        if let Some((child_ino, _)) = children.iter().find(|e| e.1.get_name() == name.to_bytes()) {
+            // lookup the child [InodeData] in [self.inode_tracker].
+            // We know the inodes for children have already been allocated.
+            let child_inode_data = inode_tracker.get(*child_ino).unwrap();
+
+            // Reply with the file attributes for the child.
+            // For child directories, we still have all data we need to reply.
+            Ok(fuse_backend_rs::api::filesystem::Entry {
+                inode: *child_ino,
+                attr: gen_file_attr(&child_inode_data, *child_ino).into(),
+                attr_timeout: Duration::MAX,
+                entry_timeout: Duration::MAX,
+                ..Default::default()
+            })
+        } else {
+            // Child not found, return ENOENT.
+            Err(io::Error::from_raw_os_error(libc::ENOENT))
         }
     }
 
     // TODO: readdirplus?
 
-    #[tracing::instrument(skip_all, fields(rq.inode = ino, rq.offset = offset))]
+    #[tracing::instrument(skip_all, fields(rq.inode = inode, rq.offset = offset))]
     fn readdir(
-        &mut self,
-        _req: &Request<'_>,
-        ino: u64,
-        _fh: u64,
-        offset: i64,
-        mut reply: fuser::ReplyDirectory,
-    ) {
+        &self,
+        _ctx: &Context,
+        inode: Self::Inode,
+        _handle: Self::Handle,
+        _size: u32,
+        offset: u64,
+        add_entry: &mut dyn FnMut(fuse_backend_rs::api::filesystem::DirEntry) -> io::Result<usize>,
+    ) -> io::Result<()> {
         debug!("readdir");
 
-        if ino == fuser::FUSE_ROOT_ID {
+        if inode == ROOT_ID {
             if !self.list_root {
-                reply.error(libc::EPERM); // same error code as ipfs/kubo
-                return;
+                return Err(io::Error::from_raw_os_error(libc::EPERM)); // same error code as ipfs/kubo
             } else {
                 for (i, path_info) in self
                     .path_info_service
@@ -334,8 +380,7 @@ impl fuser::Filesystem for FUSE {
                     let path_info = match path_info {
                         Err(e) => {
                             warn!("failed to retrieve pathinfo: {}", e);
-                            reply.error(libc::EPERM);
-                            return;
+                            return Err(io::Error::from_raw_os_error(libc::EPERM));
                         }
                         Ok(path_info) => path_info,
                     };
@@ -344,41 +389,51 @@ impl fuser::Filesystem for FUSE {
                     let root_node = path_info.node.unwrap().node.unwrap();
                     let store_path = StorePath::from_bytes(root_node.get_name()).unwrap();
 
-                    let ino = match self.store_paths.get(&store_path) {
-                        Some(ino) => *ino,
+                    let ino = {
+                        // This extra scope makes sure we drop the read lock
+                        // immediately after reading, to prevent deadlocks.
+                        let store_paths = self.store_paths.read();
+                        store_paths.get(&store_path).cloned()
+                    };
+                    let ino = match ino {
+                        Some(ino) => ino,
                         None => {
                             // insert the (sparse) inode data and register in
                             // self.store_paths.
-                            let ino = self.inode_tracker.put((&root_node).into());
-                            self.store_paths.insert(store_path.clone(), ino);
+                            let ino = self.inode_tracker.write().put((&root_node).into());
+                            self.store_paths.write().insert(store_path.clone(), ino);
                             ino
                         }
                     };
 
                     let ty = match root_node {
-                        Node::Directory(_) => fuser::FileType::Directory,
-                        Node::File(_) => fuser::FileType::RegularFile,
-                        Node::Symlink(_) => fuser::FileType::Symlink,
+                        Node::Directory(_) => libc::S_IFDIR,
+                        Node::File(_) => libc::S_IFREG,
+                        Node::Symlink(_) => libc::S_IFLNK,
                     };
 
-                    let full =
-                        reply.add(ino, offset + i as i64 + 1_i64, ty, store_path.to_string());
-                    if full {
+                    let written = add_entry(fuse_backend_rs::api::filesystem::DirEntry {
+                        ino,
+                        offset: offset + i as u64 + 1,
+                        type_: ty,
+                        name: store_path.to_string().as_bytes(),
+                    })?;
+                    // If the buffer is full, add_entry will return `Ok(0)`.
+                    if written == 0 {
                         break;
                     }
                 }
-                reply.ok();
-                return;
+                return Ok(());
             }
         }
 
         // lookup the inode data.
-        let dir_inode_data = self.inode_tracker.get(ino).unwrap();
+        let mut inode_tracker = self.inode_tracker.write();
+        let dir_inode_data = inode_tracker.get(inode).unwrap();
         let dir_inode_data = match *dir_inode_data {
             InodeData::Regular(..) | InodeData::Symlink(..) => {
                 warn!("Not a directory");
-                reply.error(libc::ENOTDIR);
-                return;
+                return Err(io::Error::from_raw_os_error(libc::ENOTDIR));
             }
             InodeData::Directory(DirectoryInodeData::Sparse(ref directory_digest, _)) => {
                 match self.fetch_directory_inode_data(directory_digest) {
@@ -387,12 +442,11 @@ impl fuser::Filesystem for FUSE {
                         // FUTUREWORK: change put to return the data after
                         // inserting, so we don't need to lookup a second
                         // time?
-                        let ino = self.inode_tracker.put(new_data);
-                        self.inode_tracker.get(ino).unwrap()
+                        let ino = inode_tracker.put(new_data.clone());
+                        inode_tracker.get(ino).unwrap()
                     }
                     Err(_e) => {
-                        reply.error(libc::EIO);
-                        return;
+                        return Err(io::Error::from_raw_os_error(libc::EIO));
                     }
                 }
             }
@@ -405,42 +459,49 @@ impl fuser::Filesystem for FUSE {
         {
             for (i, (ino, child_node)) in children.iter().skip(offset as usize).enumerate() {
                 // the second parameter will become the "offset" parameter on the next call.
-                let full = reply.add(
-                    *ino,
-                    offset + i as i64 + 1_i64,
-                    match child_node {
-                        Node::Directory(_) => fuser::FileType::Directory,
-                        Node::File(_) => fuser::FileType::RegularFile,
-                        Node::Symlink(_) => fuser::FileType::Symlink,
+                let written = add_entry(fuse_backend_rs::api::filesystem::DirEntry {
+                    ino: *ino,
+                    offset: offset + i as u64 + 1,
+                    type_: match child_node {
+                        Node::Directory(_) => libc::S_IFDIR,
+                        Node::File(_) => libc::S_IFREG,
+                        Node::Symlink(_) => libc::S_IFLNK,
                     },
-                    std::ffi::OsStr::from_bytes(child_node.get_name()),
-                );
-                if full {
+                    name: child_node.get_name(),
+                })?;
+                // If the buffer is full, add_entry will return `Ok(0)`.
+                if written == 0 {
                     break;
                 }
             }
-            reply.ok();
         } else {
             panic!("unexpected type")
         }
-    }
 
-    #[tracing::instrument(skip_all, fields(rq.inode = ino))]
-    fn open(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) {
-        // get a new file handle
-        let fh = self.next_file_handle;
+        Ok(())
+    }
 
-        if ino == fuser::FUSE_ROOT_ID {
-            reply.error(libc::ENOSYS);
-            return;
+    #[tracing::instrument(skip_all, fields(rq.inode = inode))]
+    fn open(
+        &self,
+        _ctx: &Context,
+        inode: Self::Inode,
+        _flags: u32,
+        _fuse_flags: u32,
+    ) -> io::Result<(
+        Option<Self::Handle>,
+        fuse_backend_rs::api::filesystem::OpenOptions,
+    )> {
+        if inode == ROOT_ID {
+            return Err(io::Error::from_raw_os_error(libc::ENOSYS));
         }
 
         // lookup the inode
-        match *self.inode_tracker.get(ino).unwrap() {
+        match *self.inode_tracker.read().get(inode).unwrap() {
             // read is invalid on non-files.
             InodeData::Directory(..) | InodeData::Symlink(_) => {
                 warn!("is directory");
-                reply.error(libc::EISDIR);
+                return Err(io::Error::from_raw_os_error(libc::EISDIR));
             }
             InodeData::Regular(ref blob_digest, _blob_size, _) => {
                 let span = info_span!("read", blob.digest = %blob_digest);
@@ -458,79 +519,87 @@ impl fuser::Filesystem for FUSE {
                 match blob_reader {
                     Ok(None) => {
                         warn!("blob not found");
-                        reply.error(libc::EIO);
+                        return Err(io::Error::from_raw_os_error(libc::EIO));
                     }
                     Err(e) => {
                         warn!(e=?e, "error opening blob");
-                        reply.error(libc::EIO);
+                        return Err(io::Error::from_raw_os_error(libc::EIO));
                     }
                     Ok(Some(blob_reader)) => {
-                        debug!("add file handle {}", fh);
-                        self.file_handles.insert(fh, blob_reader);
-                        reply.opened(fh, 0);
-
+                        // get a new file handle
                         // TODO: this will overflow after 2**64 operations,
                         // which is fine for now.
                         // See https://cl.tvl.fyi/c/depot/+/8834/comment/a6684ce0_d72469d1
                         // for the discussion on alternatives.
-                        self.next_file_handle += 1;
+                        let fh = self.next_file_handle.fetch_add(1, Ordering::SeqCst);
+
+                        debug!("add file handle {}", fh);
+                        self.file_handles
+                            .write()
+                            .insert(fh, Arc::new(tokio::sync::Mutex::new(blob_reader)));
+
+                        Ok((
+                            Some(fh),
+                            fuse_backend_rs::api::filesystem::OpenOptions::empty(),
+                        ))
                     }
                 }
             }
         }
     }
 
-    #[tracing::instrument(skip_all, fields(rq.inode = ino, fh = fh))]
+    #[tracing::instrument(skip_all, fields(rq.inode = inode, fh = handle))]
     fn release(
-        &mut self,
-        _req: &Request<'_>,
-        ino: u64,
-        fh: u64,
-        _flags: i32,
-        _lock_owner: Option<u64>,
+        &self,
+        _ctx: &Context,
+        inode: Self::Inode,
+        _flags: u32,
+        handle: Self::Handle,
         _flush: bool,
-        reply: fuser::ReplyEmpty,
-    ) {
+        _flock_release: bool,
+        _lock_owner: Option<u64>,
+    ) -> io::Result<()> {
         // remove and get ownership on the blob reader
-        match self.file_handles.remove(&fh) {
+        match self.file_handles.write().remove(&handle) {
             // drop it, which will close it.
             Some(blob_reader) => drop(blob_reader),
             None => {
                 // These might already be dropped if a read error occured.
-                debug!("file_handle {} not found", fh);
+                debug!("file_handle {} not found", handle);
             }
         }
 
-        reply.ok();
+        Ok(())
     }
 
-    #[tracing::instrument(skip_all, fields(rq.inode = ino, rq.offset = offset, rq.size = size))]
+    #[tracing::instrument(skip_all, fields(rq.inode = inode, rq.offset = offset, rq.size = size))]
     fn read(
-        &mut self,
-        _req: &Request<'_>,
-        ino: u64,
-        fh: u64,
-        offset: i64,
+        &self,
+        _ctx: &Context,
+        inode: Self::Inode,
+        handle: Self::Handle,
+        w: &mut dyn fuse_backend_rs::api::filesystem::ZeroCopyWriter,
         size: u32,
-        _flags: i32,
+        offset: u64,
         _lock_owner: Option<u64>,
-        reply: fuser::ReplyData,
-    ) {
+        _flags: u32,
+    ) -> io::Result<usize> {
         debug!("read");
 
         // We need to take out the blob reader from self.file_handles, so we can
         // interact with it in the separate task.
         // On success, we pass it back out of the task, so we can put it back in self.file_handles.
-        let mut blob_reader = match self.file_handles.remove(&fh) {
-            Some(blob_reader) => blob_reader,
+        let blob_reader = match self.file_handles.read().get(&handle) {
+            Some(blob_reader) => blob_reader.clone(),
             None => {
-                warn!("file handle {} unknown", fh);
-                reply.error(libc::EIO);
-                return;
+                warn!("file handle {} unknown", handle);
+                return Err(io::Error::from_raw_os_error(libc::EIO));
             }
         };
 
         let task = self.tokio_handle.spawn(async move {
+            let mut blob_reader = blob_reader.lock().await;
+
             // seek to the offset specified, which is relative to the start of the file.
             let resp = blob_reader.seek(io::SeekFrom::Start(offset as u64)).await;
 
@@ -540,68 +609,163 @@ impl fuser::Filesystem for FUSE {
                 }
                 Err(e) => {
                     warn!("failed to seek to offset {}: {}", offset, e);
-                    return Err(libc::EIO);
+                    return Err(io::Error::from_raw_os_error(libc::EIO));
                 }
             }
 
-            // As written in the fuser docs, read should send exactly the number
+            // As written in the fuse docs, read should send exactly the number
             // of bytes requested except on EOF or error.
 
             let mut buf: Vec<u8> = Vec::with_capacity(size as usize);
 
             while (buf.len() as u64) < size as u64 {
-                match blob_reader.fill_buf().await {
-                    Ok(int_buf) => {
-                        // copy things from the internal buffer into buf to fill it till up until size
+                let int_buf = blob_reader.fill_buf().await?;
+                // copy things from the internal buffer into buf to fill it till up until size
 
-                        // an empty buffer signals we reached EOF.
-                        if int_buf.is_empty() {
-                            break;
-                        }
+                // an empty buffer signals we reached EOF.
+                if int_buf.is_empty() {
+                    break;
+                }
 
-                        // calculate how many bytes we can read from int_buf.
-                        // It's either all of int_buf, or the number of bytes missing in buf to reach size.
-                        let len_to_copy = std::cmp::min(int_buf.len(), size as usize - buf.len());
+                // calculate how many bytes we can read from int_buf.
+                // It's either all of int_buf, or the number of bytes missing in buf to reach size.
+                let len_to_copy = std::cmp::min(int_buf.len(), size as usize - buf.len());
 
-                        // copy these bytes into our buffer
-                        buf.extend_from_slice(&int_buf[..len_to_copy]);
-                        // and consume them in the buffered reader.
-                        blob_reader.consume(len_to_copy);
-                    }
-                    Err(e) => return Err(e.raw_os_error().unwrap()),
-                }
+                // copy these bytes into our buffer
+                buf.extend_from_slice(&int_buf[..len_to_copy]);
+                // and consume them in the buffered reader.
+                blob_reader.consume(len_to_copy);
             }
-            Ok((buf, blob_reader))
+
+            Ok(buf)
         });
 
-        let resp = self.tokio_handle.block_on(task).unwrap();
+        let buf = self.tokio_handle.block_on(task).unwrap()?;
 
-        match resp {
-            Err(e) => reply.error(e),
-            Ok((buf, blob_reader)) => {
-                reply.data(&buf);
-                self.file_handles.insert(fh, blob_reader);
-            }
-        }
+        w.write(&buf)
     }
 
-    #[tracing::instrument(skip_all, fields(rq.inode = ino))]
-    fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: fuser::ReplyData) {
-        if ino == fuser::FUSE_ROOT_ID {
-            reply.error(libc::ENOSYS);
-            return;
+    #[tracing::instrument(skip_all, fields(rq.inode = inode))]
+    fn readlink(&self, _ctx: &Context, inode: Self::Inode) -> io::Result<Vec<u8>> {
+        if inode == ROOT_ID {
+            return Err(io::Error::from_raw_os_error(libc::ENOSYS));
         }
 
         // lookup the inode
-        match *self.inode_tracker.get(ino).unwrap() {
+        match *self.inode_tracker.read().get(inode).unwrap() {
             InodeData::Directory(..) | InodeData::Regular(..) => {
-                reply.error(libc::EINVAL);
+                Err(io::Error::from_raw_os_error(libc::EINVAL))
             }
-            InodeData::Symlink(ref target) => reply.data(target),
+            InodeData::Symlink(ref target) => Ok(target.to_vec()),
         }
     }
 }
 
-fn reply_with_entry(reply: fuser::ReplyEntry, file_attr: &FileAttr) {
-    reply.entry(&Duration::MAX, file_attr, 1 /* TODO: generation */);
+struct FuseServer<FS>
+where
+    FS: FileSystem + Sync + Send,
+{
+    server: Arc<fuse_backend_rs::api::server::Server<Arc<FS>>>,
+    channel: fuse_backend_rs::transport::FuseChannel,
+}
+
+impl<FS> FuseServer<FS>
+where
+    FS: FileSystem + Sync + Send,
+{
+    fn start(&mut self) -> io::Result<()> {
+        loop {
+            if let Some((reader, writer)) = self
+                .channel
+                .get_request()
+                .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?
+            {
+                if let Err(e) = self
+                    .server
+                    .handle_message(reader, writer.into(), None, None)
+                {
+                    match e {
+                        // This indicates the session has been shut down.
+                        fuse_backend_rs::Error::EncodeMessage(e)
+                            if e.raw_os_error() == Some(libc::EBADFD) =>
+                        {
+                            break;
+                        }
+                        error => {
+                            error!(?error, "failed to handle fuse request");
+                            continue;
+                        }
+                    }
+                }
+            } else {
+                break;
+            }
+        }
+        Ok(())
+    }
+}
+
+pub struct FuseDaemon {
+    session: FuseSession,
+    threads: Vec<thread::JoinHandle<()>>,
+}
+
+impl FuseDaemon {
+    pub fn new<FS, P>(fs: FS, mountpoint: P, threads: usize) -> Result<Self, io::Error>
+    where
+        FS: FileSystem + Sync + Send + 'static,
+        P: AsRef<Path>,
+    {
+        let server = Arc::new(fuse_backend_rs::api::server::Server::new(Arc::new(fs)));
+
+        let mut session = FuseSession::new(mountpoint.as_ref(), "tvix-store", "", true)
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+
+        session.set_allow_other(false);
+        session
+            .mount()
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+        let mut join_handles = Vec::with_capacity(threads);
+        for _ in 0..threads {
+            let mut server = FuseServer {
+                server: server.clone(),
+                channel: session
+                    .new_channel()
+                    .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?,
+            };
+            let join_handle = thread::Builder::new()
+                .name("fuse_server".to_string())
+                .spawn(move || {
+                    let _ = server.start();
+                })?;
+            join_handles.push(join_handle);
+        }
+
+        Ok(FuseDaemon {
+            session,
+            threads: join_handles,
+        })
+    }
+
+    pub fn unmount(&mut self) -> Result<(), io::Error> {
+        self.session
+            .umount()
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+
+        for thread in self.threads.drain(..) {
+            thread.join().map_err(|_| {
+                io::Error::new(io::ErrorKind::Other, "failed to join fuse server thread")
+            })?;
+        }
+
+        Ok(())
+    }
+}
+
+impl Drop for FuseDaemon {
+    fn drop(&mut self) {
+        if let Err(error) = self.unmount() {
+            error!(?error, "failed to unmont fuse filesystem")
+        }
+    }
 }
diff --git a/tvix/store/src/fuse/tests.rs b/tvix/store/src/fuse/tests.rs
index 015e27ee9988..81de8b13de58 100644
--- a/tvix/store/src/fuse/tests.rs
+++ b/tvix/store/src/fuse/tests.rs
@@ -12,7 +12,7 @@ use crate::pathinfoservice::PathInfoService;
 use crate::proto::{DirectoryNode, FileNode, PathInfo};
 use crate::tests::fixtures;
 use crate::tests::utils::{gen_blob_service, gen_directory_service, gen_pathinfo_service};
-use crate::{proto, FUSE};
+use crate::{proto, FuseDaemon, FUSE};
 
 const BLOB_A_NAME: &str = "00000000000000000000000000000000-test";
 const BLOB_B_NAME: &str = "55555555555555555555555555555555-test";
@@ -40,14 +40,14 @@ fn do_mount<P: AsRef<Path>>(
     path_info_service: Arc<dyn PathInfoService>,
     mountpoint: P,
     list_root: bool,
-) -> io::Result<fuser::BackgroundSession> {
+) -> io::Result<FuseDaemon> {
     let fs = FUSE::new(
         blob_service,
         directory_service,
         path_info_service,
         list_root,
     );
-    fuser::spawn_mount2(fs, mountpoint, &[])
+    FuseDaemon::new(fs, mountpoint.as_ref(), 4)
 }
 
 async fn populate_blob_a(
@@ -294,7 +294,7 @@ async fn mount() {
     let tmpdir = TempDir::new().unwrap();
 
     let (blob_service, directory_service, path_info_service) = gen_svcs();
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -303,7 +303,7 @@ async fn mount() {
     )
     .expect("must succeed");
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure listing the root isn't allowed
@@ -317,7 +317,7 @@ async fn root() {
     let tmpdir = TempDir::new().unwrap();
 
     let (blob_service, directory_service, path_info_service) = gen_svcs();
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -334,7 +334,7 @@ async fn root() {
         assert_eq!(std::io::ErrorKind::PermissionDenied, err.kind());
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure listing the root is allowed if configured explicitly
@@ -350,7 +350,7 @@ async fn root_with_listing() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_a(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -371,7 +371,7 @@ async fn root_with_listing() {
         assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len());
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure we can stat a file at the root
@@ -387,7 +387,7 @@ async fn stat_file_at_root() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_a(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -405,7 +405,7 @@ async fn stat_file_at_root() {
     assert!(metadata.permissions().readonly());
     assert_eq!(fixtures::BLOB_A.len() as u64, metadata.len());
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure we can read a file at the root
@@ -421,7 +421,7 @@ async fn read_file_at_root() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_a(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -439,7 +439,7 @@ async fn read_file_at_root() {
     assert_eq!(fixtures::BLOB_A.len(), data.len());
     assert_eq!(fixtures::BLOB_A.to_vec(), data);
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure we can read a large file at the root
@@ -455,7 +455,7 @@ async fn read_large_file_at_root() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_b(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -481,7 +481,7 @@ async fn read_large_file_at_root() {
     assert_eq!(fixtures::BLOB_B.len(), data.len());
     assert_eq!(fixtures::BLOB_B.to_vec(), data);
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Read the target of a symlink
@@ -497,7 +497,7 @@ async fn symlink_readlink() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_symlink(&blob_service, &directory_service, &path_info_service);
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -524,7 +524,7 @@ async fn symlink_readlink() {
     let e = fs::read(p).expect_err("must fail");
     assert_eq!(std::io::ErrorKind::NotFound, e.kind());
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Read and stat a regular file through a symlink pointing to it.
@@ -541,7 +541,7 @@ async fn read_stat_through_symlink() {
     populate_blob_a(&blob_service, &directory_service, &path_info_service).await;
     populate_symlink(&blob_service, &directory_service, &path_info_service);
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -567,7 +567,7 @@ async fn read_stat_through_symlink() {
         std::fs::read(p_symlink).expect("must succeed"),
     );
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Read a directory in the root, and validate some attributes.
@@ -583,7 +583,7 @@ async fn read_stat_directory() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_with_keep(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -599,7 +599,7 @@ async fn read_stat_directory() {
     assert!(metadata.is_dir());
     assert!(metadata.permissions().readonly());
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -615,7 +615,7 @@ async fn read_blob_inside_dir() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_with_keep(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -635,7 +635,7 @@ async fn read_blob_inside_dir() {
     let data = fs::read(&p).expect("must succeed");
     assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data);
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -652,7 +652,7 @@ async fn read_blob_deep_inside_dir() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -676,7 +676,7 @@ async fn read_blob_deep_inside_dir() {
     let data = fs::read(&p).expect("must succeed");
     assert_eq!(fixtures::EMPTY_BLOB_CONTENTS.to_vec(), data);
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure readdir works.
@@ -692,7 +692,7 @@ async fn readdir() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -732,7 +732,7 @@ async fn readdir() {
         assert!(e.file_type().expect("must succeed").is_dir());
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test]
@@ -748,7 +748,7 @@ async fn readdir_deep() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -775,7 +775,7 @@ async fn readdir_deep() {
         assert_eq!(0, e.metadata().expect("must succeed").len());
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Check attributes match how they show up in /nix/store normally.
@@ -794,7 +794,7 @@ async fn check_attributes() {
     populate_symlink(&blob_service, &directory_service, &path_info_service);
     populate_helloworld_blob(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -840,7 +840,7 @@ async fn check_attributes() {
         // crtime seems MacOS only
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test]
@@ -858,7 +858,7 @@ async fn compare_inodes_directories() {
     populate_directory_with_keep(&blob_service, &directory_service, &path_info_service).await;
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -876,7 +876,7 @@ async fn compare_inodes_directories() {
         fs::metadata(p_sibling_dir).expect("must succeed").ino()
     );
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure we allocate the same inodes for the same directory contents.
@@ -893,7 +893,7 @@ async fn compare_inodes_files() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -915,7 +915,7 @@ async fn compare_inodes_files() {
         fs::metadata(p_keep2).expect("must succeed").ino()
     );
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Ensure we allocate the same inode for symlinks pointing to the same targets.
@@ -933,7 +933,7 @@ async fn compare_inodes_symlinks() {
     populate_directory_complicated(&blob_service, &directory_service, &path_info_service).await;
     populate_symlink2(&blob_service, &directory_service, &path_info_service);
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -951,7 +951,7 @@ async fn compare_inodes_symlinks() {
         fs::symlink_metadata(p2).expect("must succeed").ino()
     );
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Check we match paths exactly.
@@ -967,7 +967,7 @@ async fn read_wrong_paths_in_root() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_a(&blob_service, &directory_service, &path_info_service).await;
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -1000,7 +1000,7 @@ async fn read_wrong_paths_in_root() {
         .join("00000000000000000000000000000000-tes")
         .exists());
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 /// Make sure writes are not allowed
@@ -1016,7 +1016,7 @@ async fn disallow_writes() {
 
     let (blob_service, directory_service, path_info_service) = gen_svcs();
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -1028,9 +1028,9 @@ async fn disallow_writes() {
     let p = tmpdir.path().join(BLOB_A_NAME);
     let e = std::fs::File::create(p).expect_err("must fail");
 
-    assert_eq!(std::io::ErrorKind::Unsupported, e.kind());
+    assert_eq!(Some(libc::EROFS), e.raw_os_error());
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test]
@@ -1045,7 +1045,7 @@ async fn missing_directory() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_pathinfo_without_directory(&blob_service, &directory_service, &path_info_service);
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -1073,7 +1073,7 @@ async fn missing_directory() {
         fs::metadata(p.join(".keep")).expect_err("must fail");
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1088,7 +1088,7 @@ async fn missing_blob() {
     let (blob_service, directory_service, path_info_service) = gen_svcs();
     populate_blob_a_without_blob(&blob_service, &directory_service, &path_info_service);
 
-    let fuser_session = do_mount(
+    let mut fuse_daemon = do_mount(
         blob_service,
         directory_service,
         path_info_service,
@@ -1109,5 +1109,5 @@ async fn missing_blob() {
         fs::read(p).expect_err("must fail");
     }
 
-    fuser_session.join()
+    fuse_daemon.unmount().expect("unmount");
 }
diff --git a/tvix/store/src/lib.rs b/tvix/store/src/lib.rs
index 417faa39237b..252506de5977 100644
--- a/tvix/store/src/lib.rs
+++ b/tvix/store/src/lib.rs
@@ -14,7 +14,7 @@ pub use digests::B3Digest;
 pub use errors::Error;
 
 #[cfg(feature = "fuse")]
-pub use fuse::FUSE;
+pub use fuse::{FuseDaemon, FUSE};
 
 #[cfg(test)]
 mod tests;