Skip to content

bug(policy): invalid landlock.compatibility values silently fall back to best_effort #2356

Description

@cdiddy77

Agent Diagnostic

  • Skills loaded: create-github-issue
  • OpenShell version tested: source-level review of main at 8cf2673c (local CLI on PATH is 0.0.72, but the finding is read from current main, not from a running sandbox)
  • Latest release checked: v0.0.86 (gh release list) — the code path below is unchanged on main as of this commit
  • Known fixes reviewed: searched main for every hard_requirement reference (16 hits); the only string comparison is the one below, and no validation exists in openshell-policy or openshell-server/src/grpc/validation.rs
  • Possible duplicates reviewed: searched open/closed issues for landlock and for compatibility validation policy. Closest is supervisor: Landlock hard_requirement aborts with "incompatible directory-only access-rights: ReadDir" for real directories on RHEL 9.6 (kernel 5.14) ABI #2218 (Landlock hard_requirement aborting on RHEL 9.6 kernel 5.14 ABI), which is about ABI compatibility, not value parsing. No duplicate found.
  • Findings: LandlockDef.compatibility is deserialized as a bare String with no value validation. The proto→runtime conversion is a single equality check against "hard_requirement", with everything else falling through to BestEffort.
  • Remaining reason for filing: the failure direction is always toward less enforcement. A typo in a security-relevant field produces a sandbox that reads as strictly configured but runs permissively, with no error, warning, or log line.

Description

Actual behavior: Any value of landlock.compatibility other than the exact string hard_requirement is silently treated as best_effort. A policy containing compatibility: hard-requirement (hyphen), compatibility: strict, or compatibility: HARD_REQUIREMENT is accepted without complaint and runs in best_effort mode — meaning Landlock failures degrade to no filesystem restriction instead of aborting startup.

#[serde(deny_unknown_fields)] catches unknown keys, so landlock: { compatibilty: ... } is rejected. It does nothing for unknown values.

Expected behavior: An unrecognized compatibility value is rejected at policy parse/validation time with an error naming the field and the accepted values. Fail closed on a security control, or at minimum emit a warning — never silently downgrade enforcement.

Reproduction Steps

  1. Write a sandbox policy containing:
    filesystem_policy:
      read_only: [/usr, /lib]
      read_write: [/sandbox, /tmp]
    
    landlock:
      compatibility: hard-requirement   # hyphen instead of underscore
  2. Create a sandbox with this policy.
  3. Observe it is accepted with no error or warning.
  4. On a host where Landlock is unavailable or a listed path cannot be opened, the sandbox starts anyway with reduced (or zero) filesystem restriction, rather than aborting as hard_requirement would.

Environment

Logs

Parsing accepts any string:

// crates/openshell-policy/src/lib.rs:73-76
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct LandlockDef {
    #[serde(default, skip_serializing_if = "String::is_empty")]
    compatibility: String,
}

The only place the value is interpreted — everything that is not an exact match falls through to BestEffort:

// crates/openshell-core/src/policy.rs:141-150
impl From<ProtoLandlockPolicy> for LandlockPolicy {
    fn from(proto: ProtoLandlockPolicy) -> Self {
        let compatibility = if proto.compatibility == "hard_requirement" {
            LandlockCompatibility::HardRequirement
        } else {
            LandlockCompatibility::BestEffort
        };
        Self { compatibility }
    }
}

Suggested Fix

Parse into the existing LandlockCompatibility enum rather than a String, either via #[serde(rename_all = "snake_case")] on the enum or a TryFrom<&str> that errors on unrecognized input, and surface that error through gateway policy validation. Docs enumerate exactly two accepted values (docs/reference/policy-schema.mdx:92), so tightening the parser matches documented behavior rather than changing it.

Related

  • A second, separate gap found alongside this one: when both filesystem_policy.read_only and read_write are empty, prepare() returns Ok(None) and Landlock is a complete no-op even under hard_requirement (crates/openshell-supervisor-process/src/sandbox/linux/landlock.rs:145-147). Arguably hard_requirement with no paths should be a configuration error. Filing separately if maintainers agree it is distinct.

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions