From 9be7bc0250605fec19a60ab6dd2b19dcec786f38 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Sat, 4 Apr 2026 16:40:08 +0400 Subject: [PATCH] fix: don't set xattrs while decompressing extensions When decompressing extensions, we might not be able to set xattrs (e.g. running rootless), so instead of setting xattrs, save them in memory and push to mksquashfs as pseudo definitions. Signed-off-by: Andrey Smirnov (cherry picked from commit d697f5538a7a624a1ac7bafdfebc67dd9418c434) --- internal/pkg/extensions/compress.go | 56 +++++++++++++++++++++- internal/pkg/extensions/extensions_test.go | 2 +- pkg/archiver/archiver.go | 18 ------- pkg/archiver/untar.go | 18 +++---- pkg/imager/embed.go | 4 +- pkg/imager/extensions/extensions.go | 4 +- pkg/imager/imager.go | 11 +++-- pkg/imager/out.go | 5 +- pkg/imager/profile/input.go | 11 +++-- pkg/machinery/constants/constants.go | 3 ++ 10 files changed, 92 insertions(+), 40 deletions(-) diff --git a/internal/pkg/extensions/compress.go b/internal/pkg/extensions/compress.go index 2a2418ca0..8bf7e80d2 100644 --- a/internal/pkg/extensions/compress.go +++ b/internal/pkg/extensions/compress.go @@ -12,6 +12,8 @@ import ( "os" "os/exec" "path/filepath" + "slices" + "strings" "github.com/siderolabs/talos/pkg/machinery/imager/quirks" ) @@ -47,7 +49,7 @@ func initramfsPaths(quirks quirks.Quirks) []string { // // Components which should be placed to the initramfs are moved to the initramfsPath. // Ucode components are moved into a separate designated location. -func (ext *Extension) Compress(ctx context.Context, squashPath, initramfsPath string, quirks quirks.Quirks) (string, error) { +func (ext *Extension) Compress(ctx context.Context, squashPath, initramfsPath string, quirks quirks.Quirks, xattrsMap map[string]string) (string, error) { if err := ext.handleUcode(initramfsPath, quirks); err != nil { return "", err } @@ -70,12 +72,62 @@ func (ext *Extension) Compress(ctx context.Context, squashPath, initramfsPath st compressArgs = []string{"-comp", "xz", "-Xdict-size", "100%"} } - cmd := exec.CommandContext(ctx, "mksquashfs", append([]string{ext.RootfsPath(), squashPath, "-all-root", "-noappend", "-no-progress"}, compressArgs...)...) + pseudoFlags, err := ext.xattrPseudoFlags(xattrsMap) + if err != nil { + return "", err + } + + cmd := exec.CommandContext(ctx, "mksquashfs", + slices.Concat( + []string{ + ext.RootfsPath(), + squashPath, + "-all-root", + "-noappend", + "-no-progress", + }, + compressArgs, + pseudoFlags, + )...) cmd.Stderr = os.Stderr return squashPath, cmd.Run() } +// xattrPseudoFlags returns a list of pseudo-flag strings for the xattrs of the extension. +// +// These pseudo-flags are used to indicate the presence of specific SELinux xattrs on files within the extension. +// The mksquashfs tool will use that to mark files with xattrs instead of reading it from the filesystem. +func (ext *Extension) xattrPseudoFlags(xattrsMap map[string]string) ([]string, error) { + if xattrsMap == nil { + return nil, nil + } + + flags := []string{"-xattrs-exclude", ".*"} // exclude all xattrs by default + + for path, xattrValue := range xattrsMap { + if strings.HasPrefix(path, ext.RootfsPath()) { + // check if the file exists still (it might have been moved to the initramfs) + if _, err := os.Lstat(path); os.IsNotExist(err) { + continue + } + + relativePath, err := filepath.Rel(ext.RootfsPath(), path) + if err != nil { + return nil, err + } + + if relativePath == "." { + relativePath = "/" + } + + flags = append(flags, "-p", fmt.Sprintf("%s x security.selinux=%s", relativePath, xattrValue)) + } + } + + return flags, nil +} + func appendBlob(dst io.Writer, srcPath string) error { src, err := os.Open(srcPath) if err != nil { diff --git a/internal/pkg/extensions/extensions_test.go b/internal/pkg/extensions/extensions_test.go index fa1a0385c..bfb81ae85 100644 --- a/internal/pkg/extensions/extensions_test.go +++ b/internal/pkg/extensions/extensions_test.go @@ -30,7 +30,7 @@ func TestCompress(t *testing.T) { ext := exts[0] squashDest, initramfsDest := t.TempDir(), t.TempDir() - squashFile, err := ext.Compress(t.Context(), squashDest, initramfsDest, quirks.New("")) + squashFile, err := ext.Compress(t.Context(), squashDest, initramfsDest, quirks.New(""), nil) assert.NoError(t, err) assert.FileExists(t, squashFile) diff --git a/pkg/archiver/archiver.go b/pkg/archiver/archiver.go index de7b9ebe4..d6a26f1b9 100644 --- a/pkg/archiver/archiver.go +++ b/pkg/archiver/archiver.go @@ -29,21 +29,3 @@ func TarGz(ctx context.Context, rootPath string, output io.Writer, walkerOptions return zw.Close() } - -// UntarGz extracts .tar.gz archive to the rootPath. -func UntarGz(ctx context.Context, input io.Reader, rootPath string) error { - zr, err := gzip.NewReader(input) - if err != nil { - return err - } - - //nolint:errcheck - defer zr.Close() - - err = Untar(ctx, zr, rootPath) - if err != nil { - return err - } - - return zr.Close() -} diff --git a/pkg/archiver/untar.go b/pkg/archiver/untar.go index 30a63c27f..84087ee76 100644 --- a/pkg/archiver/untar.go +++ b/pkg/archiver/untar.go @@ -13,15 +13,19 @@ import ( "os" "path/filepath" - "github.com/pkg/xattr" - + "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/safepath" ) // Untar extracts .tar archive from r into filesystem under rootPath. // -//nolint:gocyclo -func Untar(ctx context.Context, r io.Reader, rootPath string) error { +// If xattrsMap is not nil, it will be filled with paths and their corresponding +// SELinux xattr values instead of setting the xattrs on the filesystem. +// +// If xattrsMap is nil, the function will ignore SELinux xattr values. +// +//nolint:gocyclo,cyclop +func Untar(ctx context.Context, r io.Reader, rootPath string, xattrsMap map[string]string) error { tr := tar.NewReader(r) for { @@ -87,10 +91,8 @@ func Untar(ctx context.Context, r io.Reader, rootPath string) error { } } - if hdr.PAXRecords["SCHILY.xattr.security.selinux"] != "" { - if err = xattr.LSet(path, "security.selinux", []byte(hdr.PAXRecords["SCHILY.xattr.security.selinux"])); err != nil { - return fmt.Errorf("error setting selinux xattr for %q: %w", path, err) - } + if hdr.PAXRecords[constants.TarPaxHeaderSELinux] != "" && xattrsMap != nil { + xattrsMap[path] = hdr.PAXRecords[constants.TarPaxHeaderSELinux] } } diff --git a/pkg/imager/embed.go b/pkg/imager/embed.go index e16a1c398..14bfa49d0 100644 --- a/pkg/imager/embed.go +++ b/pkg/imager/embed.go @@ -112,10 +112,10 @@ func BuildEmbeddedConfigExtension(machineConfig []byte) (io.Reader, error) { if err = tw.WriteHeader(&tar.Header{ Name: filepath.Join("rootfs", constants.EmbeddedConfigDirectory, constants.ConfigFilename), Typeflag: tar.TypeReg, - Mode: 0o000, + Mode: 0o400, Size: int64(len(machineConfig)), PAXRecords: map[string]string{ - "SCHILY.xattr.security.selinux": constants.StateSelinuxLabel, + constants.TarPaxHeaderSELinux: constants.StateSelinuxLabel, }, }); err != nil { return nil, fmt.Errorf("failed to write embedded header: %w", err) diff --git a/pkg/imager/extensions/extensions.go b/pkg/imager/extensions/extensions.go index 4915c81de..c70d4218e 100644 --- a/pkg/imager/extensions/extensions.go +++ b/pkg/imager/extensions/extensions.go @@ -31,6 +31,8 @@ type Builder struct { Printf func(format string, v ...any) // Quirks for the Talos version being used. Quirks quirks.Quirks + // XAttrsMap is used to store paths and their corresponding SELinux xattr values during extraction of extensions. + XAttrsMap map[string]string } // Build rebuilds the initramfs.xz with extensions. @@ -122,7 +124,7 @@ func (builder *Builder) compressExtensions(ctx context.Context, extensions []*ex builder.Printf("compressing system extensions") for _, ext := range extensions { - path, err := ext.Compress(ctx, tempDir, tempDir, builder.Quirks) + path, err := ext.Compress(ctx, tempDir, tempDir, builder.Quirks, builder.XAttrsMap) if err != nil { return nil, fmt.Errorf("error compressing extension %q: %w", ext.Manifest.Metadata.Name, err) } diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index 60502a44f..c1cdddf58 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -47,12 +47,16 @@ type Imager struct { sdBootPath string ukiPath string + + // xattrsMap is used to store paths and their corresponding SELinux xattr values during extraction of extensions. + xattrsMap map[string]string } // New creates a new Imager. func New(prof profile.Profile) (*Imager, error) { return &Imager{ - prof: prof, + prof: prof, + xattrsMap: map[string]string{}, }, nil } @@ -198,7 +202,7 @@ func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) e return fmt.Errorf("failed to create overlay directory: %w", err) } - if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning})); err != nil { + if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning}), nil); err != nil { return err } @@ -316,7 +320,7 @@ func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter) return fmt.Errorf("failed to create extension directory: %w", err) } - if err := ext.Extract(ctx, extensionDir, i.prof.Arch, printf); err != nil { + if err := ext.Extract(ctx, extensionDir, i.prof.Arch, printf, i.xattrsMap); err != nil { return err } } @@ -328,6 +332,7 @@ func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter) ExtensionTreePath: extensionsCheckoutDir, Printf: printf, Quirks: quirks.New(i.prof.Version), + XAttrsMap: i.xattrsMap, } if err := builder.Build(ctx); err != nil { diff --git a/pkg/imager/out.go b/pkg/imager/out.go index a796e4f8e..95ada3059 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -97,7 +97,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor return err } - if err := i.prof.Input.ImageCache.Extract(ctx, filepath.Join(scratchSpace, "imagecache"), i.prof.Arch, printf); err != nil { + if err := i.prof.Input.ImageCache.Extract(ctx, filepath.Join(scratchSpace, "imagecache"), i.prof.Arch, printf, nil); err != nil { return err } } @@ -358,7 +358,7 @@ func (i *Imager) buildImage(ctx context.Context, path string, printf func(string return err } - if err := i.prof.Input.ImageCache.Extract(ctx, imageCacheDir, i.prof.Arch, printf); err != nil { + if err := i.prof.Input.ImageCache.Extract(ctx, imageCacheDir, i.prof.Arch, printf, nil); err != nil { return err } @@ -540,6 +540,7 @@ func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter tempOverlayPath, i.prof.Arch, progressPrintf(report, reporter.Update{Message: "pulling overlay for installer...", Status: reporter.StatusRunning}), + nil, ); err != nil { return err } diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 8b4a2cbeb..5bbb44ed7 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -320,7 +320,12 @@ func (c *ContainerAsset) pullFromOCI(arch string) (v1.Image, error) { } // Extract the container asset to the path. -func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, printf func(string, ...any)) error { +func (c *ContainerAsset) Extract( + ctx context.Context, + destination, arch string, + printf func(string, ...any), + xattrsMap map[string]string, +) error { if c.TarballPath != "" { in, err := os.Open(c.TarballPath) if err != nil { @@ -331,7 +336,7 @@ func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, printf("extracting %s...", c.TarballPath) - return archiver.Untar(ctx, in, destination) + return archiver.Untar(ctx, in, destination, xattrsMap) } img, err := c.Pull(ctx, arch, printf) @@ -356,7 +361,7 @@ func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, }) eg.Go(func() error { - if untarErr := archiver.Untar(ctx, r, destination); untarErr != nil { + if untarErr := archiver.Untar(ctx, r, destination, xattrsMap); untarErr != nil { r.CloseWithError(untarErr) return untarErr diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index e1efbd52b..b46fdd873 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -1329,6 +1329,9 @@ const ( // ImageLabelVerified is the label key for the verified image label. ImageLabelVerified = "talos.dev/verified" + + // TarPaxHeaderSELinux is the name of the PAX header for storing SELinux labels. + TarPaxHeaderSELinux = "SCHILY.xattr.security.selinux" ) // names of variable that can be substituted in the talos.config kernel parameter.