Skip to content

fuse: fix some subtle deadlocks and lock irregularities - #197

Merged
hbirth merged 5 commits into
DDNStorage:redfs-ubuntu-hwe-6.17.0-16.16-24.04.1from
hbirth:redfs-ubuntu-hwe-6.17.0-16.16-24.04.1
Aug 15, 2026
Merged

fuse: fix some subtle deadlocks and lock irregularities#197
hbirth merged 5 commits into
DDNStorage:redfs-ubuntu-hwe-6.17.0-16.16-24.04.1from
hbirth:redfs-ubuntu-hwe-6.17.0-16.16-24.04.1

Conversation

@hbirth

@hbirth hbirth commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fix some lock irregularities which most of them were introduced by our previous patches adding the rwsem to avoid serializing writer on inode lock.

Add the writeback flag to setattr to mark writeback attribute changes.

hbirth added 5 commits August 14, 2026 16:02
fuse_write_inode() -> fuse_flush_times() pushes out the mtime/ctime that
the kernel owns locally while the writeback cache is on.  On the wire
that request is indistinguishable from a userspace "touch -m": both
arrive as SETATTR with FATTR_MTIME | FATTR_CTIME | FATTR_FH, because
trust_local_cmtime makes iattr_to_fattr() send CTIME whenever the
writeback cache is enabled.  A server that wants to handle a cache flush
differently from an explicit attribute change - skipping a cluster-wide
lock, merging rather than overwriting - has no way to tell them apart.

Add FATTR_WRITEBACK, a control bit in fuse_setattr_in.valid alongside the
existing non-attribute bits FATTR_FH, FATTR_LOCKOWNER and
FATTR_KILL_SUIDGID.  It selects no attribute, it only states that the
request originates from writeback.

Bit 30 is used rather than the next free one.  libfuse mirrors the wire
bits into its own FUSE_SET_ATTR_* namespace, where bits 12 to 17 are
already taken by library-internal flags, and it masks incoming requests
against that namespace; a bit picked from the low end would collide there
and need translating on the way in.  Bit 30 is free on both sides and
clear of the sign bit of the int that the libfuse setattr operation
takes, so one value works end to end.

The bit is negotiated at INIT time with FUSE_SETATTR_WRITEBACK and is
only set on a connection whose server asked for it, so servers that do
not know the bit never receive it.

Only ->write_inode() is marked.  The other kernel-initiated SETATTR,
fuse_do_truncate() rolling back a failed extending direct-IO write, is
deliberately left unmarked: it is a size correction rather than an
attribute writeback, and conflating the two would make the flag
ambiguous.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_reverse_inval_inode() invalidates with invalidate_inode_pages2_range(),
which waits out folios under writeback and launders dirty ones.  Both need a
FUSE_WRITE reply, and while fi->writectr < 0 none can arrive:
fuse_flush_writepages() parks the request on fi->queued_writes until
fuse_release_nowrite().

A truncate holds that freeze across its whole SETATTR, and the server revokes
the truncated range from inside the SETATTR handler, so the notify blocks the
very thread that owes the reply lifting the freeze.  generic/014 deadlocks
within seconds, in folio_wait_writeback() under fuse_launder_folio() under
fuse_reverse_inval_inode().

fuse_do_setattr() already states the rule ("Only call
invalidate_inode_pages2() after removing FUSE_NOWRITE, otherwise
fuse_launder_folio() would deadlock").  Give the notify path the same: while
frozen, use invalidate_mapping_pages(), which skips dirty and under-writeback
folios instead of waiting on them.  The stale clean folios still go, the DLM
grant is revoked either way, and the freezes that span a request drop the
cache themselves when they finish.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_cache_write_iter() holds wb_inval_rwsem for read across the write,
and the kiocb_modified() -> file_remove_privs() that precedes it runs
under that same gate.  Without handle_killpriv[_v2] the privilege kill
asks the server (GETATTR, then SETATTR), and a server that invalidates
the inode from inside such a handler blocks in percpu_down_write()
draining the gate reader that is waiting for its reply.  generic/193
hangs there.

The gate only has to fence the page-cache dirtying, so run the write
checks, the privilege kill and the timestamp update before entering it.
task_io_account_write() stays behind the gate, so a write that the
forced-DIO re-check reroutes is not counted twice.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_get_cache_mask() returns 0 once the connection has DLM, so every
GETATTR reply overwrites i_size, mtime and ctime, and truncate_pagecache()
then drops the tail the client still holds dirty.  The only way a server
can make that answer true is to revoke the client from inside the handler,
which deadlocks against the coherency gate.

A write grant already guarantees that no other node can touch the range, so
keep the cached values while one is held: the size when the server reports
less than i_size and [srv_size, i_size) is fully granted, mtime and ctime
while the cache under the grant is still dirty.  A remote truncate has to
revoke first, so the smaller size that follows is applied as usual.

The attribute-driven invalidation now keys off STATX_SIZE instead of the
whole mask, so a reply that does shrink i_size still truncates the page
cache when only the timestamps were served from the cache.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
Without handle_killpriv[_v2], fuse_setattr() kills the bits by asking the
server, and fuse_do_setattr() freezes writepages around that SETATTR.
fuse_set_nowrite() asserts BUG_ON(fi->writectr < 0) under fi->lock, which
assumes the caller holds i_rwsem exclusive: with the writeback cache and
DLM, buffered writes hold it only shared.

Two writers to a suid file can then both pass dentry_needs_remove_privs()
before either has cleared the bits, and the second one hits the assert.  It
oopses inside spin_lock(&fi->lock), so fi->lock stays held and the i_rwsem
read count leaks: the inode wedges and the box follows.  The race window is
a full GETATTR plus SETATTR, so it is not narrow, and an unprivileged user
can set the bit on a file it owns.

Keep those writes off the writeback path, which is the one that relaxes
i_rwsem to shared, the way handle_killpriv_v2 writes already are.  Scoped
to DLM connections, since every other configuration already holds i_rwsem
exclusive for a buffered write, and only writes that still find the bits
set pay for it.

Note that the non-writeback path takes no DLM lock, so those writes leave
clean folios in the page cache without a grant covering them.  That gap
already exists for handle_killpriv_v2 and is not addressed here.

Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>

@yongzech yongzech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hbirth
hbirth merged commit 7887305 into DDNStorage:redfs-ubuntu-hwe-6.17.0-16.16-24.04.1 Aug 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants