Skip to content

feat(pipelines): auto-inject pipeline-declared capabilities; xcover requests CAP_SYS_ADMIN - #2607

Open
AnmolVirdi wants to merge 10 commits into
chainguard-dev:mainfrom
AnmolVirdi:xcover-cap-fix
Open

feat(pipelines): auto-inject pipeline-declared capabilities; xcover requests CAP_SYS_ADMIN#2607
AnmolVirdi wants to merge 10 commits into
chainguard-dev:mainfrom
AnmolVirdi:xcover-cap-fix

Conversation

@AnmolVirdi

@AnmolVirdi AnmolVirdi commented Aug 3, 2026

Copy link
Copy Markdown

Melange Pull Request Template

Functional Changes

  • This change can build all of Wolfi without errors (describe results in notes)

Summary

This PR adds the ability for a pipeline to declare the Linux capabilities it requires, so that those capabilities are automatically merged into the runner whenever the pipeline is used. It also wires this mechanism into the built-in xcover/profile pipeline, which now automatically requests CAP_SYS_ADMIN because xcover attaches BPF uprobes to the profiled interpreter.

Motivation

Previously, capabilities could only be declared at the top-level capabilities block of a melange manifest, which meant every package author using xcover had to remember to manually add CAP_SYS_ADMIN or the profiler would fail at runtime. This change moves that requirement into the pipeline definition itself, following the same pattern already used by needs.packages, so the capability is granted transparently and cannot be forgotten.

Changes

  • Added a Capabilities field to the Needs struct in pkg/config/config.go, allowing a pipeline to declare capabilities via needs.capabilities.add / needs.capabilities.drop.
  • Extended the Compiled struct and gatherDeps in pkg/build/compile.go to accumulate capabilities declared by pipelines as they are walked.
  • Added a mergeCapabilities helper and wired it into every build and test compile path so gathered capabilities are merged into the runner configuration.
  • Declared needs.capabilities.add: [CAP_SYS_ADMIN] on pkg/build/pipelines/xcover/profile.yaml.
  • Regenerated pkg/config/schema.json and updated pkg/config/schema.cue to reflect the new field.

Behavior

Manifest-declared capabilities and pipeline-declared capabilities are combined as a deduplicated union rather than one overwriting the other. If a manifest declares CAP_NET_ADMIN and a test uses xcover/profile, the runner receives both CAP_NET_ADMIN and CAP_SYS_ADMIN, and duplicates across the two sources are collapsed to a single entry. Because Configuration.Capabilities is shared by both the build and test runners, the union applies to whichever runner executes the pipeline; bubblewrap cannot scope a capability to an individual pipeline step.

Testing

Added TestCompileCapabilities in pkg/build/compile_test.go, which covers both the Test.Compile and Build.Compile paths and asserts that manifest and pipeline capabilities are merged into a deduplicated union without overwriting each other. go build ./... succeeds, and the full pkg/build and pkg/config test suites pass.

Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
@AnmolVirdi
AnmolVirdi marked this pull request as ready for review August 3, 2026 16:38

@toabctl toabctl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — the mechanism is the right shape and mirrors needs.packages. Two asks:

Don't merge test-pipeline caps into the build container. Build.Compile merges caps from test: pipelines into Configuration.Capabilities, which feeds the build runner (build.go:1048) — but melange build never executes test pipelines, so uses: xcover/profile under test: grants CAP_SYS_ADMIN to the container running upstream configure/make for nothing. Fix: keep the c.Capabilities merge, delete the two tc.Capabilities merges. Test.Compile is already correct — xcover keeps working under melange test.

Warn on add/drop conflicts. If one source adds a cap and another drops it, melange emits both and each runner arbitrates differently (under bubblewrap the drop silently wins — flag order; docker/qemu decide downstream). Please warn during compile when the merged Add/Drop sets intersect.

Smaller:

  • Log gathered caps in gatherDeps (Info, with pipeline id) — packages are logged, sandbox widening should be too.
  • Validate cap names at compile time; a typo currently fails at bwrap --cap-add runtime.
  • docs/BUILD-PROCESS.md describes needs: as packages-only.
  • Nit: mergeCapabilities skips dedup when src is empty, contradicting its comment.

@maxgio92 maxgio92 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism looks right and mirrors needs.packages nicely; my main concern is scoping, details inline. These are meant to add to toabctl's review, not repeat it.

Just a detail (non-blocking): the new Needs block in schema.json matches generator output, but a full go generate at this head also emits the apko runtime_keyring definitions, so a future clean regen will produce an unrelated-looking diff.

Comment thread pkg/build/compile.go Outdated
Comment thread pkg/build/compile_test.go
Comment thread pkg/config/config.go Outdated
Comment thread pkg/config/schema.cue Outdated
Comment thread pkg/build/compile.go Outdated
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
@AnmolVirdi

Copy link
Copy Markdown
Author

@toabctl, thanks for the suggestions. I've address all concerns:

  • Build container no longer gets test-pipeline caps: I dropped the two tc.Capabilities merges in Build.Compile and kept the build-pipeline one, so Test.Compile still applies them under melange test, plus a new regression test guards against leakage.
  • mergeCapabilities now warns at compile time whenever the merged Add and Drop sets intersect.
  • gatherDeps logs gathered caps at Info with the pipeline id, since widening the sandbox deserves more visibility than the package lines.
  • docs/BUILD-PROCESS.md now documents needs.capabilities in both the example and the build-process steps, including the build vs test runner distinction.
  • The dedup nit is fixed: mergeCapabilities dedups unconditionally now and the comment matches.

On compile-time cap-name validation, I left it out of this PR on purpose: there's no capability list in our deps to check against, so it would mean hardcoding and maintaining the kernel set (which drifts as caps like CAP_BPF and CAP_PERFMON get added), and validating needs.capabilities while leaving the existing top-level capabilities: block unchecked would be inconsistent. Happy to do a follow-up that validates all sources against a shared list if you'd like.

@maxgio92

Copy link
Copy Markdown
Member

@AnmolVirdi thanks for b61c13e, the build-side fix and the negative test look good. One scoping gap remains under melange test: subpackage test caps still merge into the shared t.Configuration.Capabilities, so one subpackage's CAP_SYS_ADMIN reaches every sibling test container. Scoping them per-test Compiled into the corresponding buildWorkspaceConfig call (container.Config already has a Capabilities field) would close it, the way te.Packages is scoped today.

AnmolVirdi and others added 2 commits September 1, 2026 11:05
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
@AnmolVirdi

Copy link
Copy Markdown
Author

Thanks for the follow-up review folks. I've update the PR.

@toabctl

  • All five addressed: test-pipeline caps no longer reach the build runner (and are now scoped per-test rather than shared), compile warns on add/drop intersections, gatherDeps logs gathered caps at Info with the pipeline id, CAP_* names are validated at compile time against linux/capability.h, docs/BUILD-PROCESS.md documents needs.capabilities, and the dedup-on-empty comment is fixed.

@maxgio92

  • caps are scoped to each test's runner instead of the shared Configuration.Capabilities, needs.capabilities is add-only, Packages is no longer required inside needs, and I kept the unrelated apko runtime_keyring defs out of schema.json to avoid widening the diff.

maxgio92
maxgio92 previously approved these changes Sep 8, 2026

@maxgio92 maxgio92 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Anmol! The per-test capability scoping is exactly right, and the new compile_test.go cases pin it well. 🚀

@toabctl toabctl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature — declaring capabilities next to the pipeline that needs them is the right shape, and I like that test caps are scoped per-test container rather than unioned into the build runner.

I verified the shipped path works end to end: with uses: xcover/profile, CAP_SYS_ADMIN reaches both the build runner (Build.Compile) and each test runner including subpackage tests (Test.Compile), after going through ParseConfiguration. So the xcover fix itself is good.

The problem is the other two entry points the PR documents. ParseConfiguration rebuilds pipelines and tests field-by-field (config.go:1785, config.go:1799, unconditionally — not gated on whether any substitutions exist), and replaceNeeds/replaceTest don't copy the new fields. Parsing this manifest:

capabilities:
  add: [CAP_TOP_LEVEL]
pipeline:
  - needs:
      packages: [wget]
      capabilities:
        add: [CAP_SYS_ADMIN]
    runs: echo hi
test:
  capabilities:
    add: [CAP_TEST_LEVEL]
  pipeline:
    - needs:
        capabilities:
          add: [CAP_TEST_PIPELINE]
      runs: echo test

gives:

top-level capabilities:      {Add:[CAP_TOP_LEVEL] Drop:[]}   <- survives
pipeline[0].Needs:           &{Packages:[wget] Capabilities:{Add:[]}}
test.Capabilities:           {Add:[] Drop:[]}
test.pipeline[0].Needs:      &{Packages:[] Capabilities:{Add:[]}}

needs.packages survives, every capability is gone. Same for subpackages[].pipeline[].needs.capabilities and subpackages[].test.capabilities. No error, no warning — KnownFields(true) accepts the keys because the fields exist, they're just dropped afterwards.

Two consequences:

  1. The inline form that docs/BUILD-PROCESS.md introduces the feature with is a no-op, as is test.capabilities from schema.json/schema.cue.
  2. The doc line "Names are checked while the pipeline is compiled, so a misspelled CAP_* fails the build rather than the container" doesn't hold for the inline form — needs.capabilities.add: [CAP_TOTALLY_NOT_REAL] parses clean and Compile returns nil, because the value is discarded before knownRunnerCapabilities validation ever sees it.

No regression risk (both fields are new in this PR), so this is "documented field is dead on arrival" rather than "something broke." Still worth fixing here — shipping the docs without the fix is worse than shipping neither.

The reason CI is green on this is that TestCompileCapabilities builds config.Configuration literals in Go and never goes through ParseConfiguration, so it only exercises the one path that happens to work. A parse-based case would have caught all of it.

Separately, worth considering: these replaceX functions returning a freshly-constructed struct means every future field added to Needs/Test/Pipeline is silently dropped until someone remembers to update them. out := *in followed by overriding just the fields that need substitution fails safe instead.

Comment thread pkg/config/config.go
Comment thread pkg/config/config.go
Comment thread pkg/build/compile.go Outdated
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
@AnmolVirdi

Copy link
Copy Markdown
Author

@toabctl thanks, all three are fixed in a80853b.

Added parse-based coverage: Test_capabilitiesSurviveParsing in pkg/config pins all four inline sites, and a new TestCompileCapabilities subtest goes manifest -> ParseConfiguration -> Compile, including a misspelled inline CAP_* now failing the build as documented.

On your last point: replaceNeeds, replaceTest and replacePipeline all use out := *in plus overrides now, so a field added later carries through instead of being silently dropped.

@AnmolVirdi

Copy link
Copy Markdown
Author

The check failure acid-sandbox is not linked to this. Fix is being tracked here:

@toabctl
toabctl requested a review from maxgio92 September 9, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants