Skip to content

Commit f8614fa

Browse files
committed
manifest/raw_bootc: Label / and /boot before running bootc install
When SELinux is enabled, insert a few stages before running `bootc install` that will make sure the `root` and `boot` mount points are labelled. This avoid files under `/sysroot` created by bootc to be `unlabeled_t` and this makes sure files under `/boot` created by bootupd inherit the correct `boot_t` label. See #2494 Assisted-by: Opencode.ai <Opus 4.6>
1 parent b3b7c12 commit f8614fa

2 files changed

Lines changed: 311 additions & 17 deletions

File tree

pkg/manifest/raw_bootc.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
143143
pipeline.AddStage(stage)
144144
}
145145

146+
// Label the root and /boot mount points before bootc install so that
147+
// all files in the ostree repository and /boot hierarchy are properly
148+
// labeled. Without this, the mount point directories end up as
149+
// unlabeled_t and every file written by bootc inherits that label.
150+
// See https://github.com/coreos/fedora-coreos-tracker/issues/1771
151+
// and https://github.com/coreos/fedora-coreos-tracker/issues/1772
152+
if p.OSCustomizations.SELinux != "" {
153+
selinuxStages, err := p.genMountpointSELinuxStages()
154+
if err != nil {
155+
return osbuild.Pipeline{}, err
156+
}
157+
for _, stage := range selinuxStages {
158+
pipeline.AddStage(stage)
159+
}
160+
}
161+
146162
if len(p.containerSpecs) != 1 {
147163
return osbuild.Pipeline{}, fmt.Errorf("expected a single container input got %v", p.containerSpecs)
148164
}
@@ -333,6 +349,113 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
333349
return pipeline, nil
334350
}
335351

352+
// genMountpointSELinuxStages creates stages that label the root and /boot
353+
// mount point directories with proper SELinux contexts before bootc install.
354+
//
355+
// Without this, the mount point directories on the freshly created
356+
// filesystems are unlabeled (unlabeled_t) and bootc install inherits
357+
// that label for all files it writes.
358+
//
359+
// The approach mirrors what COSA does:
360+
// 1. Create /boot (and /boot/efi on UEFI) directories on the root filesystem
361+
// 2. Label the root mount (without /boot mounted) so the /boot directory
362+
// mountpoint itself gets the correct label
363+
// 3. Label /boot (with /boot mounted) so /boot/efi gets labeled correctly
364+
//
365+
// The file_contexts used for labeling come from the source pipeline (the
366+
// extracted container tree) via an input reference.
367+
func (p *RawBootcImage) genMountpointSELinuxStages() ([]*osbuild.Stage, error) {
368+
stages := make([]*osbuild.Stage, 0, 3)
369+
370+
devices, allMounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform)
371+
if err != nil {
372+
return nil, fmt.Errorf("generating devices/mounts for mountpoint SELinux labeling: %w", err)
373+
}
374+
375+
// Partition mounts by target for selective mounting.
376+
// Note: rootAndBootMounts intentionally excludes the EFI mount - we only
377+
// need to mount /boot to create and label the /boot/efi directory, the
378+
// EFI partition itself is not mounted during pre-install labeling.
379+
var rootMounts []osbuild.Mount
380+
var rootAndBootMounts []osbuild.Mount
381+
hasBootPartition := false
382+
hasEFIPartition := false
383+
for _, mnt := range allMounts {
384+
switch mnt.Target {
385+
case "/":
386+
rootMounts = append(rootMounts, mnt)
387+
rootAndBootMounts = append(rootAndBootMounts, mnt)
388+
case "/boot":
389+
hasBootPartition = true
390+
rootAndBootMounts = append(rootAndBootMounts, mnt)
391+
case "/boot/efi":
392+
hasEFIPartition = true
393+
}
394+
}
395+
396+
// 1a. Create /boot directory on root filesystem (only root mounted,
397+
// so /boot is just a directory on the root fs, not a mount point)
398+
mkdirBootStage := osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
399+
Paths: []osbuild.MkdirStagePath{
400+
{
401+
Path: "mount://-/boot",
402+
Mode: common.ToPtr(os.FileMode(0755)),
403+
},
404+
},
405+
})
406+
mkdirBootStage.Devices = devices
407+
mkdirBootStage.Mounts = rootMounts
408+
stages = append(stages, mkdirBootStage)
409+
410+
// 1b. Create /boot/efi directory on the boot filesystem (needs both
411+
// root and boot mounted so the boot partition is accessible)
412+
if hasEFIPartition && hasBootPartition {
413+
mkdirEfiStage := osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
414+
Paths: []osbuild.MkdirStagePath{
415+
{
416+
Path: "mount://boot/efi",
417+
Mode: common.ToPtr(os.FileMode(0755)),
418+
},
419+
},
420+
})
421+
mkdirEfiStage.Devices = devices
422+
mkdirEfiStage.Mounts = rootAndBootMounts
423+
stages = append(stages, mkdirEfiStage)
424+
}
425+
426+
// Tree input for file_contexts from the source pipeline
427+
fileContextsInputName := "tree"
428+
fileContextsInput := osbuild.NewPipelineTreeInputs(fileContextsInputName, p.SourcePipeline)
429+
fileContextsPath := fmt.Sprintf("input://%s/etc/selinux/%s/contexts/files/file_contexts",
430+
fileContextsInputName, p.OSCustomizations.SELinux)
431+
432+
// 2. Label root mount point (without /boot mounted so that the
433+
// /boot directory mountpoint gets labeled)
434+
rootSELinuxStage := osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
435+
FileContexts: fileContextsPath,
436+
Target: "mount://-/",
437+
})
438+
rootSELinuxStage.Devices = devices
439+
rootSELinuxStage.Mounts = rootMounts
440+
rootSELinuxStage.Inputs = fileContextsInput
441+
stages = append(stages, rootSELinuxStage)
442+
443+
// 3. Label /boot mount point (with /boot mounted so that /boot/efi
444+
// gets labeled)
445+
if hasBootPartition {
446+
bootSELinuxStage := osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
447+
FileContexts: fileContextsPath,
448+
Target: "mount://-/boot/",
449+
})
450+
bootSELinuxStage.Devices = devices
451+
bootSELinuxStage.Mounts = rootAndBootMounts
452+
bootSELinuxStage.Inputs = fileContextsInput
453+
stages = append(stages, bootSELinuxStage)
454+
}
455+
456+
return stages, nil
457+
}
458+
336459
// XXX: duplicated from os.go
337460
func (p *RawBootcImage) getInline() []string {
338461
inlineData := []string{}

pkg/manifest/raw_bootc_test.go

Lines changed: 188 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,17 @@ func TestRawBootcImageSerializeMkdirOptions(t *testing.T) {
186186
pipeline, err := rawBootcPipeline.Serialize()
187187
assert.NoError(t, err)
188188

189-
mkdirStage := findStage("org.osbuild.mkdir", pipeline.Stages)
189+
// Use findPostInstallStages to avoid matching pre-install mkdir
190+
// stages (e.g. mountpoint SELinux labeling) if SELinux is ever
191+
// set in this test.
192+
postInstallMkdirStages := findPostInstallStages("org.osbuild.mkdir", pipeline.Stages)
190193
if len(tc.expectedMkdirPaths) > 0 {
191194
// ensure options got passed
192-
require.NotNil(t, mkdirStage)
193-
mkdirOptions := mkdirStage.Options.(*osbuild.MkdirStageOptions)
195+
require.Greater(t, len(postInstallMkdirStages), 0)
196+
mkdirOptions := postInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
194197
assert.Equal(t, tc.expectedMkdirPaths, mkdirOptions.Paths)
195198
} else {
196-
require.Nil(t, mkdirStage)
199+
assert.Equal(t, 0, len(postInstallMkdirStages))
197200
}
198201
}
199202
}
@@ -240,6 +243,29 @@ func assertBootcDeploymentAndBindMount(t *testing.T, stage *osbuild.Stage) {
240243
assert.True(t, bindMntIdx > deploymentMntIdx)
241244
}
242245

246+
// findPostInstallStages returns stages that come after the bootc install stage
247+
// and have the ostree deployment + bind mount (i.e. post-install customization stages).
248+
func findPostInstallStages(stageType string, stages []*osbuild.Stage) []*osbuild.Stage {
249+
// Find the bootc install stage index
250+
bootcIdx := -1
251+
for i, s := range stages {
252+
if s.Type == "org.osbuild.bootc.install-to-filesystem" {
253+
bootcIdx = i
254+
break
255+
}
256+
}
257+
if bootcIdx < 0 {
258+
return nil
259+
}
260+
var found []*osbuild.Stage
261+
for _, s := range stages[bootcIdx+1:] {
262+
if s.Type == stageType {
263+
found = append(found, s)
264+
}
265+
}
266+
return found
267+
}
268+
243269
func TestRawBootcImageSerializeCustomizationGenCorrectStages(t *testing.T) {
244270
rawBootcPipeline := makeFakeRawBootcPipeline()
245271

@@ -253,18 +279,21 @@ func TestRawBootcImageSerializeCustomizationGenCorrectStages(t *testing.T) {
253279
{nil, nil, "", nil},
254280
{[]users.User{{Name: "foo"}}, nil, "", []string{"org.osbuild.mkdir", "org.osbuild.users"}},
255281
{[]users.User{{Name: "foo"}}, nil, "targeted", []string{"org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.selinux"}},
256-
{[]users.User{{Name: "foo"}}, []users.Group{{Name: "bar"}}, "targeted", []string{"org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.users", "org.osbuild.selinux"}},
282+
{[]users.User{{Name: "foo"}}, []users.Group{{Name: "bar"}}, "targeted", []string{"org.osbuild.groups", "org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.selinux"}},
257283
} {
258284
rawBootcPipeline.OSCustomizations.Users = tc.users
285+
rawBootcPipeline.OSCustomizations.Groups = tc.groups
259286
rawBootcPipeline.OSCustomizations.SELinux = tc.SELinux
260287

261288
pipeline, err := rawBootcPipeline.Serialize()
262289
assert.NoError(t, err)
263290

264291
for _, expectedStage := range tc.expectedStages {
265-
stage := findStage(expectedStage, pipeline.Stages)
266-
assert.NotNil(t, stage)
267-
assertBootcDeploymentAndBindMount(t, stage)
292+
stages := findPostInstallStages(expectedStage, pipeline.Stages)
293+
assert.Greater(t, len(stages), 0, "expected post-install stage %q not found", expectedStage)
294+
for _, stage := range stages {
295+
assertBootcDeploymentAndBindMount(t, stage)
296+
}
268297
}
269298
}
270299
}
@@ -325,16 +354,15 @@ func TestRawBootcImageSerializeCreateFilesDirs(t *testing.T) {
325354
pipeline, err := rawBootcPipeline.Serialize()
326355
assert.NoError(t, err)
327356

328-
// check dirs
329-
mkdirStage := findStage("org.osbuild.mkdir", pipeline.Stages)
357+
// check dirs - look for post-install mkdir stages (with deployment mounts)
358+
postInstallMkdirStages := findPostInstallStages("org.osbuild.mkdir", pipeline.Stages)
330359
if len(tc.dirs) > 0 {
331-
// ensure options got passed
332-
require.NotNil(t, mkdirStage)
333-
mkdirOptions := mkdirStage.Options.(*osbuild.MkdirStageOptions)
360+
require.Greater(t, len(postInstallMkdirStages), 0)
361+
mkdirOptions := postInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
334362
assert.Equal(t, "/path/to/dir", mkdirOptions.Paths[0].Path)
335-
assertBootcDeploymentAndBindMount(t, mkdirStage)
363+
assertBootcDeploymentAndBindMount(t, postInstallMkdirStages[0])
336364
} else {
337-
assert.Nil(t, mkdirStage)
365+
assert.Equal(t, 0, len(postInstallMkdirStages))
338366
}
339367

340368
// check files
@@ -349,9 +377,15 @@ func TestRawBootcImageSerializeCreateFilesDirs(t *testing.T) {
349377
assert.Nil(t, copyStage)
350378
}
351379

352-
selinuxStage := findStage("org.osbuild.selinux", pipeline.Stages)
380+
// Pre-install SELinux stages for mountpoint labeling
381+
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
382+
assert.Equal(t, 2, len(preInstallSELinuxStages), "expected 2 pre-install selinux stages (root + boot)")
353383

354-
assert.NotNil(t, selinuxStage)
384+
// Post-install SELinux stages for relabeling customizations
385+
if len(tc.dirs) > 0 || len(tc.files) > 0 {
386+
postInstallSELinuxStages := findPostInstallStages("org.osbuild.selinux", pipeline.Stages)
387+
assert.Greater(t, len(postInstallSELinuxStages), 0, "expected post-install selinux relabeling stages")
388+
}
355389

356390
// XXX: we should really check that the inline
357391
// source for files got generated but that is
@@ -388,6 +422,143 @@ func TestRawBootcPipelineNoMountsStages(t *testing.T) {
388422
checkStagesForNoMounts(t, common.Must(pipeline.Serialize()).Stages)
389423
}
390424

425+
// findPreInstallStages returns stages of the given type that appear before
426+
// the bootc install stage.
427+
func findPreInstallStages(stageType string, stages []*osbuild.Stage) []*osbuild.Stage {
428+
var found []*osbuild.Stage
429+
for _, s := range stages {
430+
if s.Type == "org.osbuild.bootc.install-to-filesystem" {
431+
break
432+
}
433+
if s.Type == stageType {
434+
found = append(found, s)
435+
}
436+
}
437+
return found
438+
}
439+
440+
func TestRawBootcImageSerializeMountpointSELinuxLabeling(t *testing.T) {
441+
rawBootcPipeline := makeFakeRawBootcPipeline()
442+
rawBootcPipeline.OSCustomizations.SELinux = "targeted"
443+
444+
pipeline, err := rawBootcPipeline.Serialize()
445+
require.NoError(t, err)
446+
447+
// Pre-install mkdir stages: one for /boot (root-only), one for /boot/efi (root+boot)
448+
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
449+
require.Equal(t, 2, len(preInstallMkdirStages))
450+
451+
// First mkdir stage creates /boot with only root mounted
452+
bootMkdirOpts := preInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
453+
require.Equal(t, 1, len(bootMkdirOpts.Paths))
454+
assert.Equal(t, "mount://-/boot", bootMkdirOpts.Paths[0].Path)
455+
assert.NotEmpty(t, preInstallMkdirStages[0].Devices)
456+
bootMkdirMountTargets := make([]string, 0)
457+
for _, mnt := range preInstallMkdirStages[0].Mounts {
458+
bootMkdirMountTargets = append(bootMkdirMountTargets, mnt.Target)
459+
}
460+
assert.Contains(t, bootMkdirMountTargets, "/")
461+
assert.NotContains(t, bootMkdirMountTargets, "/boot")
462+
463+
// Second mkdir stage creates /boot/efi with root+boot mounted
464+
efiMkdirOpts := preInstallMkdirStages[1].Options.(*osbuild.MkdirStageOptions)
465+
require.Equal(t, 1, len(efiMkdirOpts.Paths))
466+
assert.Equal(t, "mount://boot/efi", efiMkdirOpts.Paths[0].Path)
467+
assert.NotEmpty(t, preInstallMkdirStages[1].Devices)
468+
efiMkdirMountTargets := make([]string, 0)
469+
for _, mnt := range preInstallMkdirStages[1].Mounts {
470+
efiMkdirMountTargets = append(efiMkdirMountTargets, mnt.Target)
471+
}
472+
assert.Contains(t, efiMkdirMountTargets, "/")
473+
assert.Contains(t, efiMkdirMountTargets, "/boot")
474+
475+
// Pre-install selinux stages: one for root, one for /boot
476+
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
477+
require.Equal(t, 2, len(preInstallSELinuxStages))
478+
479+
// First SELinux stage: labels root mount (without boot mounted)
480+
rootSELinuxOpts := preInstallSELinuxStages[0].Options.(*osbuild.SELinuxStageOptions)
481+
assert.Equal(t, "mount://-/", rootSELinuxOpts.Target)
482+
assert.Contains(t, rootSELinuxOpts.FileContexts, "input://tree/etc/selinux/targeted/contexts/files/file_contexts")
483+
// Should have inputs (tree input from source pipeline)
484+
assert.NotNil(t, preInstallSELinuxStages[0].Inputs)
485+
// Should only mount root
486+
rootMountTargets := make([]string, 0)
487+
for _, mnt := range preInstallSELinuxStages[0].Mounts {
488+
rootMountTargets = append(rootMountTargets, mnt.Target)
489+
}
490+
assert.Contains(t, rootMountTargets, "/")
491+
assert.NotContains(t, rootMountTargets, "/boot")
492+
493+
// Second SELinux stage: labels /boot (with boot mounted)
494+
bootSELinuxOpts := preInstallSELinuxStages[1].Options.(*osbuild.SELinuxStageOptions)
495+
assert.Equal(t, "mount://-/boot/", bootSELinuxOpts.Target)
496+
assert.Contains(t, bootSELinuxOpts.FileContexts, "input://tree/etc/selinux/targeted/contexts/files/file_contexts")
497+
assert.NotNil(t, preInstallSELinuxStages[1].Inputs)
498+
// Should mount both root and boot
499+
bootMountTargets := make([]string, 0)
500+
for _, mnt := range preInstallSELinuxStages[1].Mounts {
501+
bootMountTargets = append(bootMountTargets, mnt.Target)
502+
}
503+
assert.Contains(t, bootMountTargets, "/")
504+
assert.Contains(t, bootMountTargets, "/boot")
505+
}
506+
507+
func TestRawBootcImageSerializeMountpointSELinuxLabelingNoSELinux(t *testing.T) {
508+
rawBootcPipeline := makeFakeRawBootcPipeline()
509+
// No SELinux set - no pre-install labeling stages should be generated
510+
rawBootcPipeline.OSCustomizations.SELinux = ""
511+
512+
pipeline, err := rawBootcPipeline.Serialize()
513+
require.NoError(t, err)
514+
515+
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
516+
assert.Equal(t, 0, len(preInstallSELinuxStages))
517+
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
518+
assert.Equal(t, 0, len(preInstallMkdirStages))
519+
}
520+
521+
func TestRawBootcImageSerializeMountpointSELinuxLabelingNoBoot(t *testing.T) {
522+
// Test the edge case where there is no separate /boot partition
523+
// (only root + EFI). Only the root SELinux stage should be generated,
524+
// no /boot labeling stage.
525+
mani := manifest.New()
526+
r := &runner.Linux{}
527+
pf := &platform.Data{
528+
Arch: arch.ARCH_X86_64,
529+
UEFIVendor: "test",
530+
}
531+
build := manifest.NewBuildFromContainer(&mani, r, nil, nil)
532+
rawBootcPipeline := manifest.NewRawBootcImage(build, nil, pf)
533+
rawBootcPipeline.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot/efi")
534+
err := rawBootcPipeline.SerializeStart(manifest.Inputs{Containers: []container.Spec{{Source: "foo"}}})
535+
require.NoError(t, err)
536+
537+
rawBootcPipeline.OSCustomizations.SELinux = "targeted"
538+
539+
pipeline, err := rawBootcPipeline.Serialize()
540+
require.NoError(t, err)
541+
542+
// Pre-install mkdir stage should only create /boot (no /boot/efi since
543+
// there's no separate boot partition to mount it on)
544+
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
545+
require.Equal(t, 1, len(preInstallMkdirStages))
546+
mkdirOpts := preInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
547+
var mkdirPaths []string
548+
for _, p := range mkdirOpts.Paths {
549+
mkdirPaths = append(mkdirPaths, p.Path)
550+
}
551+
assert.Contains(t, mkdirPaths, "mount://-/boot")
552+
assert.NotContains(t, mkdirPaths, "mount://boot/efi")
553+
554+
// Only one pre-install selinux stage (root only, no /boot stage)
555+
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
556+
require.Equal(t, 1, len(preInstallSELinuxStages))
557+
558+
rootSELinuxOpts := preInstallSELinuxStages[0].Options.(*osbuild.SELinuxStageOptions)
559+
assert.Equal(t, "mount://-/", rootSELinuxOpts.Target)
560+
}
561+
391562
func TestRawBootcPXE(t *testing.T) {
392563
rawBootcPipeline := makeFakeRawBootcPipeline()
393564
rawBootcPipeline.KernelVersion = "5.14.0-611.4.1.el9_7.x86_64"

0 commit comments

Comments
 (0)