Skip to content

cow: serve O_PATH and other openat opens of branch entries honestly - #334

Merged
congwang-mk merged 2 commits into
mainfrom
cow-openat-flags
Oct 30, 2026
Merged

congwang-mk merged 2 commits into
mainfrom
cow-openat-flags

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Refs #325.

Two fixes for opens of entries that exist only in the COW upper layer.

Translate open/openat flags before serving them with openat2. Handlers serve opens on the child's behalf with openat2, which rejects what openat silently drops: flags beside O_PATH other than O_DIRECTORY, O_NOFOLLOW and O_CLOEXEC (glibc adds the kernel's O_LARGEFILE), and a mode without O_CREAT/O_TMPFILE. decode_open_args now builds the open_how the kernel itself builds for open/openat; openat2 callers keep their flags verbatim.

Answer failed opens instead of handing them to the kernel. handle_cow_open returned Continue when the serving open, the copy-up, or the open plan failed. The kernel cannot see the upper layer, so it reported EACCES/ENOENT for entries that exist, and a write open whose copy-up failed reached the lower file directly, outside the branch. These now return the failure's errno.

A failed fd injection still fails closed, but when ADDFD refuses an O_PATH source with EBADF the child now gets EOPNOTSUPP instead of a denial. That refusal comes from fget() in seccomp_notify_addfd(); the kernel fix ("seccomp: allow addfd to transfer O_PATH descriptors", switching it to fget_raw()) is queued in Kees Cook's tree. With it, the same code path hands the child a working O_PATH fd, with no version check or probe.

On a kernel without that fix, the #325 workload gets an honest EOPNOTSUPP but still cannot open the directory with O_PATH, so the issue stays open until the kernel fix is available.

Tests

  • Unit: openat_how_matches_what_openat2_accepts, inject_failure_of_an_o_path_source_is_eopnotsupp
  • Integration: test_seccomp_cow_openat_ignores_mode_without_creat (failed with ENOENT before), test_seccomp_cow_o_path_open_of_created_dir (failed with EACCES before; accepts a fd or EOPNOTSUPP)
  • Full sandlock-core integration suite passes locally (583).

🤖 Generated with Claude Code

Handlers that open on a child's behalf do so with openat2, which
rejects two things openat silently drops: flags beside O_PATH other
than O_DIRECTORY, O_NOFOLLOW and O_CLOEXEC (glibc always adds the
kernel's O_LARGEFILE), and a mode without O_CREAT or O_TMPFILE. The
serving open failed with EINVAL, and COW fell back to the kernel,
which cannot see the upper layer, so an O_PATH open of a directory
created during the run failed with EACCES (issue #325).

Decode open and openat into the open_how the kernel itself builds for
them. openat2 callers keep their flags verbatim, so they still get the
kernel's EINVAL for the same combinations.

Signed-off-by: Cong Wang <cwang@multikernel.io>
When the serving open, the copy-up, or the open plan failed,
handle_cow_open returned Continue. The kernel cannot see the upper
layer, so it reported EACCES or ENOENT for entries that exist in the
branch, and a write open whose copy-up failed went to the lower file
itself, landing outside the branch. Return the failure's errno.

A failed fd injection is answered with EACCES to fail closed, which
told the child an O_PATH open of an upper-only entry was denied when
the kernel's ADDFD had refused the O_PATH source with EBADF. Report
that case as EOPNOTSUPP; kernels whose ADDFD accepts O_PATH succeed
without any probe (issue #325).

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit 3017569 into main Oct 30, 2026
25 checks passed
@congwang-mk
congwang-mk deleted the cow-openat-flags branch October 30, 2026 16: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.

1 participant