OpenVZ / Virtuozzo kernel development (devel@openvz.org)
 help / color / mirror / Atom feed
From: Konstantin Khorenko <khorenko@virtuozzo.com>
To: Liu Kui <kui.liu@virtuozzo.com>, Alexey Kuznetsov <kuznet@virtuozzo.com>
Cc: devel@openvz.org, azaitsev@virtuozzo.com
Subject: Re: [Devel] [PATCH hci-8.0] fs/fuse: fix writectr imbalance when failing writeback of invalidated file
Date: Mon, 21 Sep 2026 17:55:12 +0200	[thread overview]
Message-ID: <eb77a898-57f7-4f64-9bf6-724c2df2842c@virtuozzo.com> (raw)
In-Reply-To: <179000337661.2953935.5887806686603853170.b4-review@b4>

  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


_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

  reply	other threads:[~2026-09-21 15:56 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-18 10:25 Liu Kui
2026-09-19 11:14 ` Alexey Kuznetsov
2026-09-21 15:09 ` Konstantin Khorenko
2026-09-21 15:55   ` Konstantin Khorenko [this message]
2026-09-21 15:58 ` Konstantin Khorenko

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=eb77a898-57f7-4f64-9bf6-724c2df2842c@virtuozzo.com \
    --to=khorenko@virtuozzo.com \
    --cc=azaitsev@virtuozzo.com \
    --cc=devel@openvz.org \
    --cc=kui.liu@virtuozzo.com \
    --cc=kuznet@virtuozzo.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox