[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