Skip to content

fuse: release newfd when fd migration fails after fd_create() - #4842

Draft
ThalesBarretto wants to merge 2 commits into
gluster:develfrom
ThalesBarretto:leak/fdmig/fuse-migrate-fd-open-newfd
Draft

ThalesBarretto wants to merge 2 commits into
gluster:develfrom
ThalesBarretto:leak/fdmig/fuse-migrate-fd-open-newfd

Conversation

@ThalesBarretto

Copy link
Copy Markdown
Contributor

fuse: release newfd when fd migration fails after fd_create()

Problem

fuse_migrate_fd_open() creates the new-graph fd with fd_create() and hands it over to
basefd_ctx->activefd only at the very end. The two exits in between — fuse_fd_ctx_check_n_create()
failing and the syncop_open()/syncop_opendir() on the new graph failing — jump to out:, which only
wipes the loc. newfd, the fuse fd ctx just created for it, and the inode and lk_ctx references it carries
are never released, so every failed reopen during a graph switch leaks one fd_t + one
gf_fuse_mt_fd_ctx_t and pins the new-graph inode for the life of the process. The application already
gets EBADF on that handle (migration_failed); the leak is the part it cannot see.

The GF_VALIDATE_OR_GOTO() exits also left ret at the inode_path() length, so fuse_migrate_fd() was
told the migration succeeded although activefd was never set.

Fix

  • Make every early exit past the lookup a failure (ret = -1 before the validates).
  • At out:, when ret < 0 && newfd, destroy newfd's fuse fd ctx explicitly and fd_unref() it.
    The explicit fuse_fd_ctx_destroy() is needed for directories: fd_destroy() reaches the ctx through
    the .release cbk for regular files only, no .releasedir cbk is registered. For regular files it is an
    idempotent duplicate (fd_ctx_del_ptr() returns NULL the second time).

Reproduction and A/B

Stock builds of devel a482a8578a and of this branch (default ./configure: tcmalloc, no mem-pools, no debug).
With performance.open-behind off (with it on the reopen is answered locally and cannot fail): files held open
O_RDWR on a FUSE mount, chattr +i on their brick copies so the O_RDWR reopen fails EPERM, a volume set, the mount
poked with stat (FUSE switches graphs on the next request), everything closed, client statedump. On a tcmalloc
build there is no fd_t mem-pool, so the count is the fuse xlator's memory accounting: one gf_common_mt_fd_ctx
array per live fd_t.

live fds (gf_common_mt_fd_ctx) gf_fuse_mt_fd_ctx_t itable active_size
before, 10 files 10 10 11
after, 10 files 0 0 1
before, 100 files 100 100 101
after, 100 files 0 0 1
before, 10 directories (brick nofile clamped, 9 opendir EMFILE) 9 10 10
after, 10 directories 0 1 (*) 1
[mount/fuse.fuse - usage-type gf_common_mt_fd_ctx memusage]
num_allocs=10
[mount/fuse.fuse - usage-type gf_fuse_mt_fd_ctx_t memusage]
num_allocs=10
[xlator.mount.fuse.itable]
xlator.mount.fuse.itable.active_size=11
[xlator.mount.fuse.itable.active.2]
gfid=71576eab-94b5-475a-a3a4-3140907f2892
nlookup=0
fd-count=0
ref=1
ref_by_xl:.fuse=1

(the leaked fd was never bound, hence fd-count=0; the inode stays active on the reference fd_create() took.)
(*) the remaining ctx belongs to the one successfully migrated directory: fuse registers no .releasedir
cbk, so fd_destroy() never frees a directory fd's fuse ctx — the gap #4490 closes, not this change. Verified on
stock arms carrying #4490: devel + #4490 still leaks the 9 failed newfds (9 fds / 9 ctxs, the migrated directory's
ctx now freed); this change + #4490 gives 0 / 0.

Test

tests/bugs/fuse/fd-migration-reopen-failure-leak.t: five O_RDWR handles, chattr +i on the brick copies,
one graph switch, close, expects the fuse xlator's gf_common_mt_fd_ctx allocation count (live fds; exists on
every build, unlike the fd_t mem-pool) and its gf_fuse_mt_fd_ctx_t count to be 0 in the mount statedump. Fails
on stock devel on exactly those two assertions (Got "5" instead of "0"), passes with the change.

Present since 33d9df7a93 (2012); also in release-11.

Fixes: #4841

fuse_migrate_fd_open() creates the new-graph fd with fd_create() and
hands it over to basefd_ctx->activefd only at the very end. The two
exits in between -- fuse_fd_ctx_check_n_create() failing and the
syncop_open()/syncop_opendir() on the new graph failing -- jump to
out:, which only wipes the loc. newfd, the fuse fd ctx just created for
it, and the inode and lk_ctx references it carries are never released,
so every failed reopen during a graph switch leaks one fd_t + one
gf_fuse_mt_fd_ctx_t and pins the new-graph inode for the life of the
process (the application already gets EBADF on that handle, the leak
is the part it cannot see). The GF_VALIDATE_OR_GOTO() exits also left
ret at the inode_path() length, so fuse_migrate_fd() was told the
migration succeeded although activefd was never set.

Release newfd on every failure after fd_create(), destroying its fuse
fd ctx explicitly first: fd_destroy() reaches the ctx through the
.release cbk for regular files only, no .releasedir cbk is registered.
Make every early exit past the lookup a failure (ret = -1).

Reproducible with performance.open-behind off (with it on the reopen is
answered locally and cannot fail): keep files open across a
'volume set', make the reopen fail (RLIMIT_NOFILE clamp on the brick,
or chattr +i on the brick file with an O_RDWR handle), close everything
and take a statedump: fd_t active-count and gf_fuse_mt_fd_ctx_t
num_allocs equal the number of failed reopens; 0 with this change.
Present since fd migration was introduced (33d9df7, 2012).

Fixes: gluster#4841
Signed-off-by: Thales Antunes de Oliveira Barretto <thales.barretto.git@gmail.com>
Regression test for the error path of fuse_migrate_fd_open(): keep five
files open O_RDWR on a FUSE mount with open-behind off, make the brick
copies immutable (chattr +i) so the O_RDWR reopen on the new graph fails
with EPERM, switch graphs (poking the mount, since FUSE switches on the
next request), close the now-dead handles and expect the client statedump
to show fd_t active-count 0 and no gf_fuse_mt_fd_ctx_t allocations.

Updates: gluster#4841
Signed-off-by: Thales Antunes de Oliveira Barretto <thales.barretto.git@gmail.com>
@ThalesBarretto
ThalesBarretto marked this pull request as draft September 22, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fuse: a failed fd reopen during a graph switch leaks the new fd (fd_t + fuse ctx + inode ref)

1 participant