You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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, …).
// 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 {
returntypes.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
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.
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.
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).
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.
Component test wiring the rule end-to-end with R0002 (or a new rule for flag-subset violation specifically).
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:
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.
Context
The v0.0.1 SBOB spec at §5.5 ("
flagssubset semantics") requires that for every observedopen(2)call the live flag set MUST be a subset of the flags listed in the matchedopens[]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.godoes NOT enforce this:was_path_opened_with_flagsparses theflagslist for shape and then ignores it — the match degrades to path-only. This means a profile that sayspath: /etc/resolv.conf, flags: [O_RDONLY]does NOT fire when the workload opens that pathO_WRONLY(which IS a spec violation).The deferral was deliberate at projection-v1 time — the composite
OpenFlagsByPathprojection slice would have multiplied the projected-profile cache footprint.Spec text (current §5.5)
Implementation plan
OpenFlagsByPath map[string][]string(ormap[string]uint32if we move to flag-bit packing for memory) toProjectedContainerProfile. Populate fromcp.Spec.Opens[*].FlagsinextractOpensByPath/projection_apply.go. Clone slices (same rule asExecsByPathper 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.wasPathOpenedWithFlagsto look up the path inOpenFlagsByPath, parse the observedflagslist to a uint mask, apply the kernel-internal mask, then check subset against the declared mask. On mismatch returnfalse(rule fires). Preserve the "absent map entry = no constraint" semantics so non-flags-aware producers don't false-positive.pkg/rulemanager/cel/libraries/applicationprofile/openflags.goexposingKernelInternalFlagsMask. 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).was_path_opened_with_flags(path, [O_RDONLY])— profile saysflags: [O_RDONLY]→ liveO_WRONLY→ MUST returnfalse.flags: [O_RDONLY, O_WRONLY]→ liveO_RDONLY→ MUST returntrue.flags: [](§5.4 NONE) → any non-empty live mask → MUST returnfalse.OpenFlagsByPathmap → MUST degrade to path-only (current behaviour) for v0 compatibility.O_LARGEFILE | O_RDONLY(sensor synthesisesO_LARGEFILE) → mask, compare; profile[O_RDONLY]MUST match.CodeRabbit context
CodeRabbit PR #43 review flagged the related
was_path_opened_with_flagscontract:ap.was_path_opened_with_flagscontract is being weakened by this expectation change … bakes in path-only behavior under a flag-aware function name."open.go:130HasPrefix/HasSuffix on wildcarded Patterns (related but separate matcher correctness issue).Acceptance
OpenFlagsByPathpopulated in projection, with cloning + nil-Args contract preservedWhy not just do it now
Cache-footprint concern flagged in the v1 projection design. Need a size profile before deciding between
[]stringslice (semantic-friendly, larger) anduint32mask (4 bytes per path, lossy if we add an O_-bit not in the mask). Suggest[]stringfor v0.0.3, revisit only if heap profile of a real cluster shows it dominating.