
# Troubleshooting

**The things that will bite you, and how to get out of each.**

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

This is the one that catches everyone.

A **synchronous** `fs` call against your own mountpoint, from the process serving it, deadlocks outright: the call blocks the event loop, and the request it produced can only be answered by that same event loop.

Enough concurrent **async** calls wedge it too, and less obviously. 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.

**The fix:** put the client in another process — a shell, a test child process, anything. 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`](https://github.com/pithings/mountx/blob/main/src/fuse/mount.ts).

## 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 hangs.

```ts
// 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 serving process dies without unmounting, the folder is left _stale_ rather than frozen — `ls` answers `ENOTCONN` instead of hanging. Clean it up:

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

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

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

## `unmount()` threw

It throws for two reasons, and both are recoverable — `unmount()` is retryable after a failure.

- **`umount(8)` refused** — the mountpoint 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. The fix is a grant made in advance: Full Disk Access for whatever app the process is attributed to — the terminal, your IDE, the CI agent. See [NFSv3 § the macOS consent gate](/transports/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:

```ts
import { probeTransports } from "mountx/auto";

const probe = await probeTransports();
console.log(probe.fuse.reason, probe.nfs.reason);
```

Common causes:

| reason                              | fix                                                                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| no `/dev/fuse`                      | the `fuse` module is not loaded, or a container was not given the device (`docker run --device /dev/fuse`)            |
| no `fusermount3`                    | `apt install fuse3` / `dnf install fuse3`, or run as root                                                             |
| `fusermount3` cannot elevate        | its setuid bit is gone, or `no_new_privs` has made it inert — see [below](#failed-to-open-dev-fuse-permission-denied) |
| no prebuilt addon for this platform | unprivileged FUSE mounting is unavailable here; root still works, and NFS is unaffected                               |
| no `/sbin/mount.nfs`                | `apt install nfs-common` / `dnf install nfs-utils`                                                                    |
| "mounting NFS needs root on Linux"  | `sudo`, or use FUSE                                                                                                   |
| Windows                             | neither transport mounts there. `createNfsServer()` still runs, and you can mount it from elsewhere                   |

## `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
```

**It is not the device's mode.** That is worth saying first, because `/dev/fuse` is `crw-------` on any host without a udev rule — a bare devtmpfs in a container or a microVM — and it looks like the obvious culprit. It isn't. `fusermount3` opens the device as the very first thing `mount_fuse()` does, _before_ its `drop_privs()`, and `drop_privs()` only lowers the fsuid, which `main()` has already restored:

```c
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:

| cause                                     | check                                                                       | fix                                                                |
| ----------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| the setuid bit is gone from the helper    | `ls -l $(command -v fusermount3)` — want `-rwsr-xr-x`, the `s` is the point | `sudo chmod u+s /usr/bin/fusermount3`, or mount as root            |
| `nosuid` on the filesystem holding it     | `findmnt -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 tree        | `grep NoNewPrivs /proc/self/status`                                         | nothing; it is inherited, irreversible, and `sudo` is just as dead |
| an LSM or device cgroup denies the device | `getenforce`, `sudo dmesg \| grep 'avc.*fuse'`                              | a policy change on the host; it holds for root too                 |

One command splits the four in half — **`sudo -n id`**. `sudo` is a setuid binary too, so if it prints `uid=0` the setuid mechanism works and you are looking at the last row; if it fails with "effective uid is not 0", nothing in this process tree elevates and you are looking at one of the first three.

`no_new_privs` is the one with no way out. A seccomp sandbox has to set it (installing a filter without `CAP_SYS_ADMIN` requires it), every descendant inherits it, and it cannot be cleared — the bit on the helper is present and inert. On such a host FUSE is unavailable to every process, and since [NFS needs root on Linux](/transports/nfs#where-it-mounts), mountx has no transport at all there.

Otherwise, mounting as root is the general escape: it opens `/dev/fuse` directly and runs no helper.

```sh
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 at mount time — it is a fact about the path, not about the host — and refuses with a message naming the owner. `chown` the mountpoint, or pick one you own.

## `ESTALE` on a file that is still open

You are on NFSv3, and the file was deleted while open. NFSv3 is stateless — there is no `open`/`release`, so a handle to a deleted file has nothing to refer to. FUSE keeps it readable; NFS cannot.

This is the single behavioural difference between the transports, and it is why [`mountx/auto`](/transports) prefers FUSE wherever FUSE works.

## Something the driver returned is being ignored

Check the resolved capabilities:

```ts
createLoopback(driver).capabilities;
```

`handles` and `atomicRename` cannot be inferred from a driver's shape and default to `false` — if you implemented them, [declare them](/guide/drivers/custom#capabilities). Everything else follows from which methods exist.

## Stale kernel cache

Values are correct in your driver and stale through the mountpoint? That is the [attribute/entry cache](/guide/tuning#caching-this-is-where-the-speed-is) doing its job. Either lower the timeouts, or — better — invalidate precisely:

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

## Recovering a wedged dev host

If a mount has genuinely locked up and nothing will detach it, [`.agents/environment.md`](https://github.com/pithings/mountx/blob/main/.agents/environment.md) has the verified recovery procedures — including the root-only `/sys/fs/fuse/connections/<n>/abort`, which aborts the connection and lets the parked requests fail.
