Skip to content

Support idmapped mounts, and give absent caller ids a type - #748

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

Support idmapped mounts, and give absent caller ids a type#748
cberner merged 3 commits into
masterfrom
claude/xfstests-skip-investigation-s9gdr1

Conversation

@cberner

@cberner cberner commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Three commits: the capability, a comment pass, and the API change that the
capability's first shape argued for.


1. Allow FUSE_ALLOW_IDMAP to be negotiated

InitFlags::FUSE_ALLOW_IDMAP (ABI 7.41) was in UNSUPPORTED_CAPABILITIES, with
a comment giving two reasons. Both turned out to be right, so this does not
remove them - it makes them conditions.

What negotiating it buys. An idmapped mount. Measured both ways, building the
mount the way an idmapped one has to be built - open_tree(OPEN_TREE_CLONE),
mount_setattr, move_mount:

capability mount_setattr(MOUNT_ATTR_IDMAP)
not requested fails, EINVAL
negotiated succeeds

Why it is conditional. Accepted only where the session allows other users and
default_permissions is in force - whether from MountOption::DefaultPermissions
or from negotiating InitFlags::FUSE_POSIX_ACL, which the kernel treats as
setting it. Without default_permissions the kernel refuses the connection
outright: every request answers ECONNREFUSED, measured. Without allow_other,
fuser would be offering an owner-only ACL it can no longer enforce, since it can
no longer tell the mounting user's requests from anyone else's.

2. Say what the code does rather than what changed about it

A pass over comments from the recent example and statx work. Several described
the change that introduced them rather than the behavior a reader arrives at.

One was wrong: StatxAttributes said answering FUSE_STATX is what makes
chattr +i visible to statx(2). It is not - fuse_do_statx() ignores the
attributes, as StatxAttr::attributes already said. The two were written either
side of finding that out, and only one was corrected.

3. Give absent caller ids a type, and name the owner of a new inode

Breaking. Request::uid() and gid() return Option<u32>.

The first shape of this PR reported u32::MAX when the kernel withheld the ids,
which asks every caller to know a sentinel and to remember to test for it in code
whose whole purpose is deciding who may do what. A u32 also claimed something
that is not always there.

The requests that create an inode take an Owner instead. Those are the only
ones the kernel sends ids for, and what it sends is not the caller's ids but
those ids mapped through the mount's idmapping - so on an idmapped mount, reading
the owner off the request would have been wrong even where a value is present.
Which requests those are was measured rather than assumed:

given ids withheld
create, mkdir, mknod, symlink, tmpfile lookup, getattr, open, link, rename, unlink

link creates an entry rather than an inode, which is why it is on the right.

The example does not request the capability, so it is always given ids; that
invariant is stated once in caller_uid/caller_gid rather than at each of the
ninety places it is relied on. Its rename keeps using the caller's ids for the
RENAME_WHITEOUT inode, since rename is not a request the kernel sends ids for -
the right answer for any filesystem that has not asked for idmapped mounts.

FUSE_INVALID_UIDGID is unexported again, which is all it should be once the
type says what it was standing in for.


Testing

Five tests over the capability, pinning each claim against a real kernel rather
than against the encoding: which mount configurations may negotiate it (including
that POSIX ACLs satisfy it, and that a kernel not offering them does not), that a
getattr arrives with no ids while a mkdir is given an owner, and that the
capability decides whether an idmapped mount can be created at all.

cargo test --all is 108 passed, 1 failed: mnt::test::mount_unmount_auto_unmount,
which fails identically on unmodified master here for want of a fusermount3
binary. Clippy, rustdoc and the macOS check clean with --deny warnings, the last
with --all-targets so it builds tests too.

The example's four behavior suites pass - chattr/immutable, noatime, O_TMPFILE and
btime - and the full xfstests suite is running against the rewritten call sites.

Not included

examples/simple.rs still does not request the capability, so no xfstests change
here. generic/644, 645, 656, 689, 698 and 699 need it to, and that is a change of
a different kind: the example does its own uid-based access checks throughout,
which is precisely what an idmapped mount stops it from being able to do.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q4hiZwHE9fEYdn7DK3ZrV2

@cberner
cberner force-pushed the claude/xfstests-skip-investigation-s9gdr1 branch from c8f2521 to 19000ae Compare August 5, 2026 17:25

@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: 19000ae252

ℹ️ 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/lib.rs Outdated
claude added 2 commits August 5, 2026 17:34
Negotiating it is what lets a mount be idmapped: with the capability refused,
mount_setattr(MOUNT_ATTR_IDMAP) over a fuser filesystem fails with EINVAL, and
with it negotiated the same call succeeds.

It is accepted only on a mount that carries MountOption::DefaultPermissions and
SessionACL::All, and refused otherwise. Neither condition is fuser's choice.
Without default_permissions the kernel refuses the connection outright - every
request answered ECONNREFUSED - because with the caller's ids withheld its own
checks are the only ones left. And once those ids are withheld fuser cannot tell
the mounting user's requests from anyone else's, so SessionACL::Owner and
RootAndOwner could no longer be enforced; refusing the capability is better than
continuing to offer a restriction that has quietly stopped applying.

What the kernel withholds, it withholds only from requests that do not create an
inode. Those arrive with uid and gid set to the new FUSE_INVALID_UIDGID. The
requests that do create one still carry ids, and they are the owner the new inode
should get, already mapped - which is the only thing a filesystem needs them for
once the kernel is doing the access checks. Request::uid() and Request::gid() say
so rather than changing shape, since a filesystem that does not ask for the
capability is never given an invalid id.

The tests pin all three of these against a real kernel rather than against the
encoding: which mount configurations may negotiate it, that a getattr arrives
without ids while a mkdir arrives with them, and that the capability is what
decides whether an idmapped mount can be created at all.

This is the library capability alone. The simple example does not request it, and
wiring it there is not a small change: the example does its own uid-based access
checks, which is exactly what an idmapped mount stops it from being able to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4hiZwHE9fEYdn7DK3ZrV2
Several comments described the change that introduced them rather than the
behavior a reader arrives at, which stops being useful as soon as the change is
history. They now state the rule and its reason: why tmpfile checks the parent
for search permission when the operand is the directory itself, why nothing
touches the parent's timestamps, and which kernel check copy_file_range mirrors.
Comparisons to create() go, since in each case the reason alone carries it.

Corrects one that was wrong. StatxAttributes said answering FUSE_STATX is what
makes chattr +i visible to statx(2). It is not: fuse_do_statx() takes the mask,
the creation time and the basic stats out of the reply and ignores the
attributes, as StatxAttr::attributes already said. The two were written either
side of finding that out and only one was corrected. It now points at the field
where the behavior is documented rather than restating it, as does the example.

Also drops a kernel version from StatxAttr::attributes. The behavior is the
kernel's current one either way, and stamping a version invites a reader to think
it changed in that release.

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 19000ae to 3da6162 Compare August 5, 2026 17:35
@cberner cberner changed the title Allow FUSE_ALLOW_IDMAP to be negotiated Support idmapped mounts, and give absent caller ids a type Aug 5, 2026
@cberner
cberner force-pushed the claude/xfstests-skip-investigation-s9gdr1 branch from 11a1e83 to 70ee9bf Compare August 5, 2026 23:04

@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: 11a1e83aaf

ℹ️ 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_param.rs

@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: 70ee9bfb75

ℹ️ 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/lib.rs
@cberner
cberner force-pushed the claude/xfstests-skip-investigation-s9gdr1 branch from 70ee9bf to 276a1d9 Compare August 5, 2026 23:11

@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: 276a1d981a

ℹ️ 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_param.rs
Request::uid() and gid() return Option<u32>. The kernel withholds the caller's
ids on an idmapped mount, from every request that does not create an inode, so a
u32 was claiming something that is not always there. Reporting u32::MAX in their
place asked every caller to know a sentinel, and to remember to test for it in
code whose whole purpose is deciding who may do what.

The requests that do create an inode are given an Owner instead. They are the
only ones the kernel sends ids for, and what it sends is not the caller's ids but
those ids mapped through the mount's idmapping - so on an idmapped mount, reading
the owner off the request would have been wrong even where a value is present.
Naming it separately keeps a single field from meaning two things. Which requests
those are was measured rather than assumed: create, mkdir, mknod, symlink and
tmpfile carry ids, and link does not, creating an entry rather than an inode.

The simple example does not request FUSE_ALLOW_IDMAP, so it is always given ids.
That invariant is stated once, in caller_uid and caller_gid, rather than at each
of the ninety places it is relied on. Its five creating handlers take the owner
from the argument. Its rename does not, since RENAME_WHITEOUT creates an inode
but rename is not a request the kernel sends ids for; the caller's ids are the
right answer there for any filesystem that has not asked for idmapped mounts.

FUSE_INVALID_UIDGID goes back to being an implementation detail, which is all it
is once the type says what it was standing in for.

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 276a1d9 to 1ae8a81 Compare August 5, 2026 23:19
@cberner
cberner merged commit 1c3db17 into master Aug 6, 2026
9 checks passed
@cberner
cberner deleted the claude/xfstests-skip-investigation-s9gdr1 branch August 6, 2026 17:31
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