From 7cad7edc1e79c4827e4bc00cb1700ac48e7971ad Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Wed, 22 Apr 2026 16:58:12 +0200 Subject: [PATCH 01/10] pkg/shell: Preserve error types in shell command execution The existing RunContextWithExitCode() wraps all errors from exec.Command in generic "failed to invoke" messages, which prevents callers from distinguishing between actual error types. Add RunContextWithExitCode2() and RunWithExitCode2() that return errors with their original types intact, including for ExitError. This allows callers to use errors.Is() and errors.As() to handle specific failure modes. For example, detecting a missing skopeo binary (exec.ErrNotFound) or an ENOEXEC error from inside non native containers, when an emulator is not set correctly. These new functions are meant to replace its original versions in the future. https://github.com/containers/toolbox/pull/1780 Signed-off-by: Dalibor Kricka --- src/pkg/shell/shell.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/pkg/shell/shell.go b/src/pkg/shell/shell.go index 00fa36997..6c9cfcacd 100644 --- a/src/pkg/shell/shell.go +++ b/src/pkg/shell/shell.go @@ -81,8 +81,49 @@ func RunContextWithExitCode(ctx context.Context, return 0, nil } +func RunContextWithExitCode2(ctx context.Context, + name string, + stdin io.Reader, + stdout, stderr io.Writer, + arg ...string) (int, error) { + + logLevel := logrus.GetLevel() + if stderr == nil && logLevel >= logrus.DebugLevel { + stderr = os.Stderr + } + + cmd := exec.CommandContext(ctx, name, arg...) + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + if err := cmd.Run(); err != nil { + exitCode := 1 + + if ctxErr := ctx.Err(); ctxErr != nil { + return 1, ctxErr + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + return exitCode, err + } + + return exitCode, err + } + + return 0, nil +} + func RunWithExitCode(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string) (int, error) { ctx := context.Background() exitCode, err := RunContextWithExitCode(ctx, name, stdin, stdout, stderr, arg...) return exitCode, err } + +func RunWithExitCode2(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string) (int, error) { + ctx := context.Background() + exitCode, err := RunContextWithExitCode2(ctx, name, stdin, stdout, stderr, arg...) + return exitCode, err +} From fae8f4a0614dc549fe832e343d9bc8ebc43f72a4 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Wed, 22 Apr 2026 23:21:55 +0200 Subject: [PATCH 02/10] cmd/create: Extract spinner setup into helper functions In /src/cmd/create.go, the same pattern of spinner creation and nil-safe stopping is repeated. Extract this into startSpinner() and stopSpinner() helper functions so that future callers can use spinners without duplicating the code. Replace the existing inline spinner code in createContainer() and pullImage() with calls to these new helpers. https://github.com/containers/toolbox/pull/1781 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index bdd86be37..13345289d 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -486,19 +486,15 @@ func createContainer(container, image, release, authFile string, showCommandToEn logrus.Debugf("%s", arg) } - s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout)) - if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel { - s.Prefix = fmt.Sprintf("Creating container %s: ", container) - s.Start() - defer s.Stop() - } + s := startSpinner(fmt.Sprintf("Creating container %s: ", container)) + defer stopSpinner(s) if err := shell.Run("podman", nil, nil, nil, createArgs...); err != nil { return fmt.Errorf("failed to create container %s", container) } // The spinner must be stopped before showing the 'enter' hint below. - s.Stop() + stopSpinner(s) if showCommandToEnter { fmt.Printf("Created container: %s\n", container) @@ -735,12 +731,8 @@ func pullImage(image, release, authFile string) (bool, error) { logrus.Debugf("Pulling image %s", imageFull) - if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel { - s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout)) - s.Prefix = fmt.Sprintf("Pulling %s: ", imageFull) - s.Start() - defer s.Stop() - } + s := startSpinner(fmt.Sprintf("Pulling %s: ", imageFull)) + defer stopSpinner(s) if err := podman.Pull(imageFull, authFile); err != nil { var builder strings.Builder @@ -963,6 +955,22 @@ func showPromptForDownload(imageFull string) bool { return shouldPullImage } +func startSpinner(message string) *spinner.Spinner { + if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel { + s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout)) + s.Prefix = message + s.Start() + return s + } + return nil +} + +func stopSpinner(s *spinner.Spinner) { + if s != nil { + s.Stop() + } +} + // systemdNeedsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped func systemdNeedsEscape(i int, b byte) bool { // Escape everything that is not a-z-A-Z-0-9 From ddebd00b7edb62abb32f17ba81fdec387b1f5e18 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Wed, 22 Apr 2026 23:30:29 +0200 Subject: [PATCH 03/10] pkg/utils: Add IsSupportedDistroImage for image to supported distro matching Add IsSupportedDistroImage(), which iterates over all supported distros and checks if the image basename matches any of them. This will be used by the architecture resolution code to decide whether to parse architecture suffixes from image tags, as this should be done only for natively supported images [1]. [1] Toolbx supported distributions: https://containertoolbx.org/distros/ https://github.com/containers/toolbox/pull/1781 Signed-off-by: Dalibor Kricka --- src/pkg/utils/utils.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index f3de23b1b..c88f8ef67 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -664,6 +664,20 @@ func IsP11KitClientPresent() (bool, error) { return false, err } +func IsSupportedDistroImage(image string) bool { + basename := ImageReferenceGetBasename(image) + if basename == "" { + return false + } + + for _, distroObj := range supportedDistros { + if distroObj.ImageBasename == basename { + return true + } + } + return false +} + func SetUpConfiguration() error { logrus.Debug("Setting up configuration") From 211ab9471775dbe50790ed98f6b238e69f20a3b0 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 08:12:19 +0200 Subject: [PATCH 04/10] pkg/architecture: Define core architecture types and constants Introduce the architecture package that represents the core of the Toolbx cross-architecture support, which is based on user-mode emulation using QEMU and binfmt_misc. The Architecture struct collects all per-architecture data (ELF magic/mask, OCI and binfmt naming, aliases, binfmt registration parameters) into a single map. Architectures present in the supportedArchitectures map represent the set of supported architectures within Toolbx. Define architecture ID constants NotSpecified, Aarch64, Ppc64le, and X86_64, along with their supportedArchitectures entries. Add core query functions: - ParseArgArchValue() for resolving user-supplied architecture strings - GetArchNameBinfmt() and GetArchNameOCI() for name lookups (one architecture can have multiple valid names [1]) - HasContainerNativeArch() for comparing against the host - ImageReferenceGetArchFromTag() for extracting architecture from image tag suffixes like "42-aarch64" for architecture detection Expose the HostArchID package variable, which is set in the init() function, so the variable can be accessed in the early init() state from every inheritor that utilizes the architecture package (HostArchID serves as a default value for initContainer --arch flag), and the Config struct for preserving the architecture ID and the QEMU emulator path, through the call chain. [1] https://itsfoss.com/arm-aarch64-x86_64/ https://github.com/containers/toolbox/pull/1782 Signed-off-by: Dalibor Kricka --- src/pkg/architecture/architecture.go | 165 +++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/pkg/architecture/architecture.go diff --git a/src/pkg/architecture/architecture.go b/src/pkg/architecture/architecture.go new file mode 100644 index 000000000..24f732230 --- /dev/null +++ b/src/pkg/architecture/architecture.go @@ -0,0 +1,165 @@ +/* + * Copyright © 2019 – 2026 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package architecture + +import ( + "fmt" + "runtime" + "strings" + + "github.com/containers/toolbox/pkg/utils" + "github.com/sirupsen/logrus" +) + +type Architecture struct { + ID int + NameBinfmt string + NameOCI string + Aliases []string + ELFMagic []byte + ELFMask []byte + + BinfmtFlags string + BinfmtName string + BinfmtMagicType string + BinfmtOffset string +} + +type Config struct { + ID int + QemuEmulatorPath string +} + +const ( + NotSpecified = iota + Aarch64 + Ppc64le + X86_64 +) + +var supportedArchitectures = map[int]Architecture{ + Aarch64: { + ID: Aarch64, + NameBinfmt: "aarch64", + NameOCI: "arm64", + Aliases: []string{"aarch64", "arm64"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xb7, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff}, + }, + Ppc64le: { + ID: Ppc64le, + NameBinfmt: "ppc64le", + NameOCI: "ppc64le", + Aliases: []string{"ppc64le"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x15, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x00}, + }, + X86_64: { + ID: X86_64, + NameBinfmt: "x86_64", + NameOCI: "amd64", + Aliases: []string{"x86_64", "amd64"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff}, + }, +} + +var ( + HostArchID int + supportedArgArchValues map[string]int +) + +func init() { + supportedArgArchValues = make(map[string]int) + for archID, arch := range supportedArchitectures { + for _, alias := range arch.Aliases { + supportedArgArchValues[alias] = archID + } + } + + HostArchID, _ = ParseArgArchValue(runtime.GOARCH) +} + +func GetArchConfigDefault() Config { + return Config{ + ID: HostArchID, + QemuEmulatorPath: "", + } +} + +func getArchitecture(archID int) (Architecture, bool) { + arch, exists := supportedArchitectures[archID] + return arch, exists +} + +func getArchNameBinfmt(arch int) string { + if arch == NotSpecified { + logrus.Warnf("Getting arch name for not specified architecture") + return "arch_not_specified" + } + if archObj, exists := supportedArchitectures[arch]; exists { + return archObj.NameBinfmt + } + return "" +} + +func GetArchNameOCI(arch int) string { + if arch == NotSpecified { + logrus.Warnf("Getting arch name for not specified architecture") + return "arch_not_specified" + } + if archObj, exists := supportedArchitectures[arch]; exists { + return archObj.NameOCI + } + return "" +} + +func HasContainerNativeArch(archID int) bool { + return archID == HostArchID +} + +func ImageReferenceGetArchFromTag(image string) int { + tag := utils.ImageReferenceGetTag(image) + + if tag == "" { + return NotSpecified + } + + i := strings.LastIndexByte(tag, '-') + if i == -1 { + return NotSpecified + } + + archInTag := tag[i+1:] + + for archID, arch := range supportedArchitectures { + if arch.NameBinfmt == archInTag || arch.NameOCI == archInTag { + return archID + } + } + + return NotSpecified +} + +func ParseArgArchValue(value string) (int, error) { + archID, exists := supportedArgArchValues[value] + if !exists { + return NotSpecified, fmt.Errorf("architecture '%s' is not supported by Toolbx", value) + } + + return archID, nil +} From a0a4ede111d194d263a9219ca7ae9864a6e50748 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 08:37:54 +0200 Subject: [PATCH 05/10] pkg/architecture: Add sandboxed binfmt_misc registration support Cross-architecture containers need QEMU binfmt_misc handlers registered within the container so that non-native architecture binaries can be executed via the host's kernel. Add the Registration struct that models a binfmt_misc registration entry, including name, magic type, offset, ELF magic/mask bytes, interpreter path, and flags. Add functions: - MountBinfmtMisc() to mount the sanboxed binfmt_misc filesystem inside a container, which enables setting the C flag in binfmt_misc registration without affecting the host system. The C flag presents a threat of privilege escalation when registered on the host, that why we want to have it isolated [1] - getDefaultRegistration() to fill a Registration struct containing all necessary binfmt_misc information taken from the architecture.supportedArchitectures data - RegisterBinfmtMisc() to write the registration string to /proc/sys/fs/binfmt_misc/register, which makes the non-native binary perception active - bytesToEscapedString() helper that formats byte slices into the \xHH-escaped string format required by the binfmt_misc register interface [1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=21ca59b365c0 https://github.com/containers/toolbox/pull/1782 Signed-off-by: Dalibor Kricka --- src/pkg/architecture/binfmt_misc.go | 151 ++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/pkg/architecture/binfmt_misc.go diff --git a/src/pkg/architecture/binfmt_misc.go b/src/pkg/architecture/binfmt_misc.go new file mode 100644 index 000000000..3dc8eddbb --- /dev/null +++ b/src/pkg/architecture/binfmt_misc.go @@ -0,0 +1,151 @@ +/* + * Copyright © 2019 – 2026 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package architecture + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containers/toolbox/pkg/shell" + "github.com/sirupsen/logrus" +) + +type Registration struct { + Name string + MagicType string + Offset string + Magic []byte + Mask []byte + Interpreter string + Flags string +} + +const ( + defaultMagicType = "M" + defaultFlags = "FC" + defaultOffset = "0" + binfmtMiscPath = "/proc/sys/fs/binfmt_misc" +) + +func (r *Registration) buildRegistrationString() string { + return fmt.Sprintf(":%s:%s:%s:%s:%s:%s:%s", + r.Name, r.MagicType, r.Offset, + bytesToEscapedString(r.Magic), + bytesToEscapedString(r.Mask), + r.Interpreter, r.Flags) +} + +func (r *Registration) register() error { + logrus.Debugf("Registering binfmt_misc for %s", r.Name) + + regString := r.buildRegistrationString() + logrus.Debugf("Registration string: %s", regString) + + if err := os.WriteFile(filepath.Join(binfmtMiscPath, "register"), []byte(regString), 0200); err != nil { + return fmt.Errorf("failed to register binfmt_misc handler: %w", err) + } + return nil +} + +func bytesToEscapedString(bytes []byte) string { + var result strings.Builder + for _, b := range bytes { + result.WriteString(fmt.Sprintf("\\x%02x", b)) + } + return result.String() +} + +func getDefaultRegistration(archID int, interpreterPath string) *Registration { + arch, exists := getArchitecture(archID) + if !exists { + return nil + } + + var name string + flags := defaultFlags + magicType := defaultMagicType + offset := defaultOffset + + if arch.BinfmtName != "" { + name = arch.BinfmtName + } else { + name = "qemu-" + arch.NameBinfmt + } + + if arch.BinfmtFlags != "" { + flags = arch.BinfmtFlags + } + + if arch.BinfmtMagicType != "" { + magicType = arch.BinfmtMagicType + } + + if arch.BinfmtOffset != "" { + offset = arch.BinfmtOffset + } + + interpreter := interpreterPath + if !strings.HasPrefix(interpreterPath, "/run/host/") { + interpreter = filepath.Join("/run/host", interpreter) + } + + return &Registration{ + Name: name, + MagicType: magicType, + Offset: offset, + Magic: arch.ELFMagic, + Mask: arch.ELFMask, + Interpreter: interpreter, + Flags: flags, + } +} + +func MountBinfmtMisc() error { + args := []string{ + "binfmt_misc", + "-t", + "binfmt_misc", + binfmtMiscPath, + } + + var stdout bytes.Buffer + + if err := shell.Run("mount", nil, &stdout, nil, args...); err != nil { + return fmt.Errorf("failed to mount binfmt_misc: %w", err) + } + + logrus.Debugf("Result of mount command: %s", stdout.String()) + + return nil +} + +func RegisterBinfmtMisc(archID int, interpreterPath string) error { + reg := getDefaultRegistration(archID, interpreterPath) + if reg == nil { + logrus.Debugf("Unable to register binfmt_misc for architecture '%s'", GetArchNameOCI(archID)) + return fmt.Errorf("Toolbx does not support architecture '%s'", GetArchNameOCI(archID)) + } + + if err := reg.register(); err != nil { + return err + } + + return nil +} From 8fe8ef5d4245809c0750f42ce32d9c706a362759 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 13:32:40 +0200 Subject: [PATCH 06/10] pkg/architecture: Add architecture support validation Before creating or initializing a cross-architecture container, the system must be checked for the required QEMU emulator and binfmt_misc registration. This prevents users from creating or running non-native containers when their host system doesn't meet the requirements, and provides users with an informative error message referring to the problem. Add IsArchSupportedOnCreation(), which searches for a statically linked QEMU binary on the host using exec.LookPath() and verifies that a matching binfmt_misc registration exists. It returns the path to the QEMU binary for use during container creation, which is meant to be passed to the init-container and registered through sandboxed binfmt_misc within the container. Add IsArchSupportedOnInitialization() which performs similar checks from inside the container, looking at the interpreter path passed from the host and falling back to standard host-mounted locations under /run/host/usr/bin/. Add isStaticallyLinkedELF() helper that uses debug/elf to verify a binary is statically linked. Only a statically linked QEMU interpreter can be used, because a dynamically linked one would cause the kernel to attempt to resolve its host-native shared libraries (such as libc.so) within the container, resulting in an immediate crash. Add validateBinfmtRegistration(), which checks for the presence of qemu- entries in binfmt_misc (or qemu--static, since it can differ based on the system). https://github.com/containers/toolbox/pull/1783 Signed-off-by: Dalibor Kricka --- src/pkg/architecture/architecture.go | 128 +++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/pkg/architecture/architecture.go b/src/pkg/architecture/architecture.go index 24f732230..652ed224d 100644 --- a/src/pkg/architecture/architecture.go +++ b/src/pkg/architecture/architecture.go @@ -17,7 +17,11 @@ package architecture import ( + "debug/elf" + "errors" "fmt" + "os" + "os/exec" "runtime" "strings" @@ -155,6 +159,108 @@ func ImageReferenceGetArchFromTag(image string) int { return NotSpecified } +func IsArchSupportedOnCreation(archID int) (string, error) { + archName := getArchNameBinfmt(archID) + archNameDebug := GetArchNameOCI(archID) + logrus.Debugf("Checking QEMU emulation support for architecture %s", archNameDebug) + + qemuBinaryPossibleNames := []string{ + fmt.Sprintf("qemu-%s-static", archName), + fmt.Sprintf("qemu-%s", archName), + } + + foundQemuBinaryPath := "" + for _, qemuName := range qemuBinaryPossibleNames { + qemuBinaryPath, err := exec.LookPath(qemuName) + + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + continue + } + + return "", fmt.Errorf("failed to look up binary '%s': %w", qemuName, err) + } + + if isStaticallyLinkedELF(qemuBinaryPath) { + foundQemuBinaryPath = qemuBinaryPath + break + } + } + + if foundQemuBinaryPath == "" { + err := fmt.Errorf("The host system does not have the required support: No %s statically linked QEMU emulator binary found", archNameDebug) + return "", err + } + + if !validateBinfmtRegistration(archID, false) { + err := fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + return "", err + } + + return foundQemuBinaryPath, nil +} + +func IsArchSupportedOnInitialization(archID int, interpreterPath string) (string, error) { + archName := getArchNameBinfmt(archID) + archNameDebug := GetArchNameOCI(archID) + logrus.Debugf("Checking QEMU emulation support for architecture %s", archNameDebug) + + if isStaticallyLinkedELF(interpreterPath) { + if !validateBinfmtRegistration(archID, true) { + return "", fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + } + return interpreterPath, nil + } + + // Fallback: check standard locations on the host + logrus.Debugf("Interpreter at %s not found or not statically linked, checking fallback locations in '/run/host/usr/bin/'", interpreterPath) + fmt.Fprintf(os.Stderr, "Warning: QEMU emulator not found at expected path '%s', using fallback at '/run/host/usr/bin/'\n", interpreterPath) + + qemuBinaryPossiblePaths := []string{ + fmt.Sprintf("/run/host/usr/bin/qemu-%s-static", archName), + fmt.Sprintf("/run/host/usr/bin/qemu-%s", archName), + } + + for _, qemuPath := range qemuBinaryPossiblePaths { + if isStaticallyLinkedELF(qemuPath) { + logrus.Debugf("Found valid QEMU binary at %s", qemuPath) + + if !validateBinfmtRegistration(archID, true) { + return "", fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + } + return qemuPath, nil + } + } + + return "", fmt.Errorf("The host system does not have the required support: No %s statically linked QEMU emulator binary found", archNameDebug) +} + +func isStaticallyLinkedELF(filePath string) bool { + if !utils.PathExists(filePath) { + logrus.Debugf("File '%s' does not exist\n", filePath) + return false + } + + f, err := elf.Open(filePath) + if err != nil { + logrus.Debugf("File '%s' is not an ELF file\n", filePath) + return false + } + defer f.Close() + + // Check for PT_INTERP program header + for _, prog := range f.Progs { + if prog.Type == elf.PT_INTERP { + // Dynamically linked + logrus.Debugf("File '%s' is dynamically linked\n", filePath) + return false + } + } + + // Statically linked + return true +} + func ParseArgArchValue(value string) (int, error) { archID, exists := supportedArgArchValues[value] if !exists { @@ -163,3 +269,25 @@ func ParseArgArchValue(value string) (int, error) { return archID, nil } + +func validateBinfmtRegistration(archID int, withinContainer bool) bool { + archName := getArchNameBinfmt(archID) + inContainerPathPrefix := "" + + if withinContainer { + inContainerPathPrefix = "/run/host" + } + + qemuBinfmtPossiblePaths := []string{ + fmt.Sprintf("%s/proc/sys/fs/binfmt_misc/qemu-%s", inContainerPathPrefix, archName), + fmt.Sprintf("%s/proc/sys/fs/binfmt_misc/qemu-%s-static", inContainerPathPrefix, archName), + } + + for _, binfmtPath := range qemuBinfmtPossiblePaths { + if utils.PathExists(binfmtPath) { + logrus.Debugf("Architecture %s is supported", archName) + return true + } + } + return false +} From 689883263f67f37677d637520acf870372cc34f5 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 15:21:13 +0200 Subject: [PATCH 07/10] pkg/skopeo: Extend Skopeo Image struct and add size computation methods Add Architecture and NameFull fields to the Skopeo Image struct so that callers can inspect the architecture of a remote image. Move the image size computation from the /cmd layer into GetSize() and GetSizeHuman() methods on Image, since the skopeo package owns the layer data. Add VerifyArchitectureMatch() method to Image that validates the image's architecture field against an expected architecture ID. The purpose of this function is to check whether the image architecture matches the demanded architecture before it is pulled. Specifically, this verification applies to the images that support only a single architecture (they are not part of a multi-platform manifest list), because the skopeo inspect proceeds successfully even when the value of a flag --override-arch does not match the actual image architecture (for a multi-architecture image the skopeo inspect with not-matching --override-arch would fail). Like this, the user can be prevented from incompatible images. https://github.com/containers/toolbox/pull/1784 Signed-off-by: Dalibor Kricka --- src/pkg/skopeo/skopeo.go | 57 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/pkg/skopeo/skopeo.go b/src/pkg/skopeo/skopeo.go index 8b15c7370..8454e6a80 100644 --- a/src/pkg/skopeo/skopeo.go +++ b/src/pkg/skopeo/skopeo.go @@ -20,15 +20,70 @@ import ( "bytes" "context" "encoding/json" + "errors" + "fmt" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" + "github.com/docker/go-units" + "github.com/sirupsen/logrus" ) type Layer struct { Size json.Number } + type Image struct { - LayersData []Layer + Architecture string `json:"Architecture"` + LayersData []Layer + NameFull string +} + +func (image *Image) GetSize() (float64, error) { + var imageSizeFloat float64 + + if image.LayersData == nil { + return -1, errors.New("'skopeo inspect' did not have LayersData") + } + + for _, layer := range image.LayersData { + if layerSize, err := layer.Size.Float64(); err != nil { + return -1, err + } else { + imageSizeFloat += layerSize + } + } + + return imageSizeFloat, nil +} + +func (image *Image) GetSizeHuman() (string, error) { + imageSizeFloat, err := image.GetSize() + if err != nil { + return "", err + } + + imageSizeHuman := units.HumanSize(imageSizeFloat) + return imageSizeHuman, nil +} + +func (image *Image) VerifyArchitectureMatch(expectedArchID int) error { + expectedArchName := architecture.GetArchNameOCI(expectedArchID) + logrus.Debugf("Verifying image %s supports architecture %s", image.NameFull, expectedArchName) + + actualArchID, err := architecture.ParseArgArchValue(image.Architecture) + if err != nil { + return err + } + + if actualArchID != expectedArchID { + // Single-arch image mismatch + return fmt.Errorf("image %s is a single-architecture image for %s, but %s was requested", + image.NameFull, image.Architecture, expectedArchName) + } + + logrus.Debugf("Architecture verification passed: %s", expectedArchName) + return nil } func Inspect(ctx context.Context, target string) (*Image, error) { From fba0a72ddafeef0634efdbc16828f3debfb56bfe Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 15:57:10 +0200 Subject: [PATCH 08/10] pkg/skopeo: Add architecture-aware inspect and cross-arch copy Change Inspect() to accept archID and authfile parameters. When the requested architecture differs from the host's, --override-arch is passed to skopeo, which then inspects the correct manifest in a multi-arch image (if it exists for the given architecture, otherwise the inspection fails). It also uses RunContextWithExitCode2() so callers can detect a missing skopeo binary via errors.Is(err, exec.ErrNotFound), which is only a soft dependency of the Toolbx package, as it is not required for running native containers. Add CopyOverrideArch(), which uses 'skopeo copy --override-arch' to pull a specific architecture variant of a multi-arch image into Podman's local container storage. This is used instead of 'podman pull' because Podman does not support pulling a foreign architecture image into a locally addressable name. The way in which the cross-arch extension chooses the name for non-native images (and also containers) is described in the discussion at [1] [1] https://github.com/containers/podman/discussions/27780#discussioncomment-16662213 https://github.com/containers/toolbox/pull/1784 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 3 ++- src/pkg/skopeo/skopeo.go | 45 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index 13345289d..09b444a61 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -26,6 +26,7 @@ import ( "time" "github.com/briandowns/spinner" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/skopeo" @@ -564,7 +565,7 @@ func getEnterCommand(container string) string { } func getImageSizeFromRegistry(ctx context.Context, imageFull string) (string, error) { - image, err := skopeo.Inspect(ctx, imageFull) + image, err := skopeo.Inspect(ctx, imageFull, architecture.HostArchID, "") if err != nil { return "", err } diff --git a/src/pkg/skopeo/skopeo.go b/src/pkg/skopeo/skopeo.go index 8454e6a80..7fd52be90 100644 --- a/src/pkg/skopeo/skopeo.go +++ b/src/pkg/skopeo/skopeo.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" @@ -86,13 +87,49 @@ func (image *Image) VerifyArchitectureMatch(expectedArchID int) error { return nil } -func Inspect(ctx context.Context, target string) (*Image, error) { +func CopyOverrideArch(source, destination string, archID int, authfile string) error { + + destinationWithTransport := "containers-storage:" + destination + sourceWithTransport := "docker://" + source + args := []string{"copy", "--override-arch", architecture.GetArchNameOCI(archID)} + + if authfile != "" { + args = append(args, []string{"--src-authfile", authfile}...) + } + + args = append(args, sourceWithTransport, destinationWithTransport) + + if logrus.GetLevel() < logrus.DebugLevel { + if err := shell.Run("skopeo", nil, nil, nil, args...); err != nil { + return err + } + } else { + if err := shell.Run("skopeo", nil, os.Stderr, nil, args...); err != nil { + return err + } + } + + return nil +} + +func Inspect(ctx context.Context, target string, archID int, authfile string) (*Image, error) { var stdout bytes.Buffer targetWithTransport := "docker://" + target - args := []string{"inspect", "--format", "json", targetWithTransport} + args := []string{"inspect", "--format", "json"} + + if !architecture.HasContainerNativeArch(archID) { + archName := architecture.GetArchNameOCI(archID) + args = append(args, []string{"--override-arch", archName}...) + } - if err := shell.RunContext(ctx, "skopeo", nil, &stdout, nil, args...); err != nil { + if authfile != "" { + args = append(args, []string{"--authfile", authfile}...) + } + + args = append(args, targetWithTransport) + + if _, err := shell.RunContextWithExitCode2(ctx, "skopeo", nil, &stdout, nil, args...); err != nil { return nil, err } @@ -102,5 +139,7 @@ func Inspect(ctx context.Context, target string) (*Image, error) { return nil, err } + image.NameFull = target + return &image, nil } From fb20f4dee5587874de32b2ce56f649286291e4dc Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Thu, 23 Apr 2026 21:41:06 +0200 Subject: [PATCH 09/10] cmd/utils: Add architecture resolution from --arch flag, image tags, and names Add resolveArchitectureID(), which combines the --arch command-line flag with architecture detection from image tag suffixes (e.g., "fedora-toolbox:42-aarch64"). This detection applies only to images from distributions that Toolbx explicitly supports (see [1]), to avoid a false architecture approach on custom images where a dash-separated component might not represent an architecture, since there is no standard set regarding preserving architecture in the tag (see detailed explanation at [2]). When both sources specify an architecture, it validates that they do not conflict. Add resolveImageNameWithArchitectureSuffix(), which appends the OCI architecture name to supported distro image references when the target architecture differs from the host, to ensure the local Toolbx images naming convention [2]. Again, this applies only to supported distros. [1] https://containertoolbx.org/distros/ [2] https://github.com/containers/podman/discussions/27780#discussioncomment-16662213 https://github.com/containers/toolbox/pull/1786 Signed-off-by: Dalibor Kricka --- src/cmd/utils.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/cmd/utils.go b/src/cmd/utils.go index 191da4ef6..ac50a516a 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -31,6 +31,7 @@ import ( "strings" "syscall" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/utils" "github.com/sirupsen/logrus" @@ -261,6 +262,18 @@ func discardInputAsync(ctx context.Context) (<-chan int, <-chan error) { return retValCh, errCh } +func createErrorConflictingArchSpecs(archCLI, archTag int) error { + var builder strings.Builder + fmt.Fprintf(&builder, "conflicting architecture specifications\n") + fmt.Fprintf(&builder, "--arch=%s but image tag specifies %s\n", + architecture.GetArchNameOCI(archCLI), + architecture.GetArchNameOCI(archTag)) + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) +} + func createErrorContainerNotFound(container string) error { var builder strings.Builder fmt.Fprintf(&builder, "container %s not found\n", container) @@ -483,6 +496,35 @@ func poll(pollFn pollFunc, eventFD int32, fds ...int32) error { } } +func resolveArchitectureID(arch string, image string) (int, error) { + archID := architecture.NotSpecified + if arch != "" { + archIDParsed, err := architecture.ParseArgArchValue(arch) + if err != nil { + return architecture.NotSpecified, err + } + archID = archIDParsed + } + + if image != "" && utils.IsSupportedDistroImage(image) { + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + + if archID == architecture.NotSpecified && archIDFromTag != architecture.NotSpecified { + logrus.Debug("non-native architecture was detected in the image tag -> cross-architecture approach is going to be used") + + archID = archIDFromTag + } else if archID != archIDFromTag && archIDFromTag != architecture.NotSpecified { + return architecture.NotSpecified, createErrorConflictingArchSpecs(archID, archIDFromTag) + } + } + + if archID == architecture.NotSpecified { + archID = architecture.HostArchID + } + + return archID, nil +} + func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string) ( string, string, string, error, ) { @@ -543,6 +585,21 @@ func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, return container, image, release, nil } +func resolveImageNameWithArchitectureSuffix(image string, archID int) string { + if architecture.HasContainerNativeArch(archID) { + return image + } + + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + isSupportedDistroImage := utils.IsSupportedDistroImage(image) + + if isSupportedDistroImage && archIDFromTag == architecture.NotSpecified { + return image + "-" + architecture.GetArchNameOCI(archID) + } + + return image +} + // showManual tries to open the specified manual page using man on stdout func showManual(manual string) error { manBinary, err := exec.LookPath("man") From 78dace15faf3756d26017867d09e48e4d636207d Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 07:38:55 +0200 Subject: [PATCH 10/10] cmd/utils: Update resolveContainerAndImageNames for cross-arch support Change resolveContainerAndImageNames() to accept an archID parameter. When the target architecture is non-native, and the container name was auto-generated (was not set by a user), append the architecture suffix to the container name (e.g., "fedora-toolbox-arm64") to distinguish it from native containers. Temporarily update the callers of resolveContainerAndImageNames() to pass in architecture.HostArchID to the updated signature, to maintain a default native behavior. Once implemented, the --arch argument in the callers will pass the actual architecture information. https://github.com/containers/toolbox/pull/1786 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 3 ++- src/cmd/enter.go | 4 +++- src/cmd/rootMigrationPath.go | 3 ++- src/cmd/run.go | 4 +++- src/cmd/utils.go | 15 ++++++++++++++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index 09b444a61..d662d05ff 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -175,7 +175,8 @@ func create(cmd *cobra.Command, args []string) error { containerArg, createFlags.distro, createFlags.image, - createFlags.release) + createFlags.release, + architecture.HostArchID) if err != nil { return err diff --git a/src/cmd/enter.go b/src/cmd/enter.go index 9ba84d6ad..570803161 100644 --- a/src/cmd/enter.go +++ b/src/cmd/enter.go @@ -21,6 +21,7 @@ import ( "fmt" "os" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) @@ -108,7 +109,8 @@ func enter(cmd *cobra.Command, args []string) error { containerArg, enterFlags.distro, "", - enterFlags.release) + enterFlags.release, + architecture.HostArchID) if err != nil { return err diff --git a/src/cmd/rootMigrationPath.go b/src/cmd/rootMigrationPath.go index 33f970145..0464e4aed 100644 --- a/src/cmd/rootMigrationPath.go +++ b/src/cmd/rootMigrationPath.go @@ -24,6 +24,7 @@ import ( "os" "strings" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) @@ -56,7 +57,7 @@ func rootRunImpl(cmd *cobra.Command, args []string) error { return &exitError{exitCode, err} } - container, image, release, err := resolveContainerAndImageNames("", "", "", "", "") + container, image, release, err := resolveContainerAndImageNames("", "", "", "", "", architecture.HostArchID) if err != nil { return err } diff --git a/src/cmd/run.go b/src/cmd/run.go index ed421aa68..035288121 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -30,6 +30,7 @@ import ( "syscall" "time" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/nvidia" "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/shell" @@ -145,7 +146,8 @@ func run(cmd *cobra.Command, args []string) error { "--container", runFlags.distro, "", - runFlags.release) + runFlags.release, + architecture.HostArchID) if err != nil { return err diff --git a/src/cmd/utils.go b/src/cmd/utils.go index ac50a516a..df4f06a34 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -525,9 +525,11 @@ func resolveArchitectureID(arch string, image string) (int, error) { return archID, nil } -func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string) ( +func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string, archID int) ( string, string, string, error, ) { + containerWasEmpty := container == "" + container, image, release, err := utils.ResolveContainerAndImageNames(container, distroCLI, imageCLI, @@ -582,6 +584,17 @@ func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, } } + if containerWasEmpty && !architecture.HasContainerNativeArch(archID) { + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + + if archIDFromTag == architecture.NotSpecified { + archName := architecture.GetArchNameOCI(archID) + if archName != "" { + container = container + "-" + archName + } + } + } + return container, image, release, nil }