Skip to content

feat(core): Landlock fs_read axis with loader/system base allow-list (#31)#54

Merged
hartsock merged 1 commit into
mainfrom
issue-31/landlock-read-axis
Jun 26, 2026
Merged

feat(core): Landlock fs_read axis with loader/system base allow-list (#31)#54
hartsock merged 1 commit into
mainfrom
issue-31/landlock-read-axis

Conversation

@hartsock

Copy link
Copy Markdown
Member

Part of #31 (checklist item 1) — adds the Landlock fs_read axis with a loader/system base allow-list, closing the read half of the ADR-0001 L3 gap. Open for review (risk:high), like #53.

What

LandlockSandbox::apply now governs reads as well as writes:

  • When fs_read is restricted (Only), the ruleset read-allows the granted read roots plus BASE_READ_PATHS (the dynamic linker, shared libs, /etc/ld.so.cache, locale, name-resolution config, /proc/self, /dev essentials) and denies everything else.
  • Reads use ReadFile | ReadDir only — Execute is left ungoverned this increment so libraries can be mmap-exec'd without an execute allow-list (the exec axis is a follow-up).
  • When fs_read is All, reads stay ambient (nothing to confine).
  • New scope_roots() helper shares existing-path resolution across both axes; apply() stays fail-closed (ADR 0006 D4).

Why

A permitted program can no longer read user data outside fs_read — e.g. grep -f /etc/shadow is denied because /etc is not granted wholesale (only /etc/ld.so.cache as a single file) — while still loading libc and running.

Testing (Linux + linux-landlock, self-skips without Landlock)

  • fs_read in-scope read succeeds; out-of-scope is EACCES (kernel).
  • A real dynamically-linked binary (cat) still loads under read confinement and reads the in-scope file, but is denied the out-of-scope one — proves the base list lets binaries start and that reads are really confined.
  • fs_read: All leaves reads ambient.
  • All prior write-axis tests pass. Verified on a 6.8 kernel; just check green.

Apple Silicon / macOS

Core change behind cfg(all(target_os="linux", feature="linux-landlock")) — macOS builds unaffected (honest None). No engine change here; composes with #53.

Scope / follow-ups (still #31)

Test plan

just check green (fmt + clippy --all-features & --no-default-features + workspace tests; the --all-features run executed the new read-axis enforcement on a Landlock-capable kernel).

🤖 Generated with Claude Code

#31)

WHAT: LandlockSandbox::apply now governs reads, not just writes. When fs_read is restricted (Only), the ruleset read-allows the granted read roots PLUS BASE_READ_PATHS (the dynamic linker, shared libs, ld.so.cache, locale, name-resolution config, /proc/self and /dev essentials) and denies everything else. Reads use ReadFile|ReadDir only — Execute is left ungoverned this increment so libraries can be mmap-exec'd without an execute allow-list. When fs_read is All, reads stay ambient (nothing to confine, no base list needed). A scope_roots() helper factors the existing-path resolution shared by both axes.

WHY (#31 checklist item 1): closes the read half of the ADR-0001 L3 gap — a PERMITTED external program can no longer read user data outside fs_read (e.g. grep -f /etc/shadow is denied: /etc is NOT granted wholesale, only /etc/ld.so.cache as a single file), while still being able to load libc and run. apply() stays fail-closed (ADR 0006 D4). The base list is tuned for a glibc/FHS layout (the CI target); non-existent paths are skipped so extra entries are harmless, and a musl/Nix layout may need more (documented).

TEST (Linux+feature kernel tests, self-skip without Landlock): fs_read in-scope read succeeds / out-of-scope is EACCES; a REAL dynamically-linked binary (cat) still loads under read confinement and reads the in-scope file but is denied the out-of-scope one (proves base-list adequacy + real enforcement); fs_read: All leaves reads ambient. All prior write-axis tests still pass. Verified on a 6.8 kernel; just check green.

Part of #31 — exec axis (block find -exec curl via exec-Caveat path resolution) and net (netns/Landlock-net) remain. Composes with #53 (engine wiring): once both land, restricting fs_read confines the spawned interior's reads. NOTE: intended_sandbox_kind in #53 currently triggers on fs_write Only; a tiny follow-up will also trigger on fs_read Only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01HMGPEApE4XfwgMhgFbRn6c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high High-risk change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant