[Devel] [PATCH hci-8.0] fs/fuse: fix writectr imbalance when failing writeback of invalidated file
Kui Liu
kui.liu at virtuozzo.com
Tue Sep 22 12:07:31 MSK 2026
The ff leak is real, It is there regardless of this patch. The bug was introduced after rebased to RH10 when mainline fuse code changed the way how ff's ref is taken in the fuse_writepages path. I didn't catch the change during rebase; I'll work on another patch fixing the ff leak.
________________________________
From: Konstantin Khorenko <khorenko at virtuozzo.com>
Sent: 21 September 2026 23:55
To: Kui Liu <kui.liu at virtuozzo.com>; Alexey Kuznetsov <kuznet at virtuozzo.com>
Cc: Andrey Zaitsev <azaitsev at virtuozzo.com>; devel at openvz.org <devel at openvz.org>
Subject: Re: [Devel] [PATCH hci-8.0] fs/fuse: fix writectr imbalance when failing writeback of invalidated file
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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.openvz.org/pipermail/devel/attachments/20260922/1cd277db/attachment-0001.html>
More information about the Devel
mailing list