[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