Skip to content

supervisor: Landlock hard_requirement aborts with "incompatible directory-only access-rights: ReadDir" for real directories on RHEL 9.6 (kernel 5.14) ABI #2218

Closed

Description

@flg77

Description

On a Kubernetes (OpenShift) deployment backed by RHCOS = RHEL 9.6, kernel 5.14.0, openshell sandbox create with a policy whose landlock.compatibility: hard_requirement fails at sandbox preparation:

Failed to prepare sandbox: incompatible directory-only access-rights: BitFlags<AccessFs>(0b1000, ReadDir)

This happens even when the policy's filesystem_policy lists only real directories (no device files, no symlinks). ReadDir (0b1000 = LANDLOCK_ACCESS_FS_READ_DIR) is a standard directory-only access right that has existed since Landlock ABI v1 (kernel 5.13), yet the supervisor rejects it as incompatible on this kernel's (RHEL-backported) Landlock ABI instead of applying it — or gracefully restricting to the supported subset.

Setting landlock.compatibility: best_effort brings the workspace up cleanly. This confirms the issue is specific to the hard_requirement ABI/access-right compatibility check.

Environment

  • OpenShell: 0.0.76
  • Platform: OpenShift 4.21, RHCOS 9.6, kernel 5.14.0-570.112.1.el9_6.x86_64

(full draft in the saved file)

Activity

  1. TaylorMutch commented on Jul 10, 2026

    @TaylorMutch
    Collaborator

    Agent Diagnostic

    Loaded the create-github-issue and debug-openshell-cluster skills and traced this error through current OpenShell main and the pinned landlock crate (0.4.4).

    Finding

    This is a real OpenShell bug, but the primary cause is path-type handling rather than the RHEL kernel rejecting ReadDir as an ABI feature.

    OpenShell currently builds one access mask per policy class:

    • read-only paths receive AccessFs::from_read(ABI::V2), which contains Execute | ReadFile | ReadDir;
    • read-write paths receive AccessFs::from_all(ABI::V2), which also contains directory-only rights.

    Those masks are passed unchanged to PathBeneath::new() for every path in crates/openshell-supervisor-process/src/sandbox/linux/landlock.rs, regardless of whether the opened path is a directory or a regular/device file.

    The effective policy contains non-directory paths even when the user-authored filesystem policy lists only directories:

    • proxy baseline enrichment adds /dev/urandom as read-only;
    • the restrictive default also includes /dev/urandom and /dev/null;
    • GPU enrichment may add /dev/nvidia* or /dev/dxg device nodes.

    The landlock crate calls fstat() on each PathFd. When the target is not a directory, ReadDir and other directory-only bits are incompatible. In BestEffort mode the crate removes those superfluous bits; in HardRequirement mode it returns exactly:

    incompatible directory-only access-rights: BitFlags<AccessFs>(0b1000, ReadDir)
    

    That explains why changing only landlock.compatibility makes the sandbox start. It does not indicate that RHEL's Landlock ABI lacks ReadDir; it indicates that a directory-only permission was attached to a non-directory PathFd.

    Recommended fix

    OpenShell should determine each opened path's file type before constructing the rule and tailor the allowed mask explicitly:

    • directories retain AccessFs::from_read() or AccessFs::from_all();
    • non-directories intersect the intended mask with AccessFs::from_file(abi).

    This preserves hard_requirement semantics for kernel/ABI support while removing rights that are nonsensical for the target file type. Relying on per-rule best-effort compatibility would be less precise because it could also hide genuinely unsupported access rights.

    Regression coverage should include, in hard-requirement mode:

    1. a read-only regular/device file such as /dev/urandom;
    2. a read-write file such as /dev/null;
    3. ordinary read-only and read-write directories;
    4. mixed directory and device-node baseline paths;
    5. verification that truly unsupported ABI rights still fail closed.

    The current best_effort setting is a functional workaround, but it weakens the operator's requested fail-closed guarantee and should not be treated as the resolution. No duplicate issue or active implementation PR was found.

  2. added theissue type on Jul 10, 2026
  3. TaylorMutch commented on Jul 10, 2026

    @TaylorMutch
    Collaborator

    📋 triage-agent

    Triage Assessment

    Classification: bug-confirmed

    Summary

    The failure is reproducible from the current implementation and is not a RHEL Landlock ABI deficiency. OpenShell applies directory-only access rights to non-directory baseline paths, which the landlock crate correctly rejects under hard_requirement.

    Investigation

    openshell-supervisor-process constructs one read-only and one read-write AccessFs mask, then applies each mask unchanged to every PathBeneath. Effective policies include non-directories such as /dev/urandom, /dev/null, and optional GPU device nodes. landlock 0.4.4 uses fstat() and returns DirectoryAccess { incompatible: ReadDir } for these rules in hard-requirement mode; best-effort mode silently removes the nonsensical bits, explaining the reported workaround.

    Recommendation

    Keep the issue open as a confirmed supervisor compatibility bug. Tailor each rule from the already-opened file descriptor: retain directory masks for directories and intersect non-directory masks with AccessFs::from_file(abi). Add hard-requirement regression coverage for read-only/read-write files, directories, mixed baseline paths, and genuinely unsupported ABI rights. The GitHub issue is already typed as Bug; implementation still requires the human state:agent-ready decision.

  4. added
    os:linuxIssue affects Linux hosts
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Jul 10, 2026
  5. jyaunches commented on Jul 20, 2026

    @jyaunches

    🏗️ build-plan

    Implementation Plan

    Issue type: fix
    Complexity: Medium
    Confidence: High — clear path

    Summary

    The supervisor currently applies class-wide Landlock access masks to every opened policy path. OpenShell's baseline policy enrichment adds non-directory paths such as /dev/urandom and /dev/null; in hard_requirement mode, rust-landlock correctly rejects directory-only rights such as ReadDir when those masks are attached to a file or device. The fix will classify the inode through the already-open PathFd and remove directory-only rights for non-directories while preserving the requested access level and the existing fail-closed compatibility behavior.

    Scope

    • crates/openshell-supervisor-process/src/sandbox/linux/landlock.rs: derive each rule's access mask from the opened path type, using fstat on the same descriptor to avoid a path-classification race; add focused unit coverage.
    • e2e/python/test_sandbox_landlock.py: add a hard_requirement regression covering a policy with both directories and baseline file/device paths, while retaining allow/deny enforcement assertions.
    • architecture/security-policy.md: document that Landlock rule rights are tailored to the opened inode type and that hard_requirement still fails on genuine preparation or ABI errors.

    Implementation Steps

    1. Add a small helper that inspects the already-open PathFd with fstat. Keep the intended access mask unchanged for directories and intersect non-directory masks with AccessFs::from_file(ABI::V2).
    2. Use the tailored mask when adding both read-only and read-write PathBeneath rules. Propagate classification failures and preserve the existing global handled-access mask and compatibility levels so unsupported Landlock capabilities still fail closed.
    3. Add unit tests for directory masks, regular files, character devices, and symlink targets, covering read-only and read-write rules and ensuring no rule receives an empty or broader mask.
    4. Add a Linux E2E regression using landlock.compatibility: hard_requirement with ordinary directories plus baseline device paths. Verify expected reads/writes succeed and writes to read-only or outside-policy paths remain denied.
    5. Update the security-policy architecture note with the per-inode rule behavior and hard-requirement invariant.

    Test Plan

    • Unit tests: Extend the landlock.rs test module to verify directories retain directory rights, non-directories receive only file-compatible rights, symlinks are classified through their opened target, and fstat errors propagate.
    • Integration tests: N/A — rule preparation is covered directly by unit tests and the full sandbox boundary is exercised by E2E.
    • E2E tests: Extend e2e/python/test_sandbox_landlock.py with a hard_requirement mixed-path case that reproduces the baseline device-path failure and rechecks filesystem allow/deny enforcement. Run the relevant Linux sandbox E2E lane in addition to mise run pre-commit.

    Risks & Open Questions

    • This code is part of the filesystem security boundary. The proposed transformation is permission-subtractive for non-directories and mirrors rust-landlock's path-type handling; it must not downgrade per-rule or global compatibility to best_effort.
    • Classification must use the same open descriptor passed to Landlock. A separate path metadata lookup would introduce a TOCTOU window and is out of scope.
    • Regular files, device nodes, FIFOs, and sockets should all use the non-directory mask. Newer-ABI device IOCTL controls and unrelated compatibility-value parsing are not part of this fix.
    • The affected E2E requires a Linux host with Landlock support; CI or a maintainer-run E2E lane may be needed if the local development host cannot execute it.

    Documentation Impact

    Update architecture/security-policy.md to describe per-inode Landlock rule masks and confirm that hard_requirement continues to reject genuine unsupported or failed setup. No gateway configuration keys, defaults, Helm rendering, or compute-driver setup change.


    Revision 1 — initial plan

  6. KyleZheng1284 commented on Jul 20, 2026

    @KyleZheng1284
    Contributor

    Additional Ubuntu 24.04 reproduction and downstream validation context

    This adds a second, independent reproduction for the path-type diagnosis already captured above.

    Agent diagnostic

    Current main still passes AccessFs::from_read(ABI::V2) or AccessFs::from_all(ABI::V2) unchanged to every PathBeneath rule in crates/openshell-supervisor-process/src/sandbox/linux/landlock.rs.

    Ubuntu reproduction

    The policy used:

    version: 1
    
    filesystem_policy:
      include_workdir: true
      read_only:
        - /usr
        - /lib
        - /etc
        - /proc
        - /dev/urandom
      read_write:
        - /sandbox
        - /tmp
        - /dev/null
    
    landlock:
      compatibility: hard_requirement
    
    process:
      run_as_user: sandbox
      run_as_group: sandbox
    
    network_policies:
      landlock_regression:
        name: landlock_regression
        endpoints:
          - host: example.com
            port: 443
            protocol: rest
            enforcement: enforce
            access: read-only
        binaries:
          - path: /usr/bin/curl

    Sandbox preparation fails with:

    Failed to prepare sandbox: incompatible directory-only access-rights:
    BitFlags<AccessFs>(0b1000, ReadDir)
    

    Removing /dev/urandom and /dev/null allows an offline sandbox to reach Ready, but that is not a valid downstream workaround. A non-empty network policy activates effective-policy enrichment in crates/openshell-sandbox/src/lib.rs, which adds /dev/urandom back before preparation. The same failure then occurs without making any external network request.

    This affects AI-Q's shipped network-enabled policy, and the same generic path can affect GPU device nodes such as /dev/nvidia* and /dev/dxg.

    Proposed generic solution

    After PathFd::new() succeeds, inspect metadata through that exact opened descriptor (for example, fstat) to avoid a pathname TOCTOU race.

    • Directories retain the full requested mask.
    • Every non-directory inode receives requested_access & AccessFs::from_file(abi).
    • Keep ABI::V2, hard_requirement, best_effort, existing path-open/error handling, OCSF behavior, privilege ordering, and the global handled-access mask unchanged.
    • Do not special-case /dev/null, /dev/urandom, or device types.

    This is permission-subtractive for non-directories and preserves fail-closed behavior for genuine ABI/setup failures.

    Validation plan

    Focused Rust tests should prove:

    1. directories retain their requested directory and file-content rights;
    2. read-only non-directories equal AccessFs::from_read(abi) & AccessFs::from_file(abi);
    3. read-write non-directories equal AccessFs::from_all(abi) & AccessFs::from_file(abi);
    4. file masks contain no rights outside AccessFs::from_file(abi).

    For the Linux Docker E2E, prefer the existing Rust SandboxGuard pattern in the mise run e2e:docker lane. Use hard_requirement, /dev/urandom read-only, /dev/null read-write, and a non-empty network policy. Verify the sandbox reaches Ready, reads 16 random bytes, writes to /dev/null, denies a write outside the allowlist, and is deleted even on assertion failure. No successful external egress is required.

    Downstream AI-Q validation should keep its shipped policy unchanged, network policies enabled, and Landlock at hard_requirement; all three Linux live OpenShell integration tests should pass against locally built fixed images, effective-policy attestation should succeed, and cleanup should leave no sandboxes. The AI-Q version bump should remain a separate PR after a fixed OpenShell release is published.

  7. KyleZheng1284 commented on Jul 20, 2026

    @KyleZheng1284
    Contributor
  8. self-assigned this
    on Jul 21, 2026
  9. added a commit that references this issue on Jul 21, 2026
    f169084
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions