Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions pkg/manifest/raw_bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
pipeline.AddStage(stage)
}

// Label the root and /boot mount points before bootc install so that
// all files in the ostree repository and /boot hierarchy are properly
// labeled. Without this, the mount point directories end up as
// unlabeled_t and every file written by bootc inherits that label.
// See https://github.com/coreos/fedora-coreos-tracker/issues/1771
// and https://github.com/coreos/fedora-coreos-tracker/issues/1772
if p.OSCustomizations.SELinux != "" {
selinuxStages, err := p.genMountpointSELinuxStages()
if err != nil {
return osbuild.Pipeline{}, err
}
for _, stage := range selinuxStages {
pipeline.AddStage(stage)
}
}

if len(p.containerSpecs) != 1 {
return osbuild.Pipeline{}, fmt.Errorf("expected a single container input got %v", p.containerSpecs)
}
Expand Down Expand Up @@ -333,6 +349,113 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
return pipeline, nil
}

// genMountpointSELinuxStages creates stages that label the root and /boot
// mount point directories with proper SELinux contexts before bootc install.
//
// Without this, the mount point directories on the freshly created
// filesystems are unlabeled (unlabeled_t) and bootc install inherits
// that label for all files it writes.
//
// The approach mirrors what COSA does:
// 1. Create /boot (and /boot/efi on UEFI) directories on the root filesystem
// 2. Label the root mount (without /boot mounted) so the /boot directory
// mountpoint itself gets the correct label
// 3. Label /boot (with /boot mounted) so /boot/efi gets labeled correctly
//
// The file_contexts used for labeling come from the source pipeline (the
// extracted container tree) via an input reference.
func (p *RawBootcImage) genMountpointSELinuxStages() ([]*osbuild.Stage, error) {
stages := make([]*osbuild.Stage, 0, 3)

devices, allMounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform)
if err != nil {
return nil, fmt.Errorf("generating devices/mounts for mountpoint SELinux labeling: %w", err)
}

// Partition mounts by target for selective mounting.
// Note: rootAndBootMounts intentionally excludes the EFI mount - we only
// need to mount /boot to create and label the /boot/efi directory, the
// EFI partition itself is not mounted during pre-install labeling.
var rootMounts []osbuild.Mount
var rootAndBootMounts []osbuild.Mount
hasBootPartition := false
hasEFIPartition := false
for _, mnt := range allMounts {
switch mnt.Target {
case "/":
rootMounts = append(rootMounts, mnt)
rootAndBootMounts = append(rootAndBootMounts, mnt)
case "/boot":
hasBootPartition = true
rootAndBootMounts = append(rootAndBootMounts, mnt)
case "/boot/efi":
hasEFIPartition = true
}
}

// 1a. Create /boot directory on root filesystem (only root mounted,
// so /boot is just a directory on the root fs, not a mount point)
mkdirBootStage := osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
Paths: []osbuild.MkdirStagePath{
{
Path: "mount://-/boot",
Mode: common.ToPtr(os.FileMode(0755)),
},
},
})
mkdirBootStage.Devices = devices
mkdirBootStage.Mounts = rootMounts
stages = append(stages, mkdirBootStage)

// 1b. Create /boot/efi directory on the boot filesystem (needs both
// root and boot mounted so the boot partition is accessible)
if hasEFIPartition && hasBootPartition {
mkdirEfiStage := osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
Paths: []osbuild.MkdirStagePath{
{
Path: "mount://boot/efi",
Mode: common.ToPtr(os.FileMode(0755)),
},
},
})
mkdirEfiStage.Devices = devices
mkdirEfiStage.Mounts = rootAndBootMounts
stages = append(stages, mkdirEfiStage)
}

// Tree input for file_contexts from the source pipeline
fileContextsInputName := "tree"
fileContextsInput := osbuild.NewPipelineTreeInputs(fileContextsInputName, p.SourcePipeline)
fileContextsPath := fmt.Sprintf("input://%s/etc/selinux/%s/contexts/files/file_contexts",
fileContextsInputName, p.OSCustomizations.SELinux)

// 2. Label root mount point (without /boot mounted so that the
// /boot directory mountpoint gets labeled)
rootSELinuxStage := osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
FileContexts: fileContextsPath,
Target: "mount://-/",
})
rootSELinuxStage.Devices = devices
rootSELinuxStage.Mounts = rootMounts
rootSELinuxStage.Inputs = fileContextsInput
stages = append(stages, rootSELinuxStage)

// 3. Label /boot mount point (with /boot mounted so that /boot/efi
// gets labeled)
if hasBootPartition {
bootSELinuxStage := osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
FileContexts: fileContextsPath,
Target: "mount://-/boot/",
})
bootSELinuxStage.Devices = devices
bootSELinuxStage.Mounts = rootAndBootMounts
bootSELinuxStage.Inputs = fileContextsInput
stages = append(stages, bootSELinuxStage)
}

return stages, nil
}

// XXX: duplicated from os.go
func (p *RawBootcImage) getInline() []string {
inlineData := []string{}
Expand Down
205 changes: 188 additions & 17 deletions pkg/manifest/raw_bootc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,17 @@ func TestRawBootcImageSerializeMkdirOptions(t *testing.T) {
pipeline, err := rawBootcPipeline.Serialize()
assert.NoError(t, err)

mkdirStage := findStage("org.osbuild.mkdir", pipeline.Stages)
// Use findPostInstallStages to avoid matching pre-install mkdir
// stages (e.g. mountpoint SELinux labeling) if SELinux is ever
// set in this test.
postInstallMkdirStages := findPostInstallStages("org.osbuild.mkdir", pipeline.Stages)
if len(tc.expectedMkdirPaths) > 0 {
// ensure options got passed
require.NotNil(t, mkdirStage)
mkdirOptions := mkdirStage.Options.(*osbuild.MkdirStageOptions)
require.Greater(t, len(postInstallMkdirStages), 0)
mkdirOptions := postInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
assert.Equal(t, tc.expectedMkdirPaths, mkdirOptions.Paths)
} else {
require.Nil(t, mkdirStage)
assert.Equal(t, 0, len(postInstallMkdirStages))
}
}
}
Expand Down Expand Up @@ -240,6 +243,29 @@ func assertBootcDeploymentAndBindMount(t *testing.T, stage *osbuild.Stage) {
assert.True(t, bindMntIdx > deploymentMntIdx)
}

// findPostInstallStages returns stages that come after the bootc install stage
// and have the ostree deployment + bind mount (i.e. post-install customization stages).
func findPostInstallStages(stageType string, stages []*osbuild.Stage) []*osbuild.Stage {
// Find the bootc install stage index
bootcIdx := -1
for i, s := range stages {
if s.Type == "org.osbuild.bootc.install-to-filesystem" {
bootcIdx = i
break
}
}
if bootcIdx < 0 {
return nil
}
var found []*osbuild.Stage
for _, s := range stages[bootcIdx+1:] {
if s.Type == stageType {
found = append(found, s)
}
}
return found
}

func TestRawBootcImageSerializeCustomizationGenCorrectStages(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()

Expand All @@ -253,18 +279,21 @@ func TestRawBootcImageSerializeCustomizationGenCorrectStages(t *testing.T) {
{nil, nil, "", nil},
{[]users.User{{Name: "foo"}}, nil, "", []string{"org.osbuild.mkdir", "org.osbuild.users"}},
{[]users.User{{Name: "foo"}}, nil, "targeted", []string{"org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.selinux"}},
{[]users.User{{Name: "foo"}}, []users.Group{{Name: "bar"}}, "targeted", []string{"org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.users", "org.osbuild.selinux"}},
{[]users.User{{Name: "foo"}}, []users.Group{{Name: "bar"}}, "targeted", []string{"org.osbuild.groups", "org.osbuild.mkdir", "org.osbuild.users", "org.osbuild.selinux"}},
} {
rawBootcPipeline.OSCustomizations.Users = tc.users
rawBootcPipeline.OSCustomizations.Groups = tc.groups
rawBootcPipeline.OSCustomizations.SELinux = tc.SELinux

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

for _, expectedStage := range tc.expectedStages {
stage := findStage(expectedStage, pipeline.Stages)
assert.NotNil(t, stage)
assertBootcDeploymentAndBindMount(t, stage)
stages := findPostInstallStages(expectedStage, pipeline.Stages)
assert.Greater(t, len(stages), 0, "expected post-install stage %q not found", expectedStage)
for _, stage := range stages {
assertBootcDeploymentAndBindMount(t, stage)
}
}
}
}
Expand Down Expand Up @@ -325,16 +354,15 @@ func TestRawBootcImageSerializeCreateFilesDirs(t *testing.T) {
pipeline, err := rawBootcPipeline.Serialize()
assert.NoError(t, err)

// check dirs
mkdirStage := findStage("org.osbuild.mkdir", pipeline.Stages)
// check dirs - look for post-install mkdir stages (with deployment mounts)
postInstallMkdirStages := findPostInstallStages("org.osbuild.mkdir", pipeline.Stages)
if len(tc.dirs) > 0 {
// ensure options got passed
require.NotNil(t, mkdirStage)
mkdirOptions := mkdirStage.Options.(*osbuild.MkdirStageOptions)
require.Greater(t, len(postInstallMkdirStages), 0)
mkdirOptions := postInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
assert.Equal(t, "/path/to/dir", mkdirOptions.Paths[0].Path)
assertBootcDeploymentAndBindMount(t, mkdirStage)
assertBootcDeploymentAndBindMount(t, postInstallMkdirStages[0])
} else {
assert.Nil(t, mkdirStage)
assert.Equal(t, 0, len(postInstallMkdirStages))
}

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

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

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

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

// findPreInstallStages returns stages of the given type that appear before
// the bootc install stage.
func findPreInstallStages(stageType string, stages []*osbuild.Stage) []*osbuild.Stage {
var found []*osbuild.Stage
for _, s := range stages {
if s.Type == "org.osbuild.bootc.install-to-filesystem" {
break
}
if s.Type == stageType {
found = append(found, s)
}
}
return found
}

func TestRawBootcImageSerializeMountpointSELinuxLabeling(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.OSCustomizations.SELinux = "targeted"

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)

// Pre-install mkdir stages: one for /boot (root-only), one for /boot/efi (root+boot)
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
require.Equal(t, 2, len(preInstallMkdirStages))

// First mkdir stage creates /boot with only root mounted
bootMkdirOpts := preInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
require.Equal(t, 1, len(bootMkdirOpts.Paths))
assert.Equal(t, "mount://-/boot", bootMkdirOpts.Paths[0].Path)
assert.NotEmpty(t, preInstallMkdirStages[0].Devices)
bootMkdirMountTargets := make([]string, 0)
for _, mnt := range preInstallMkdirStages[0].Mounts {
bootMkdirMountTargets = append(bootMkdirMountTargets, mnt.Target)
}
assert.Contains(t, bootMkdirMountTargets, "/")
assert.NotContains(t, bootMkdirMountTargets, "/boot")

// Second mkdir stage creates /boot/efi with root+boot mounted
efiMkdirOpts := preInstallMkdirStages[1].Options.(*osbuild.MkdirStageOptions)
require.Equal(t, 1, len(efiMkdirOpts.Paths))
assert.Equal(t, "mount://boot/efi", efiMkdirOpts.Paths[0].Path)
assert.NotEmpty(t, preInstallMkdirStages[1].Devices)
efiMkdirMountTargets := make([]string, 0)
for _, mnt := range preInstallMkdirStages[1].Mounts {
efiMkdirMountTargets = append(efiMkdirMountTargets, mnt.Target)
}
assert.Contains(t, efiMkdirMountTargets, "/")
assert.Contains(t, efiMkdirMountTargets, "/boot")

// Pre-install selinux stages: one for root, one for /boot
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
require.Equal(t, 2, len(preInstallSELinuxStages))

// First SELinux stage: labels root mount (without boot mounted)
rootSELinuxOpts := preInstallSELinuxStages[0].Options.(*osbuild.SELinuxStageOptions)
assert.Equal(t, "mount://-/", rootSELinuxOpts.Target)
assert.Contains(t, rootSELinuxOpts.FileContexts, "input://tree/etc/selinux/targeted/contexts/files/file_contexts")
// Should have inputs (tree input from source pipeline)
assert.NotNil(t, preInstallSELinuxStages[0].Inputs)
// Should only mount root
rootMountTargets := make([]string, 0)
for _, mnt := range preInstallSELinuxStages[0].Mounts {
rootMountTargets = append(rootMountTargets, mnt.Target)
}
assert.Contains(t, rootMountTargets, "/")
assert.NotContains(t, rootMountTargets, "/boot")

// Second SELinux stage: labels /boot (with boot mounted)
bootSELinuxOpts := preInstallSELinuxStages[1].Options.(*osbuild.SELinuxStageOptions)
assert.Equal(t, "mount://-/boot/", bootSELinuxOpts.Target)
assert.Contains(t, bootSELinuxOpts.FileContexts, "input://tree/etc/selinux/targeted/contexts/files/file_contexts")
assert.NotNil(t, preInstallSELinuxStages[1].Inputs)
// Should mount both root and boot
bootMountTargets := make([]string, 0)
for _, mnt := range preInstallSELinuxStages[1].Mounts {
bootMountTargets = append(bootMountTargets, mnt.Target)
}
assert.Contains(t, bootMountTargets, "/")
assert.Contains(t, bootMountTargets, "/boot")
}

func TestRawBootcImageSerializeMountpointSELinuxLabelingNoSELinux(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
// No SELinux set - no pre-install labeling stages should be generated
rawBootcPipeline.OSCustomizations.SELinux = ""

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)

preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
assert.Equal(t, 0, len(preInstallSELinuxStages))
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
assert.Equal(t, 0, len(preInstallMkdirStages))
}

func TestRawBootcImageSerializeMountpointSELinuxLabelingNoBoot(t *testing.T) {
// Test the edge case where there is no separate /boot partition
// (only root + EFI). Only the root SELinux stage should be generated,
// no /boot labeling stage.
mani := manifest.New()
r := &runner.Linux{}
pf := &platform.Data{
Arch: arch.ARCH_X86_64,
UEFIVendor: "test",
}
build := manifest.NewBuildFromContainer(&mani, r, nil, nil)
rawBootcPipeline := manifest.NewRawBootcImage(build, nil, pf)
rawBootcPipeline.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot/efi")
err := rawBootcPipeline.SerializeStart(manifest.Inputs{Containers: []container.Spec{{Source: "foo"}}})
require.NoError(t, err)

rawBootcPipeline.OSCustomizations.SELinux = "targeted"

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)

// Pre-install mkdir stage should only create /boot (no /boot/efi since
// there's no separate boot partition to mount it on)
preInstallMkdirStages := findPreInstallStages("org.osbuild.mkdir", pipeline.Stages)
require.Equal(t, 1, len(preInstallMkdirStages))
mkdirOpts := preInstallMkdirStages[0].Options.(*osbuild.MkdirStageOptions)
var mkdirPaths []string
for _, p := range mkdirOpts.Paths {
mkdirPaths = append(mkdirPaths, p.Path)
}
assert.Contains(t, mkdirPaths, "mount://-/boot")
assert.NotContains(t, mkdirPaths, "mount://boot/efi")

// Only one pre-install selinux stage (root only, no /boot stage)
preInstallSELinuxStages := findPreInstallStages("org.osbuild.selinux", pipeline.Stages)
require.Equal(t, 1, len(preInstallSELinuxStages))

rootSELinuxOpts := preInstallSELinuxStages[0].Options.(*osbuild.SELinuxStageOptions)
assert.Equal(t, "mount://-/", rootSELinuxOpts.Target)
}

func TestRawBootcPXE(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.KernelVersion = "5.14.0-611.4.1.el9_7.x86_64"
Expand Down
Loading