From 0751259bde38f1e6ff25207eba37322219a90e5e Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 12 Aug 2026 16:52:09 +0200 Subject: [PATCH 1/2] vendor: update common to v0.69.1 and image to v5.41.1 Signed-off-by: Paul Holzinger --- go.mod | 4 +- go.sum | 8 +- .../go.podman.io/common/pkg/config/config.go | 21 +- .../common/pkg/config/containers.conf | 18 +- .../common/pkg/config/containers.conf-freebsd | 18 +- vendor/go.podman.io/common/version/version.go | 2 +- .../image/v5/docker/docker_image_src.go | 428 +++++++++++++++--- .../go.podman.io/image/v5/version/version.go | 2 +- vendor/modules.txt | 4 +- 9 files changed, 426 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 198427a6fa..8dc017dbc3 100644 --- a/go.mod +++ b/go.mod @@ -65,8 +65,8 @@ require ( github.com/vishvananda/netlink v1.3.1 go.etcd.io/bbolt v1.5.0 go.podman.io/buildah v1.45.0 - go.podman.io/common v0.69.0 - go.podman.io/image/v5 v5.41.0 + go.podman.io/common v0.69.1 + go.podman.io/image/v5 v5.41.1 go.podman.io/storage v1.64.0 golang.org/x/crypto v0.54.0 golang.org/x/net v0.57.0 diff --git a/go.sum b/go.sum index 79ff921e1c..a09d24032a 100644 --- a/go.sum +++ b/go.sum @@ -435,10 +435,10 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.podman.io/buildah v1.45.0 h1:/LAS2Sgf+xmRoLVo5j52HjEPL+g5HKjomnCvCk42iAU= go.podman.io/buildah v1.45.0/go.mod h1:cTVrqG7Hu24euHntdC31/sU2nvqPoLmHmpkz+JIl2Lw= -go.podman.io/common v0.69.0 h1:tAM8gJRCgoPmyfXnYP+6VRfOQ6JySgafpY28OhDZpWc= -go.podman.io/common v0.69.0/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg= -go.podman.io/image/v5 v5.41.0 h1:xOan4jlwbT4R5qhe3ruDAGJLnOC2z0eR61WhZjHbe4E= -go.podman.io/image/v5 v5.41.0/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk= +go.podman.io/common v0.69.1 h1:hTIv8wzHJfHAMMZRocjv1849TiWrf5HDhH0eD74qJqY= +go.podman.io/common v0.69.1/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg= +go.podman.io/image/v5 v5.41.1 h1:iPrhIt7/aNfRBMuzoxbMUVpQmR7Q75ahnrzkIC4JWLQ= +go.podman.io/image/v5 v5.41.1/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk= go.podman.io/storage v1.64.0 h1:ryHCZO+Zl0ZG7OTHF+Qc4C0zLNuoe9W35zgE7mh2pYw= go.podman.io/storage v1.64.0/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= diff --git a/vendor/go.podman.io/common/pkg/config/config.go b/vendor/go.podman.io/common/pkg/config/config.go index 22d161e27f..8e6a415259 100644 --- a/vendor/go.podman.io/common/pkg/config/config.go +++ b/vendor/go.podman.io/common/pkg/config/config.go @@ -300,9 +300,12 @@ type EngineConfig struct { // host when they are forwarded to containers. When enabled, when ports are // forwarded to containers, they are held open by conmon as long as the // container is running, ensuring that they cannot be reused by other - // programs on the host. However, this can cause significant memory usage if - // a container has many ports forwarded to it. Disabling this can save - // memory. + // programs on the host. However, this can cause increased memory usage + // if a container has many ports forwarded to it. Disabling this can save + // memory but is not recommended as port conflicts are not noticed. + // Note this option is only used for rootful container when ports are + // forwarded via firewall rules. Rootless containers using pasta(1) + // always have to bind each port. EnablePortReservation bool `toml:"enable_port_reservation,omitempty"` // Environment variables to be used when running the container engine (e.g., Podman, Buildah). For example "http_proxy=internal.proxy.company.com" @@ -323,6 +326,18 @@ type EngineConfig struct { // information about the container. EventsContainerCreateInspectData bool `toml:"events_container_create_inspect_data,omitempty"` + // ForcePortListen forces the port reservation to use listen(). + // For rootful containers when reserving the ports Podman will not use listen() + // on TCP ports by default as connections are forwarded via firewall rules. + // This is done to ensure if the firewall rule is bypassed, i.e. a connection + // to "::1", the connection will be correctly refused and not hang forever as we + // never accept() them. + // For applications which only monitor the listening state of a socket it means they + // will not see the bound port then. In that case this option can be set to true in + // order to force Podman to also call listen() on the ports. + // Also see the **enable_port_reservation** option. + ForcePortListen bool `toml:"force_port_listen,omitempty"` + // graphRoot internal stores the location of the graphroot graphRoot string diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf b/vendor/go.podman.io/common/pkg/config/containers.conf index 9c998dde09..c07f1afce6 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf +++ b/vendor/go.podman.io/common/pkg/config/containers.conf @@ -529,8 +529,10 @@ default_sysctls = [ # forwarded to containers. When enabled, when ports are forwarded to containers, # ports are held open by as long as the container is running, ensuring that # they cannot be reused by other programs on the host. However, this can cause -# significant memory usage if a container has many ports forwarded to it. -# Disabling this can save memory. +# increased memory usage if a container has many ports forwarded to it. +# Disabling this can save memory but is not recommended as port conflicts are not noticed. +# Note this option is only used for rootful container when ports are forwarded via firewall rules. +# Rootless containers using pasta(1) always have to bind each port. # #enable_port_reservation = true @@ -566,6 +568,18 @@ default_sysctls = [ # with detailed information about the container. #events_container_create_inspect_data = false +# For rootful containers when reserving the ports Podman will not use listen() +# on TCP ports by default as connections are forwarded via firewall rules. +# This is done to ensure if the firewall rule is bypassed, i.e. a connection +# to "::1", the connection will be correctly refused and not hang forever as we +# never accept() them. +# For applications which only monitor the listening state of a socket it means they +# will not see the bound port then. In that case this option can be set to true in +# order to force Podman to also call listen() on the ports. +# Also see the enable_port_reservation option. +# +#force_port_listen=false + # Whenever Podman should log healthcheck events. # With many running healthcheck on short interval Podman will spam the event # log a lot as it generates a event for each single healthcheck run. Because diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd index 2f7713bd0c..9c0d4e4ba6 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd +++ b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd @@ -372,8 +372,10 @@ default_sysctls = [ # forwarded to containers. When enabled, when ports are forwarded to containers, # ports are held open by as long as the container is running, ensuring that # they cannot be reused by other programs on the host. However, this can cause -# significant memory usage if a container has many ports forwarded to it. -# Disabling this can save memory. +# increased memory usage if a container has many ports forwarded to it. +# Disabling this can save memory but is not recommended as port conflicts are not noticed. +# Note this option is only used for rootful container when ports are forwarded via firewall rules. +# Rootless containers using pasta(1) always have to bind each port. # #enable_port_reservation = true @@ -405,6 +407,18 @@ default_sysctls = [ # #events_logger = "file" +# For rootful containers when reserving the ports Podman will not use listen() +# on TCP ports by default as connections are forwarded via firewall rules. +# This is done to ensure if the firewall rule is bypassed, i.e. a connection +# to "::1", the connection will be correctly refused and not hang forever as we +# never accept() them. +# For applications which only monitor the listening state of a socket it means they +# will not see the bound port then. In that case this option can be set to true in +# order to force Podman to also call listen() on the ports. +# Also see the enable_port_reservation option. +# +#force_port_listen=false + # Whenever Podman should log healthcheck events. # With many running healthcheck on short interval Podman will spam the event # log a lot as it generates a event for each single healthcheck run. Because diff --git a/vendor/go.podman.io/common/version/version.go b/vendor/go.podman.io/common/version/version.go index 50d3d10664..653f0bde12 100644 --- a/vendor/go.podman.io/common/version/version.go +++ b/vendor/go.podman.io/common/version/version.go @@ -1,4 +1,4 @@ package version // Version is the version of the build. -const Version = "0.69.0" +const Version = "0.69.1" diff --git a/vendor/go.podman.io/image/v5/docker/docker_image_src.go b/vendor/go.podman.io/image/v5/docker/docker_image_src.go index e93d338d28..4a4a029198 100644 --- a/vendor/go.podman.io/image/v5/docker/docker_image_src.go +++ b/vendor/go.podman.io/image/v5/docker/docker_image_src.go @@ -8,15 +8,20 @@ import ( "fmt" "io" "math" + "math/rand/v2" "mime" "mime/multipart" + "net" "net/http" "net/url" "os" "os/exec" "strings" "sync" + "time" + "github.com/docker/distribution/registry/api/errcode" + v2 "github.com/docker/distribution/registry/api/v2" digest "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" @@ -48,6 +53,31 @@ type dockerImageSource struct { // State cachedManifest []byte // nil if not loaded yet cachedManifestMIMEType string // Only valid if cachedManifest != nil + + // Mirror fallback: when a blob fetch fails with a fallback-worthy error, + // try remaining pull sources before giving up. Protected by mirrorMu. + mirrorMu sync.Mutex + mirrorOverride *mirrorSource // If non-nil, this is the mirror and physicalRef for the override client + prevOverrides []*dockerClient // mirror clients replaced by a newer mirrorOverride; kept open because other goroutines may still be reading blobs through them, closed in Close() + remainingSources []sysregistriesv2.PullSource // later pull sources not yet tried, consumed front-to-back during fallback; nil when selected source is already the original source. + fallbackSys *types.SystemContext + fallbackRegConfig *registryConfiguration +} + +type mirrorSource struct { + client *dockerClient + ref dockerReference +} + +type pullEndpoint struct { + client *dockerClient + ref dockerReference + endpointSys *types.SystemContext +} + +type sourceAttempt struct { + ref reference.Named + err error } // newImageSource creates a new ImageSource for the specified image reference. @@ -86,12 +116,8 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef if err != nil { return nil, err } - type attempt struct { - ref reference.Named - err error - } - attempts := []attempt{} - for _, pullSource := range pullSources { + attempts := []sourceAttempt{} + for i, pullSource := range pullSources { if sys != nil && sys.DockerLogMirrorChoice { logrus.Infof("Trying to access %q", pullSource.Reference) } else { @@ -99,10 +125,15 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef } s, err := newImageSourceAttempt(ctx, sys, ref, pullSource, registryConfig) if err == nil { + if i+1 < len(pullSources) { + s.remainingSources = pullSources[i+1:] + s.fallbackSys = sys + s.fallbackRegConfig = registryConfig + } return s, nil } logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err) - attempts = append(attempts, attempt{ + attempts = append(attempts, sourceAttempt{ ref: pullSource.Reference, err: err, }) @@ -131,6 +162,63 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource, registryConfig *registryConfiguration, ) (*dockerImageSource, error) { + endpoint, err := newPullClient(sys, logicalRef, pullSource, registryConfig) + if err != nil { + return nil, err + } + + s := &dockerImageSource{ + PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ + HasThreadSafeGetBlob: true, + }), + + logicalRef: logicalRef, + physicalRef: endpoint.ref, + c: endpoint.client, + } + s.Compat = impl.AddCompat(s) + + if err := s.ensureManifestIsLoaded(ctx); err != nil { + endpoint.client.Close() + return nil, err + } + + if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpoint.endpointSys); err == nil && h != "" { + acf := map[string]struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + IdentityToken string `json:"identityToken,omitempty"` + }{ + endpoint.ref.ref.String(): { + Username: endpoint.client.auth.Username, + Password: endpoint.client.auth.Password, + IdentityToken: endpoint.client.auth.IdentityToken, + }, + } + acfD, err := json.Marshal(acf) + if err != nil { + logrus.Warnf("failed to marshal auth config: %v", err) + } else { + cmd := exec.Command(h) + cmd.Stdin = bytes.NewReader(acfD) + if err := cmd.Run(); err != nil { + var stderr string + if ee, ok := err.(*exec.ExitError); ok { + stderr = string(ee.Stderr) + } + logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err) + } + } + } + return s, nil +} + +// newPullClient creates a dockerClient, reference, and endpoint-specific SystemContext +// for the given pull source. +// The caller must call client.Close() when done. +func newPullClient(sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource, + registryConfig *registryConfiguration, +) (*pullEndpoint, error) { physicalRef, err := newReference(pullSource.Reference, false) if err != nil { return nil, err @@ -152,50 +240,7 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure client.namespaceProxy = pullSource.Endpoint.NamespaceProxy - s := &dockerImageSource{ - PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ - HasThreadSafeGetBlob: true, - }), - - logicalRef: logicalRef, - physicalRef: physicalRef, - c: client, - } - s.Compat = impl.AddCompat(s) - - if err := s.ensureManifestIsLoaded(ctx); err != nil { - client.Close() - return nil, err - } - - if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpointSys); err == nil && h != "" { - acf := map[string]struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - IdentityToken string `json:"identityToken,omitempty"` - }{ - physicalRef.ref.String(): { - Username: client.auth.Username, - Password: client.auth.Password, - IdentityToken: client.auth.IdentityToken, - }, - } - acfD, err := json.Marshal(acf) - if err != nil { - logrus.Warnf("failed to marshal auth config: %v", err) - } else { - cmd := exec.Command(h) - cmd.Stdin = bytes.NewReader(acfD) - if err := cmd.Run(); err != nil { - var stderr string - if ee, ok := err.(*exec.ExitError); ok { - stderr = string(ee.Stderr) - } - logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err) - } - } - } - return s, nil + return &pullEndpoint{client: client, ref: physicalRef, endpointSys: endpointSys}, nil } // Reference returns the reference used to set up this source, _as specified by the user_ @@ -206,6 +251,18 @@ func (s *dockerImageSource) Reference() types.ImageReference { // Close removes resources associated with an initialized ImageSource, if any. func (s *dockerImageSource) Close() error { + s.mirrorMu.Lock() + prev := s.prevOverrides + override := s.mirrorOverride + s.prevOverrides = nil + s.mirrorOverride = nil + s.mirrorMu.Unlock() + for _, m := range prev { + m.Close() + } + if override != nil { + override.client.Close() + } return s.c.Close() } @@ -416,9 +473,88 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo, if err := info.Digest.Validate(); err != nil { // Make sure info.Digest.String() does not contain any unexpected characters return nil, nil, err } - path := fmt.Sprintf(blobsPath, reference.Path(s.physicalRef.ref), info.Digest.String()) + + client, physRef, hasRemaining := s.getActiveSource() + streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, hasRemaining) + if err == nil || !hasRemaining { + return streams, errs, err + } + if isMirrorTransientError(err) || isMirrorFallbackError(err) { + logrus.Debugf("Partial blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err) + return s.getBlobAtWithMirrorFallback(ctx, info, chunks, headers, err, client) + } + return streams, errs, err +} + +// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). +// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided. +// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location. +func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) { + client, physRef, hasRemaining := s.getActiveSource() + reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, hasRemaining) + if err == nil || !hasRemaining { + return reader, size, err + } + if isMirrorTransientError(err) || isMirrorFallbackError(err) { + logrus.Debugf("Blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err) + return s.getBlobWithMirrorFallback(ctx, info, cache, err, client) + } + return reader, size, err +} + +func (s *dockerImageSource) getActiveSource() (*dockerClient, dockerReference, bool) { + s.mirrorMu.Lock() + defer s.mirrorMu.Unlock() + hasRemaining := len(s.remainingSources) > 0 + if s.mirrorOverride != nil { + return s.mirrorOverride.client, s.mirrorOverride.ref, hasRemaining + } + return s.c, s.physicalRef, hasRemaining +} + +// tryGetBlob attempts to fetch a blob, retrying once on transient errors if fallback mirrors remain. +func tryGetBlob(ctx context.Context, client *dockerClient, physRef dockerReference, + info types.BlobInfo, cache types.BlobInfoCache, hasRemainingSources bool, +) (io.ReadCloser, int64, error) { + reader, size, err := client.getBlob(ctx, physRef, info, cache) + if err != nil && hasRemainingSources && isMirrorTransientError(err) { + logrus.Debugf("Transient error fetching blob %s from %q, retrying: %v", info.Digest, physRef.ref, err) + delay := time.Second + rand.N(time.Second/10) // same as retry.IfNecessary first attempt: 1s + 10% jitter + select { + case <-ctx.Done(): + return nil, 0, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err) + case <-time.After(delay): + } + reader, size, err = client.getBlob(ctx, physRef, info, cache) + } + return reader, size, err +} + +// tryGetBlobAt attempts to fetch a partial blob (range request), retrying once on transient errors +// if fallback mirrors remain. Mirrors tryGetBlob for the partial-pull (zstd:chunked) path. +func tryGetBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference, + info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, hasRemainingSources bool, +) (chan io.ReadCloser, chan error, error) { + streams, errs, err := fetchBlobAt(ctx, client, physRef, info, chunks, headers) + if err != nil && hasRemainingSources && isMirrorTransientError(err) { + logrus.Debugf("Transient error fetching partial blob %s from %q, retrying: %v", info.Digest, physRef.ref, err) + delay := time.Second + rand.N(time.Second/10) + select { + case <-ctx.Done(): + return nil, nil, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err) + case <-time.After(delay): + } + streams, errs, err = fetchBlobAt(ctx, client, physRef, info, chunks, headers) + } + return streams, errs, err +} + +func fetchBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference, + info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, +) (chan io.ReadCloser, chan error, error) { + path := fmt.Sprintf(blobsPath, reference.Path(physRef.ref), info.Digest.String()) logrus.Debugf("Downloading %s", path) - res, err := s.c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil) + res, err := client.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil) if err != nil { return nil, nil, err } @@ -436,27 +572,195 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo, if err != nil { return nil, nil, err } - streams := make(chan io.ReadCloser) errs := make(chan error) - go handle206Response(streams, errs, res.Body, chunks, mediaType, params) return streams, errs, nil case http.StatusBadRequest: res.Body.Close() return nil, nil, private.BadPartialRequestError{Status: res.Status} default: - err := registryHTTPResponseToError(res) + httpErr := registryHTTPResponseToError(res) res.Body.Close() - return nil, nil, fmt.Errorf("fetching partial blob: %w", err) + return nil, nil, fmt.Errorf("fetching partial blob: %w", httpErr) } } -// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). -// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided. -// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location. -func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) { - return s.c.getBlob(ctx, s.physicalRef, info, cache) +// isMirrorFallbackError returns true for errors where the blob is not present +// on this source but may be available from another — warranting a fallback attempt. +// All tested registries (docker.io, registry.redhat.io, quay.io, registry.access.redhat.com) +// return BLOB_UNKNOWN for blob 404s, matching the OCI distribution spec. +func isMirrorFallbackError(err error) bool { + var ec errcode.ErrorCoder + if errors.As(err, &ec) { + switch ec.ErrorCode() { + case v2.ErrorCodeBlobUnknown: + // Blob endpoint returns HTTP 404 with errcode JSON body + // {"errors":[{"code":"BLOB_UNKNOWN",...}]}. + // registryHTTPResponseToError calls handleErrorResponse → parseHTTPErrorResponse, + // then unwraps errcode.Errors to the first errcode.Error. + return true + case errcode.ErrorCodeTooManyRequests: + // makeRequestToResolvedURL already retried 5 times with exponential + // backoff. This source's rate limit is exhausted — skip straight to + // the next mirror which has its own rate limit budget. + return true + } + } + + // UnexpectedHTTPStatusError (capital U) is NOT matched here — for regular blobs, + // handleErrorResponse never produces it for 4xx (only for 5xx). For external blobs, + // getExternalBlob() produces it via newUnexpectedHTTPStatusError() directly for any + // non-200 including 404, but external URLs are fixed and mirror-independent — + // a different registry mirror cannot serve them. + return false +} + +// isMirrorTransientError returns true for errors that are transient; the source +// probably has the blob but temporarily cannot serve it. +func isMirrorTransientError(err error) bool { + // HTTP 5xx: handleErrorResponse returns UnexpectedHTTPStatusError for status + // codes outside 400–499. Server-side error, another mirror may succeed. + var httpErr UnexpectedHTTPStatusError + if errors.As(err, &httpErr) && httpErr.StatusCode >= 500 { + return true + } + + // Network timeout: makeRequest returns net.Error with Timeout() == true. + // The mirror is reachable but slow — worth retrying on another. + var netErr net.Error + return errors.As(err, &netErr) && netErr.Timeout() +} + +// retireStaleOverride checks if the current mirrorOverride was set by another +// goroutine and differs from failedClient. If so, it returns the override's +// client and ref for the caller to retry. If the override is the same client +// that already failed, or is nil, it returns nil. Must be called with mirrorMu held. +func (s *dockerImageSource) retireStaleOverride(failedClient *dockerClient) (*dockerClient, dockerReference, bool) { + if s.mirrorOverride != nil && s.mirrorOverride.client != failedClient { + return s.mirrorOverride.client, s.mirrorOverride.ref, true + } + return nil, dockerReference{}, false +} + +// retireCurrentOverride moves the current mirrorOverride to prevOverrides. +// Must be called with mirrorMu held. +func (s *dockerImageSource) retireCurrentOverride() { + if s.mirrorOverride != nil { + s.prevOverrides = append(s.prevOverrides, s.mirrorOverride.client) + s.mirrorOverride = nil + } +} + +func formatFallbackError(attempts []sourceAttempt, originalErr error) error { + if len(attempts) == 0 { + return originalErr + } + extras := make([]string, 0, len(attempts)) + for _, a := range attempts { + extras = append(extras, fmt.Sprintf("[%s: %v]", a.ref.String(), a.err)) + } + return fmt.Errorf("(Fallback sources also failed: %s): %w", strings.Join(extras, "\n"), originalErr) +} + +func (s *dockerImageSource) getBlobWithMirrorFallback(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache, originalErr error, failedClient *dockerClient) (io.ReadCloser, int64, error) { + // Held for the full fallback loop, including network I/O. This serializes + // concurrent blob fetchers during fallback — only one goroutine probes + // sources while the rest block on getActiveSource(). Acceptable trade-off: + // fallback is rare and serialization prevents thundering-herd probing. + s.mirrorMu.Lock() + defer s.mirrorMu.Unlock() + + if client, physRef, ok := s.retireStaleOverride(failedClient); ok { + reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, len(s.remainingSources) > 0) + if err == nil { + return reader, size, nil + } + s.retireCurrentOverride() + } + + attempts := []sourceAttempt{} + for len(s.remainingSources) > 0 { + pullSource := s.remainingSources[0] + s.remainingSources = s.remainingSources[1:] + + logrus.Debugf("Trying to access %q", pullSource.Reference) + + fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig) + if clientErr != nil { + logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr) + attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr}) + continue + } + + reader, size, err := tryGetBlob(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, cache, len(s.remainingSources) > 0) + if err != nil { + fallbackEndpoint.client.Close() + logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err) + attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err}) + if !isMirrorTransientError(err) && !isMirrorFallbackError(err) { + // non-transient errors are unlikely to be resolved by trying another source: stop probing further. + // break here to log the attempts and return originalErr below. + break + } + continue + } + + s.retireCurrentOverride() + s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref} + logrus.Debugf("Blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference) + + return reader, size, nil + } + return nil, 0, formatFallbackError(attempts, originalErr) +} + +func (s *dockerImageSource) getBlobAtWithMirrorFallback(ctx context.Context, info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, originalErr error, failedClient *dockerClient) (chan io.ReadCloser, chan error, error) { + s.mirrorMu.Lock() + defer s.mirrorMu.Unlock() + + if client, physRef, ok := s.retireStaleOverride(failedClient); ok { + streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, len(s.remainingSources) > 0) + if err == nil { + return streams, errs, nil + } + s.retireCurrentOverride() + } + + attempts := []sourceAttempt{} + for len(s.remainingSources) > 0 { + pullSource := s.remainingSources[0] + s.remainingSources = s.remainingSources[1:] + + logrus.Debugf("Trying to access %q", pullSource.Reference) + + fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig) + if clientErr != nil { + logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr) + attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr}) + continue + } + + streams, errs, err := tryGetBlobAt(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, chunks, headers, len(s.remainingSources) > 0) + if err != nil { + fallbackEndpoint.client.Close() + logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err) + attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err}) + if !isMirrorTransientError(err) && !isMirrorFallbackError(err) { + // non-transient errors are unlikely to be resolved by trying another source: stop probing further. + // break here to log the attempts and return originalErr below. + break + } + continue + } + + s.retireCurrentOverride() + s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref} + logrus.Debugf("Partial blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference) + + return streams, errs, nil + } + return nil, nil, formatFallbackError(attempts, originalErr) } // GetSignaturesWithFormat returns the image's signatures. It may use a remote (= slow) service. diff --git a/vendor/go.podman.io/image/v5/version/version.go b/vendor/go.podman.io/image/v5/version/version.go index adb2667d4f..40852fed62 100644 --- a/vendor/go.podman.io/image/v5/version/version.go +++ b/vendor/go.podman.io/image/v5/version/version.go @@ -8,7 +8,7 @@ const ( // VersionMinor is for functionality in a backwards-compatible manner VersionMinor = 41 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 0 + VersionPatch = 1 // VersionDev indicates development branch. Releases will be empty string. VersionDev = "" diff --git a/vendor/modules.txt b/vendor/modules.txt index 230a004cc2..3119db6f75 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -777,7 +777,7 @@ go.podman.io/buildah/pkg/sshagent go.podman.io/buildah/pkg/util go.podman.io/buildah/pkg/volumes go.podman.io/buildah/util -# go.podman.io/common v0.69.0 +# go.podman.io/common v0.69.1 ## explicit; go 1.25.7 go.podman.io/common/internal go.podman.io/common/libimage @@ -843,7 +843,7 @@ go.podman.io/common/pkg/umask go.podman.io/common/pkg/util go.podman.io/common/pkg/version go.podman.io/common/version -# go.podman.io/image/v5 v5.41.0 +# go.podman.io/image/v5 v5.41.1 ## explicit; go 1.25.7 go.podman.io/image/v5/copy go.podman.io/image/v5/directory From 6fee8ae5f33922984115d3eadcf1691e53529ae4 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Tue, 11 Aug 2026 17:36:01 +0200 Subject: [PATCH 2/2] libpod: implement force_port_listen containers.conf option While we do not want to use listen() by default for bound ports it seems it is needed in some special cases such as WSL, see https://github.com/podman-container-tools/podman/issues/29377 Signed-off-by: Paul Holzinger (cherry picked from commit cc762db629a919f22623c952066b55f405ea6000) Signed-off-by: Paul Holzinger --- libpod/networking_common.go | 2 +- libpod/oci_util.go | 15 +++++++++++---- test/system/500-networking.bats | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/libpod/networking_common.go b/libpod/networking_common.go index 0da0a84413..d51b7b82b5 100644 --- a/libpod/networking_common.go +++ b/libpod/networking_common.go @@ -28,7 +28,7 @@ func (c *Container) bindPorts() ([]*os.File, error) { if !c.runtime.config.Engine.EnablePortReservation || rootless.IsRootless() || !c.config.NetMode.IsBridge() { return nil, nil } - return bindPorts(c.convertPortMappings()) + return bindPorts(c.convertPortMappings(), c.runtime.config.Engine.ForcePortListen) } // convertPortMappings will remove the HostIP part from the ports when running inside podman machine. diff --git a/libpod/oci_util.go b/libpod/oci_util.go index bc316b5a74..8129c365bb 100644 --- a/libpod/oci_util.go +++ b/libpod/oci_util.go @@ -31,7 +31,7 @@ type ociError struct { } // Bind ports to keep them closed on the host -func bindPorts(ports []types.PortMapping) ([]*os.File, error) { +func bindPorts(ports []types.PortMapping, forceListen bool) ([]*os.File, error) { var files []*os.File sctpWarning := true for _, port := range ports { @@ -42,7 +42,7 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) { protocols := strings.SplitSeq(port.Protocol, ",") for protocol := range protocols { for i := uint16(0); i < port.Range; i++ { - f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning) + f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning, forceListen) if err != nil { // close all open ports in case of early error so we do not // rely on the garbage collector to close them @@ -60,9 +60,9 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) { return files, nil } -// bindPort reserves a port on the host using socket+bind without listen. +// bindPort reserves a port on the host using socket+bind, listen() only when listen is set to true // Dual-stack bind by default unless hostIP is specified. -func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) { +func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool, forceListen bool) (*os.File, error) { switch protocol { case "tcp", "udp": sockType := unix.SOCK_STREAM @@ -94,6 +94,13 @@ func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool return nil, fmt.Errorf("cannot bind %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err) } + if forceListen && sockType == unix.SOCK_STREAM { + if err := unix.Listen(fd, 0); err != nil { + unix.Close(fd) + return nil, fmt.Errorf("cannot listen on %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err) + } + } + return os.NewFile(uintptr(fd), fmt.Sprintf("reservation-%s-%d", protocol, port)), nil case "sctp": diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 6652fdeaf9..abe7abb0cb 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -1257,4 +1257,25 @@ EOF run_podman rm -f -t0 $cid1 } +# bats test_tags=ci:parallel +@test "podman run - test force_port_listen containers.conf option" { + skip_if_rootless "force_port_listen is only used as root" + skip_if_remote "force_port_listen would need to be set on the server side" + myport=$(random_free_port) + cname="c1-$(safename)" + + containersconf=$PODMAN_TMPDIR/containers.conf + cat >$containersconf <