From 7cad7edc1e79c4827e4bc00cb1700ac48e7971ad Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Wed, 22 Apr 2026 16:58:12 +0200 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 } From a79eb9ddc4a7f12e7aef549b98fd693ff27cda95 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 09:58:20 +0200 Subject: [PATCH 11/15] cmd/create: Integrate cross-architecture support into container creation Add the --arch flag to the 'create' command, allowing users to create Toolbx containers for architectures different from the host (e.g., 'toolbox create --arch arm64'). Utilize the architecture resolution pipeline in create() by using resolveArchitectureID() (added in [1]) to determine the target architecture from the --arch flag and image tags. Validate host support via IsArchSupportedOnCreation() (added in [2]), which checks for the required QEMU emulator and binfmt_misc registration. Pass architecture ID to resolveContainerAndImageNames() (updated in [1]) so that non-native containers get architecture-suffixed names. Update pullImage() to handle cross-architecture image pulling: when the target architecture is non-native, use skopeo.CopyOverrideArch() (added in [3]) instead of podman.Pull(), since Podman does not support pulling foreign architecture images into locally addressable names. The need for this is explained in a discussion in [4]. Add a 'toolbox-arch' label to created containers to record the target architecture in OCI format. Extract the image pull error formatting into createErrorImagePull() in utils.go to avoid duplication between the native and cross-arch pull paths. Update the createContainer() call in run.go to pass the default architecture config via GetArchConfigDefault(), maintaining the existing native-architecture behavior. [1] https://github.com/containers/toolbox/pull/1786 [2] https://github.com/containers/toolbox/pull/1783 [3] https://github.com/containers/toolbox/pull/1784 [4] https://github.com/containers/podman/discussions/27780 https://github.com/containers/toolbox/pull/1787 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 84 +++++++++++++++++++++++++++++++++++------------ src/cmd/run.go | 2 +- src/cmd/utils.go | 9 +++++ 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index d662d05ff..7e29f3d5d 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -50,6 +50,7 @@ const ( var ( createFlags struct { + arch string authFile string container string distro string @@ -76,6 +77,12 @@ var createCmd = &cobra.Command{ func init() { flags := createCmd.Flags() + flags.StringVarP(&createFlags.arch, + "arch", + "a", + "", + "Create a Toolbx container for a different architecture than the host") + flags.StringVar(&createFlags.authFile, "authfile", "", @@ -171,25 +178,43 @@ func create(cmd *cobra.Command, args []string) error { containerArg = "--container" } + var archConfig architecture.Config + + archID, err := resolveArchitectureID(createFlags.arch, createFlags.image) + if err != nil { + return err + } + archConfig.ID = archID + + if !architecture.HasContainerNativeArch(archConfig.ID) { + archName := architecture.GetArchNameOCI(archConfig.ID) + qemuEmulatorPath, err := architecture.IsArchSupportedOnCreation(archID) + if err != nil { + errNotSupported := fmt.Errorf("Cannot create container for architecture %s\n%s", archName, err) + return errNotSupported + } + archConfig.QemuEmulatorPath = qemuEmulatorPath + } + container, image, release, err := resolveContainerAndImageNames(container, containerArg, createFlags.distro, createFlags.image, createFlags.release, - architecture.HostArchID) + archConfig.ID) if err != nil { return err } - if err := createContainer(container, image, release, createFlags.authFile, true); err != nil { + if err := createContainer(container, image, release, createFlags.authFile, archConfig, true); err != nil { return err } return nil } -func createContainer(container, image, release, authFile string, showCommandToEnter bool) error { +func createContainer(container, image, release, authFile string, archConfig architecture.Config, showCommandToEnter bool) error { if container == "" { panic("container not specified") } @@ -216,14 +241,19 @@ func createContainer(container, image, release, authFile string, showCommandToEn return errors.New(errMsg) } - pulled, err := pullImage(image, release, authFile) + pulled, couldBeNonnativeArch, err := pullImage(image, release, authFile, archConfig.ID) if err != nil { return err } + if !pulled { return nil } + if couldBeNonnativeArch { + image = resolveImageNameWithArchitectureSuffix(image, archConfig.ID) + } + imageFull, err := podman.GetFullyQualifiedImageFromRepoTags(image) if err != nil { var errImage *podman.ImageError @@ -448,6 +478,10 @@ func createContainer(container, image, release, authFile string, showCommandToEn "--label", "com.github.containers.toolbox=true", }...) + createArgs = append(createArgs, []string{ + "--label", "toolbox-arch=" + architecture.GetArchNameOCI(archConfig.ID), + }...) + createArgs = append(createArgs, devPtsMount...) createArgs = append(createArgs, []string{ @@ -663,11 +697,13 @@ func getServiceSocket(serviceName string, unitName string) (string, error) { return "", fmt.Errorf("failed to find a SOCK_STREAM socket for %s", unitName) } -func pullImage(image, release, authFile string) (bool, error) { +func pullImage(image, release, authFile string, archID int) (bool, bool, error) { + isNonNativeArch := !architecture.HasContainerNativeArch(archID) + if ok := utils.ImageReferenceCanBeID(image); ok { logrus.Debugf("Looking up image %s", image) if _, err := podman.ImageExists(image); err == nil { - return true, nil + return true, false, nil } } @@ -678,7 +714,7 @@ func pullImage(image, release, authFile string) (bool, error) { logrus.Debugf("Looking up image %s", imageLocal) if _, err := podman.ImageExists(imageLocal); err == nil { - return true, nil + return true, false, nil } } @@ -690,13 +726,15 @@ func pullImage(image, release, authFile string) (bool, error) { var err error imageFull, err = utils.GetFullyQualifiedImageFromDistros(image, release) if err != nil { - return false, fmt.Errorf("image %s not found in local storage and known registries", image) + return false, false, fmt.Errorf("image %s not found in local storage and known registries", image) } } - logrus.Debugf("Looking up image %s", imageFull) - if _, err := podman.ImageExists(imageFull); err == nil { - return true, nil + imageFullWithArch := resolveImageNameWithArchitectureSuffix(imageFull, archID) + + logrus.Debugf("Looking up image %s", imageFullWithArch) + if _, err := podman.ImageExists(imageFullWithArch); err == nil { + return true, isNonNativeArch, nil } domain := utils.ImageReferenceGetDomain(imageFull) @@ -721,14 +759,14 @@ func pullImage(image, release, authFile string) (bool, error) { fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) errMsg := builder.String() - return false, errors.New(errMsg) + return false, false, errors.New(errMsg) } shouldPullImage = showPromptForDownload(imageFull) } if !shouldPullImage { - return false, nil + return false, false, nil } logrus.Debugf("Pulling image %s", imageFull) @@ -736,17 +774,21 @@ func pullImage(image, release, authFile string) (bool, error) { s := startSpinner(fmt.Sprintf("Pulling %s: ", imageFull)) defer stopSpinner(s) - if err := podman.Pull(imageFull, authFile); err != nil { - var builder strings.Builder - fmt.Fprintf(&builder, "failed to pull image %s\n", imageFull) - fmt.Fprintf(&builder, "If it was a private image, log in with: podman login %s\n", domain) - fmt.Fprintf(&builder, "Use '%s --verbose ...' for further details.", executableBase) + if !isNonNativeArch { + logrus.Debugf("'podman pull' is used for pulling image %s", imageFull) - errMsg := builder.String() - return false, errors.New(errMsg) + if err := podman.Pull(imageFull, authFile); err != nil { + return false, false, createErrorImagePull(imageFull, domain) + } + } else { + logrus.Debugf("'skopeo copy' is used for pulling non-native architecture image %s", imageFull) + + if err := skopeo.CopyOverrideArch(imageFull, imageFullWithArch, archID, authFile); err != nil { + return false, false, createErrorImagePull(imageFull, domain) + } } - return true, nil + return true, isNonNativeArch, nil } func createPromptForDownload(imageFull, imageSize string) string { diff --git a/src/cmd/run.go b/src/cmd/run.go index 035288121..4f915c63c 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -227,7 +227,7 @@ func runCommand(container string, return nil } - if err := createContainer(container, image, release, "", false); err != nil { + if err := createContainer(container, image, release, "", architecture.GetArchConfigDefault(), false); err != nil { return err } } else if containersCount == 1 && defaultContainer { diff --git a/src/cmd/utils.go b/src/cmd/utils.go index df4f06a34..9100a8378 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -294,6 +294,15 @@ func createErrorDistroWithoutRelease(distro string) error { return errors.New(errMsg) } +func createErrorImagePull(image, domain string) error { + var builder strings.Builder + fmt.Fprintf(&builder, "failed to pull image %s\n", image) + fmt.Fprintf(&builder, "If it was a private image, log in with: podman login %s\n", domain) + fmt.Fprintf(&builder, "Use '%s --verbose ...' for further details.", executableBase) + + return errors.New(builder.String()) +} + func createErrorInvalidContainer(containerArg string) error { var builder strings.Builder fmt.Fprintf(&builder, "invalid argument for '%s'\n", containerArg) From 6fc831d027b8c3bc9ec1d0a8ec90e4c7f5e20e0c Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 10:50:34 +0200 Subject: [PATCH 12/15] cmd/create: Rework download prompt flow for cross-arch verification Rework the image download prompt flow to support architecture verification before pulling non-native images. The new implementation ensures that the image inspection completes for the non-native creation path before it is pulled, so the image's architecture can be verified. The previous implementation used promptForDownloadError as a control flow mechanism between the first and second download prompts. Replace this with the pullImageDecision enum (pullNo, pullYes, pullUnknown) for clearer three-state signaling. Replaced getImageSizeFromRegistryAsync() with getImageFromRegistryAsync(), which now returns the full skopeo.Image struct instead of just the image size string. It calls skopeo.Inspect() (updated in [1]), making image metadata available throughout the download prompt flow for both size display and architecture verification in a single inspect call. Use Image.GetSizeHuman() (added in [1]) for image size display in the second download prompt, replacing the local size computation. Update showPromptForDownloadFirst() to return (pullImageDecision, *skopeo.Image, error). For non-native architectures, when the user confirms the download, the function now waits for the skopeo inspect to complete (with a spinner) before returning, ensuring that architecture verification can happen before the pull begins. Update pullImage() to verify the image architecture before pulling non-native images by calling VerifyArchitectureMatch() (added in [1]) to catch incompatible single-architecture images. Handle the case where the inspect returns nil (multi-arch manifest has no matching variant) with an explicit error. Detect a missing skopeo binary via exec.ErrNotFound, which is only a soft dependency of the Toolbx package, as it is not required for running non-native containers, and report it through createErrorSkopeoNotFound() added in utils.go. [1] https://github.com/containers/toolbox/pull/1784 https://github.com/containers/toolbox/pull/1787 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 220 ++++++++++++++++++++++++++-------------------- src/cmd/utils.go | 8 ++ 2 files changed, 135 insertions(+), 93 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index 7e29f3d5d..e1a8910cb 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "time" @@ -32,22 +33,25 @@ import ( "github.com/containers/toolbox/pkg/skopeo" "github.com/containers/toolbox/pkg/term" "github.com/containers/toolbox/pkg/utils" - "github.com/docker/go-units" "github.com/godbus/dbus/v5" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -type promptForDownloadError struct { - ImageSize string -} - const ( alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` num = `0123456789` alphanum = alpha + num ) +type pullImageDecision int + +const ( + pullNo pullImageDecision = iota // 0 - User declined or default + pullYes // 1 - User confirmed + pullUnknown // 2 - Need second prompt +) + var ( createFlags struct { arch string @@ -540,6 +544,13 @@ func createContainer(container, image, release, authFile string, archConfig arch return nil } +func boolToPullDecision(shouldPull bool) pullImageDecision { + if shouldPull { + return pullYes + } + return pullNo +} + func createHelp(cmd *cobra.Command, args []string) { if utils.IsInsideContainer() { if !utils.IsInsideToolboxContainer() { @@ -599,42 +610,19 @@ func getEnterCommand(container string) string { return enterCommand } -func getImageSizeFromRegistry(ctx context.Context, imageFull string) (string, error) { - image, err := skopeo.Inspect(ctx, imageFull, architecture.HostArchID, "") - if err != nil { - return "", err - } - - if image.LayersData == nil { - return "", errors.New("'skopeo inspect' did not have LayersData") - } - - var imageSizeFloat float64 - - for _, layer := range image.LayersData { - if layerSize, err := layer.Size.Float64(); err != nil { - return "", err - } else { - imageSizeFloat += layerSize - } - } - - imageSizeHuman := units.HumanSize(imageSizeFloat) - return imageSizeHuman, nil -} - -func getImageSizeFromRegistryAsync(ctx context.Context, imageFull string) (<-chan string, <-chan error) { - retValCh := make(chan string) +func getImageFromRegistryAsync(ctx context.Context, imageFull string, archID int, authFile string) (<-chan *skopeo.Image, <-chan error) { + retValCh := make(chan *skopeo.Image) errCh := make(chan error) go func() { - imageSize, err := getImageSizeFromRegistry(ctx, imageFull) + image, err := skopeo.Inspect(ctx, imageFull, archID, authFile) + if err != nil { errCh <- err return } - retValCh <- imageSize + retValCh <- image }() return retValCh, errCh @@ -745,10 +733,18 @@ func pullImage(image, release, authFile string, archID int) (bool, bool, error) promptForDownload := true var shouldPullImage bool + var imageInfo *skopeo.Image + var imageInspectErr error if rootFlags.assumeYes || domain == "localhost" { promptForDownload = false shouldPullImage = true + + if isNonNativeArch { + s := startSpinner("Fetching non-native architecture image info: ") + imageInfo, imageInspectErr = skopeo.Inspect(context.Background(), imageFull, archID, authFile) + stopSpinner(s) + } } if promptForDownload { @@ -762,13 +758,22 @@ func pullImage(image, release, authFile string, archID int) (bool, bool, error) return false, false, errors.New(errMsg) } - shouldPullImage = showPromptForDownload(imageFull) + shouldPullImage, imageInfo, imageInspectErr = showPromptForDownload(imageFull, archID, authFile) } if !shouldPullImage { return false, false, nil } + if imageInspectErr != nil && isNonNativeArch { + if errors.Is(imageInspectErr, exec.ErrNotFound) { + return false, false, createErrorSkopeoNotFound(imageFull, archID) + } + + // For now, log and continue (imageInfo will be nil) + logrus.Debugf("Failed to inspect image: %s", imageInspectErr) + } + logrus.Debugf("Pulling image %s", imageFull) s := startSpinner(fmt.Sprintf("Pulling %s: ", imageFull)) @@ -783,6 +788,17 @@ func pullImage(image, release, authFile string, archID int) (bool, bool, error) } else { logrus.Debugf("'skopeo copy' is used for pulling non-native architecture image %s", imageFull) + if imageInfo == nil { + // Multi-arch image mismatch + expectedArchName := architecture.GetArchNameOCI(archID) + return false, false, fmt.Errorf("failed to verify: image %s does not support architecture %s or the image does not exists at all", + imageFull, expectedArchName) + } + + if err := imageInfo.VerifyArchitectureMatch(archID); err != nil { + return false, false, err + } + if err := skopeo.CopyOverrideArch(imageFull, imageFullWithArch, archID, authFile); err != nil { return false, false, createErrorImagePull(imageFull, domain) } @@ -802,8 +818,9 @@ func createPromptForDownload(imageFull, imageSize string) string { return prompt } -func showPromptForDownloadFirst(imageFull string) (bool, error) { +func showPromptForDownloadFirst(imageFull string, archID int, authFile string) (pullImageDecision, *skopeo.Image, error) { prompt := createPromptForDownload(imageFull, " ... MB") + isNonnativeArch := !architecture.HasContainerNativeArch(archID) parentCtx := context.Background() askCtx, askCancel := context.WithCancelCause(parentCtx) @@ -811,48 +828,69 @@ func showPromptForDownloadFirst(imageFull string) (bool, error) { askCh, askErrCh := askForConfirmationAsync(askCtx, prompt, nil) - imageSizeCtx, imageSizeCancel := context.WithCancelCause(parentCtx) - defer imageSizeCancel(errors.New("clean-up")) + imageCtx, imageCancel := context.WithCancelCause(parentCtx) + defer imageCancel(errors.New("clean-up")) - imageSizeCh, imageSizeErrCh := getImageSizeFromRegistryAsync(imageSizeCtx, imageFull) + imageCh, imageErrCh := getImageFromRegistryAsync(imageCtx, imageFull, archID, authFile) - var imageSize string - var shouldPullImage bool + var image *skopeo.Image = nil + var shouldPullImage pullImageDecision = pullNo + var imageInspectErr error = nil select { case val := <-askCh: - shouldPullImage = val - cause := fmt.Errorf("%w: received confirmation without image size", context.Canceled) - imageSizeCancel(cause) + shouldPullImage = boolToPullDecision(val) + + if isNonnativeArch { + if shouldPullImage == pullNo { + return pullNo, nil, nil + } + + s := startSpinner("Fetching non-native architecture image info: ") + + select { + case img := <-imageCh: + stopSpinner(s) + image = img + return shouldPullImage, image, nil + case err := <-imageErrCh: + stopSpinner(s) + return shouldPullImage, nil, err + } + } else { + cause := fmt.Errorf("%w: received confirmation without image info", context.Canceled) + imageCancel(cause) + } case err := <-askErrCh: - shouldPullImage = false - cause := fmt.Errorf("failed to ask for confirmation without image size: %w", err) - imageSizeCancel(cause) - case val := <-imageSizeCh: - imageSize = val - cause := fmt.Errorf("%w: received image size", context.Canceled) + shouldPullImage = pullNo + cause := fmt.Errorf("failed to ask for confirmation without image info: %w", err) + imageCancel(cause) + case val := <-imageCh: + image = val + cause := fmt.Errorf("%w: received image info", context.Canceled) askCancel(cause) - case err := <-imageSizeErrCh: - cause := fmt.Errorf("failed to get image size: %w", err) + case err := <-imageErrCh: + imageInspectErr = err + cause := fmt.Errorf("failed to get image info: %w", err) askCancel(cause) } - if imageSizeCtx.Err() != nil && askCtx.Err() == nil { - cause := context.Cause(imageSizeCtx) - logrus.Debugf("Show prompt for download: image size canceled: %s", cause) - return shouldPullImage, nil + if imageCtx.Err() != nil && askCtx.Err() == nil { + cause := context.Cause(imageCtx) + logrus.Debugf("Show prompt for download: image info canceled: %s", cause) + return shouldPullImage, nil, nil } var done bool - if imageSizeCtx.Err() == nil && askCtx.Err() != nil { + if imageCtx.Err() == nil && askCtx.Err() != nil { select { case val := <-askCh: - logrus.Debugf("Show prompt for download: received pending confirmation without image size") - shouldPullImage = val + logrus.Debugf("Show prompt for download: received pending confirmation without image info") + shouldPullImage = boolToPullDecision(val) done = true case err := <-askErrCh: - logrus.Debugf("Show prompt for download: failed to ask for confirmation without image size: %s", + logrus.Debugf("Show prompt for download: failed to ask for confirmation without image info: %s", err) } } else { @@ -863,13 +901,13 @@ func showPromptForDownloadFirst(imageFull string) (bool, error) { logrus.Debugf("Show prompt for download: ask canceled: %s", cause) if done { - return shouldPullImage, nil + return shouldPullImage, image, imageInspectErr + } else { + return pullUnknown, image, imageInspectErr } - - return false, &promptForDownloadError{imageSize} } -func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadError) bool { +func showPromptForDownloadSecond(imageFull, imageSize string) bool { oldState, err := term.GetState(os.Stdin) if err != nil { logrus.Debugf("Show prompt for download: failed to get terminal state: %s", err) @@ -895,12 +933,7 @@ func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadEr discardCh, discardErrCh := discardInputAsync(discardCtx) - var prompt string - if errors.Is(errFirst, context.Canceled) { - prompt = createPromptForDownload(imageFull, errFirst.ImageSize) - } else { - prompt = createPromptForDownload(imageFull, "") - } + prompt := createPromptForDownload(imageFull, imageSize) fmt.Printf("\r") @@ -981,22 +1014,37 @@ func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadEr return shouldPullImage } -func showPromptForDownload(imageFull string) bool { +func showPromptForDownload(imageFull string, archID int, authFile string) (bool, *skopeo.Image, error) { fmt.Println("Image required to create Toolbx container.") - shouldPullImage, err := showPromptForDownloadFirst(imageFull) - if err == nil { - return shouldPullImage + var shouldPullImageFirst pullImageDecision + var image *skopeo.Image + var imageInspectErr error + + shouldPullImageFirst, image, imageInspectErr = showPromptForDownloadFirst(imageFull, archID, authFile) + + switch shouldPullImageFirst { + case pullYes: + return true, image, imageInspectErr + case pullNo: + return false, image, imageInspectErr } - var errPromptForDownload *promptForDownloadError - if !errors.As(err, &errPromptForDownload) { - panicMsg := fmt.Sprintf("unexpected %T: %s", err, err) - panic(panicMsg) + var imageSize string + var shouldPullImageSecond bool + var getSizeErr error + + if image == nil { + imageSize = "n/a" + } else { + imageSize, getSizeErr = image.GetSizeHuman() + if getSizeErr != nil { + imageSize = "n/a" + } } - shouldPullImage = showPromptForDownloadSecond(imageFull, errPromptForDownload) - return shouldPullImage + shouldPullImageSecond = showPromptForDownloadSecond(imageFull, imageSize) + return shouldPullImageSecond, image, imageInspectErr } func startSpinner(message string) *spinner.Spinner { @@ -1042,17 +1090,3 @@ func systemdPathBusEscape(path string) string { } return string(n) } - -func (err *promptForDownloadError) Error() string { - innerErr := err.Unwrap() - errMsg := innerErr.Error() - return errMsg -} - -func (err *promptForDownloadError) Unwrap() error { - if err.ImageSize == "" { - return errors.New("failed to get image size") - } - - return context.Canceled -} diff --git a/src/cmd/utils.go b/src/cmd/utils.go index 9100a8378..c3762b48a 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -367,6 +367,14 @@ func createErrorProfileDNotFound() error { return errors.New(errMsg) } +func createErrorSkopeoNotFound(imageFull string, archID int) error { + archName := architecture.GetArchNameOCI(archID) + return fmt.Errorf( + "Cannot inspect image %s for architecture %s: skopeo is not installed.\n"+ + "Skopeo is required for creating non-native architecture containers.", + imageFull, archName) +} + func createErrorSudoersDNotFound() error { const sudoersD = "/etc/sudoers.d" From f8afb273a8799abbd3923a16d79bee2811a04d97 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 12:16:18 +0200 Subject: [PATCH 13/15] cmd/initContainer: Set up QEMU emulation for cross-arch containers Add the --arch and --arch-emulator-path flags to the init-container command, passed from the create command when creating a cross-architecture container. The --arch flag defaults to the host architecture ID so that existing native containers continue to work without changes. When the container's architecture differs from the host, the init-container entry point configures QEMU emulation inside the container before any foreign-architecture binaries can run: 1. Validate QEMU emulation by running the 'true' command, which fails with ENOEXEC if the host's binfmt_misc registration is not working (detected via RunWithExitCode2() added in [1]), because it is necessary to have host emulation working to emulate the binfmt_misc registration in the following step. 2. Mount a fresh binfmt_misc filesystem inside the container via MountBinfmtMisc() (added in [2]) to create a sandboxed binfmt_misc registration with the C flag. 3. Validate architecture support via IsArchSupportedOnInitialization() (added in [3]), which verifies the QEMU interpreter at the host-mounted path under /run/host. 4. Register the QEMU interpreter with the C flag via RegisterBinfmtMisc() (added and explained in [2]) The binfmt_misc registration is performed inside the container rather than relying on the host's registration, as explained in [2]. Update showEntryPointLog() in run.go to propagate lines prefixed with 'Warning:' to stderr on the host, instead of treating them as errors. This is needed because the cross-architecture initialization may emit warnings that should be visible to the user but are not fatal. [1] https://github.com/containers/toolbox/pull/1780 [2] https://github.com/containers/toolbox/pull/1782 [3] https://github.com/containers/toolbox/pull/1783 https://github.com/containers/toolbox/pull/1788 Signed-off-by: Dalibor Kricka --- src/cmd/create.go | 2 ++ src/cmd/initContainer.go | 60 ++++++++++++++++++++++++++++++++++++++++ src/cmd/run.go | 11 ++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index e1a8910cb..925b8384d 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -449,6 +449,8 @@ func createContainer(container, image, release, authFile string, archConfig arch entryPoint := []string{ "toolbox", "--log-level", "debug", "init-container", + "--arch", fmt.Sprintf("%d", archConfig.ID), + "--arch-emulator-path", archConfig.QemuEmulatorPath, "--gid", currentUser.Gid, "--home", currentUserHomeDir, "--shell", userShell, diff --git a/src/cmd/initContainer.go b/src/cmd/initContainer.go index b3a7bd983..74f3b7fbf 100644 --- a/src/cmd/initContainer.go +++ b/src/cmd/initContainer.go @@ -28,6 +28,7 @@ import ( "syscall" "time" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/utils" "github.com/fsnotify/fsnotify" @@ -41,6 +42,8 @@ import ( var ( initContainerFlags struct { + archID int + archInterp string gid int home string homeLink bool @@ -85,6 +88,16 @@ var initContainerCmd = &cobra.Command{ func init() { flags := initContainerCmd.Flags() + flags.IntVar(&initContainerFlags.archID, + "arch", + architecture.HostArchID, + "Specify the Toolbx container's architecture ID.") + + flags.StringVar(&initContainerFlags.archInterp, + "arch-emulator-path", + "", + "Register an emulator using binfmt_misc with PATH as the interpreter for a non-native architecture container.") + flags.IntVar(&initContainerFlags.gid, "gid", 0, @@ -257,6 +270,31 @@ func initContainer(cmd *cobra.Command, args []string) error { } } + if !architecture.HasContainerNativeArch(initContainerFlags.archID) { + archName := architecture.GetArchNameOCI(initContainerFlags.archID) + interpreterPath := "/run/host" + initContainerFlags.archInterp + + if err := validateCrossArchEmulation(initContainerFlags.archID); err != nil { + return err + } + + logrus.Debugf("Mounting binfmt_misc file system in container for architecture %s", archName) + if err := architecture.MountBinfmtMisc(); err != nil { + return err + } + + resolvedInterpreterPath, err := architecture.IsArchSupportedOnInitialization(initContainerFlags.archID, interpreterPath) + if err != nil { + errNotSupported := fmt.Errorf("Cannot run container for architecture %s:\n%s", archName, err) + return errNotSupported + } + + logrus.Debugf("Registering QEMU emulator for architecture %s in binfmt_misc", archName) + if err := architecture.RegisterBinfmtMisc(initContainerFlags.archID, resolvedInterpreterPath); err != nil { + return err + } + } + for _, mount := range initContainerMounts { if err := mountBind(mount.containerPath, mount.source, mount.flags); err != nil { return err @@ -1223,6 +1261,28 @@ func updateTimeZoneFromLocalTime() error { return nil } +func validateCrossArchEmulation(archID int) error { + archName := architecture.GetArchNameOCI(archID) + logrus.Debugf("Testing QEMU emulation for architecture %s", archName) + + _, err := shell.RunWithExitCode2("true", nil, nil, nil) + + if err != nil { + if errors.Is(err, syscall.ENOEXEC) { + return fmt.Errorf( + "QEMU emulation for architecture %s is not working\n"+ + "Please verify that:\n"+ + " 1. QEMU user-mode emulation is installed on the host system: qemu-user-static package\n"+ + " 2. binfmt_misc is properly configured on the host system", + archName) + } + return fmt.Errorf("failed to test QEMU emulation for architecture %s: %w", archName, err) + } + + logrus.Debugf("Test of QEMU emulation for architecture %s has succeeded", archName) + return nil +} + func writeTimeZone(timeZone string) error { const etcTimeZone = "/etc/timezone" diff --git a/src/cmd/run.go b/src/cmd/run.go index 4f915c63c..2e9d63dc2 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -964,8 +964,15 @@ func showEntryPointLog(line string) error { } if !logLevelFound { - errMsg, _ := strings.CutPrefix(line, "Error: ") - return &entryPointError{errMsg} + // Messages sent to stderr with a 'Warning:' prefix in the entry point + // are propagated to stderr on the host + if strings.HasPrefix(line, "Warning:") { + fmt.Fprintf(os.Stderr, "%s\n", line) + return nil + } else { + errMsg, _ := strings.CutPrefix(line, "Error: ") + return &entryPointError{errMsg} + } } logger := logrus.StandardLogger() From 90e5e6681fa93bd4354adea2bbd0b756996e69bc Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 22:04:39 +0200 Subject: [PATCH 14/15] cmd/enter: Add --arch flag for container architecture selection Add the --arch / -a flag to the enter command, allowing users to enter cross-architecture containers by specifying the target architecture (e.g., toolbox enter --arch arm64). Can be used with flags --distro and --release, just as for container creation. The flag value is resolved through resolveArchitectureID() (added in [1]) and passed to resolveContainerAndImageNames() (updated for cross-arch in [1]) so that it resolves to the architecture-suffixed container name. [1] https://github.com/containers/toolbox/pull/1786 https://github.com/containers/toolbox/pull/1789 Signed-off-by: Dalibor Kricka --- src/cmd/enter.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cmd/enter.go b/src/cmd/enter.go index 570803161..851868769 100644 --- a/src/cmd/enter.go +++ b/src/cmd/enter.go @@ -21,13 +21,13 @@ import ( "fmt" "os" - "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) var ( enterFlags struct { + arch string container string distro string release string @@ -44,6 +44,12 @@ var enterCmd = &cobra.Command{ func init() { flags := enterCmd.Flags() + flags.StringVarP(&enterFlags.arch, + "arch", + "a", + "", + "Enter a Toolbx container for a different architecture than the host") + flags.StringVarP(&enterFlags.container, "container", "c", @@ -105,12 +111,17 @@ func enter(cmd *cobra.Command, args []string) error { defaultContainer = false } + archID, err := resolveArchitectureID(enterFlags.arch, "") + if err != nil { + return err + } + container, image, release, err := resolveContainerAndImageNames(container, containerArg, enterFlags.distro, "", enterFlags.release, - architecture.HostArchID) + archID) if err != nil { return err From 2bc3d13979424d1b029d987bba9d2de6ec780274 Mon Sep 17 00:00:00 2001 From: Dalibor Kricka Date: Fri, 24 Apr 2026 22:19:15 +0200 Subject: [PATCH 15/15] cmd/run: Add --arch flag for container architecture selection Add the --arch / -a flag to the run command, allowing users to run commands inside cross-architecture containers by specifying the target architecture (e.g., toolbox run --arch arm64 uname -m). Can be used with flags --distro and --release, just as for container creation. The flag value is resolved through resolveArchitectureID() (added in [1]) and passed to resolveContainerAndImageNames() (updated for cross-arch in [1]) so that it resolves to the architecture-suffixed container name. [1] https://github.com/containers/toolbox/pull/1786 https://github.com/containers/toolbox/pull/1789 Signed-off-by: Dalibor Kricka --- src/cmd/run.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/cmd/run.go b/src/cmd/run.go index 2e9d63dc2..1e7f7f853 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -53,6 +53,7 @@ type entryPointError struct { var ( runFlags struct { + arch string container string distro string preserveFDs uint @@ -74,6 +75,12 @@ func init() { flags := runCmd.Flags() flags.SetInterspersed(false) + flags.StringVarP(&runFlags.arch, + "arch", + "a", + "", + "Run command inside a Toolbx container for a different architecture than the host") + flags.StringVarP(&runFlags.container, "container", "c", @@ -142,12 +149,17 @@ func run(cmd *cobra.Command, args []string) error { command := args + archID, err := resolveArchitectureID(runFlags.arch, "") + if err != nil { + return err + } + container, image, release, err := resolveContainerAndImageNames(runFlags.container, "--container", runFlags.distro, "", runFlags.release, - architecture.HostArchID) + archID) if err != nil { return err