Skip to content

Implement opens[].flags subset check (spec §5.5) #47

@entlein

Description

@entlein

Context

The v0.0.1 SBOB spec at §5.5 ("flags subset semantics") requires that for every observed open(2) call the live flag set MUST be a subset of the flags listed in the matched opens[] entry, with kernel-internal flags masked out (O_LARGEFILE, O_NOATIME, O_NOCTTY, O_SYNC, …).

The current runtime matcher in pkg/rulemanager/cel/libraries/applicationprofile/open.go does NOT enforce this:

// pkg/rulemanager/cel/libraries/applicationprofile/open.go:71
// flags projection (OpenFlagsByPath) is out of scope for v1; degrade to path-only matching.
if _, err := celparse.ParseList[string](flags); err != nil {
    return types.NewErr("failed to parse flags: %v", err)
}

was_path_opened_with_flags parses the flags list for shape and then ignores it — the match degrades to path-only. This means a profile that says path: /etc/resolv.conf, flags: [O_RDONLY] does NOT fire when the workload opens that path O_WRONLY (which IS a spec violation).

The deferral was deliberate at projection-v1 time — the composite OpenFlagsByPath projection slice would have multiplied the projected-profile cache footprint.

Spec text (current §5.5)

For opens[].flags, the live open(2) flags MUST be a subset of the listed flags. Verifiers MUST mask out kernel-internal flags (O_LARGEFILE, O_NOATIME, O_NOCTTY, O_SYNC, etc.) before comparison. The exact mask is implementation-defined; v0.0.1 lists the flags producers MAY use in §4.6.

Implementation plan

  1. Projection layer. Add an OpenFlagsByPath map[string][]string (or map[string]uint32 if we move to flag-bit packing for memory) to ProjectedContainerProfile. Populate from cp.Spec.Opens[*].Flags in extractOpensByPath/projection_apply.go. Clone slices (same rule as ExecsByPath per the PR merge: upstream/main + recover wildcards/signing on projection-v1 #43 rabbit review). Treat absent map entry as "no flag constraint" so v0 profiles without flags continue to behave as before.
  2. CEL matcher. Update wasPathOpenedWithFlags to look up the path in OpenFlagsByPath, parse the observed flags list to a uint mask, apply the kernel-internal mask, then check subset against the declared mask. On mismatch return false (rule fires). Preserve the "absent map entry = no constraint" semantics so non-flags-aware producers don't false-positive.
  3. Kernel-internal mask constant. Single source of truth: pkg/rulemanager/cel/libraries/applicationprofile/openflags.go exposing KernelInternalFlagsMask. Initial set: O_LARGEFILE | O_NOATIME | O_NOCTTY | O_SYNC | O_DSYNC | O_TMPFILE (any other flags that the runtime sensor synthesises but the producer cannot reasonably declare).
  4. Pinning tests (per the regression-harness rule):
    • was_path_opened_with_flags(path, [O_RDONLY]) — profile says flags: [O_RDONLY] → live O_WRONLY → MUST return false.
    • Profile says flags: [O_RDONLY, O_WRONLY] → live O_RDONLY → MUST return true.
    • Profile entry has empty flags: [] (§5.4 NONE) → any non-empty live mask → MUST return false.
    • Profile entry has no OpenFlagsByPath map → MUST degrade to path-only (current behaviour) for v0 compatibility.
    • Live mask carries O_LARGEFILE | O_RDONLY (sensor synthesises O_LARGEFILE) → mask, compare; profile [O_RDONLY] MUST match.
  5. Component test wiring the rule end-to-end with R0002 (or a new rule for flag-subset violation specifically).
  6. Spec edit. Remove the §5.5 ↔ code gap callout from §9 "Open issues for v0.0.2" once landed; reference this issue from the §5.5 spec note until then.

CodeRabbit context

CodeRabbit PR #43 review flagged the related was_path_opened_with_flags contract:

Acceptance

  • OpenFlagsByPath populated in projection, with cloning + nil-Args contract preserved
  • Matcher honours the §5.5 subset rule
  • Kernel-internal mask is named, documented, single-sourced
  • Five pinning tests above all green
  • Component test exercises the path end-to-end
  • Spec §5.5 cross-references this issue or marks the gap closed

Why not just do it now

Cache-footprint concern flagged in the v1 projection design. Need a size profile before deciding between []string slice (semantic-friendly, larger) and uint32 mask (4 bytes per path, lossy if we add an O_-bit not in the mask). Suggest []string for v0.0.3, revisit only if heap profile of a real cluster shows it dominating.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions