Skip to content

Let the simple example be mounted idmapped - #750

Merged
cberner merged 2 commits into
masterfrom
claude/xfstests-skip-investigation-s9gdr1
Aug 8, 2026
Merged

Let the simple example be mounted idmapped#750
cberner merged 2 commits into
masterfrom
claude/xfstests-skip-investigation-s9gdr1

Conversation

@cberner

@cberner cberner commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Adds --idmap, which requests FUSE_ALLOW_IDMAP and mounts with the two things
that capability needs: default_permissions, without which the kernel refuses
the connection, and allow_other, without which fuser refuses the capability.

Why it is a flag rather than the default

Negotiating it withholds the caller's ids from every request, whether or not
anyone has made an idmapped mount - and this filesystem's permission and privilege
logic is built on those ids. That has a measurable cost, below.

Access and privilege need opposite answers

This is the part worth reviewing. An idmapped mount has no caller to check, and
the two kinds of question that reached for one do not fall the same way:

Access - may the caller act? default_permissions is what makes the
capability available at all, so the kernel has already decided, against ids it
can map and this filesystem cannot. An unknown caller is allowed. The ids reached
85 places in three shapes - a mode-bit check, a test for root, a test for owning
an inode - now may_access(), caller_is_root() and caller_owns().

Privilege - do setuid and setgid survive? default_permissions decides who
may act, not whose privileges survive acting, so nothing has decided this on the
filesystem's behalf. An unknown caller keeps nothing:
caller_keeps_privileges() and caller_outside_group() are deliberately the
other way round, so a bit that cannot be shown to have been earned is dropped.

I had these the same way round at first, which retained setgid where it should
have gone - the unsafe direction. generic/683, 684 and 685 caught it.

FUSE_HANDLE_KILLPRIV_V2 follows from the same reasoning and is not negotiated
under --idmap at all: taking that job on means judging a caller, and not every
operation that drops these bits carries a flag saying so. The kernel keeps it,
knowing the mapping.

Two things that were not permission checks

  • truncate() took a uid and gid only to check access, and one caller passed 0
    to skip the check - a file handle already granted write access carries it
    whatever the mode says now. That is an argument now rather than a sentinel.
  • has_fsetid() takes the uid only for when /proc cannot be read, so it takes
    an Option and an unknown caller falls back to nothing rather than to root.

What it does and does not enable

generic/644, 656, 689 and 698 pass with the flag - measured, not assumed.

The suite does not set it, and xfstests.sh is unchanged. Enabling it
suite-wide costs generic/193, 683, 684 and 685, which check what this filesystem
does with suid and sgid and depend on the ids the capability withholds. Four
tests gained for four lost is not a trade worth making, and the four that break
are the more fundamental ones.

Two of the six would stay excluded either way, neither for anything to do with
idmapping:

  • generic/645 creates thousands of files in one directory. Resolving a single
    name here reads the directory's whole content, so the test's own cost is
    quadratic - it makes steady progress (16k requests in the first 30s, still
    climbing at 55k after five minutes) and would finish, given long enough for
    that to be worth doing.
  • generic/699 mounts overlayfs over the filesystem under test and reaches
    ESTALE, which is what generic/631 is already excluded for: overlayfs asks for
    file handles this filesystem does not hand out.

Testing

Full suite, green: 175 tests run, 0 failures, no filesystem panics - the run that
matters, since this rewrote every permission check in the example.

Behavior checked directly as well: chattr's immutable and append-only rules, the
tmpfile round trip, creation times, and setgid inheritance in a setgid directory
all still hold, and an unprivileged caller is still refused a 0700 directory and
still allowed a 0777 one.

All six of CI's gates clean locally - fmt, clippy at each of the three feature
sets make pre uses, the macOS check with --all-targets, rustdoc, and the test
suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q4hiZwHE9fEYdn7DK3ZrV2


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d03c5612e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread examples/simple.rs Outdated
Comment on lines +906 to +910
self.idmapped = config.add_capabilities(InitFlags::FUSE_ALLOW_IDMAP).is_ok();
if self.idmapped {
info!("idmapped mounts enabled; the kernel makes the access checks");
} else {
warn!("idmapped mounts unavailable on this mount");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail instead of falling back to allow_other

When --idmap is used on a kernel that does not advertise FUSE_ALLOW_IDMAP (or any other case where add_capabilities refuses it), this branch only logs a warning and continues, but main has already mounted with DefaultPermissions and SessionACL::All. That silently turns a failed idmap request into a normal allow_other mount visible to other local users, which is broader access than the user asked for; returning an init error here would avoid exposing the mount in the fallback case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and fixed in 8738966. init() now returns ErrorKind::Unsupported rather than warning and serving on.

The exposure is exactly as described: allow_other is not something --idmap asks for on its own merits, it is the price the capability charges, and continuing without the capability leaves the price paid for nothing - a mount every local user can reach.

Verified both paths rather than only the one this kernel takes. On a kernel that does offer it, the mount comes up and serves requests as before. For the refusal, I withheld default_permissions in main to force add_capabilities to refuse, which is the same branch an older kernel reaches:

error reported: does not offer FUSE_ALLOW_IDMAP
mount left behind? 0
mountpoint clean

So the mount is torn down rather than left dangling, which was the part worth checking - an init error arrives after the mount is already up.

One thing I left alone: the process still exits 0, because main only maps PermissionDenied to a non-zero status and every other mount failure logs and returns. That is the example's existing behavior for all mount errors rather than anything this adds, and changing it would change the exit status for every failure mode at once, including for the xfstests harness. Worth doing, but not quietly here.


Generated by Claude Code

@cberner
cberner force-pushed the claude/xfstests-skip-investigation-s9gdr1 branch from d03c561 to 8738966 Compare August 7, 2026 23:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87389661f3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/request.rs Outdated
Comment on lines +655 to +656
(owner.uid != crate::FUSE_INVALID_UIDGID && owner.gid != crate::FUSE_INVALID_UIDGID)
.then_some(owner)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return no owner for non-whiteout renames

For a normal rename on a non-idmapped mount, the request header still has the caller's uid/gid, so this helper returns Some(owner) even when RENAME_WHITEOUT is not set. The new Filesystem::rename argument is documented as the owner for the inode created by a whiteout; exposing Some for every ordinary rename makes downstream implementations that use owner.is_some() to detect or validate whiteout creation take the wrong path. Pass None unless the rename flags actually include RENAME_WHITEOUT, and Some only for that case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and fixed in bce2947. The flags decide it now, so Some means the rename creates an inode, on an idmapped mount or otherwise.

I had made the Option mean "the kernel sent ids" and then documented my way around the gap - the doc says it is Some whatever the flags say off an idmapped mount. Documenting a type that says the wrong thing is not the same as fixing it, and owner.is_some() is the obvious reading for a filesystem to take.

It is the same conflation the rest of #748 set out to remove: ids arriving is not the same fact as an inode being created, and the argument should carry the one that matters to the caller. Off an idmapped mount the two coincide for every request, which is exactly why the wrong one looked right.

Pinned by a test rather than left to the doc, the doc being what let it through. It renames twice over a live mount, once plainly and once with RENAME_WHITEOUT, and asserts which of the two was named an owner - on a non-idmapped mount, where the header carries ids for both and only the flags separate them.

This is library code rather than the example the rest of this PR is about, so it is its own commit; happy to land it separately if you would rather.


Generated by Claude Code

claude added 2 commits August 7, 2026 23:58
Adds --idmap, which requests FUSE_ALLOW_IDMAP and mounts with the two things that
capability needs: default_permissions, without which the kernel refuses the
connection, and allow_other, without which fuser refuses the capability. Off by
default, for a reason worth stating: negotiating it withholds the caller's ids
from every request, whether or not anyone has made an idmapped mount, and this
filesystem's own permission and privilege logic is built on those ids.

An idmapped mount has no caller for the filesystem to check, so every check has
to come from somewhere else - and does, for access. default_permissions is what
makes the capability available at all, so the kernel has already decided, against
ids it can map and this filesystem cannot. The caller's ids reached 85 places in
three shapes - a mode-bit check, a test for root, and a test for owning an inode -
and each is now may_access(), caller_is_root() or caller_owns(), which answer
"allowed" where there is no caller to ask about.

Privileges are the other half, and they go the other way. default_permissions
decides who may act, not whose setuid and setgid survive acting, so nothing has
decided that on the filesystem's behalf; caller_keeps_privileges() and
caller_outside_group() answer so that a bit which cannot be shown to have been
earned is dropped. FUSE_HANDLE_KILLPRIV_V2 is not negotiated at all under --idmap:
taking that job on means judging a caller, and not every operation which drops
these bits carries a flag saying so. The kernel keeps it, knowing the mapping.

Two things that were not permission checks had to keep their meaning. truncate()
took a uid and gid only to check access, and one caller passed 0 to skip that
check, since a file handle already granted write access carries it whatever the
mode says now; that is an argument now rather than a sentinel. And has_fsetid()
takes the uid only for when /proc cannot be read, so it takes an Option and an
unknown caller falls back to nothing rather than to root.

generic/644, 656, 689 and 698 pass with the flag. The suite does not set it: the
withheld ids cost generic/193, 683, 684 and 685, which check what this filesystem
does with suid and sgid, and enabling four tests by breaking four others is not a
trade worth making. generic/645 and 699 would stay excluded either way - 645
creates thousands of files in one directory, which is quadratic here, and 699
wants overlayfs on top, as the already-excluded 631 does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4hiZwHE9fEYdn7DK3ZrV2
`Option<Owner>` on rename meant "the kernel sent ids" rather than "there is an
inode to own", and off an idmapped mount every request carries ids. So an
ordinary rename, which creates nothing, was handed an owner all the same, and a
filesystem reading `owner.is_some()` as "this is a whiteout" would have been told
yes for every rename.

The flags decide it now. A rename creates an inode only with RENAME_WHITEOUT, so
that is the one that gets an owner, on an idmapped mount or otherwise. Pinned by
a test that renames twice over a live mount, once plainly and once with the flag,
and checks which of the two was named an owner - the case the documentation
described but nothing held it to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4hiZwHE9fEYdn7DK3ZrV2
@cberner
cberner force-pushed the claude/xfstests-skip-investigation-s9gdr1 branch from bce2947 to 5eab41e Compare August 7, 2026 23:58
@cberner
cberner merged commit c0420fc into master Aug 8, 2026
9 checks passed
@cberner
cberner deleted the claude/xfstests-skip-investigation-s9gdr1 branch August 8, 2026 02:05
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.

2 participants