<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<style type="text/css" style="display:none;"> P {margin-top:0;margin-bottom:0;} </style>
</head>
<body dir="ltr">
<div class="elementToProof" style="font-family: Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb(0, 0, 0);">
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. </div>
<div style="font-family: Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb(0, 0, 0);">
<br>
</div>
<hr style="display: inline-block; width: 98%;">
<div id="divRplyFwdMsg">
<div style="direction: ltr; font-family: Calibri, sans-serif; font-size: 11pt; color: rgb(0, 0, 0);">
<b>From:</b> Konstantin Khorenko <khorenko@virtuozzo.com><br>
<b>Sent:</b> 21 September 2026 23:55<br>
<b>To:</b> Kui Liu <kui.liu@virtuozzo.com>; Alexey Kuznetsov <kuznet@virtuozzo.com><br>
<b>Cc:</b> Andrey Zaitsev <azaitsev@virtuozzo.com>; devel@openvz.org <devel@openvz.org><br>
<b>Subject:</b> Re: [Devel] [PATCH hci-8.0] fs/fuse: fix writectr imbalance when failing writeback of invalidated file</div>
<div style="direction: ltr;"> </div>
</div>
<div style="font-size: 11pt;"> 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,<br>
page P, vstorage mount (close_wait=1, writeback_cache=1).<br>
<br>
Part 1. Setup: a stale open makes B the head of write_files<br>
<br>
step who what happens state after the step<br>
---- ------- ---------------------------------------------------------- ------------------------------------<br>
1 T1 open(f) -> fuse_send_open(): FUSE_OPEN for B sent to fused, B: open_seq = N, no reply yet<br>
waiting for the reply<br>
<br>
2 fused writes FUSE_NOTIFY_INVAL_FILES(I) to /dev/fuse fi->inval_files_seq = N+1<br>
-> fuse_invalidate_files(): every ff on fi->rw_files (old ones):<br>
set_bit(FAIL_IMMEDIATELY) on every ff in fi->rw_files FAIL_IMMEDIATELY<br>
set_bit(FUSE_I_INVAL_FILES, &fi->state) INVAL bit set<br>
queue_delayed_work(inval_files_work)<br>
<br>
3 kworker fuse_inval_files_work(): page cache of I is empty<br>
truncate_pagecache_range(I, 0, -1) INVAL bit cleared<br>
clear_bit(FUSE_I_INVAL_FILES)<br>
<br>
4 T2 open(f) -> FUSE_OPEN for C, fused replies C: open_seq = N+2 > N+1, not stale<br>
fuse_finish_open() -> fuse_link_write_file(): list_add fi->write_files = [C, ...]<br>
fuse_link_rw_file(): open_seq >= inval_files_seq, fine<br>
<br>
5 fused replies to FUSE_OPEN for B (step 1)<br>
T1 fuse_finish_open() -> fuse_link_write_file(): list_add fi->write_files = [B, C, ...]<br>
fuse_link_rw_file(): B->open_seq N < inval_files_seq N+1 B: FAIL_IMMEDIATELY<br>
-> set_bit(FUSE_S_FAIL_IMMEDIATELY, &B->ff_state)<br>
B->count = 1 (the open only)<br>
<br>
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.<br>
<br>
Part 2. Concurrent part: primary waits in the queue, aux attaches to it, out_free<br>
<br>
One column per thread, time runs top to bottom. Rightmost column is shared state.<br>
<br>
T2 (ff C) T3 (ff C, mmap) flusher state<br>
------------------------------ ---------------------------- ----------------------------------- ----------------------------<br>
6. mmap(C); writes into P P dirty, PTE writable<br>
page_mkwrite() ok, C not FAIL<br>
<br>
7. read(X, addr(P), O_DIRECT)<br>
GUP: FOLL_WRITE, P already<br>
dirty and writable, pinned<br>
request sent to disk, waiting<br>
<br>
8. ftruncate(C)<br>
fuse_do_setattr():<br>
inode_lock(I)<br>
fuse_set_nowrite():<br>
writectr += FUSE_NOWRITE writectr = FUSE_NOWRITE<br>
wait writectr == NOWRITE (nothing in flight, returns)<br>
FUSE_SETATTR -> fused,<br>
waiting for the reply<br>
9. fuse_writepages(I)<br>
fill(P):<br>
INVAL bit clear -> proceed<br>
data->ff = write_file_get() = B B->count = 2<br>
clear_page_dirty_for_io(P) PTE of P write-protected<br>
wpa1 = args_setup(P, B)<br>
fuse_file_get(B) B->count = 3<br>
tree_insert(wpa1) P in fi->writepages<br>
fuse_writepages_send():<br>
list_add_tail(queued_writes) wpa1 queued<br>
fuse_flush_writepages():<br>
writectr < 0 -> nothing<br>
end_page_writeback(P)<br>
fuse_release_ff(B) B->count = 2<br>
(before the patch: shoot on<br>
sight, wpa1 completed at<br>
once, never queued)<br>
<br>
10. disk replied, DIO read<br>
completes:<br>
unpin_user_pages_dirty_lock()<br>
-> set_page_dirty(P) P dirty again,<br>
no page_mkwrite, hence no fuse_page_is_writeback(P)<br>
fuse_wait_on_page_writeback = true (wpa1 in the tree)<br>
<br>
11. fuse_writepages(I), next pass<br>
fill(P):<br>
data->ff = B B->count = 3<br>
wpa2 = args_setup(P, B)<br>
fuse_file_get(B) B->count = 4<br>
fuse_writepage_add(wpa2, P):<br>
insert_writeback finds wpa1<br>
no P among wpa1->next<br>
wpa1->next = wpa2 wpa2 = aux of wpa1<br>
return false<br>
folio_end_writeback(P)<br>
fuse_release_ff(B) B->count = 3<br>
(1 open + wpa1 + wpa2)<br>
<br>
12. fused replied to SETATTR<br>
fuse_do_setattr():<br>
spin_lock(fi->lock)<br>
__fuse_release_nowrite():<br>
writectr = 0<br>
fuse_flush_writepages():<br>
fuse_send_writepage(wpa1):<br>
writectr++ writectr = 1<br>
fail_immediately(B) -> true <== new check from the patch<br>
mapping_set_error(-EIO)<br>
goto out_free:<br>
writectr-- writectr = 0<br>
rb_erase(wpa1)<br>
fuse_writepage_finish(wpa1):<br>
close_wait -> __fuse_file_put(B) B->count = 2<br>
spin_unlock<br>
for aux = wpa1->next (wpa2):<br>
fuse_writepage_finish_stat(wpa2)<br>
fuse_writepage_free(wpa2):<br>
close_wait -> put is NOT done <== the leak B->count = 2, should be 1<br>
kfree(wpa2)<br>
fuse_writepage_free(wpa1): no put (close_wait), kfree<br>
spin_unlock(fi->lock)<br>
inode_unlock(I)<br>
<br>
Part 3. Outcome<br>
<br>
step who what happens result<br>
---- ---- -------------------------------------------------------------- ---------------------------------<br>
13 T1 close(B) -> fuse_release():<br>
write_inode_now(I, 1) no dirty pages left<br>
list_del_init(&B->write_entry)<br>
wait_event(fi->page_waitq,<br>
refcount_read(&B->count) == 1) B->count = 2 forever,<br>
no timeout, nobody to wake it:<br>
close() never returns<br>
<br>
i think the leak is not that probable and i can commit the current patch as is<br>
and we can go production without fixing that leak.<br>
<br>
What do you think?<br>
<br>
--<br>
Best regards,<br>
<br>
Konstantin Khorenko,<br>
Virtuozzo Linux Kernel Team<br>
<br>
<br>
</div>
</body>
</html>