From 3daa29e0e35dc4042ef112d51e1149d9155fd43a Mon Sep 17 00:00:00 2001 From: Mark Zvenigorodsky Date: Sat, 4 Apr 2026 15:48:57 +0300 Subject: [PATCH 1/3] common: abort load on ENOSPC and output clear error Signed-off-by: Mark Zvenigorodsky --- storage/pkg/chrootarchive/archive_unix.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/storage/pkg/chrootarchive/archive_unix.go b/storage/pkg/chrootarchive/archive_unix.go index bc649bbbe6..7fb70a4dbf 100644 --- a/storage/pkg/chrootarchive/archive_unix.go +++ b/storage/pkg/chrootarchive/archive_unix.go @@ -10,6 +10,7 @@ import ( "io" "io/fs" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -82,6 +83,10 @@ func untar() { } if err := archive.Unpack(os.Stdin, dst, &options); err != nil { + if errors.Is(err, unix.ENOSPC) { + fmt.Fprint(os.Stderr, err) + os.Exit(int(unix.ENOSPC)) + } fatal(err) } // fully consume stdin in case it is zero padded @@ -155,6 +160,20 @@ func invokeUnpack(decompressedArchive io.Reader, dest *unpackDestination, option w.Close() if err := cmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode := exitErr.ExitCode() + if exitCode == int(unix.ENOSPC) { + procPath := fmt.Sprintf("/proc/self/fd/%d", dest.root.Fd()) + + path, err := os.Readlink(procPath) + + if err == nil { + fmt.Fprintf(os.Stderr, "no space left on device: %s\n", path) + os.Exit(28) + } + } + } + errorOut := fmt.Errorf("unpacking failed (error: %w; output: %s)", err, output) // when `xz -d -c -q | storage-untar ...` failed on storage-untar side, // we need to exhaust `xz`'s output, otherwise the `xz` side will be From e0cf0e77853f19a5c9fddbd4cedcb8a16c49e0c7 Mon Sep 17 00:00:00 2001 From: Mark Zvenigorodsky Date: Wed, 8 Apr 2026 00:38:53 +0300 Subject: [PATCH 2/3] common: abort on matching ErrorDetails Signed-off-by: Mark Zvenigorodsky --- common/libimage/load.go | 8 ++++++ storage/pkg/chrootarchive/archive_unix.go | 32 ++++++++++++++--------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/common/libimage/load.go b/common/libimage/load.go index 598bf39cdc..ada9a282cd 100644 --- a/common/libimage/load.go +++ b/common/libimage/load.go @@ -16,6 +16,7 @@ import ( "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" + "go.podman.io/storage/pkg/chrootarchive" ) type LoadOptions struct { @@ -110,6 +111,13 @@ func (r *Runtime) Load(ctx context.Context, path string, options *LoadOptions) ( return loadedImages, err } logrus.Debugf("Error loading %s (%s): %v", path, transportName, err) + + var eDetail *chrootarchive.ErrorDetail + + if errors.As(err, &eDetail) { + return nil, fmt.Errorf("%s", eDetail.Message) + } + loadErrors = append(loadErrors, fmt.Errorf("%s: %v", transportName, err)) } diff --git a/storage/pkg/chrootarchive/archive_unix.go b/storage/pkg/chrootarchive/archive_unix.go index 7fb70a4dbf..2f228fe18b 100644 --- a/storage/pkg/chrootarchive/archive_unix.go +++ b/storage/pkg/chrootarchive/archive_unix.go @@ -10,7 +10,6 @@ import ( "io" "io/fs" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -26,6 +25,15 @@ type unpackDestination struct { dest string } +type ErrorDetail struct { + Message string `json:"message"` + Errno int `json:"errno"` +} + +func (e *ErrorDetail) Error() string { + return fmt.Sprintf("errno %d: %s", e.Errno, e.Message) +} + func (dst *unpackDestination) Close() error { return dst.root.Close() } @@ -84,8 +92,12 @@ func untar() { if err := archive.Unpack(os.Stdin, dst, &options); err != nil { if errors.Is(err, unix.ENOSPC) { - fmt.Fprint(os.Stderr, err) - os.Exit(int(unix.ENOSPC)) + enospcLog := ErrorDetail{ + Message: "no space left on device", + Errno: int(unix.ENOSPC), + } + json.NewEncoder(os.Stderr).Encode(enospcLog) + os.Exit(1) } fatal(err) } @@ -160,17 +172,11 @@ func invokeUnpack(decompressedArchive io.Reader, dest *unpackDestination, option w.Close() if err := cmd.Wait(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - exitCode := exitErr.ExitCode() - if exitCode == int(unix.ENOSPC) { - procPath := fmt.Sprintf("/proc/self/fd/%d", dest.root.Fd()) - - path, err := os.Readlink(procPath) + var eDetail ErrorDetail - if err == nil { - fmt.Fprintf(os.Stderr, "no space left on device: %s\n", path) - os.Exit(28) - } + if json.Unmarshal(output.Bytes(), &eDetail) == nil { + if eDetail.Errno == int(unix.ENOSPC) { + return fmt.Errorf("%w", &eDetail) } } From 9f5ba5c64a673f566deb330e2861a1dfe742dcf2 Mon Sep 17 00:00:00 2001 From: Mark Zvenigorodsky Date: Wed, 8 Apr 2026 23:33:39 +0300 Subject: [PATCH 3/3] common: removed errorDetails from chrootarchive Signed-off-by: Mark Zvenigorodsky --- common/libimage/load.go | 11 +++++----- storage/pkg/chrootarchive/archive_unix.go | 26 ++++++----------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/common/libimage/load.go b/common/libimage/load.go index ada9a282cd..cb36b8f709 100644 --- a/common/libimage/load.go +++ b/common/libimage/load.go @@ -16,7 +16,7 @@ import ( "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" - "go.podman.io/storage/pkg/chrootarchive" + "golang.org/x/sys/unix" ) type LoadOptions struct { @@ -112,12 +112,11 @@ func (r *Runtime) Load(ctx context.Context, path string, options *LoadOptions) ( } logrus.Debugf("Error loading %s (%s): %v", path, transportName, err) - var eDetail *chrootarchive.ErrorDetail - - if errors.As(err, &eDetail) { - return nil, fmt.Errorf("%s", eDetail.Message) + if errors.Is(err, unix.ENOSPC) { + // %.0w makes e visible to error.Unwrap() without including any text + return nil, fmt.Errorf("no space left on device%.0w", err) } - + loadErrors = append(loadErrors, fmt.Errorf("%s: %v", transportName, err)) } diff --git a/storage/pkg/chrootarchive/archive_unix.go b/storage/pkg/chrootarchive/archive_unix.go index 2f228fe18b..725a172347 100644 --- a/storage/pkg/chrootarchive/archive_unix.go +++ b/storage/pkg/chrootarchive/archive_unix.go @@ -10,6 +10,7 @@ import ( "io" "io/fs" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -25,15 +26,6 @@ type unpackDestination struct { dest string } -type ErrorDetail struct { - Message string `json:"message"` - Errno int `json:"errno"` -} - -func (e *ErrorDetail) Error() string { - return fmt.Sprintf("errno %d: %s", e.Errno, e.Message) -} - func (dst *unpackDestination) Close() error { return dst.root.Close() } @@ -92,12 +84,7 @@ func untar() { if err := archive.Unpack(os.Stdin, dst, &options); err != nil { if errors.Is(err, unix.ENOSPC) { - enospcLog := ErrorDetail{ - Message: "no space left on device", - Errno: int(unix.ENOSPC), - } - json.NewEncoder(os.Stderr).Encode(enospcLog) - os.Exit(1) + os.Exit(2) } fatal(err) } @@ -172,11 +159,10 @@ func invokeUnpack(decompressedArchive io.Reader, dest *unpackDestination, option w.Close() if err := cmd.Wait(); err != nil { - var eDetail ErrorDetail - - if json.Unmarshal(output.Bytes(), &eDetail) == nil { - if eDetail.Errno == int(unix.ENOSPC) { - return fmt.Errorf("%w", &eDetail) + if exitErr, ok := err.(*exec.ExitError); ok { + status := exitErr.ExitCode() + if status == 2 { + return unix.ENOSPC } }