Commit Graph

2572 Commits

Author SHA1 Message Date
dependabot[bot] e23cf7c0f6 build(deps): bump github/codeql-action/analyze from 4.35.1 to 4.36.3
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.35.1 to 4.36.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 22:04:48 +00:00
dependabot[bot] c9100ce217 build(deps): bump actions/setup-python from 6.2.0 to 6.3.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/a309ff8b426b58ec0e2a45f0f869d46889d02405...ece7cb06caefa5fff74198d8649806c4678c61a1)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 12:49:44 +02:00
dependabot[bot] cd88eb4da1 build(deps): bump vmactions/freebsd-vm from 1.4.8 to 1.5.0
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.4.8 to 1.5.0.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/b84ab5559b5a1bb4b8ee2737d2506a16e1737636...5a72679103d223925653750faa878a143340fbd0)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 12:49:24 +02:00
dependabot[bot] 1ff3e38dd9 build(deps): bump github/codeql-action/upload-sarif
Bumps [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) from 34950e1b113b30df4edee1a6d3a605242df0c40b to 9cea5827c668a1fe7165dbce6e80c3f9cf3f83ac.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/34950e1b113b30df4edee1a6d3a605242df0c40b...9cea5827c668a1fe7165dbce6e80c3f9cf3f83ac)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 9cea5827c668a1fe7165dbce6e80c3f9cf3f83ac
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 12:49:01 +02:00
Bernd Schubert 3a3dd7b02d fuse: add FUSE_CONN_FLAG_NO_INTERRUPT conn flag
fuse_set_conn_flag() collects the libfuse-side connection hints that are
not negotiated with the kernel. no_interrupt was the one such hint still
set only by direct field assignment.

Define FUSE_CONN_FLAG_NO_INTERRUPT alongside FUSE_CONN_FLAG_SINGLE_ISSUER
and map it onto conn->no_interrupt in the same switch, and convert the
example init() handlers to set it through the setter.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00
Bernd Schubert 2ad90f04dc examples: enable io_uring single-issuer mode
These example filesystems always send their reply from the per-queue
io-uring worker thread. The ones that run a helper thread use it only
for cache-invalidation notifications over /dev/fuse, not ring
submissions, so they too satisfy the io_uring_single_issuer contract.

Call fuse_set_conn_flag(conn, FUSE_CONN_FLAG_SINGLE_ISSUER) in their
init() handlers so they take the lock-free, registered-ring-fd fast
path. It is harmless when io-uring is not in use.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00
Bernd Schubert 1615ff35af fuse_uring: register the ring fd for single-issuer queues
For single-issuer queues only fuse_uring_thread() ever issues
io_uring_enter() on the ring, so the per-thread registered ring-fd index
is always valid. Registering it lets io_uring skip the fd grab/put on
every enter, which is measurable with libfuse's shared, threaded file
table.

A ring shared between separate submit and reap threads cannot use this
(each issuing thread would need its own index), so it stays gated on
io_uring_single_issuer. Registration needs kernel >= 5.18 and is
non-fatal: on failure the queue simply keeps using the normal ring fd.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00
Bernd Schubert 670f938336 fuse_uring: restore inline-reply batching, add single-issuer fast path
Part 1 dropped cqe_processing and made the serving loop wait-only, so
the default path now issues one io_uring_submit() per inline reply
instead of coalescing a cqe batch into a single submit.

Restore the batching: the loop splits submit_and_wait so the flush
stays under ring_lock (part 1's race fix), and cqe_processing - now an
_Atomic bool set lock-free around handle_cqes() - again gates the
per-reply submit. The SQ stays serialised, so a batched reply is always
flushed before the next wait, never stranded.

Add a conn flag, io_uring_single_issuer, for a filesystem that replies
only from the per-queue uring thread: it skips ring_lock entirely and
runs the combined io_uring_submit_and_wait() loop, sound because only
that thread touches the ring. The flag reuses a reserved padding bit
(ABI unchanged; flag 0 keeps the multi-issuer path) and is mirrored
into the ring pool after op.init(), before serving starts.

A filesystem enables it with fuse_set_conn_flag(conn,
FUSE_CONN_FLAG_SINGLE_ISSUER), a new exported setter modelled on
fuse_set_feature_flag(); the io_uring_single_issuer bitfield is the
internal storage.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00
Bernd Schubert 6910b07ade fuse_uring: serialise all ring submission under ring_lock
The per-queue uring thread flushed the submission queue from
io_uring_submit_and_wait() in its serving loop outside ring_lock, while
off-thread repliers flushed it from io_uring_submit() under ring_lock in
fuse_uring_commit_sqe(). liburing's SQ is single-producer in userspace,
so two concurrent flushes corrupt sqe_head/ktail: when a filesystem
replies from a thread other than the per-queue uring thread this shows
up as spurious "Failed to get a ring SQEs", a doubled commit/fetch for
one commit_id, or an eventual hang. The commit path also keyed locking
on pthread_self() != queue->tid, a fragile proxy for lock ownership.

This serialisation is required, not just prudent: io_uring_setup_flags.7
documents that sharing a ring between threads "requires additional
synchronization". It is orthogonal to IORING_SETUP_SINGLE_ISSUER, which
only makes the kernel reject a second submitter task with -EEXIST and
does nothing for the userspace SQ bookkeeping; the flush race exists
with or without that flag.

Make the rule uniform: every submission-side SQ access takes ring_lock,
in commit_sqe() and resubmit() alike, replacing the pthread_self()
guess. ring_lock can no longer wrap handle_cqes() - an inline commit on
the uring thread would self-deadlock on the non-recursive mutex - so it
is taken only around the individual SQ accesses.

Submission stays batched via the cqe_processing flag, now _Atomic: while
the uring thread runs handle_cqes() it is set, so replies fill their SQE
under ring_lock but defer the submit; the thread flushes them with one
io_uring_submit() when it clears it. A reply that sees the flag clear -
the uring thread idle in io_uring_wait_cqe() - submits at once to wake
it. CQ access stays lock-free, as only the uring thread reaps.

Closes: https://github.com/libfuse/libfuse/issues/1443
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-29 00:21:20 +02:00
dependabot[bot] dbbf3cf778 build(deps): bump vmactions/freebsd-vm from 1.4.6 to 1.4.8
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.4.6 to 1.4.8.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/a6de9343ef5747433d9c25784c90e84998b9d69a...b84ab5559b5a1bb4b8ee2737d2506a16e1737636)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 02:25:22 +02:00
dependabot[bot] f064b465b3 build(deps): bump actions/checkout from 6.0.3 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 02:24:58 +02:00
Bernd Schubert a19bb4f0c6 fuse_uring: continue submission on a failing commit SQE
Replies are not submitted as they are produced: while a queue thread is
in fuse_uring_queue_handle_cqes() (cqe_processing set), each reply only
prepares a COMMIT_AND_FETCH SQE and the per-reply io_uring_submit() is
skipped. The whole batch, plus the eventfd poll re-arm, is flushed by the
next io_uring_submit_and_wait().

Without IORING_SETUP_SUBMIT_ALL the kernel aborts io_submit_sqes() at the
first SQE whose prep fails, leaving the rest of the batch unconsumed until
the following loop iteration -- head-of-line blocking the other in-flight
replies behind one bad commit. Set the flag so the kernel drains the whole
batch and only the failing SQE gets an error CQE.

This might have resulted in fuse-over-io-uring stalls, if applications
were waiting for results and libfuse was waiting for CQEs, but
not all SQEs had been submitted yet.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-23 02:12:09 +02:00
Sam James 07a1b913b1 Add newer ioctl prototype for CUSE too
Mirror the fuse-side change commit abdd45f83c
("Make ioctl prototype conditional on FUSE_USE_VERSION. (#482)")'
for cuse too.

Crank versions to 3.19 as this is an ABI change.

Also increase the default FUSE_USE_VERSION to 30 in
include/cuse_lowlevel.h, 29 was below the libfuse3
min version.

Closes: https://github.com/libfuse/libfuse/issues/1406
Signed-off-by: Sam James <sam@gentoo.org>
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-20 13:40:52 +02:00
Bernd Schubert 681a646017 cuse: make cuse_lowlevel_ops extensible via op_size and version
cuse_prep_data() copied sizeof(struct cuse_lowlevel_ops) bytes from the
caller's clop using the library's struct size, so any field appended to
cuse_lowlevel_ops would make the library read past an older application's
struct. The libfuse version recorded in the session was also the
library's, not the application's, defeating the runtime ABI-fixup hook.

Add size/version-aware entry points cuse_lowlevel_{new,setup,main}_319
that thread the caller's sizeof(struct cuse_lowlevel_ops) and
libfuse_version into a clamped memcpy and fuse_session_new_versioned().
Header macros inject both at the call site, exactly as fuse_session_new()
does, so application source is unchanged. The bare FUSE_3.0 symbols
remain as compat forwarders passing the frozen 3.0 struct size and an
unknown version. No symbol versioning (FUSE_SYMVER) is required.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-20 13:40:52 +02:00
Bernd Schubert da15a2f6ed mount_util: derive add/remove-mount result from exit status
add_mount(), exec_umount() and remove_mount() returned waitpid()'s pid
on success rather than 0, contradicting the documented '0 or -1' contract
(harmless today only because every caller tests == -1). They also tested
the raw wait status, and read it uninitialized when waitpid() itself
failed.

Read the status only on the success path and decode it with
WIFEXITED()/WEXITSTATUS(), matching how check_is_mount() in fusermount.c
already does it, so these helpers return 0 or -1 as advertised.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-18 16:26:11 +02:00
Bernd Schubert 6806ad4e1c Replace fork by posix_spawn in mount_util.c
This completes commit bb9cecbf67
("Use posix_spawn instead of fork+exec") and replaces remaining
fork by posix_spawn.

Not modified is mount_bsd.d as I cannot test it.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-18 16:26:11 +02:00
Jingbo Xu 4230b91081 fuse_lowlevel: check fuse_ll_ops2 in uring CQE dispatch
The uring CQE path dispatches handlers from fuse_ll_ops2[].

Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com>
2026-06-18 16:25:19 +02:00
Jingbo Xu 9e22f4e59e lib: use quote-aware tokenizer for mount option parsing
The strtok_r(opts, ",", ...) based option splitter does not understand
double-quoted values.  SELinux MLS labels can contain commas inside
quotes (e.g., context="system_u:object_r:root_t:s0:c0,c1"), which
would be incorrectly split into separate tokens.

Replace strtok_r with a quote-aware next_mount_opt() helper that skips
commas inside double-quoted regions.

Signed-off-by: Jingbo Xu <jingbo.xu@linux.alibaba.com>
Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com>
2026-06-18 08:48:59 +02:00
Jingbo Xu 24d40b2a7c lib: strip enclosing quotes from fsconfig option values
With the old mount(2) syscall, the kernel's SELinux code
(selinux_sb_eat_lsm_opts) internally strips double quotes from context
option values like context="system_u:object_r:root_t:s0". However, with
the new fsconfig() API the value is passed through verbatim, causing
SELinux to reject the mount with ENODEV.

Strip enclosing double quotes from key=value option values in
apply_opt_key_value() before passing them to fsconfig().

Signed-off-by: Jingbo Xu <jingbo.xu@linux.alibaba.com>
2026-06-18 08:48:59 +02:00
Bernd Schubert 1a3cc241e9 memfs_ll: add writeback_cache and no_timeout options
Add -o writeback_cache to enable the kernel writeback cache, and
-o no_timeout to cache attributes and dentries indefinitely (a 1-day
timeout) instead of the default 0s, so the example can exercise caching
behaviour.

The MEMFS_ATTR_TIMEOUT/MEMFS_ENTRY_TIMEOUT macros select the default or
the long timeout based on no_timeout, leaving the handler call sites
unchanged. writeback_cache is negotiated from a new init handler via
fuse_set_feature_flag().

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-18 00:21:41 +02:00
Bernd Schubert f420570bf5 memfs_ll: add mknod support
memfs only created regular files (via create) and directories (via
mkdir), so making a FIFO, socket, or device node failed with ENOSYS.
Add a mknod handler so these node types can be created.

The new dev_t rdev field on Inode stores the device number for
character and block special files and is reported back through
get_attr's st_rdev. The type bits in the mode argument select the
node kind, so the same handler covers all mknod-able types.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 19:14:35 +02:00
Bernd Schubert 2137d4b0cc memfs_ll: reject out-of-range read and write offsets
memfs_write passed offset and size straight to Inode::write_content,
which computes offset + size to size the content vector. A negative
offset converts to a huge unsigned value in that expression and aborts
the resize; an end position past the vector's index ceiling truncates
on the resize/iterator arithmetic (size_t/ptrdiff_t), which on a 32-bit
build is below the off_t range the kernel can hand us.

memfs_read only guarded offset >= content_size(). On a 64-bit build the
unsigned compare incidentally rejects a negative offset, but on 32-bit
the comparison is signed, so a negative offset slips through, truncates
in (size_t)offset, and indexes before the content buffer.

Validate at both handlers before touching the content store: reject a
negative offset with EINVAL, and in write reject an end position past
PTRDIFF_MAX (the std::vector size/index limit on both 32- and 64-bit
builds) with EFBIG.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert f884d0f362 memfs_ll: add -o fuse_dio for direct I/O and parallel direct writes
Add a -o fuse_dio mount option that, on open and create, sets fi->direct_io
and fi->parallel_direct_writes. parallel_direct_writes only takes effect
alongside direct_io, so both are enabled together under the one option.
Useful for benchmarking the direct-I/O path, where the kernel no longer
serializes writes per inode and no longer clamps reads at i_size.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 5c2aeefc78 memfs_ll: add -o null_io to bypass the content store
For benchmarking the FUSE interface itself rather than memfs's std::vector
content management, add a -o null_io mount option. Writes still consume
the incoming buffer (folding it into a volatile sink so the reads are not
elided) but discard it; reads return random data served from a scratch
buffer filled once at startup. The content store is never touched on
either path.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 2f6f54ba4c memfs_ll: fix rename overwrite use-after-free and nlink double-decrement
When memfs_rename overwrites an existing target, it called
remove_child(newname) -- which deletes the matched Dentry -- and then
dereferenced existing_dentry again via existing_dentry->get_inode() to
decrement the replaced inode's nlink, reading the freed Dentry: a
use-after-free. Resolve the target
inode before remove_child and use the saved pointer afterwards; the
inode stays alive through the Inodes map reference.

The same branch also decremented newparent's nlink twice when the target
was a directory: once explicitly and again inside remove_child(), which
already decrements a directory's parent. A directory-over-directory
rename therefore left newparent's link count one too low (rmdir
decrements the parent exactly once, via remove_child). Drop the
redundant explicit decrement.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert a22569e617 memfs_ll: take a lookup reference in memfs_link
memfs_link replies with fuse_reply_entry, which hands the kernel a
new lookup reference on the inode, but it only bumped nlink and never
bumped nlookup.  Every other entry-replying handler (lookup, create,
mkdir) accounts for that reference. Without it, the kernel's accumulated
forget count for a hard-linked inode exceeds nlookup, and dec_lookup()
underflows and panics with "Lookup count mismatch detected" -- readily
reproduced by a concurrent stress loop that hard-links files. Increment
nlookup on the successful link reply.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 7952b4b69d memfs_ll: lock rename's two directories in canonical inode order
memfs_rename locked the source then destination directory positionally.
Two concurrent renames in opposite directions could therefore deadlock
if the global inodes_mutex were ever relaxed to allow them to run
in parallel.  Today the exclusive Inodes.lock() held for the whole
operation serializes them, so the deadlock is masked, but the positional
order is a latent hazard.

Order the two directory locks by inode number -- always lock the lower
ino first -- so any two renames acquire the per-directory locks in the
same order. The ordering locals are declared with the other rename
locals, before the first goto, because the validation-failure goto
would otherwise cross an initialized declaration into its scope. The
operation body still acts on the directories by role; both are locked
regardless of which was taken first.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert b137cc849e memfs_ll: refcount inodes via shared_ptr to fix find() use-after-free
Inodes::find() returned a raw Inode* after releasing its shared lock,
so every handler dereferenced an inode with nothing preventing a
concurrent forget from erasing and deleting it -- a use-after-free.
Related defects: forget erased an inode at nlookup==0 regardless of
nlink, freeing still-linked inodes; readdir dereferenced raw Dentry
pointers snapshotted at opendir; lookup dereferenced a Dentry after
dropping the parent lock; forget_multi resolved and decremented without
a lock and never erased at zero.

Make the inode table own std::shared_ptr<Inode> and have
find()/find_locked()/create() return shared_ptr, so a handler holds a
strong reference for the whole operation. A directory link (Dentry) now
holds a shared_ptr too, so a linked inode cannot be freed while
reachable. get_children()/DirHandle snapshot strong references, fixing
the readdir UAF. lookup copies the child's shared_ptr under the parent
lock before using it. An inode is erased only once it has no kernel
references and no links (nlookup==0 && nlink==0); forget and the
rewritten forget_multi both apply that guard under inodes_mutex.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 8aa8ba677d memfs_ll: fix memfs_link self-deadlocks and dangling dentry
memfs_link took inodes_mutex exclusively via Inodes.lock(), then resolved
the source and parent inodes with Inodes.find(), which takes a shared lock
on the same mutex -- a self-deadlock on a non-recursive shared_mutex. It
then called parent_inode->add_child(), which re-locks the parent inode
mutex already held via parent_inode->lock() -- a second self-deadlock.
The new Dentry was owned by a unique_ptr whose raw pointer was handed
to the parent and then freed at scope exit, leaving the parent with
a dangling dentry.

Resolve both inodes with find_locked() (inodes_mutex already held)
and link with add_child_locked() (parent inode mutex already held).
Heap-allocate the Dentry and hand ownership to the parent, which frees
it in remove_child(), matching create()/mkdir(). On add_child_locked()
failure, delete the dentry and undo the inc_nlink() before replying
the error.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 9fbe6a0f17 memfs_ll: fix create/mkdir error-cleanup paths
memfs_mkdir's out_cleanup path called erase_locked() without holding
inodes_mutex, an unsynchronized map erase on the failure path. Use the
locking erase() variant.

memfs_create ignored add_child()'s return value. Its early find_child()
EEXIST check and the later add_child() are two separate parent-lock
acquisitions, so two concurrent create("foo") calls can both pass
the check, both allocate distinct inodes, and both reach add_child();
the loser gets EEXIST, which was silently discarded. That leaked the
loser's Dentry and orphaned its inode (left in the map, no link) while
still replying success.  Capture the return and, on failure, delete
the dentry and erase the orphan inode before replying the error. The
inode was never linked or returned to the kernel, so the unconditional
erase() is correct.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 00b1268128 memfs_ll: guard Inodes::size() with the table's own mutex
Inodes::size() locked a separate std::mutex that protected nothing while
reading the inode map, which is guarded by inodes_mutex. Concurrent
create/erase could therefore reshape the map under an unsynchronized
size() read. Take a shared lock on inodes_mutex instead and drop the
stray mutex member, which had no other use.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Bernd Schubert 237af76347 memfs_ll: clean two-lock partition for content and attributes
The per-inode state is split across two mutexes for concurrency:
Inode::mutex (content, mtime, dentries, nlookup) and Inode::attr_mutex
(mode, uid, gid, atime). A chmod/chown/atime update should not serialize
behind a write() resizing the content vector, so the two domains keep
separate locks.

The original split was inconsistent, which was the actual bug:
write_content() updated content+mtime under mutex while truncate()
did the same under attr_mutex, and get_attr() read content.size()
under attr_mutex. So content and mtime were each touched under both
locks depending on the path, and a getattr could race a concurrent
write resizing the content. get_mode()/get_mtime() read mutable fields
with no lock at all; get_children() and is_empty() iterated the dentry
vector unlocked.

Give each field a single owning lock. content, mtime, size: mutex (truncate
and set_mtime move onto it). mode, uid, gid, atime: attr_mutex. ctime
is set once at construction and never mutated, so it stays lock-free.
get_attr() needs a consistent cross-domain snapshot, so it is the one
place that holds both, always in the order mutex -> attr_mutex; no
setter holds both, so there is no ABBA risk.  memfs_setattr drops
its outer lock()/unlock() because the setters and the final get_attr
now self-lock; keeping it would deadlock the non-recursive mutex. A
concurrent getattr may then observe a partially-applied setattr, which
is acceptable for an example filesystem.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-17 17:32:10 +02:00
Benjamin Peterson 57954f4a5f libfuse: remove libnuma dependency
As discussed on https://github.com/libfuse/libfuse/issues/1515, libfuse has
a very light dependency on libnuma, only using very light wrappers over
mmap, mbind, and munmap. Remove the libnuma dependency by calling those
syscalls directly.

Populate memory after mmap to avoid SIGBUS later.

Signed-off-by: Benjamin Peterson <benjamin@python.org>
2026-06-15 23:05:11 +02:00
Darrick J. Wong dc6ce72129 libfuse: remove unmount command from the service api
Bernd received a complaint via github that the "unmount" command in the
new fuse service API is prone to malicious symlink creation races
because the umount2 call doesn't use UMOUNT_NOFOLLOW.  Therefore, if you
can trick a fuse service into asking the mount service helper to undo
the mount just after you've replaced the mount with a symlink to a
sensitive mount (e.g. /sys) then you whack a system hard.

I don't know why this unmounting functionality exists in libfuse, and
apparently Bernd doesn't like it.  Since the fuse service API has not
yet been released, let's just rip out the command and pretend it never
existed.

Fixes: 5d8b9a39a1 ("mount_service: add systemd socket service mounting helper")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong d4e199cb63 mountservice: advertise fuseblk support
Amazingly, permission for the use of the "blkdev" mount option (aka
mounting with fuseblk.SUBTYPE instead of fuse.SUBTYPE) rests solely on a
getuid()==0 check in userspace.  That /might/ be an indication that the
kernel also won't allow this, but as that's the criteria, let's
advertise fuseblk explicitly so that a fuse server can discover the
capability before calling fuse_service_session_mount().

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong 413437630c mountservice: don't print bdev preparation error when quiet mode is enabled
If the fuse server tries to open a file with
FUSE_SERVICE_REQUEST_FILE_QUIET, that means they don't want the mount
helper to emit any error messages about the file open() failing.  We did
this for the main open() call, but not for any of the code in prep_bdev.
Oops.

Fixes: 5d8b9a39a1 ("mount_service: add systemd socket service mounting helper")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong 57abfc3c1b mountservice: improve checking of protocol version
Codex complains that the protocol checking here is too stringent.  For
example, if the mount helper advertises min=1,max=2 but libfuse inside
the container is older and only supports protocol version 1, the second
check results in service mount failure when we could just use v1.

Fix this by requiring that maxver >= minver, and then looking for a lack
of overlap between what libfuse supports and what the helper advertised.

Fixes: 5d8b9a39a1 ("mount_service: add systemd socket service mounting helper")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong 50f6c907b2 mount_service: call the new fsmount ms_flags helpers in the right order
Codex noticed a discrepancy between the new fsmount code that Bernd
wrote and my port of the even newer mount service code to use the
helpers that Bernd wrote.  Specifically, set_fsconfig_ms_flags only
clears bits from MS_FLAGS if there's no corresponding MOUNT_ATTR_ flag,
whereas ms_flags_to_mount_attrs always clears them.

In other words, set_fsconfig_ms_flags MUST be called before
ms_flags_to_mount_attrs or we can lose the 'ro' option.  Fix this.

Fixes: 7211953256 ("mount_service: use the fsmount API helpers from mount_fsmount.c")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong 3c30d1c0c2 mount_service: don't pass the chopped buffer to apply_fsconfig_mount_opts
If mount_service_handle_mntopts_cmd previously looked at the mount
options to screen for allow_{other,root}, then tokstr (aka oc->value)
has been modified and not put back.  This causes truncation of the mount
options passed to fsconfig, so let's use the snapshot that we took
previously.  Codex noticed this.

Fixes: 7211953256 ("mount_service: use the fsmount API helpers from mount_fsmount.c")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Darrick J. Wong 6ef101c808 fsmount: don't truncate ms_flags in set_fsconfig_ms_flags
The mount(2) flags (i.e. MS_*) are unsigned long.  Don't truncate them
in this function by assigning to int.

Fixes: 14cb7b93bb ("Add support for the new linux mount API")
Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
2026-06-12 12:57:19 +02:00
Bernd Schubert 1fbf44e06c fusermount: restrict pin_mountpoint owner check to sticky directories
The pinned-fd TOCTOU fix rejected any mountpoint not owned by the
mounting user, but check_nonroot_dir_access() only enforces ownership
for sticky directories. Restrict the pinned-inode check to match.

Fixes: bad8b22c91 ("fusermount: fix sync-init TOCTOU by mounting on a pinned mountpoint fd")
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-12 12:08:57 +02:00
Bernd Schubert 3fedbac9b6 mount.fuse: clear error and test skip when binary unreachable after privilege drop
With drop_privileges, mount.fuse3 execs the file system binary only
after dropping all capabilities (CAP_DAC_OVERRIDE included) while
keeping uid 0. The kernel then applies plain DAC checks to the exec, so
a binary under a directory the capability-less process cannot traverse
(e.g. a user-owned mode-0700 home) fails with EACCES and /bin/sh prints
a bare 'Permission denied', surfacing in the tests as 'file system
process terminated prematurely'.

Resolving and opening the binary before the drop was rejected: it would
perform a privileged path resolution and open() on invoker-influenced
input (confused-deputy / privilege-escalation risk). The drop-first,
resolve-after order is preserved.

Instead, pre-flight the upcoming PATH lookup after the drop and fail
with a diagnostic that names the binary and points at mount.fuse3(8).
The check runs with the final credentials, so it is deny-only. The test
detects the same situation up front (uid/gid-aware traversal check) and
skips with a precise reason; root-owned CI checkouts keep running.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-12 12:08:57 +02:00
Bernd Schubert 4633c30df6 checkpatch: Disable LINE_SPACING
I disagree with this style annotation - just takes another line
without gaining much.

WARNING:LINE_SPACING: Missing a blank line after declarations
+			int ret = access_executable(test_path);
+			if (ret == 0)

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-12 12:08:57 +02:00
Jingbo Xu a59ffa424f fuse-io-uring: fix req_header_sz size calculation
The header buffer is described by "struct fuse_uring_req_header" rather
than "struct fuse_ring_ent".

Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com>
2026-06-12 09:10:38 +02:00
Bernd Schubert bad8b22c91 fusermount: fix sync-init TOCTOU by mounting on a pinned mountpoint fd
The --sync-init code path validates the mountpoint in
mount_fuse_prepare() (check_perm() chdir()s into the directory and
rewrites the path to ".", or opens a regular-file mountpoint and
rewrites it to /proc/self/fd/N), but that pinned reference is a local
variable that is discarded. mount_fuse_finish_fsmount() later mounts
using the original path string, which move_mount() re-resolves through
whatever the symlinks now say.

An unprivileged user controls the sync-init socket, so
wait_for_signal() gives an unbounded check-to-use window: validate an
attacker-owned directory, swap a parent component to a symlink into
/etc, then signal -- the mount lands on a root-owned directory,
defeating check_nonroot_dir_access(). With user_allow_other this yields
local root via a fake sudoers drop-in.

Pin the validated inode as a single fd (mount_context.mnt_fd) obtained
from the already-chdir'd CWD or the open mountpoint fd, mount onto it
via move_mount() with MOVE_MOUNT_T_EMPTY_PATH (no path re-resolution),
and assert at the mount site that fstat(mnt_fd) still matches the stat
captured during validation. The legacy mount() path and the library
direct-mount path are unchanged (mnt_fd == -1 keeps path resolution).

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-11 15:38:47 +02:00
Abhinav Agarwal b6cb345591 fuse_loop_mt: Remove duplicate pthread_mutex_destroy of mt_lock
fuse_session_loop_mt_312() unconditionally destroys se->mt_lock before
returning. The caller then invokes fuse_session_destroy(), which
destroys the same mutex again. This is undefined behavior per POSIX
on every MT shutdown.

fuse_session_reset() does not re-initialize mt_lock between these
calls, so the second destroy always operates on an already-destroyed
mutex.

Remove the destroy from fuse_session_loop_mt_312 and let
fuse_session_destroy remain the sole owner of mutex teardown,
consistent with how se->mt_finish (the semaphore) is already handled.

Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
2026-06-11 10:45:20 +02:00
Abhinav Agarwal 6c055dddf7 fuse_lowlevel: Free mountpoint in fuse_session_destroy
If fuse_session_mount() succeeds but fuse_session_unmount() is never
called (e.g., on an error path), the mountpoint string allocated by
strdup in fuse_session_mount() is leaked. Free it in
fuse_session_destroy() using the same atomic_exchange pattern as
fuse_session_unmount() to handle concurrent access safely.

Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
2026-06-11 10:45:20 +02:00
Abhinav Agarwal 79aefd4fd9 fuse_lowlevel: Add O_CLOEXEC to pipe-max-size procfs open
grow_pipe_to_max() opens /proc/sys/fs/pipe-max-size without O_CLOEXEC.
In a multi-threaded FUSE daemon, a concurrent fork+exec (e.g., to
invoke fusermount) between the open() and close() calls would leak this
fd into the child process.

Add the same O_CLOEXEC fallback guard used in mount.c and
fuse_loop_mt.c for portability to older headers.

Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
2026-06-11 10:45:20 +02:00
Abhinav Agarwal 349b83231b fusermount: Check fstat return value for comm fd validation
The fstat() call on the communication fd from _FUSE_COMMFD was not
checking its return value. On failure (e.g., invalid fd number),
statbuf remains uninitialized and the subsequent S_ISSOCK check
operates on garbage stack data, potentially bypassing the socket
type validation.

Check the return value and fail closed on error.

Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
2026-06-11 10:45:20 +02:00
Abhinav Agarwal 42e57a8019 fuse_lowlevel: Use snprintf for /proc status path construction
fuse_req_getgroups() uses sprintf() to write into a fixed 128-byte
buffer. While the current format string cannot overflow in practice,
using snprintf() with sizeof(path) provides bounds safety as a
defense-in-depth measure.

Signed-off-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
2026-06-11 10:45:20 +02:00