[Devel] [PATCH hci-8.0] fs/fuse: fix writectr imbalance when failing writeback of invalidated file
Konstantin Khorenko
khorenko at virtuozzo.com
Mon Sep 21 18:55:12 MSK 2026
Actors: T1 opens and later closes the file through ff B; T2 writes and truncates through ff C; T3 does an O_DIRECT read into C's mmap; flusher is background writeback; fused is the userspace daemon; kworker runs fuse_inval_files_work(). Inode I,
page P, vstorage mount (close_wait=1, writeback_cache=1).
Part 1. Setup: a stale open makes B the head of write_files
step who what happens state after the step
---- ------- ---------------------------------------------------------- ------------------------------------
1 T1 open(f) -> fuse_send_open(): FUSE_OPEN for B sent to fused, B: open_seq = N, no reply yet
waiting for the reply
2 fused writes FUSE_NOTIFY_INVAL_FILES(I) to /dev/fuse fi->inval_files_seq = N+1
-> fuse_invalidate_files(): every ff on fi->rw_files (old ones):
set_bit(FAIL_IMMEDIATELY) on every ff in fi->rw_files FAIL_IMMEDIATELY
set_bit(FUSE_I_INVAL_FILES, &fi->state) INVAL bit set
queue_delayed_work(inval_files_work)
3 kworker fuse_inval_files_work(): page cache of I is empty
truncate_pagecache_range(I, 0, -1) INVAL bit cleared
clear_bit(FUSE_I_INVAL_FILES)
4 T2 open(f) -> FUSE_OPEN for C, fused replies C: open_seq = N+2 > N+1, not stale
fuse_finish_open() -> fuse_link_write_file(): list_add fi->write_files = [C, ...]
fuse_link_rw_file(): open_seq >= inval_files_seq, fine
5 fused replies to FUSE_OPEN for B (step 1)
T1 fuse_finish_open() -> fuse_link_write_file(): list_add fi->write_files = [B, C, ...]
fuse_link_rw_file(): B->open_seq N < inval_files_seq N+1 B: FAIL_IMMEDIATELY
-> set_bit(FUSE_S_FAIL_IMMEDIATELY, &B->ff_state)
B->count = 1 (the open only)
From here on __fuse_write_file_get() for I returns B: it is first in the list and it is "dead". This is old behaviour, unrelated to the patch.
Part 2. Concurrent part: primary waits in the queue, aux attaches to it, out_free
One column per thread, time runs top to bottom. Rightmost column is shared state.
T2 (ff C) T3 (ff C, mmap) flusher state
------------------------------ ---------------------------- ----------------------------------- ----------------------------
6. mmap(C); writes into P P dirty, PTE writable
page_mkwrite() ok, C not FAIL
7. read(X, addr(P), O_DIRECT)
GUP: FOLL_WRITE, P already
dirty and writable, pinned
request sent to disk, waiting
8. ftruncate(C)
fuse_do_setattr():
inode_lock(I)
fuse_set_nowrite():
writectr += FUSE_NOWRITE writectr = FUSE_NOWRITE
wait writectr == NOWRITE (nothing in flight, returns)
FUSE_SETATTR -> fused,
waiting for the reply
9. fuse_writepages(I)
fill(P):
INVAL bit clear -> proceed
data->ff = write_file_get() = B B->count = 2
clear_page_dirty_for_io(P) PTE of P write-protected
wpa1 = args_setup(P, B)
fuse_file_get(B) B->count = 3
tree_insert(wpa1) P in fi->writepages
fuse_writepages_send():
list_add_tail(queued_writes) wpa1 queued
fuse_flush_writepages():
writectr < 0 -> nothing
end_page_writeback(P)
fuse_release_ff(B) B->count = 2
(before the patch: shoot on
sight, wpa1 completed at
once, never queued)
10. disk replied, DIO read
completes:
unpin_user_pages_dirty_lock()
-> set_page_dirty(P) P dirty again,
no page_mkwrite, hence no fuse_page_is_writeback(P)
fuse_wait_on_page_writeback = true (wpa1 in the tree)
11. fuse_writepages(I), next pass
fill(P):
data->ff = B B->count = 3
wpa2 = args_setup(P, B)
fuse_file_get(B) B->count = 4
fuse_writepage_add(wpa2, P):
insert_writeback finds wpa1
no P among wpa1->next
wpa1->next = wpa2 wpa2 = aux of wpa1
return false
folio_end_writeback(P)
fuse_release_ff(B) B->count = 3
(1 open + wpa1 + wpa2)
12. fused replied to SETATTR
fuse_do_setattr():
spin_lock(fi->lock)
__fuse_release_nowrite():
writectr = 0
fuse_flush_writepages():
fuse_send_writepage(wpa1):
writectr++ writectr = 1
fail_immediately(B) -> true <== new check from the patch
mapping_set_error(-EIO)
goto out_free:
writectr-- writectr = 0
rb_erase(wpa1)
fuse_writepage_finish(wpa1):
close_wait -> __fuse_file_put(B) B->count = 2
spin_unlock
for aux = wpa1->next (wpa2):
fuse_writepage_finish_stat(wpa2)
fuse_writepage_free(wpa2):
close_wait -> put is NOT done <== the leak B->count = 2, should be 1
kfree(wpa2)
fuse_writepage_free(wpa1): no put (close_wait), kfree
spin_unlock(fi->lock)
inode_unlock(I)
Part 3. Outcome
step who what happens result
---- ---- -------------------------------------------------------------- ---------------------------------
13 T1 close(B) -> fuse_release():
write_inode_now(I, 1) no dirty pages left
list_del_init(&B->write_entry)
wait_event(fi->page_waitq,
refcount_read(&B->count) == 1) B->count = 2 forever,
no timeout, nobody to wake it:
close() never returns
i think the leak is not that probable and i can commit the current patch as is
and we can go production without fixing that leak.
What do you think?
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
More information about the Devel
mailing list