Troubleshooting

Use these procedures to diagnose common failures and recover safely.

#Don't use your own mount from the serving process

This problem is common.

A synchronous fs call from the server process to its own mount point causes a deadlock. The call blocks the event loop. Only that event loop can answer the generated request.

A large number of concurrent asynchronous calls can also stop progress. Every /dev/fuse read parks a libuv threadpool thread, and so does every fs call your driver makes. fs.rm(dir, { recursive: true }) over a few hundred entries will exhaust the pool that the mount's read loop also needs, and the process stops.

Fix: Run the client in another process, such as a shell or a test child process.

If you truly must drive it in-process, keep the concurrency well below the pool size and raise UV_THREADPOOL_SIZE.

The full explanation is at the top of src/fuse/mount.ts.

#Don't spawn a binary that lives on your own 9P mount

9P does not have the thread-pool form of the previous problem. P9Session answers from the event loop over a socket, not from a thread blocked in read(2). Therefore, ordinary asynchronous fs calls to a 9P mount from its server process can work. However, child_process.spawn() can still cause a deadlock.

uv_spawn blocks the one thread that replies to 9P requests until the child has chdired and exec'd.

A current working directory (cwd) inside the mount point causes this problem.

Spawning a binary that lives on the mount point causes a more serious deadlock. The child blocks in p9_client_rpc ← p9_client_walk ← v9fs_vfs_atomic_open_dotl ← do_open_execat. It waits for the same thread that it blocks.

Killing the server process does not help. fork gave the child its own copy of the server socket. Therefore, the connection outlives the process that created it. umount -f answers target is busy.

Fix: Run kill -9 on the blocked child (p9_client_rpc waits killably, so it really does die), then a plain umount. Alternatively, avoid the deadlock condition. Let a shell run the executable (spawn("sh", ["-c", command])) rather than execing a path on the mount directly.

#Don't call process.exit() while mounted

Node's exit path waits for the threadpool, and a live mount always has reads parked there. The process does not exit.

// wrong
process.exit(1);

// right
await mounted.unmount();
process.exitCode = 1;

This is also why mountx's built-in signal handlers unmount and then re-raise the signal instead of exiting directly.

#A stale mountpoint after a crash

If the server process ends without unmounting, the mount point becomes stale instead of remaining blocked. Each transport reports this state differently.

FUSE answers ENOTCONN instead of hanging. After a crash, 9P returns ECONNRESET on the first access. It immediately returns EIO on each later access. There is no timeout and no need to force the connection down.

Clean it up:

# FUSE, mounted unprivileged
fusermount3 -u /mnt/point

# FUSE, mounted as root
sudo umount -l /mnt/point

# 9P — always root, and a plain umount clears it; there is nothing to force
sudo umount /mnt/point

# NFS
sudo umount -f /mnt/point   # macOS: see the consent note below

9P leaves one item that the other transports do not leave. After SIGKILL, no server process remains to remove its private mkdtemp socket directory (/tmp/mountx-9p-*). Remove it separately:

sudo rm -rf /tmp/mountx-9p-*

#unmount() threw

unmount() throws for two recoverable reasons. You can call it again after a failure.

  • umount(8) refused: the mount point is busy. Something has a working directory or an open file inside it. lsof +D /mnt/point (or fuser -m /mnt/point) finds it.
  • The deadline passed: unmountTimeout (default 10 s) expired and the connection had to be forced down. Usually a driver that stopped answering. If yours can legitimately take longer to quiesce, raise the timeout.

#unmount() hangs on macOS

macOS puts network volumes behind a sandbox approval that is never prompted for a command-line process. Without it, umount blocks and umount -f answers EPERM.

mountx names this case specifically rather than blaming your driver, and tells you the mount survived. Before you mount, grant Full Disk Access to the application that owns the process. This can be the terminal, your integrated development environment (IDE), or the continuous integration (CI) agent. See NFS § the macOS consent gate.

#"no transport can mount on this host"

mount() from mountx/auto throws this when the probe finds nothing usable, and the message names what each transport is missing. Ask directly for the detail:

import { probeTransports } from "mountx/auto";

const probe = await probeTransports();
console.log(probe.fuse.reason, probe["9p"].reason, probe.nfs.reason);

Check these common causes:

reasonfix
no /dev/fusethe fuse module is not loaded, or a container was not given the device (docker run --device /dev/fuse)
no fusermount3apt install fuse3 / dnf install fuse3, or run as root
fusermount3 cannot elevateits setuid bit is gone, or no_new_privs has made it inert — see below
no prebuilt addon for this platformunprivileged FUSE mounting is unavailable here; root still works, and 9P/NFS are unaffected
no 9p in /proc/filesystems, no module treethis host has no 9P client at all — see below
"mounting 9P needs root"9P has no unprivileged path on any host — sudo, or use FUSE
"no 9pnet_fd … virtio-only guest"auto refused 9P specifically rather than choose a transport that would fail — mountx --transport 9p tries anyway
no /sbin/mount.nfsapt install nfs-common / dnf install nfs-utils
"mounting NFS needs root on Linux"sudo, or use FUSE
Windowsno transport mounts there. createNfsServer()/createP9Server() still run, and you can mount from elsewhere

#No 9P kernel module

p9ClientProbe() and mountx --transport 9p distinguish two kernel modules. /proc/filesystems can list 9p, while mount9p() needs 9pnet_fd. The 9pnet_fd module registers trans=unix, trans=tcp, and trans=fd:

import { p9ClientProbe } from "mountx/9p";

const probe = p9ClientProbe();
probe.kernel; // is `9p` listed in /proc/filesystems?
probe.transport; // is `9pnet_fd` visible in /sys/module?
probe.modules; // is there a module tree for this kernel to load anything from?

The kernel can list 9p when only 9pnet_virtio is loaded. A VM guest uses that transport to reach its hypervisor, but it is not sufficient for mount9p().

The kernel does not automatically load a missing transport module. v9fs_get_trans_by_name() only fails. Therefore, a mount without 9pnet_fd fails. As root:

sudo modprobe 9pnet_fd

and to survive a reboot, pin it the way this project's own dev host does:

echo 9pnet_fd | sudo tee /etc/modules-load.d/9p.conf

If probe.modules is false, /lib/modules/$(uname -r) does not exist. Minimal container images often have this state. There is no module tree for modprobe, so the kernel cannot acquire a 9P client. mountx/auto selects NFS instead, or refuses if the process is unprivileged. probe.reason tells the two cases apart in one sentence rather than sending you to try a modprobe that was never going to work.

#failed to open /dev/fuse: Permission denied

Error: mountx: mounting /home/you/mountx failed — /usr/bin/fusermount3 -o fsname=mountx,default_permissions: exit 1:
/usr/bin/fusermount3: failed to open /dev/fuse: Permission denied

The device mode is not the cause. /dev/fuse is crw------- on a host without a udev rule.

This is common with a basic devtmpfs in a container or microVM, so the mode can appear to be the cause.

However, fusermount3 opens the device at the start of mount_fuse(), before drop_privs(). drop_privs() only lowers the fsuid, which main() has already restored:

fd = open_fuse_device(&dev);   // fsuid is still 0 here
if (fd == -1) return -1;
drop_privs();

A helper that really became root opens a 0600 root-owned device without trouble. So chmod 666 /dev/fuse is not the fix, and the message means the helper never became root. Four things do that:

causecheckfix
the setuid bit is gone from the helperls -l $(command -v fusermount3) — want -rwsr-xr-x, the s is the pointsudo chmod u+s /usr/bin/fusermount3, or mount as root
nosuid on the filesystem holding itfindmnt -no OPTIONS -T $(command -v fusermount3)mount as root — a disarmed setuid bit cannot be re-armed in place
no_new_privs on the process treegrep NoNewPrivs /proc/self/statusnothing; it is inherited, irreversible, and sudo is just as dead
an LSM or device cgroup denies the devicegetenforce, sudo dmesg | grep 'avc.*fuse'a policy change on the host; it holds for root too

Use sudo -n id to distinguish the first three causes from the last cause. sudo is also a set-user-ID binary. If it prints uid=0, the mechanism works and the last row applies. If it reports effective uid is not 0, no process in this tree can elevate. One of the first three rows applies.

no_new_privs cannot be reversed.

A secure computing (seccomp) sandbox must set this flag to install a filter without CAP_SYS_ADMIN. Every descendant inherits the flag, and no process can clear it. The helper still has its set-user-ID bit, but the bit has no effect.

On such a host FUSE is unavailable to every process, and since NFS needs root on Linux, mountx has no transport at all there.

For the other cases, mount as root: it opens /dev/fuse directly and runs no helper.

sudo "$(command -v node)" your-script.mjs   # root's PATH usually lacks a version-managed node

probeTransports() catches the missing setuid bit and no_new_privs before anything is opened, so mountx/auto reports those as reasons rather than as a failed mount. The other two are only visible once the helper has run, and mountx names them in the error above.

#allowOther was refused

An unprivileged FUSE mount additionally needs user_allow_other in /etc/fuse.conf. Without it fusermount3 refuses the mount and says so. Add the line, or mount as root.

#EPERM mounting NFS on macOS

macOS needs no root for NFS, but an ordinary user may only mount onto a directory that user owns. mountx checks this path-specific fact at mount time. If the check fails, the error names the owner. Use chown on the mount point, or select one that you own.

#ESTALE on a file that is still open

You are on NFS, and the file was deleted while open.

NFSv3 is stateless and has no open or release. A handle to a deleted file therefore has no target.

NFSv4.1 has real open state but has the same limitation. Both versions share one path-keyed handle table. Unlink removes the path from that table even when a v4.1 stateid keeps the file open.

FUSE and 9P both keep it readable, because both are stateful; NFS cannot, on either version.

This is the main behavioural difference NFSv3 costs, and it is why mountx/auto prefers FUSE, then 9P, over NFS wherever either can mount.

#Something the driver returned is being ignored

Check the resolved capabilities:

createLoopback(driver).capabilities;

handles and atomicRename cannot be inferred from a driver's shape and default to false. If you implemented them, declare them. Everything else follows from which methods exist.

#Stale kernel cache

If the driver has current values but the mount point shows old values, the attribute and entry caches are probably the cause. Lower the timeouts, or invalidate only the affected data:

if (mounted.transport === "fuse") mounted.notifyInvalInode(ino);

#Recovering a wedged dev host

If a mount does not respond and cannot detach, use the verified procedures in .agents/environment.md. They include the root-only /sys/fs/fuse/connections/<n>/abort control. It aborts the connection and lets blocked requests fail.

mountx  Write a filesystem in JavaScript, mount it as a real folder.