From 89ddae8b1216f01b3c686edddd3a0e37deef4898 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 01:50:27 +0000 Subject: [PATCH] Update common, image, and storage deps to 94e31d2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 10 +- go.sum | 24 +- vendor/github.com/moby/moby/client/README.md | 17 +- vendor/github.com/moby/moby/client/client.go | 82 ++-- .../client/{options.go => client_options.go} | 101 +++-- vendor/github.com/moby/moby/client/envvars.go | 2 +- vendor/github.com/moby/moby/client/ping.go | 33 +- vendor/github.com/moby/moby/client/request.go | 84 ++-- .../moby/moby/client/service_create.go | 56 +-- .../moby/moby/client/service_update.go | 10 +- vendor/github.com/moby/moby/client/utils.go | 42 ++ .../opencontainers/runc/internal/linux/doc.go | 3 + .../runc/internal/linux/eintr.go | 28 ++ .../runc/internal/linux/linux.go | 126 ++++++ .../runc/internal/pathrs/mkdirall.go | 51 +++ .../internal/pathrs/mkdirall_pathrslite.go | 30 +- .../runc/internal/pathrs/path.go | 82 ++++ .../runc/internal/pathrs/root_pathrslite.go | 13 +- .../runc/libcontainer/apparmor/apparmor.go | 22 +- .../libcontainer/apparmor/apparmor_linux.go | 10 +- .../runc/libcontainer/utils/cmsg.go | 135 ------ .../runc/libcontainer/utils/utils.go | 115 ----- .../runc/libcontainer/utils/utils_unix.go | 277 ------------ .../image/v5/directory/directory_dest.go | 31 +- .../image/v5/directory/directory_src.go | 21 +- .../image/v5/directory/directory_transport.go | 17 +- .../image/v5/directory/version.go | 62 +++ .../image/v5/docker/daemon/client.go | 1 - vendor/go.podman.io/storage/drivers/driver.go | 16 +- vendor/go.podman.io/storage/drivers/fsdiff.go | 2 +- .../storage/drivers/overlay/overlay.go | 181 +++++--- .../go.podman.io/storage/drivers/template.go | 52 --- .../storage/drivers/vfs/driver.go | 4 +- .../storage/internal/tempdir/tempdir.go | 38 ++ vendor/go.podman.io/storage/layers.go | 408 ++++++++++++++---- .../storage/pkg/lockfile/lockfile.go | 5 +- vendor/go.podman.io/storage/store.go | 175 ++++++-- vendor/go.podman.io/storage/userns.go | 2 +- vendor/modules.txt | 14 +- 39 files changed, 1384 insertions(+), 998 deletions(-) rename vendor/github.com/moby/moby/client/{options.go => client_options.go} (75%) create mode 100644 vendor/github.com/opencontainers/runc/internal/linux/doc.go create mode 100644 vendor/github.com/opencontainers/runc/internal/linux/eintr.go create mode 100644 vendor/github.com/opencontainers/runc/internal/linux/linux.go create mode 100644 vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall.go delete mode 100644 vendor/github.com/opencontainers/runc/libcontainer/utils/cmsg.go delete mode 100644 vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go delete mode 100644 vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go create mode 100644 vendor/go.podman.io/image/v5/directory/version.go delete mode 100644 vendor/go.podman.io/storage/drivers/template.go diff --git a/go.mod b/go.mod index dcf95397d0..e13dd7a311 100644 --- a/go.mod +++ b/go.mod @@ -64,9 +64,9 @@ require ( github.com/stretchr/testify v1.11.1 github.com/vbauerster/mpb/v8 v8.11.2 github.com/vishvananda/netlink v1.3.1 - go.podman.io/common v0.66.1-0.20251126122123-4fc82df3fdc0 - go.podman.io/image/v5 v5.38.1-0.20251126122123-4fc82df3fdc0 - go.podman.io/storage v1.61.1-0.20251125064110-c4e25180a61d + go.podman.io/common v0.66.1-0.20251128185259-94e31d2e45ba + go.podman.io/image/v5 v5.38.1-0.20251128185259-94e31d2e45ba + go.podman.io/storage v1.61.1-0.20251128185259-94e31d2e45ba golang.org/x/crypto v0.45.0 golang.org/x/net v0.47.0 golang.org/x/sync v0.18.0 @@ -141,7 +141,7 @@ require ( github.com/moby/buildkit v0.25.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/moby/api v1.52.0 // indirect - github.com/moby/moby/client v0.1.0 // indirect + github.com/moby/moby/client v0.2.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect @@ -149,7 +149,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/runc v1.3.3 // indirect + github.com/opencontainers/runc v1.4.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.10 // indirect diff --git a/go.sum b/go.sum index 18f7564abf..c0871b98b3 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWh github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.0.4+incompatible h1:mffN/hPqaI39vx/4QiSkdldHeM0rP1ZZBIXRUOPI5+I= -github.com/docker/cli v29.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.1+incompatible h1:gGQk5qx62yPKRm3bUdKBzmDBSQzp17hlSLbV1F7jjys= +github.com/docker/cli v29.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= @@ -265,8 +265,8 @@ github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8= -github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= +github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k= +github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -303,8 +303,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lkFQr4= -github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI= +github.com/opencontainers/runc v1.4.0 h1:FG1Hw0GBYPsNki+mBz1QOrSzbwbAcerhrAD2r097QCc= +github.com/opencontainers/runc v1.4.0/go.mod h1:sch3Bh3c1NlyAkALoAUz5Br9ubMLZzFcxuovZbnkErk= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= @@ -448,12 +448,12 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -go.podman.io/common v0.66.1-0.20251126122123-4fc82df3fdc0 h1:thgaDhbFqO3H7FfZl7DNJwuR1QbquZf5jQWtnct6FOg= -go.podman.io/common v0.66.1-0.20251126122123-4fc82df3fdc0/go.mod h1:Hkrtv0G+eJBvJxXlDBB5O10fIhHuq7Jq4MGJYDkHApw= -go.podman.io/image/v5 v5.38.1-0.20251126122123-4fc82df3fdc0 h1:avWT4xM195wNR6FruVPa80T/X5D8o8gXiGdmVelBj0E= -go.podman.io/image/v5 v5.38.1-0.20251126122123-4fc82df3fdc0/go.mod h1:5YJtk/ShzEmtIxrJTjiF/0Q4SfuDfWDt9c/kKpwu7MQ= -go.podman.io/storage v1.61.1-0.20251125064110-c4e25180a61d h1:P7G3IS6pU+4tIaVzWk+mi+fYjb3MYGoxliGsvAziM18= -go.podman.io/storage v1.61.1-0.20251125064110-c4e25180a61d/go.mod h1:4p18A5ymiiJTllTu2Eo7CX88SO+V110TMzi31pXsb1s= +go.podman.io/common v0.66.1-0.20251128185259-94e31d2e45ba h1:rQiNXdDOa/A2bLvUQI9xeKuttkzytuUMXQWIeDwebZ8= +go.podman.io/common v0.66.1-0.20251128185259-94e31d2e45ba/go.mod h1:wpnuqmlKx7El9QgMIMJiZq9SM+InftBZF4QUlEKGAZI= +go.podman.io/image/v5 v5.38.1-0.20251128185259-94e31d2e45ba h1:0Mg1+Rgmil+i1BmLwk1YtddzCRmAjRHh8WDOZBEWZkY= +go.podman.io/image/v5 v5.38.1-0.20251128185259-94e31d2e45ba/go.mod h1:bXKvkbQgtxD3hibVOhAeGANyKPqo6lVwdwOxt93+V40= +go.podman.io/storage v1.61.1-0.20251128185259-94e31d2e45ba h1:fC+hvOKAsgVWpFPoEZjOioKAJFZNi3UpPHUQzZOcSVg= +go.podman.io/storage v1.61.1-0.20251128185259-94e31d2e45ba/go.mod h1:izI0U2FxSpqivwmkVbTO7ECcoER6FnM6ZL7CFPu0Tfs= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/vendor/github.com/moby/moby/client/README.md b/vendor/github.com/moby/moby/client/README.md index c8c5b96f92..115e604dbc 100644 --- a/vendor/github.com/moby/moby/client/README.md +++ b/vendor/github.com/moby/moby/client/README.md @@ -23,19 +23,28 @@ import ( ) func main() { - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + // Create a new client that handles common environment variables + // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does + // API-version negotiation to allow downgrading the API version + // when connecting with an older daemon version. + apiClient, err := client.New(client.FromEnv) if err != nil { panic(err) } defer apiClient.Close() - containers, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{All: true}) + // List all containers (both stopped and running). + result, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{ + All: true, + }) if err != nil { panic(err) } - for _, ctr := range containers { - fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status) + // Print each container's ID, status and the image it was created from. + fmt.Printf("%s %-22s %s\n", "ID", "STATUS", "IMAGE") + for _, ctr := range result.Items { + fmt.Printf("%s %-22s %s\n", ctr.ID, ctr.Status, ctr.Image) } } ``` diff --git a/vendor/github.com/moby/moby/client/client.go b/vendor/github.com/moby/moby/client/client.go index 3d11861440..0567ba11d4 100644 --- a/vendor/github.com/moby/moby/client/client.go +++ b/vendor/github.com/moby/moby/client/client.go @@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/ You use the library by constructing a client object using [New] and calling methods on it. The client can be configured from environment -variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation] -option to allow downgrading the API version used when connecting with an older -daemon version. Other options cen be configured manually by passing any of -the available [Opt] options. +variables by passing the [FromEnv] option. Other options can be configured +manually by passing any of the available [Opt] options. For example, to list running containers (the equivalent of "docker ps"): @@ -30,7 +28,7 @@ For example, to list running containers (the equivalent of "docker ps"): // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { log.Fatal(err) } @@ -103,18 +101,16 @@ import ( const DummyHost = "api.moby.localhost" // MaxAPIVersion is the highest REST API version supported by the client. -// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], -// [Client.NegotiateAPIVersion]), the client may downgrade its API version. -// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding -// the version. +// If API-version negotiation is enabled, the client may downgrade its API version. +// Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow +// overriding the version and disable API-version negotiation. // // This version may be lower than the version of the api library module used. const MaxAPIVersion = "1.52" -// fallbackAPIVersion is the version to fall back to if API-version negotiation -// fails. API versions below this version are not supported by the client, -// and not considered when negotiating. -const fallbackAPIVersion = "1.44" +// MinAPIVersion is the minimum API version supported by the client. API versions +// below this version are not considered when performing API-version negotiation. +const MinAPIVersion = "1.44" // Ensure that Client always implements APIClient. var _ APIClient = &Client{} @@ -174,8 +170,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) { // It takes an optional list of [Opt] functional arguments, which are applied in // the order they're provided, which allows modifying the defaults when creating // the client. For example, the following initializes a client that configures -// itself with values from environment variables ([FromEnv]), and has automatic -// API version negotiation enabled ([WithAPIVersionNegotiation]). +// itself with values from environment variables ([FromEnv]). +// +// By default, the client automatically negotiates the API version to use when +// making requests. API version negotiation is performed on the first request; +// subsequent requests do not re-negotiate. Use [WithAPIVersion] or +// [WithAPIVersionFromEnv] to configure the client with a fixed API version +// and disable API version negotiation. // // cli, err := client.New( // client.FromEnv, @@ -213,6 +214,12 @@ func New(ops ...Opt) (*Client, error) { } } + if cfg.envAPIVersion != "" { + c.setAPIVersion(cfg.envAPIVersion) + } else if cfg.manualAPIVersion != "" { + c.setAPIVersion(cfg.manualAPIVersion) + } + if tr, ok := c.client.Transport.(*http.Transport); ok { // Store the base transport before we wrap it in tracing libs below // This is used, as an example, to close idle connections when the client is closed @@ -278,7 +285,7 @@ func (cli *Client) Close() error { // be negotiated when making the actual requests, and for which cases // we cannot do the negotiation lazily. func (cli *Client) checkVersion(ctx context.Context) error { - if cli.manualOverride || !cli.negotiateVersion || cli.negotiated.Load() { + if cli.negotiated.Load() { return nil } _, err := cli.Ping(ctx, PingOptions{ @@ -306,36 +313,47 @@ func (cli *Client) ClientVersion() string { } // negotiateAPIVersion updates the version to match the API version from -// the ping response. It falls back to the lowest version supported if the -// API version is empty, or returns an error if the API version is lower than -// the lowest supported API version, in which case the version is not modified. +// the ping response. +// +// It returns an error if version is invalid, or lower than the minimum +// supported API version in which case the client's API version is not +// updated, and negotiation is not marked as completed. func (cli *Client) negotiateAPIVersion(pingVersion string) error { - pingVersion = strings.TrimPrefix(pingVersion, "v") - if pingVersion == "" { - // TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487 - pingVersion = fallbackAPIVersion - } else if versions.LessThan(pingVersion, fallbackAPIVersion) { - return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, fallbackAPIVersion)) + var err error + pingVersion, err = parseAPIVersion(pingVersion) + if err != nil { + return err + } + + if versions.LessThan(pingVersion, MinAPIVersion) { + return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, MinAPIVersion)) } // if the client is not initialized with a version, start with the latest supported version - if cli.version == "" { - cli.version = MaxAPIVersion + negotiatedVersion := cli.version + if negotiatedVersion == "" { + negotiatedVersion = MaxAPIVersion } // if server version is lower than the client version, downgrade - if versions.LessThan(pingVersion, cli.version) { - cli.version = pingVersion + if versions.LessThan(pingVersion, negotiatedVersion) { + negotiatedVersion = pingVersion } // Store the results, so that automatic API version negotiation (if enabled) // won't be performed on the next request. - if cli.negotiateVersion { - cli.negotiated.Store(true) - } + cli.setAPIVersion(negotiatedVersion) return nil } +// setAPIVersion sets the client's API version and marks API version negotiation +// as completed, so that automatic API version negotiation (if enabled) won't +// be performed on the next request. +func (cli *Client) setAPIVersion(version string) { + cli.version = version + cli.negotiated.Store(true) +} + // DaemonHost returns the host address used by the client func (cli *Client) DaemonHost() string { return cli.host diff --git a/vendor/github.com/moby/moby/client/options.go b/vendor/github.com/moby/moby/client/client_options.go similarity index 75% rename from vendor/github.com/moby/moby/client/options.go rename to vendor/github.com/moby/moby/client/client_options.go index 1b1e92bbba..295d299180 100644 --- a/vendor/github.com/moby/moby/client/options.go +++ b/vendor/github.com/moby/moby/client/client_options.go @@ -38,14 +38,22 @@ type clientConfig struct { userAgent *string // custom HTTP headers configured by users. customHTTPHeaders map[string]string - // manualOverride is set to true when the version was set by users. - manualOverride bool - // negotiateVersion indicates if the client should automatically negotiate - // the API version to use when making requests. API version negotiation is - // performed on the first request, after which negotiated is set to "true" - // so that subsequent requests do not re-negotiate. - negotiateVersion bool + // manualAPIVersion contains the API version set by users. This field + // will only be non-empty if a valid-formed version was set through + // [WithAPIVersion]. + // + // If both manualAPIVersion and envAPIVersion are set, manualAPIVersion + // takes precedence. Either field disables API-version negotiation. + manualAPIVersion string + + // envAPIVersion contains the API version set by users. This field + // will only be non-empty if a valid-formed version was set through + // [WithAPIVersionFromEnv]. + // + // If both manualAPIVersion and envAPIVersion are set, manualAPIVersion + // takes precedence. Either field disables API-version negotiation. + envAPIVersion string // traceOpts is a list of options to configure the tracing span. traceOpts []otelhttp.Option @@ -56,7 +64,7 @@ type Opt func(*clientConfig) error // FromEnv configures the client with values from environment variables. It // is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv], -// and [WithVersionFromEnv] options. +// and [WithAPIVersionFromEnv] options. // // FromEnv uses the following environment variables: // @@ -71,7 +79,7 @@ func FromEnv(c *clientConfig) error { ops := []Opt{ WithTLSClientConfigFromEnv(), WithHostFromEnv(), - WithVersionFromEnv(), + WithAPIVersionFromEnv(), } for _, op := range ops { if err := op(c); err != nil { @@ -241,18 +249,59 @@ func WithTLSClientConfigFromEnv() Opt { } } -// WithVersion overrides the client version with the specified one. If an empty -// version is provided, the value is ignored to allow version negotiation -// (see [WithAPIVersionNegotiation]). +// WithAPIVersion overrides the client's API version with the specified one, +// and disables API version negotiation. If an empty version is provided, +// this option is ignored to allow version negotiation. The given version +// should be formatted "." (for example, "1.52"). It returns +// an error if the given value not in the correct format. // -// WithVersion does not validate if the client supports the given version, -// and callers should verify if the version is in the correct format and -// lower than the maximum supported version as defined by [MaxAPIVersion]. -func WithVersion(version string) Opt { +// WithAPIVersion does not validate if the client supports the given version, +// and callers should verify if the version lower than the maximum supported +// version as defined by [MaxAPIVersion]. +// +// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and +// [WithAPIVersionFromEnv] are both set. +func WithAPIVersion(version string) Opt { return func(c *clientConfig) error { - if v := strings.TrimPrefix(version, "v"); v != "" { - c.version = v - c.manualOverride = true + version = strings.TrimSpace(version) + if val := strings.TrimPrefix(version, "v"); val != "" { + ver, err := parseAPIVersion(val) + if err != nil { + return fmt.Errorf("invalid API version (%s): %w", version, err) + } + c.manualAPIVersion = ver + } + return nil + } +} + +// WithVersion overrides the client version with the specified one. +// +// Deprecated: use [WithAPIVersion] instead. +func WithVersion(version string) Opt { + return WithAPIVersion(version) +} + +// WithAPIVersionFromEnv overrides the client version with the version specified in +// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. +// If DOCKER_API_VERSION is not set, or set to an empty value, the version +// is not modified. +// +// WithAPIVersion does not validate if the client supports the given version, +// and callers should verify if the version lower than the maximum supported +// version as defined by [MaxAPIVersion]. +// +// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and +// [WithAPIVersionFromEnv] are both set. +func WithAPIVersionFromEnv() Opt { + return func(c *clientConfig) error { + version := strings.TrimSpace(os.Getenv(EnvOverrideAPIVersion)) + if val := strings.TrimPrefix(version, "v"); val != "" { + ver, err := parseAPIVersion(val) + if err != nil { + return fmt.Errorf("invalid API version (%s): %w", version, err) + } + c.envAPIVersion = ver } return nil } @@ -260,25 +309,21 @@ func WithVersion(version string) Opt { // WithVersionFromEnv overrides the client version with the version specified in // the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. -// If DOCKER_API_VERSION is not set, or set to an empty value, the version -// is not modified. // -// WithVersion does not validate if the client supports the given version, -// and callers should verify if the version is in the correct format and -// lower than the maximum supported version as defined by [MaxAPIVersion]. +// Deprecated: use [WithAPIVersionFromEnv] instead. func WithVersionFromEnv() Opt { - return func(c *clientConfig) error { - return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c) - } + return WithAPIVersionFromEnv() } // WithAPIVersionNegotiation enables automatic API version negotiation for the client. // With this option enabled, the client automatically negotiates the API version // to use when making requests. API version negotiation is performed on the first // request; subsequent requests do not re-negotiate. +// +// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion] +// or [WithAPIVersionFromEnv] to disable API version negotiation. func WithAPIVersionNegotiation() Opt { return func(c *clientConfig) error { - c.negotiateVersion = true return nil } } diff --git a/vendor/github.com/moby/moby/client/envvars.go b/vendor/github.com/moby/moby/client/envvars.go index 2b0e3f6b5a..a02295d162 100644 --- a/vendor/github.com/moby/moby/client/envvars.go +++ b/vendor/github.com/moby/moby/client/envvars.go @@ -13,7 +13,7 @@ const ( // be used to override the API version to use. Value must be // formatted as MAJOR.MINOR, for example, "1.19". // - // This env-var is read by [FromEnv] and [WithVersionFromEnv] and when set to a + // This env-var is read by [FromEnv] and [WithAPIVersionFromEnv] and when set to a // non-empty value, takes precedence over API version negotiation. // // This environment variable should be used for debugging purposes only, as diff --git a/vendor/github.com/moby/moby/client/ping.go b/vendor/github.com/moby/moby/client/ping.go index 15c54faa0c..d315e4b98b 100644 --- a/vendor/github.com/moby/moby/client/ping.go +++ b/vendor/github.com/moby/moby/client/ping.go @@ -20,7 +20,7 @@ type PingOptions struct { // // If a manual override is in place, either through the "DOCKER_API_VERSION" // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized - // with a fixed version ([WithVersion]), no negotiation is performed. + // with a fixed version ([WithAPIVersion]), no negotiation is performed. // // If the API server's ping response does not contain an API version, or if the // client did not get a successful ping response, it assumes it is connected with @@ -29,9 +29,8 @@ type PingOptions struct { NegotiateAPIVersion bool // ForceNegotiate forces the client to re-negotiate the API version, even if - // API-version negotiation already happened. This option cannot be - // used if the client is configured with a fixed version using (using - // [WithVersion] or [WithVersionFromEnv]). + // API-version negotiation already happened or it the client is configured + // with a fixed version (using [WithAPIVersion] or [WithAPIVersionFromEnv]). // // This option has no effect if NegotiateAPIVersion is not set. ForceNegotiate bool @@ -72,10 +71,12 @@ type SwarmStatus struct { // for other non-success status codes, failing to connect to the API, or failing // to parse the API response. func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) { - if cli.manualOverride { + if !options.NegotiateAPIVersion { + // No API version negotiation needed; just return ping response. return cli.ping(ctx) } - if !options.NegotiateAPIVersion && !cli.negotiateVersion { + if cli.negotiated.Load() && !options.ForceNegotiate { + // API version was already negotiated or manually set. return cli.ping(ctx) } @@ -85,10 +86,19 @@ func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, e ping, err := cli.ping(ctx) if err != nil { - return cli.ping(ctx) + return ping, err } if cli.negotiated.Load() && !options.ForceNegotiate { + // API version was already negotiated or manually set. + // + // We check cli.negotiated again under lock, to account for race + // conditions with the check at the start of this function. + return ping, nil + } + + if ping.APIVersion == "" { + cli.setAPIVersion(MaxAPIVersion) return ping, nil } @@ -112,10 +122,15 @@ func (cli *Client) ping(ctx context.Context) (PingResult, error) { // response-body to get error details from. return newPingResult(resp), nil } + // close to allow reusing connection. + ensureReaderClosed(resp) // HEAD failed or returned a non-OK status; fallback to GET. - req.Method = http.MethodGet - resp, err = cli.doRequest(req) + req2, err := cli.buildRequest(ctx, http.MethodGet, path.Join(cli.basePath, "/_ping"), nil, nil) + if err != nil { + return PingResult{}, err + } + resp, err = cli.doRequest(req2) defer ensureReaderClosed(resp) if err != nil { // Failed to connect. diff --git a/vendor/github.com/moby/moby/client/request.go b/vendor/github.com/moby/moby/client/request.go index f5c2b956a0..50c12eb157 100644 --- a/vendor/github.com/moby/moby/client/request.go +++ b/vendor/github.com/moby/moby/client/request.go @@ -67,31 +67,22 @@ func (cli *Client) delete(ctx context.Context, path string, query url.Values, he // prepareJSONRequest encodes the given body to JSON and returns it as an [io.Reader], and sets the Content-Type // header. If body is nil, or a nil-interface, a "nil" body is returned without // error. -// -// TODO(thaJeztah): should this return an error if a different Content-Type is already set? -// TODO(thaJeztah): is "nil" the appropriate approach for an empty body, or should we use [http.NoBody] (or similar)? func prepareJSONRequest(body any, headers http.Header) (io.Reader, http.Header, error) { - if body == nil { - return nil, headers, nil - } - // encoding/json encodes a nil pointer as the JSON document `null`, - // irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler. - // That is almost certainly not what the caller intended as the request body. - // - // TODO(thaJeztah): consider moving this to jsonEncode, which would also allow returning an (empty) reader instead of nil. - if reflect.TypeOf(body).Kind() == reflect.Ptr && reflect.ValueOf(body).IsNil() { - return nil, headers, nil - } - jsonBody, err := jsonEncode(body) if err != nil { return nil, headers, err } + if jsonBody == nil || jsonBody == http.NoBody { + // no content-type is set on empty requests. + return jsonBody, headers, nil + } + hdr := http.Header{} if headers != nil { hdr = headers.Clone() } + // TODO(thaJeztah): should this return an error if a different Content-Type is already set? hdr.Set("Content-Type", "application/json") return jsonBody, hdr, nil } @@ -110,9 +101,6 @@ func (cli *Client) buildRequest(ctx context.Context, method, path string, body i req.Host = DummyHost } - if body != nil && req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "text/plain") - } return req, nil } @@ -248,7 +236,11 @@ func checkResponseErr(serverResp *http.Response) (retErr error) { if statusMsg == "" { statusMsg = http.StatusText(serverResp.StatusCode) } - if serverResp.Body != nil { + var reqMethod string + if serverResp.Request != nil { + reqMethod = serverResp.Request.Method + } + if serverResp.Body != nil && reqMethod != http.MethodHead { bodyMax := 1 * 1024 * 1024 // 1 MiB bodyR := &io.LimitedReader{ R: serverResp.Body, @@ -333,25 +325,49 @@ func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Requ } func jsonEncode(data any) (io.Reader, error) { - var params bytes.Buffer - if data != nil { - if err := json.NewEncoder(¶ms).Encode(data); err != nil { - return nil, err + switch x := data.(type) { + case nil: + return http.NoBody, nil + case io.Reader: + // http.NoBody or other readers + return x, nil + case json.RawMessage: + if len(x) == 0 { + return http.NoBody, nil } + return bytes.NewReader(x), nil } - return ¶ms, nil + + // encoding/json encodes a nil pointer as the JSON document `null`, + // irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler. + // That is almost certainly not what the caller intended as the request body. + if v := reflect.ValueOf(data); v.Kind() == reflect.Ptr && v.IsNil() { + return http.NoBody, nil + } + + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + return bytes.NewReader(b), nil } func ensureReaderClosed(response *http.Response) { - if response != nil && response.Body != nil { - // Drain up to 512 bytes and close the body to let the Transport reuse the connection - // see https://github.com/google/go-github/pull/317/files#r57536827 - // - // TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib, - // and check if context-cancellation should handle this as well. If still needed, consider - // wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related - // methods. - _, _ = io.CopyN(io.Discard, response.Body, 512) - _ = response.Body.Close() + if response == nil || response.Body == nil { + return } + if response.ContentLength == 0 || (response.Request != nil && response.Request.Method == http.MethodHead) { + // No need to drain head requests or zero-length responses. + _ = response.Body.Close() + return + } + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + // see https://github.com/google/go-github/pull/317/files#r57536827 + // + // TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib, + // and check if context-cancellation should handle this as well. If still needed, consider + // wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related + // methods. + _, _ = io.CopyN(io.Discard, response.Body, 512) + _ = response.Body.Close() } diff --git a/vendor/github.com/moby/moby/client/service_create.go b/vendor/github.com/moby/moby/client/service_create.go index 204d21816c..319bca6f4c 100644 --- a/vendor/github.com/moby/moby/client/service_create.go +++ b/vendor/github.com/moby/moby/client/service_create.go @@ -59,16 +59,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg } if options.QueryRegistry { - resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } case options.Spec.TaskTemplate.PluginSpec != nil: if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg } if options.QueryRegistry { - resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } } @@ -93,35 +95,33 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio } func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil { - warning = digestWarning(taskSpec.ContainerSpec.Image) - } else { - taskSpec.ContainerSpec.Image = img - if len(imgPlatforms) > 0 { - if taskSpec.Placement == nil { - taskSpec.Placement = &swarm.Placement{} - } - taskSpec.Placement.Platforms = imgPlatforms - } + img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth) + if err != nil { + return digestWarning(taskSpec.ContainerSpec.Image) } - return warning + taskSpec.ContainerSpec.Image = img + if len(imgPlatforms) > 0 { + if taskSpec.Placement == nil { + taskSpec.Placement = &swarm.Placement{} + } + taskSpec.Placement.Platforms = imgPlatforms + } + return "" } func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil { - warning = digestWarning(taskSpec.PluginSpec.Remote) - } else { - taskSpec.PluginSpec.Remote = img - if len(imgPlatforms) > 0 { - if taskSpec.Placement == nil { - taskSpec.Placement = &swarm.Placement{} - } - taskSpec.Placement.Platforms = imgPlatforms - } + img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth) + if err != nil { + return digestWarning(taskSpec.PluginSpec.Remote) } - return warning + taskSpec.PluginSpec.Remote = img + if len(imgPlatforms) > 0 { + if taskSpec.Placement == nil { + taskSpec.Placement = &swarm.Placement{} + } + taskSpec.Placement.Platforms = imgPlatforms + } + return "" } func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) { diff --git a/vendor/github.com/moby/moby/client/service_update.go b/vendor/github.com/moby/moby/client/service_update.go index 9e6b52781e..2505fe4b8e 100644 --- a/vendor/github.com/moby/moby/client/service_update.go +++ b/vendor/github.com/moby/moby/client/service_update.go @@ -82,16 +82,18 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, options options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg } if options.QueryRegistry { - resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } case options.Spec.TaskTemplate.PluginSpec != nil: if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg } if options.QueryRegistry { - resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } } diff --git a/vendor/github.com/moby/moby/client/utils.go b/vendor/github.com/moby/moby/client/utils.go index 6a5fc46ea1..f2ba4744c4 100644 --- a/vendor/github.com/moby/moby/client/utils.go +++ b/vendor/github.com/moby/moby/client/utils.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "sync" @@ -32,6 +33,47 @@ func trimID(objType, id string) (string, error) { return id, nil } +// parseAPIVersion checks v to be a well-formed (".") +// API version. It returns an error if the value is empty or does not +// have the correct format, but does not validate if the API version is +// within the supported range ([MinAPIVersion] <= v <= [MaxAPIVersion]). +// +// It returns version after normalizing, or an error if validation failed. +func parseAPIVersion(version string) (string, error) { + if strings.TrimPrefix(strings.TrimSpace(version), "v") == "" { + return "", cerrdefs.ErrInvalidArgument.WithMessage("value is empty") + } + major, minor, err := parseMajorMinor(version) + if err != nil { + return "", err + } + return fmt.Sprintf("%d.%d", major, minor), nil +} + +// parseMajorMinor is a helper for parseAPIVersion. +func parseMajorMinor(v string) (major, minor int, _ error) { + if strings.HasPrefix(v, "v") { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted .") + } + if strings.TrimSpace(v) == "" { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("value is empty") + } + + majVer, minVer, ok := strings.Cut(v, ".") + if !ok { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted .") + } + major, err := strconv.Atoi(majVer) + if err != nil { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid major version: must be formatted .") + } + minor, err = strconv.Atoi(minVer) + if err != nil { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid minor version: must be formatted .") + } + return major, minor, nil +} + // encodePlatforms marshals the given platform(s) to JSON format, to // be used for query-parameters for filtering / selecting platforms. func encodePlatforms(platform ...ocispec.Platform) ([]string, error) { diff --git a/vendor/github.com/opencontainers/runc/internal/linux/doc.go b/vendor/github.com/opencontainers/runc/internal/linux/doc.go new file mode 100644 index 0000000000..4d1eb90010 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/linux/doc.go @@ -0,0 +1,3 @@ +// Package linux provides minimal wrappers around Linux system calls, primarily +// to provide support for automatic EINTR-retries. +package linux diff --git a/vendor/github.com/opencontainers/runc/internal/linux/eintr.go b/vendor/github.com/opencontainers/runc/internal/linux/eintr.go new file mode 100644 index 0000000000..36a6e3e29e --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/linux/eintr.go @@ -0,0 +1,28 @@ +package linux + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +// retryOnEINTR takes a function that returns an error and calls it +// until the error returned is not EINTR. +func retryOnEINTR(fn func() error) error { + for { + err := fn() + if !errors.Is(err, unix.EINTR) { + return err + } + } +} + +// retryOnEINTR2 is like retryOnEINTR, but it returns 2 values. +func retryOnEINTR2[T any](fn func() (T, error)) (T, error) { + for { + val, err := fn() + if !errors.Is(err, unix.EINTR) { + return val, err + } + } +} diff --git a/vendor/github.com/opencontainers/runc/internal/linux/linux.go b/vendor/github.com/opencontainers/runc/internal/linux/linux.go new file mode 100644 index 0000000000..88fe5e0dfd --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/linux/linux.go @@ -0,0 +1,126 @@ +package linux + +import ( + "os" + "unsafe" + + "golang.org/x/sys/unix" +) + +// Dup3 wraps [unix.Dup3]. +func Dup3(oldfd, newfd, flags int) error { + err := retryOnEINTR(func() error { + return unix.Dup3(oldfd, newfd, flags) + }) + return os.NewSyscallError("dup3", err) +} + +// Exec wraps [unix.Exec]. +func Exec(cmd string, args []string, env []string) error { + err := retryOnEINTR(func() error { + return unix.Exec(cmd, args, env) + }) + if err != nil { + return &os.PathError{Op: "exec", Path: cmd, Err: err} + } + return nil +} + +// Getwd wraps [unix.Getwd]. +func Getwd() (wd string, err error) { + wd, err = retryOnEINTR2(unix.Getwd) + return wd, os.NewSyscallError("getwd", err) +} + +// Open wraps [unix.Open]. +func Open(path string, mode int, perm uint32) (fd int, err error) { + fd, err = retryOnEINTR2(func() (int, error) { + return unix.Open(path, mode, perm) + }) + if err != nil { + return -1, &os.PathError{Op: "open", Path: path, Err: err} + } + return fd, nil +} + +// Openat wraps [unix.Openat]. +func Openat(dirfd int, path string, mode int, perm uint32) (fd int, err error) { + fd, err = retryOnEINTR2(func() (int, error) { + return unix.Openat(dirfd, path, mode, perm) + }) + if err != nil { + return -1, &os.PathError{Op: "openat", Path: path, Err: err} + } + return fd, nil +} + +// Recvfrom wraps [unix.Recvfrom]. +func Recvfrom(fd int, p []byte, flags int) (n int, from unix.Sockaddr, err error) { + err = retryOnEINTR(func() error { + n, from, err = unix.Recvfrom(fd, p, flags) + return err + }) + if err != nil { + return 0, nil, os.NewSyscallError("recvfrom", err) + } + return n, from, err +} + +// Sendmsg wraps [unix.Sendmsg]. +func Sendmsg(fd int, p, oob []byte, to unix.Sockaddr, flags int) error { + err := retryOnEINTR(func() error { + return unix.Sendmsg(fd, p, oob, to, flags) + }) + return os.NewSyscallError("sendmsg", err) +} + +// SetMempolicy wraps set_mempolicy. +func SetMempolicy(mode uint, mask *unix.CPUSet) error { + err := retryOnEINTR(func() error { + _, _, errno := unix.Syscall(unix.SYS_SET_MEMPOLICY, uintptr(mode), uintptr(unsafe.Pointer(mask)), unsafe.Sizeof(*mask)*8) + if errno != 0 { + return errno + } + return nil + }) + return os.NewSyscallError("set_mempolicy", err) +} + +// Readlinkat wraps [unix.Readlinkat]. +func Readlinkat(dir *os.File, path string) (string, error) { + size := 4096 + for { + linkBuf := make([]byte, size) + n, err := retryOnEINTR2(func() (int, error) { + return unix.Readlinkat(int(dir.Fd()), path, linkBuf) + }) + if err != nil { + return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err} + } + if n != size { + return string(linkBuf[:n]), nil + } + // Possible truncation, resize the buffer. + size *= 2 + } +} + +// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER). +func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) { + // Make sure O_NOCTTY is always set -- otherwise runc might accidentally + // gain it as a controlling terminal. O_CLOEXEC also needs to be set to + // make sure we don't leak the handle either. + flags |= unix.O_NOCTTY | unix.O_CLOEXEC + + // There is no nice wrapper for this kind of ioctl in unix. + peerFd, _, errno := unix.Syscall( + unix.SYS_IOCTL, + ptyFd, + uintptr(unix.TIOCGPTPEER), + uintptr(flags), + ) + if errno != 0 { + return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno) + } + return os.NewFile(peerFd, unsafePeerPath), nil +} diff --git a/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall.go b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall.go new file mode 100644 index 0000000000..3a896f4841 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2024-2025 Aleksa Sarai + * Copyright (C) 2024-2025 SUSE LLC + * + * 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 pathrs + +import ( + "fmt" + "os" + "path/filepath" +) + +// MkdirAllParentInRoot is like [MkdirAllInRoot] except that it only creates +// the parent directory of the target path, returning the trailing component so +// the caller has more flexibility around constructing the final inode. +// +// Callers need to be very careful operating on the trailing path, as trivial +// mistakes like following symlinks can cause security bugs. Most people +// should probably just use [MkdirAllInRoot] or [CreateInRoot]. +func MkdirAllParentInRoot(root, unsafePath string, mode os.FileMode) (*os.File, string, error) { + // MkdirAllInRoot also does hallucinateUnsafePath, but we need to do it + // here first because when we split unsafePath into (dir, file) components + // we want to be doing so with the hallucinated path (so that trailing + // dangling symlinks are treated correctly). + unsafePath, err := hallucinateUnsafePath(root, unsafePath) + if err != nil { + return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err) + } + + dirPath, filename := filepath.Split(unsafePath) + if filepath.Join("/", filename) == "/" { + return nil, "", fmt.Errorf("create parent dir in root subpath %q has bad trailing component %q", unsafePath, filename) + } + + dirFd, err := MkdirAllInRoot(root, dirPath, mode) + return dirFd, filename, err +} diff --git a/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go index a9a0157c68..c2578e051f 100644 --- a/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/mkdirall_pathrslite.go @@ -21,14 +21,13 @@ package pathrs import ( "fmt" "os" - "path/filepath" "github.com/cyphar/filepath-securejoin/pathrs-lite" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) -// MkdirAllInRootOpen attempts to make +// MkdirAllInRoot attempts to make // // path, _ := securejoin.SecureJoin(root, unsafePath) // os.MkdirAll(path, mode) @@ -49,19 +48,10 @@ import ( // handling if unsafePath has already been scoped within the rootfs (this is // needed for a lot of runc callers and fixing this would require reworking a // lot of path logic). -func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) { - // If the path is already "within" the root, get the path relative to the - // root and use that as the unsafe path. This is necessary because a lot of - // MkdirAllInRootOpen callers have already done SecureJoin, and refactoring - // all of them to stop using these SecureJoin'd paths would require a fair - // amount of work. - // TODO(cyphar): Do the refactor to libpathrs once it's ready. - if IsLexicallyInRoot(root, unsafePath) { - subPath, err := filepath.Rel(root, unsafePath) - if err != nil { - return nil, err - } - unsafePath = subPath +func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) { + unsafePath, err := hallucinateUnsafePath(root, unsafePath) + if err != nil { + return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) } // Check for any silly mode bits. @@ -87,13 +77,3 @@ func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, er return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) }) } - -// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the -// returned handle, for callers that don't need to use it. -func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error { - f, err := MkdirAllInRootOpen(root, unsafePath, mode) - if err == nil { - _ = f.Close() - } - return err -} diff --git a/vendor/github.com/opencontainers/runc/internal/pathrs/path.go b/vendor/github.com/opencontainers/runc/internal/pathrs/path.go index 1ee7c795d5..77be989241 100644 --- a/vendor/github.com/opencontainers/runc/internal/pathrs/path.go +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/path.go @@ -19,7 +19,11 @@ package pathrs import ( + "os" + "path/filepath" "strings" + + securejoin "github.com/cyphar/filepath-securejoin" ) // IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"), @@ -32,3 +36,81 @@ func IsLexicallyInRoot(root, path string) bool { path = strings.TrimRight(path, "/") return strings.HasPrefix(path+"/", root+"/") } + +// LexicallyCleanPath makes a path safe for use with filepath.Join. This is +// done by not only cleaning the path, but also (if the path is relative) +// adding a leading '/' and cleaning it (then removing the leading '/'). This +// ensures that a path resulting from prepending another path will always +// resolve to lexically be a subdirectory of the prefixed path. This is all +// done lexically, so paths that include symlinks won't be safe as a result of +// using CleanPath. +func LexicallyCleanPath(path string) string { + // Deal with empty strings nicely. + if path == "" { + return "" + } + + // Ensure that all paths are cleaned (especially problematic ones like + // "/../../../../../" which can cause lots of issues). + + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + + // If the path isn't absolute, we need to do more processing to fix paths + // such as "../../../..//some/path". We also shouldn't convert absolute + // paths to relative ones. + path = filepath.Clean(string(os.PathSeparator) + path) + // This can't fail, as (by definition) all paths are relative to root. + path, _ = filepath.Rel(string(os.PathSeparator), path) + + return path +} + +// LexicallyStripRoot returns the passed path, stripping the root path if it +// was (lexicially) inside it. Note that both passed paths will always be +// treated as absolute, and the returned path will also always be absolute. In +// addition, the paths are cleaned before stripping the root. +func LexicallyStripRoot(root, path string) string { + // Make the paths clean and absolute. + root, path = LexicallyCleanPath("/"+root), LexicallyCleanPath("/"+path) + switch { + case path == root: + path = "/" + case root == "/": + // do nothing + default: + path = strings.TrimPrefix(path, root+"/") + } + return LexicallyCleanPath("/" + path) +} + +// hallucinateUnsafePath creates a new unsafePath which has all symlinks +// (including dangling symlinks) fully resolved and any non-existent components +// treated as though they are real. This is effectively just a wrapper around +// [securejoin.SecureJoin] that strips the root. This path *IS NOT* safe to use +// as-is, you *MUST* operate on the returned path with pathrs-lite. +// +// The reason for this methods is that in previous runc versions, we would +// tolerate nonsense paths with dangling symlinks as path components. +// pathrs-lite does not support this, so instead we have to emulate this +// behaviour by doing SecureJoin *purely to get a semi-reasonable path to use* +// and then we use pathrs-lite to operate on the path safely. +// +// It would be quite difficult to emulate this in a race-free way in +// pathrs-lite, so instead we use [securejoin.SecureJoin] to simply produce a +// new candidate path for operations like [MkdirAllInRoot] so they can then +// operate on the new unsafePath as if it was what the user requested. +// +// If unsafePath is already lexically inside root, it is stripped before +// re-resolving it (this is done to ensure compatibility with legacy callers +// within runc that call SecureJoin before calling into pathrs). +func hallucinateUnsafePath(root, unsafePath string) (string, error) { + unsafePath = LexicallyStripRoot(root, unsafePath) + weirdPath, err := securejoin.SecureJoin(root, unsafePath) + if err != nil { + return "", err + } + unsafePath = LexicallyStripRoot(root, weirdPath) + return unsafePath, nil +} diff --git a/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go b/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go index 899af27036..51db77440d 100644 --- a/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go +++ b/vendor/github.com/opencontainers/runc/internal/pathrs/root_pathrslite.go @@ -19,12 +19,12 @@ package pathrs import ( - "fmt" "os" - "path/filepath" "github.com/cyphar/filepath-securejoin/pathrs-lite" "golang.org/x/sys/unix" + + "github.com/opencontainers/runc/internal/linux" ) // OpenInRoot opens the given path inside the root with the provided flags. It @@ -48,12 +48,7 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) { // include it in the passed flags. The fileMode argument uses unix.* mode bits, // *not* os.FileMode. func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) { - dir, filename := filepath.Split(subpath) - if filepath.Join("/", filename) == "/" { - return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename) - } - - dirFd, err := MkdirAllInRootOpen(root, dir, 0o755) + dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755) if err != nil { return nil, err } @@ -64,7 +59,7 @@ func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, e // trailing symlinks, so this is safe to do. libpathrs's Root::create_file // works the same way. flags |= unix.O_CREAT | unix.O_NOFOLLOW - fd, err := unix.Openat(int(dirFd.Fd()), filename, flags, fileMode) + fd, err := linux.Openat(int(dirFd.Fd()), filename, flags, fileMode) if err != nil { return nil, err } diff --git a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go index 4b03d4c715..e2a26488e0 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go +++ b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go @@ -2,15 +2,17 @@ package apparmor import "errors" -var ( - // IsEnabled returns true if apparmor is enabled for the host. - IsEnabled = isEnabled +// IsEnabled returns true if apparmor is enabled for the host. +func IsEnabled() bool { + return isEnabled() +} - // ApplyProfile will apply the profile with the specified name to the process after - // the next exec. It is only supported on Linux and produces an ErrApparmorNotEnabled - // on other platforms. - ApplyProfile = applyProfile +// ApplyProfile will apply the profile with the specified name to the process +// after the next exec. It is only supported on Linux and produces an +// [ErrApparmorNotEnabled] on other platforms. +func ApplyProfile(name string) error { + return applyProfile(name) +} - // ErrApparmorNotEnabled indicates that AppArmor is not enabled or not supported. - ErrApparmorNotEnabled = errors.New("apparmor: config provided but apparmor not supported") -) +// ErrApparmorNotEnabled indicates that AppArmor is not enabled or not supported. +var ErrApparmorNotEnabled = errors.New("apparmor: config provided but apparmor not supported") diff --git a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_linux.go b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_linux.go index a3a8e93258..9bb4fb2cd2 100644 --- a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_linux.go +++ b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_linux.go @@ -9,7 +9,6 @@ import ( "golang.org/x/sys/unix" "github.com/opencontainers/runc/internal/pathrs" - "github.com/opencontainers/runc/libcontainer/utils" ) var ( @@ -29,7 +28,7 @@ func isEnabled() bool { } func setProcAttr(attr, value string) error { - attr = utils.CleanPath(attr) + attr = pathrs.LexicallyCleanPath(attr) attrSubPath := "attr/apparmor/" + attr if _, err := os.Stat("/proc/self/" + attrSubPath); errors.Is(err, os.ErrNotExist) { // fall back to the old convention @@ -50,7 +49,7 @@ func setProcAttr(attr, value string) error { return err } -// changeOnExec reimplements aa_change_onexec from libapparmor in Go +// changeOnExec reimplements aa_change_onexec from libapparmor in Go. func changeOnExec(name string) error { if err := setProcAttr("exec", "exec "+name); err != nil { return fmt.Errorf("apparmor failed to apply profile: %w", err) @@ -58,9 +57,8 @@ func changeOnExec(name string) error { return nil } -// applyProfile will apply the profile with the specified name to the process after -// the next exec. It is only supported on Linux and produces an error on other -// platforms. +// applyProfile will apply the profile with the specified name to the process +// after the next exec. func applyProfile(name string) error { if name == "" { return nil diff --git a/vendor/github.com/opencontainers/runc/libcontainer/utils/cmsg.go b/vendor/github.com/opencontainers/runc/libcontainer/utils/cmsg.go deleted file mode 100644 index 3aca5bdacc..0000000000 --- a/vendor/github.com/opencontainers/runc/libcontainer/utils/cmsg.go +++ /dev/null @@ -1,135 +0,0 @@ -package utils - -/* - * Copyright 2016, 2017 SUSE LLC - * - * 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. - */ - -import ( - "fmt" - "os" - "runtime" - - "golang.org/x/sys/unix" -) - -// MaxNameLen is the maximum length of the name of a file descriptor being sent -// using SendFile. The name of the file handle returned by RecvFile will never be -// larger than this value. -const MaxNameLen = 4096 - -// oobSpace is the size of the oob slice required to store a single FD. Note -// that unix.UnixRights appears to make the assumption that fd is always int32, -// so sizeof(fd) = 4. -var oobSpace = unix.CmsgSpace(4) - -// RecvFile waits for a file descriptor to be sent over the given AF_UNIX -// socket. The file name of the remote file descriptor will be recreated -// locally (it is sent as non-auxiliary data in the same payload). -func RecvFile(socket *os.File) (_ *os.File, Err error) { - name := make([]byte, MaxNameLen) - oob := make([]byte, oobSpace) - - sockfd := socket.Fd() - var ( - n, oobn int - err error - ) - - for { - n, oobn, _, _, err = unix.Recvmsg(int(sockfd), name, oob, unix.MSG_CMSG_CLOEXEC) - if err != unix.EINTR { //nolint:errorlint // unix errors are bare - break - } - } - - if err != nil { - return nil, os.NewSyscallError("recvmsg", err) - } - if n >= MaxNameLen || oobn != oobSpace { - return nil, fmt.Errorf("recvfile: incorrect number of bytes read (n=%d oobn=%d)", n, oobn) - } - // Truncate. - name = name[:n] - oob = oob[:oobn] - - scms, err := unix.ParseSocketControlMessage(oob) - if err != nil { - return nil, err - } - - // We cannot control how many SCM_RIGHTS we receive, and upon receiving - // them all of the descriptors are installed in our fd table, so we need to - // parse all of the SCM_RIGHTS we received in order to close all of the - // descriptors on error. - var fds []int - defer func() { - for i, fd := range fds { - if i == 0 && Err == nil { - // Only close the first one on error. - continue - } - // Always close extra ones. - _ = unix.Close(fd) - } - }() - var lastErr error - for _, scm := range scms { - if scm.Header.Type == unix.SCM_RIGHTS { - scmFds, err := unix.ParseUnixRights(&scm) - if err != nil { - lastErr = err - } else { - fds = append(fds, scmFds...) - } - } - } - if lastErr != nil { - return nil, lastErr - } - - // We do this after collecting the fds to make sure we close them all when - // returning an error here. - if len(scms) != 1 { - return nil, fmt.Errorf("recvfd: number of SCMs is not 1: %d", len(scms)) - } - if len(fds) != 1 { - return nil, fmt.Errorf("recvfd: number of fds is not 1: %d", len(fds)) - } - return os.NewFile(uintptr(fds[0]), string(name)), nil -} - -// SendFile sends a file over the given AF_UNIX socket. file.Name() is also -// included so that if the other end uses RecvFile, the file will have the same -// name information. -func SendFile(socket *os.File, file *os.File) error { - name := file.Name() - if len(name) >= MaxNameLen { - return fmt.Errorf("sendfd: filename too long: %s", name) - } - err := SendRawFd(socket, name, file.Fd()) - runtime.KeepAlive(file) - return err -} - -// SendRawFd sends a specific file descriptor over the given AF_UNIX socket. -func SendRawFd(socket *os.File, msg string, fd uintptr) error { - oob := unix.UnixRights(int(fd)) - for { - err := unix.Sendmsg(int(socket.Fd()), []byte(msg), oob, nil, 0) - if err != unix.EINTR { //nolint:errorlint // unix errors are bare - return os.NewSyscallError("sendmsg", err) - } - } -} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go b/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go deleted file mode 100644 index 17259de980..0000000000 --- a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils.go +++ /dev/null @@ -1,115 +0,0 @@ -package utils - -import ( - "encoding/json" - "io" - "os" - "path/filepath" - "strings" - - "golang.org/x/sys/unix" -) - -const ( - exitSignalOffset = 128 -) - -// ExitStatus returns the correct exit status for a process based on if it -// was signaled or exited cleanly -func ExitStatus(status unix.WaitStatus) int { - if status.Signaled() { - return exitSignalOffset + int(status.Signal()) - } - return status.ExitStatus() -} - -// WriteJSON writes the provided struct v to w using standard json marshaling -// without a trailing newline. This is used instead of json.Encoder because -// there might be a problem in json decoder in some cases, see: -// https://github.com/docker/docker/issues/14203#issuecomment-174177790 -func WriteJSON(w io.Writer, v interface{}) error { - data, err := json.Marshal(v) - if err != nil { - return err - } - _, err = w.Write(data) - return err -} - -// CleanPath makes a path safe for use with filepath.Join. This is done by not -// only cleaning the path, but also (if the path is relative) adding a leading -// '/' and cleaning it (then removing the leading '/'). This ensures that a -// path resulting from prepending another path will always resolve to lexically -// be a subdirectory of the prefixed path. This is all done lexically, so paths -// that include symlinks won't be safe as a result of using CleanPath. -func CleanPath(path string) string { - // Deal with empty strings nicely. - if path == "" { - return "" - } - - // Ensure that all paths are cleaned (especially problematic ones like - // "/../../../../../" which can cause lots of issues). - - if filepath.IsAbs(path) { - return filepath.Clean(path) - } - - // If the path isn't absolute, we need to do more processing to fix paths - // such as "../../../..//some/path". We also shouldn't convert absolute - // paths to relative ones. - path = filepath.Clean(string(os.PathSeparator) + path) - // This can't fail, as (by definition) all paths are relative to root. - path, _ = filepath.Rel(string(os.PathSeparator), path) - - return path -} - -// StripRoot returns the passed path, stripping the root path if it was -// (lexicially) inside it. Note that both passed paths will always be treated -// as absolute, and the returned path will also always be absolute. In -// addition, the paths are cleaned before stripping the root. -func StripRoot(root, path string) string { - // Make the paths clean and absolute. - root, path = CleanPath("/"+root), CleanPath("/"+path) - switch { - case path == root: - path = "/" - case root == "/": - // do nothing - default: - path = strings.TrimPrefix(path, root+"/") - } - return CleanPath("/" + path) -} - -// SearchLabels searches through a list of key=value pairs for a given key, -// returning its value, and the binary flag telling whether the key exist. -func SearchLabels(labels []string, key string) (string, bool) { - key += "=" - for _, s := range labels { - if val, ok := strings.CutPrefix(s, key); ok { - return val, true - } - } - return "", false -} - -// Annotations returns the bundle path and user defined annotations from the -// libcontainer state. We need to remove the bundle because that is a label -// added by libcontainer. -func Annotations(labels []string) (bundle string, userAnnotations map[string]string) { - userAnnotations = make(map[string]string) - for _, l := range labels { - name, value, ok := strings.Cut(l, "=") - if !ok { - continue - } - if name == "bundle" { - bundle = value - } else { - userAnnotations[name] = value - } - } - return bundle, userAnnotations -} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go b/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go deleted file mode 100644 index 7dbec54dc9..0000000000 --- a/vendor/github.com/opencontainers/runc/libcontainer/utils/utils_unix.go +++ /dev/null @@ -1,277 +0,0 @@ -//go:build !windows - -package utils - -import ( - "fmt" - "math" - "os" - "path/filepath" - "runtime" - "strconv" - "sync" - _ "unsafe" // for go:linkname - - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/opencontainers/runc/internal/pathrs" - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" -) - -var ( - haveCloseRangeCloexecBool bool - haveCloseRangeCloexecOnce sync.Once -) - -func haveCloseRangeCloexec() bool { - haveCloseRangeCloexecOnce.Do(func() { - // Make sure we're not closing a random file descriptor. - tmpFd, err := unix.FcntlInt(0, unix.F_DUPFD_CLOEXEC, 0) - if err != nil { - return - } - defer unix.Close(tmpFd) - - err = unix.CloseRange(uint(tmpFd), uint(tmpFd), unix.CLOSE_RANGE_CLOEXEC) - // Any error means we cannot use close_range(CLOSE_RANGE_CLOEXEC). - // -ENOSYS and -EINVAL ultimately mean we don't have support, but any - // other potential error would imply that even the most basic close - // operation wouldn't work. - haveCloseRangeCloexecBool = err == nil - }) - return haveCloseRangeCloexecBool -} - -type fdFunc func(fd int) - -// fdRangeFrom calls the passed fdFunc for each file descriptor that is open in -// the current process. -func fdRangeFrom(minFd int, fn fdFunc) error { - fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC) - if err != nil { - return fmt.Errorf("get handle to /proc/thread-self/fd: %w", err) - } - defer closer() - defer fdDir.Close() - - fdList, err := fdDir.Readdirnames(-1) - if err != nil { - return err - } - for _, fdStr := range fdList { - fd, err := strconv.Atoi(fdStr) - // Ignore non-numeric file names. - if err != nil { - continue - } - // Ignore descriptors lower than our specified minimum. - if fd < minFd { - continue - } - // Ignore the file descriptor we used for readdir, as it will be closed - // when we return. - if uintptr(fd) == fdDir.Fd() { - continue - } - // Run the closure. - fn(fd) - } - return nil -} - -// CloseExecFrom sets the O_CLOEXEC flag on all file descriptors greater or -// equal to minFd in the current process. -func CloseExecFrom(minFd int) error { - // Use close_range(CLOSE_RANGE_CLOEXEC) if possible. - if haveCloseRangeCloexec() { - err := unix.CloseRange(uint(minFd), math.MaxInt32, unix.CLOSE_RANGE_CLOEXEC) - if err == nil { - return nil - } - - logrus.Debugf("close_range failed, closing range one at a time (error: %v)", err) - - // If close_range fails, we fall back to the standard loop. - } - // Otherwise, fall back to the standard loop. - return fdRangeFrom(minFd, unix.CloseOnExec) -} - -//go:linkname runtime_IsPollDescriptor internal/poll.IsPollDescriptor - -// In order to make sure we do not close the internal epoll descriptors the Go -// runtime uses, we need to ensure that we skip descriptors that match -// "internal/poll".IsPollDescriptor. Yes, this is a Go runtime internal thing, -// unfortunately there's no other way to be sure we're only keeping the file -// descriptors the Go runtime needs. Hopefully nothing blows up doing this... -func runtime_IsPollDescriptor(fd uintptr) bool //nolint:revive - -// UnsafeCloseFrom closes all file descriptors greater or equal to minFd in the -// current process, except for those critical to Go's runtime (such as the -// netpoll management descriptors). -// -// NOTE: That this function is incredibly dangerous to use in most Go code, as -// closing file descriptors from underneath *os.File handles can lead to very -// bad behaviour (the closed file descriptor can be re-used and then any -// *os.File operations would apply to the wrong file). This function is only -// intended to be called from the last stage of runc init. -func UnsafeCloseFrom(minFd int) error { - // We cannot use close_range(2) even if it is available, because we must - // not close some file descriptors. - return fdRangeFrom(minFd, func(fd int) { - if runtime_IsPollDescriptor(uintptr(fd)) { - // These are the Go runtimes internal netpoll file descriptors. - // These file descriptors are operated on deep in the Go scheduler, - // and closing those files from underneath Go can result in panics. - // There is no issue with keeping them because they are not - // executable and are not useful to an attacker anyway. Also we - // don't have any choice. - return - } - // There's nothing we can do about errors from close(2), and the - // only likely error to be seen is EBADF which indicates the fd was - // already closed (in which case, we got what we wanted). - _ = unix.Close(fd) - }) -} - -// NewSockPair returns a new SOCK_STREAM unix socket pair. -func NewSockPair(name string) (parent, child *os.File, err error) { - fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) - if err != nil { - return nil, nil, err - } - return os.NewFile(uintptr(fds[1]), name+"-p"), os.NewFile(uintptr(fds[0]), name+"-c"), nil -} - -// WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...) -// corresponding to the unsafePath resolved within the root. Before passing the -// fd, this path is verified to have been inside the root -- so operating on it -// through the passed fdpath should be safe. Do not access this path through -// the original path strings, and do not attempt to use the pathname outside of -// the passed closure (the file handle will be freed once the closure returns). -func WithProcfd(root, unsafePath string, fn func(procfd string) error) error { - // Remove the root then forcefully resolve inside the root. - unsafePath = StripRoot(root, unsafePath) - fullPath, err := securejoin.SecureJoin(root, unsafePath) - if err != nil { - return fmt.Errorf("resolving path inside rootfs failed: %w", err) - } - - procSelfFd, closer := ProcThreadSelf("fd/") - defer closer() - - // Open the target path. - fh, err := os.OpenFile(fullPath, unix.O_PATH|unix.O_CLOEXEC, 0) - if err != nil { - return fmt.Errorf("open o_path procfd: %w", err) - } - defer fh.Close() - - procfd := filepath.Join(procSelfFd, strconv.Itoa(int(fh.Fd()))) - // Double-check the path is the one we expected. - if realpath, err := os.Readlink(procfd); err != nil { - return fmt.Errorf("procfd verification failed: %w", err) - } else if realpath != fullPath { - return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath) - } - - return fn(procfd) -} - -// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd], intended -// to make migrating from [WithProcfd] and [WithProcfdPath] usage easier. The -// caller is responsible for making sure that the provided file handle is -// actually safe to operate on. -func WithProcfdFile(file *os.File, fn func(procfd string) error) error { - fdpath, closer := ProcThreadSelfFd(file.Fd()) - defer closer() - - return fn(fdpath) -} - -type ProcThreadSelfCloser func() - -var ( - haveProcThreadSelf bool - haveProcThreadSelfOnce sync.Once -) - -// ProcThreadSelf returns a string that is equivalent to -// /proc/thread-self/, with a graceful fallback on older kernels where -// /proc/thread-self doesn't exist. This method DOES NOT use SecureJoin, -// meaning that the passed string needs to be trusted. The caller _must_ call -// the returned procThreadSelfCloser function (which is runtime.UnlockOSThread) -// *only once* after it has finished using the returned path string. -func ProcThreadSelf(subpath string) (string, ProcThreadSelfCloser) { - haveProcThreadSelfOnce.Do(func() { - if _, err := os.Stat("/proc/thread-self/"); err == nil { - haveProcThreadSelf = true - } else { - logrus.Debugf("cannot stat /proc/thread-self (%v), falling back to /proc/self/task/", err) - } - }) - - // We need to lock our thread until the caller is done with the path string - // because any non-atomic operation on the path (such as opening a file, - // then reading it) could be interrupted by the Go runtime where the - // underlying thread is swapped out and the original thread is killed, - // resulting in pull-your-hair-out-hard-to-debug issues in the caller. In - // addition, the pre-3.17 fallback makes everything non-atomic because the - // same thing could happen between unix.Gettid() and the path operations. - // - // In theory, we don't need to lock in the atomic user case when using - // /proc/thread-self/, but it's better to be safe than sorry (and there are - // only one or two truly atomic users of /proc/thread-self/). - runtime.LockOSThread() - - threadSelf := "/proc/thread-self/" - if !haveProcThreadSelf { - // Pre-3.17 kernels did not have /proc/thread-self, so do it manually. - threadSelf = "/proc/self/task/" + strconv.Itoa(unix.Gettid()) + "/" - if _, err := os.Stat(threadSelf); err != nil { - // Unfortunately, this code is called from rootfs_linux.go where we - // are running inside the pid namespace of the container but /proc - // is the host's procfs. Unfortunately there is no real way to get - // the correct tid to use here (the kernel age means we cannot do - // things like set up a private fsopen("proc") -- even scanning - // NSpid in all of the tasks in /proc/self/task/*/status requires - // Linux 4.1). - // - // So, we just have to assume that /proc/self is acceptable in this - // one specific case. - if os.Getpid() == 1 { - logrus.Debugf("/proc/thread-self (tid=%d) cannot be emulated inside the initial container setup -- using /proc/self instead: %v", unix.Gettid(), err) - } else { - // This should never happen, but the fallback should work in most cases... - logrus.Warnf("/proc/thread-self could not be emulated for pid=%d (tid=%d) -- using more buggy /proc/self fallback instead: %v", os.Getpid(), unix.Gettid(), err) - } - threadSelf = "/proc/self/" - } - } - return threadSelf + subpath, runtime.UnlockOSThread -} - -// ProcThreadSelfFd is small wrapper around ProcThreadSelf to make it easier to -// create a /proc/thread-self handle for given file descriptor. -// -// It is basically equivalent to ProcThreadSelf(fmt.Sprintf("fd/%d", fd)), but -// without using fmt.Sprintf to avoid unneeded overhead. -func ProcThreadSelfFd(fd uintptr) (string, ProcThreadSelfCloser) { - return ProcThreadSelf("fd/" + strconv.FormatUint(uint64(fd), 10)) -} - -// Openat is a Go-friendly openat(2) wrapper. -func Openat(dir *os.File, path string, flags int, mode uint32) (*os.File, error) { - dirFd := unix.AT_FDCWD - if dir != nil { - dirFd = int(dir.Fd()) - } - flags |= unix.O_CLOEXEC - - fd, err := unix.Openat(dirFd, path, flags, mode) - if err != nil { - return nil, &os.PathError{Op: "openat", Path: path, Err: err} - } - return os.NewFile(uintptr(fd), dir.Name()+"/"+path), nil -} diff --git a/vendor/go.podman.io/image/v5/directory/directory_dest.go b/vendor/go.podman.io/image/v5/directory/directory_dest.go index fc82c969f1..5c704e8027 100644 --- a/vendor/go.podman.io/image/v5/directory/directory_dest.go +++ b/vendor/go.podman.io/image/v5/directory/directory_dest.go @@ -20,8 +20,6 @@ import ( "go.podman.io/storage/pkg/fileutils" ) -const version = "Directory Transport Version: 1.1\n" - // ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created // using the 'dir' transport var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data") @@ -33,7 +31,8 @@ type dirImageDestination struct { stubs.NoPutBlobPartialInitialize stubs.AlwaysSupportsSignatures - ref dirReference + ref dirReference + usesNonSHA256Digest bool } // newImageDestination returns an ImageDestination for writing to a directory. @@ -76,9 +75,14 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im return nil, err } // check if contents of version file is what we expect it to be - if string(contents) != version { + versionStr := string(contents) + parsedVersion, err := parseVersion(versionStr) + if err != nil { return nil, ErrNotContainerImageDir } + if parsedVersion.isGreaterThan(maxSupportedVersion) { + return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath} + } } else { return nil, ErrNotContainerImageDir } @@ -94,11 +98,6 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im return nil, fmt.Errorf("unable to create directory %q: %w", ref.resolvedPath, err) } } - // create version file - err = os.WriteFile(ref.versionPath(), []byte(version), 0o644) - if err != nil { - return nil, fmt.Errorf("creating version file %q: %w", ref.versionPath(), err) - } d := &dirImageDestination{ PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ @@ -151,13 +150,17 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io. } }() - digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo) + digester, stream := putblobdigest.DigestIfUnknown(stream, inputInfo) + // TODO: This can take quite some time, and should ideally be cancellable using ctx.Done(). size, err := io.Copy(blobFile, stream) if err != nil { return private.UploadedBlob{}, err } blobDigest := digester.Digest() + if blobDigest.Algorithm() != digest.Canonical { // compare the special case in layerPath + d.usesNonSHA256Digest = true + } if inputInfo.Size != -1 && size != inputInfo.Size { return private.UploadedBlob{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", blobDigest, inputInfo.Size, size) } @@ -257,6 +260,14 @@ func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signa // - Uploaded data MAY be visible to others before CommitWithOptions() is called // - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed) func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error { + versionToWrite := version1_1 + if d.usesNonSHA256Digest { + versionToWrite = version1_2 + } + err := os.WriteFile(d.ref.versionPath(), []byte(versionToWrite.String()), 0o644) + if err != nil { + return fmt.Errorf("writing version file %q: %w", d.ref.versionPath(), err) + } return nil } diff --git a/vendor/go.podman.io/image/v5/directory/directory_src.go b/vendor/go.podman.io/image/v5/directory/directory_src.go index b7fb3855c5..e2953b4233 100644 --- a/vendor/go.podman.io/image/v5/directory/directory_src.go +++ b/vendor/go.podman.io/image/v5/directory/directory_src.go @@ -26,7 +26,24 @@ type dirImageSource struct { // newImageSource returns an ImageSource reading from an existing directory. // The caller must call .Close() on the returned ImageSource. -func newImageSource(ref dirReference) private.ImageSource { +func newImageSource(ref dirReference) (private.ImageSource, error) { + versionPath := ref.versionPath() + contents, err := os.ReadFile(versionPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("reading version file %q: %w", versionPath, err) + } + } else { + versionStr := string(contents) + parsedVersion, err := parseVersion(versionStr) + if err != nil { + return nil, fmt.Errorf("invalid version file content: %q", versionStr) + } + if parsedVersion.isGreaterThan(maxSupportedVersion) { + return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath} + } + } + s := &dirImageSource{ PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ HasThreadSafeGetBlob: false, @@ -36,7 +53,7 @@ func newImageSource(ref dirReference) private.ImageSource { ref: ref, } s.Compat = impl.AddCompat(s) - return s + return s, nil } // Reference returns the reference used to set up this source, _as specified by the user_ diff --git a/vendor/go.podman.io/image/v5/directory/directory_transport.go b/vendor/go.podman.io/image/v5/directory/directory_transport.go index 77c0e7be57..165159e588 100644 --- a/vendor/go.podman.io/image/v5/directory/directory_transport.go +++ b/vendor/go.podman.io/image/v5/directory/directory_transport.go @@ -146,7 +146,7 @@ func (ref dirReference) NewImage(ctx context.Context, sys *types.SystemContext) // NewImageSource returns a types.ImageSource for this reference. // The caller must call .Close() on the returned ImageSource. func (ref dirReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { - return newImageSource(ref), nil + return newImageSource(ref) } // NewImageDestination returns a types.ImageDestination for this reference. @@ -172,12 +172,19 @@ func (ref dirReference) manifestPath(instanceDigest *digest.Digest) (string, err } // layerPath returns a path for a layer tarball within a directory using our conventions. -func (ref dirReference) layerPath(digest digest.Digest) (string, error) { - if err := digest.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly. +func (ref dirReference) layerPath(d digest.Digest) (string, error) { + if err := d.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly. return "", err } - // FIXME: Should we keep the digest identification? - return filepath.Join(ref.path, digest.Encoded()), nil + + var filename string + if d.Algorithm() == digest.Canonical { + filename = d.Encoded() + } else { + filename = d.Algorithm().String() + "-" + d.Encoded() + } + + return filepath.Join(ref.path, filename), nil } // signaturePath returns a path for a signature within a directory using our conventions. diff --git a/vendor/go.podman.io/image/v5/directory/version.go b/vendor/go.podman.io/image/v5/directory/version.go new file mode 100644 index 0000000000..9d9f92b3a4 --- /dev/null +++ b/vendor/go.podman.io/image/v5/directory/version.go @@ -0,0 +1,62 @@ +package directory + +import ( + "fmt" +) + +const ( + versionPrefix = "Directory Transport Version: " +) + +// version represents a parsed directory transport version +type version struct { + major int + minor int +} + +// Supported versions +// Write version file based on digest algorithm used. +// 1.1 for sha256-only images, 1.2 otherwise. +var ( + version1_1 = version{major: 1, minor: 1} + version1_2 = version{major: 1, minor: 2} + maxSupportedVersion = version1_2 +) + +// String formats a version as a string suitable for writing to the version file +func (v version) String() string { + return fmt.Sprintf("%s%d.%d\n", versionPrefix, v.major, v.minor) +} + +// parseVersion parses a version string into major and minor components. +// Returns an error if the format is invalid. +func parseVersion(versionStr string) (version, error) { + var v version + expectedFormat := versionPrefix + "%d.%d\n" + // Sscanf parsing is a bit loose (treats spaces specially), but a strict check immediately follows + n, err := fmt.Sscanf(versionStr, expectedFormat, &v.major, &v.minor) + if err != nil || n != 2 || versionStr != v.String() { + return version{}, fmt.Errorf("invalid version format") + } + return v, nil +} + +// TODO: Potential refactor for better interoperability with `cmp` +// https://github.com/containers/container-libs/pull/475#discussion_r2571131267 +// isGreaterThan returns true if v is greater than other +func (v version) isGreaterThan(other version) bool { + if v.major != other.major { + return v.major > other.major + } + return v.minor > other.minor +} + +// UnsupportedVersionError indicates that the directory uses a version newer than we support +type UnsupportedVersionError struct { + Version string // The unsupported version string found + Path string // The path to the directory +} + +func (e UnsupportedVersionError) Error() string { + return fmt.Sprintf("unsupported directory transport version %q at %s", e.Version, e.Path) +} diff --git a/vendor/go.podman.io/image/v5/docker/daemon/client.go b/vendor/go.podman.io/image/v5/docker/daemon/client.go index 4ed456ff6e..004a95fe77 100644 --- a/vendor/go.podman.io/image/v5/docker/daemon/client.go +++ b/vendor/go.podman.io/image/v5/docker/daemon/client.go @@ -19,7 +19,6 @@ func newDockerClient(sys *types.SystemContext) (*dockerclient.Client, error) { opts := []dockerclient.Opt{ dockerclient.WithHost(host), - dockerclient.WithAPIVersionNegotiation(), } // We conditionalize building the TLS configuration only to TLS sockets: diff --git a/vendor/go.podman.io/storage/drivers/driver.go b/vendor/go.podman.io/storage/drivers/driver.go index fed80751b5..38706dc999 100644 --- a/vendor/go.podman.io/storage/drivers/driver.go +++ b/vendor/go.podman.io/storage/drivers/driver.go @@ -47,7 +47,6 @@ type CreateOpts struct { MountLabel string StorageOpt map[string]string *idtools.IDMappings - ignoreChownErrors bool } // MountOpts contains optional arguments for Driver.Get() methods. @@ -184,7 +183,7 @@ type DiffDriver interface { // layer with the specified id and parent, returning the size of the // new layer in bytes. // The io.Reader must be an uncompressed stream. - ApplyDiff(id string, parent string, options ApplyDiffOpts) (size int64, err error) + ApplyDiff(id string, options ApplyDiffOpts) (size int64, err error) // DiffSize calculates the changes between the specified id // and its parent and returns the size in bytes of the changes // relative to its base filesystem directory. @@ -299,6 +298,19 @@ type DriverWithDiffer interface { DifferTarget(id string) (string, error) } +// ApplyDiffStaging is an interface for driver who can apply the diff without holding the main storage lock. +// This API is experimental and can be changed without bumping the major version number. +type ApplyDiffStaging interface { + // StartStagingDiffToApply applies the new layer into a temporary directory. + // It returns a CleanupTempDirFunc which can be nil or set regardless if the function return an error or not. + // StagedAddition is only set when there is no error returned and the int64 value returns the size of the layer. + // This can be done without holding the storage lock, if a parent is given the caller must check for existence + // beforehand while holding a lock. + StartStagingDiffToApply(parent string, options ApplyDiffOpts) (tempdir.CleanupTempDirFunc, *tempdir.StagedAddition, int64, error) + // CommitStagedLayer commits the staged layer from StartStagingDiffToApply(). This must be done while holding the storage lock. + CommitStagedLayer(id string, commit *tempdir.StagedAddition) error +} + // Capabilities defines a list of capabilities a driver may implement. // These capabilities are not required; however, they do determine how a // graphdriver can be used. diff --git a/vendor/go.podman.io/storage/drivers/fsdiff.go b/vendor/go.podman.io/storage/drivers/fsdiff.go index 77f98d49da..89d18e6d35 100644 --- a/vendor/go.podman.io/storage/drivers/fsdiff.go +++ b/vendor/go.podman.io/storage/drivers/fsdiff.go @@ -151,7 +151,7 @@ func (gdw *NaiveDiffDriver) Changes(id string, idMappings *idtools.IDMappings, p // ApplyDiff extracts the changeset from the given diff into the // layer with the specified id and parent, returning the size of the // new layer in bytes. -func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, options ApplyDiffOpts) (int64, error) { +func (gdw *NaiveDiffDriver) ApplyDiff(id string, options ApplyDiffOpts) (int64, error) { driver := gdw.ProtoDriver if options.Mappings == nil { diff --git a/vendor/go.podman.io/storage/drivers/overlay/overlay.go b/vendor/go.podman.io/storage/drivers/overlay/overlay.go index c08e060466..2eb720c188 100644 --- a/vendor/go.podman.io/storage/drivers/overlay/overlay.go +++ b/vendor/go.podman.io/storage/drivers/overlay/overlay.go @@ -995,6 +995,49 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) (retErr return d.create(id, parent, opts, true) } +// getLayerPermissions returns the base permissions to use for the layer directories. +// The first return value is the idPair to create the possible parent directories with. +// The second return value is the mode how it should be stored on disk. +// The third return value is the mode the layer expects to have which may be stored +// in an xattr when using forceMask, without forceMask both values are the same. +func (d *Driver) getLayerPermissions(parent string, uidMaps, gidMaps []idtools.IDMap) (idtools.IDPair, idtools.Stat, idtools.Stat, error) { + rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + if err != nil { + return idtools.IDPair{}, idtools.Stat{}, idtools.Stat{}, err + } + + idPair := idtools.IDPair{ + UID: rootUID, + GID: rootGID, + } + + st := idtools.Stat{IDs: idPair, Mode: defaultPerms} + + if parent != "" { + parentBase := d.dir(parent) + parentDiff := filepath.Join(parentBase, "diff") + if xSt, err := idtools.GetContainersOverrideXattr(parentDiff); err == nil { + st = xSt + } else { + systemSt, err := system.Stat(parentDiff) + if err != nil { + return idtools.IDPair{}, idtools.Stat{}, idtools.Stat{}, err + } + st.IDs.UID = int(systemSt.UID()) + st.IDs.GID = int(systemSt.GID()) + st.Mode = os.FileMode(systemSt.Mode()) + } + } + + forcedSt := st + if d.options.forceMask != nil { + forcedSt.IDs = idPair + forcedSt.Mode = *d.options.forceMask + } + + return idPair, forcedSt, st, nil +} + func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnly bool) (retErr error) { dir, homedir, _ := d.dir2(id, readOnly) @@ -1013,38 +1056,15 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnl return err } - rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) + idPair, forcedSt, st, err := d.getLayerPermissions(parent, uidMaps, gidMaps) if err != nil { return err } - idPair := idtools.IDPair{ - UID: rootUID, - GID: rootGID, - } - if err := idtools.MkdirAllAndChownNew(path.Dir(dir), 0o755, idPair); err != nil { return err } - st := idtools.Stat{IDs: idPair, Mode: defaultPerms} - - if parent != "" { - parentBase := d.dir(parent) - parentDiff := filepath.Join(parentBase, "diff") - if xSt, err := idtools.GetContainersOverrideXattr(parentDiff); err == nil { - st = xSt - } else { - systemSt, err := system.Stat(parentDiff) - if err != nil { - return err - } - st.IDs.UID = int(systemSt.UID()) - st.IDs.GID = int(systemSt.GID()) - st.Mode = os.FileMode(systemSt.Mode()) - } - } - if err := fileutils.Lexists(dir); err == nil { logrus.Warnf("Trying to create a layer %#v while directory %q already exists; removing it first", id, dir) // Don’t just os.RemoveAll(dir) here; d.Remove also removes the link in linkDir, @@ -1088,12 +1108,6 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnl } } - forcedSt := st - if d.options.forceMask != nil { - forcedSt.IDs = idPair - forcedSt.Mode = *d.options.forceMask - } - diff := path.Join(dir, "diff") if err := idtools.MkdirAs(diff, forcedSt.Mode, forcedSt.IDs.UID, forcedSt.IDs.GID); err != nil { return err @@ -1356,6 +1370,14 @@ func (d *Driver) getTempDirRoot(id string) string { return filepath.Join(d.home, tempDirName) } +// getTempDirRootForNewLayer returns the correct temp directory root based on where +// the layer should be created. +// +// This must be kept in sync with GetTempDirRootDirs(). +func (d *Driver) getTempDirRootForNewLayer() string { + return filepath.Join(d.homeDirForImageStore(), tempDirName) +} + func (d *Driver) DeferredRemove(id string) (tempdir.CleanupTempDirFunc, error) { tempDirRoot := d.getTempDirRoot(id) t, err := tempdir.NewTempDir(tempDirRoot) @@ -2369,31 +2391,94 @@ func (d *Driver) DifferTarget(id string) (string, error) { return d.getDiffPath(id) } -// ApplyDiff applies the new layer into a root -func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) (size int64, err error) { - if !d.isParent(id, parent) { - if d.options.ignoreChownErrors { - options.IgnoreChownErrors = d.options.ignoreChownErrors - } - if d.options.forceMask != nil { - options.ForceMask = d.options.forceMask - } - return d.naiveDiff.ApplyDiff(id, parent, options) +// StartStagingDiffToApply applies the new layer into a temporary directory. +// It returns a CleanupTempDirFunc which can be nil or set regardless if the function return an error or not. +// StagedAddition is only set when there is no error returned and the int64 value returns the size of the layer. +// This can be done without holding the storage lock, if a parent is given the caller must check for existence +// beforehand while holding a lock. +// +// This API is experimental and can be changed without bumping the major version number. +func (d *Driver) StartStagingDiffToApply(parent string, options graphdriver.ApplyDiffOpts) (tempdir.CleanupTempDirFunc, *tempdir.StagedAddition, int64, error) { + tempDirRoot := d.getTempDirRootForNewLayer() + t, err := tempdir.NewTempDir(tempDirRoot) + if err != nil { + return nil, nil, -1, err } + sa, err := t.StageAddition() + if err != nil { + return t.Cleanup, nil, -1, err + } + + _, forcedSt, st, err := d.getLayerPermissions(parent, options.Mappings.UIDs(), options.Mappings.GIDs()) + if err != nil { + // If we have a ENOENT it means the parent was removed which can happen as we are unlocked here. + // In this case also wrap ErrLayerUnknown which some callers can handle to retry after recreating the parent. + if errors.Is(err, fs.ErrNotExist) { + err = fmt.Errorf("parent layer %q: %w: %w", parent, graphdriver.ErrLayerUnknown, err) + } + return t.Cleanup, nil, -1, err + } + + if err := idtools.MkdirAs(sa.Path, forcedSt.Mode, forcedSt.IDs.UID, forcedSt.IDs.GID); err != nil { + return t.Cleanup, nil, -1, err + } + + if d.options.forceMask != nil { + st.Mode |= os.ModeDir + if err := idtools.SetContainersOverrideXattr(sa.Path, st); err != nil { + return t.Cleanup, nil, -1, err + } + } + + size, err := d.applyDiff(sa.Path, options) + if err != nil { + return t.Cleanup, nil, -1, err + } + + return t.Cleanup, sa, size, nil +} + +// CommitStagedLayer that was created with StartStagingDiffToApply(). +// +// This API is experimental and can be changed without bumping the major version number. +func (d *Driver) CommitStagedLayer(id string, sa *tempdir.StagedAddition) error { + applyDir, err := d.getDiffPath(id) + if err != nil { + return err + } + + // The os.Rename() function used by CommitFunc errors when the target directory already + // exists, as such delete the dir. The create() function creates it and it would be more + // complicated to code in a way that it didn't create it. + if err := os.Remove(applyDir); err != nil { + return err + } + + return sa.Commit(applyDir) +} + +// ApplyDiff applies the new layer into a root +func (d *Driver) ApplyDiff(id string, options graphdriver.ApplyDiffOpts) (size int64, err error) { + applyDir, err := d.getDiffPath(id) + if err != nil { + return 0, err + } + return d.applyDiff(applyDir, options) +} + +// ApplyDiff applies the new layer into a root. +// This can run concurrently with any other driver operations, as such it is the +// callers responsibility to ensure the target path passed is safe to use if that is the case. +func (d *Driver) applyDiff(target string, options graphdriver.ApplyDiffOpts) (size int64, err error) { idMappings := options.Mappings if idMappings == nil { idMappings = &idtools.IDMappings{} } - applyDir, err := d.getDiffPath(id) - if err != nil { - return 0, err - } - - logrus.Debugf("Applying tar in %s", applyDir) + logrus.Debugf("Applying tar in %s", target) // Overlay doesn't need the parent id to apply the diff - if err := untar(options.Diff, applyDir, &archive.TarOptions{ + if err := untar(options.Diff, target, &archive.TarOptions{ UIDMaps: idMappings.UIDs(), GIDMaps: idMappings.GIDs(), IgnoreChownErrors: d.options.ignoreChownErrors, @@ -2404,7 +2489,7 @@ func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) return 0, err } - return directory.Size(applyDir) + return directory.Size(target) } func (d *Driver) getComposefsData(id string) string { diff --git a/vendor/go.podman.io/storage/drivers/template.go b/vendor/go.podman.io/storage/drivers/template.go deleted file mode 100644 index 03da13c5bb..0000000000 --- a/vendor/go.podman.io/storage/drivers/template.go +++ /dev/null @@ -1,52 +0,0 @@ -package graphdriver - -import ( - "github.com/sirupsen/logrus" - "go.podman.io/storage/pkg/idtools" -) - -// TemplateDriver is just barely enough of a driver that we can implement a -// naive version of CreateFromTemplate on top of it. -type TemplateDriver interface { - DiffDriver - CreateReadWrite(id, parent string, opts *CreateOpts) error - Create(id, parent string, opts *CreateOpts) error - Remove(id string) error -} - -// CreateFromTemplate creates a layer with the same contents and parent as -// another layer. Internally, it may even depend on that other layer -// continuing to exist, as if it were actually a child of the child layer. -func NaiveCreateFromTemplate(d TemplateDriver, id, template string, templateIDMappings *idtools.IDMappings, parent string, parentIDMappings *idtools.IDMappings, opts *CreateOpts, readWrite bool) error { - var err error - if readWrite { - err = d.CreateReadWrite(id, parent, opts) - } else { - err = d.Create(id, parent, opts) - } - if err != nil { - return err - } - diff, err := d.Diff(template, templateIDMappings, parent, parentIDMappings, opts.MountLabel) - if err != nil { - if err2 := d.Remove(id); err2 != nil { - logrus.Errorf("Removing layer %q: %v", id, err2) - } - return err - } - defer diff.Close() - - applyOptions := ApplyDiffOpts{ - Diff: diff, - Mappings: templateIDMappings, - MountLabel: opts.MountLabel, - IgnoreChownErrors: opts.ignoreChownErrors, - } - if _, err = d.ApplyDiff(id, parent, applyOptions); err != nil { - if err2 := d.Remove(id); err2 != nil { - logrus.Errorf("Removing layer %q: %v", id, err2) - } - return err - } - return nil -} diff --git a/vendor/go.podman.io/storage/drivers/vfs/driver.go b/vendor/go.podman.io/storage/drivers/vfs/driver.go index ffd3bd24ea..b90c2046cf 100644 --- a/vendor/go.podman.io/storage/drivers/vfs/driver.go +++ b/vendor/go.podman.io/storage/drivers/vfs/driver.go @@ -132,11 +132,11 @@ func (d *Driver) CreateFromTemplate(id, template string, templateIDMappings *idt } // ApplyDiff applies the new layer into a root -func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) (size int64, err error) { +func (d *Driver) ApplyDiff(id string, options graphdriver.ApplyDiffOpts) (size int64, err error) { if d.ignoreChownErrors { options.IgnoreChownErrors = d.ignoreChownErrors } - return d.naiveDiff.ApplyDiff(id, parent, options) + return d.naiveDiff.ApplyDiff(id, options) } // CreateReadWrite creates a layer that is writable for use as a container diff --git a/vendor/go.podman.io/storage/internal/tempdir/tempdir.go b/vendor/go.podman.io/storage/internal/tempdir/tempdir.go index 6522c45d18..bd852688c8 100644 --- a/vendor/go.podman.io/storage/internal/tempdir/tempdir.go +++ b/vendor/go.podman.io/storage/internal/tempdir/tempdir.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path/filepath" + "strconv" "strings" "github.com/sirupsen/logrus" @@ -91,6 +92,26 @@ type TempDir struct { counter uint64 } +// StagedAddition is a temporary object which holds the information of where to +// put the data into and then use Commit() to move the data into the final location. +type StagedAddition struct { + // Path is the temporary path. The path is not created so caller must create + // a file or directory on it in order to use Commit(). The path is only valid + // until Commit() is called or until the TempDir instance Cleanup() method is used. + Path string +} + +// Commit the staged content into its final destination by using os.Rename(). +// That means the dest must be on the same on the same fs as the root directory +// that was given to NewTempDir() and the dest must not exist yet. +// Commit must only be called once per instance returned from the +// StagedAddition() call. +func (s *StagedAddition) Commit(destination string) error { + err := os.Rename(s.Path, destination) + s.Path = "" // invalidate Path to avoid reuse + return err +} + // CleanupTempDirFunc is a function type that can be returned by operations // which need to perform cleanup actions later. type CleanupTempDirFunc func() error @@ -190,6 +211,23 @@ func NewTempDir(rootDir string) (*TempDir, error) { return td, nil } +// StageAddition creates a new temporary path that is returned as field in the StagedAddition +// struct. The returned type StagedAddition has a Commit() function to move the content from +// the temporary location to the final one. +// +// The caller MUST call Commit() before Cleanup() is called on the TempDir, otherwise the +// staged content will be deleted and the Commit() will fail. +// If the TempDir has been cleaned up already, this method will return an error. +func (td *TempDir) StageAddition() (*StagedAddition, error) { + if td.tempDirLock == nil { + return nil, fmt.Errorf("temp dir instance not initialized or already cleaned up") + } + fileName := strconv.FormatUint(td.counter, 10) + "-addition" + tmpAddPath := filepath.Join(td.tempDirPath, fileName) + td.counter++ + return &StagedAddition{Path: tmpAddPath}, nil +} + // StageDeletion moves the specified file into the instance's temporary directory. // The temporary directory must already exist (created during NewTempDir). // Files are renamed with a counter-based prefix (e.g., "0-filename", "1-filename") to ensure uniqueness. diff --git a/vendor/go.podman.io/storage/layers.go b/vendor/go.podman.io/storage/layers.go index 64d3f5c72c..d485c9b4fa 100644 --- a/vendor/go.podman.io/storage/layers.go +++ b/vendor/go.podman.io/storage/layers.go @@ -31,6 +31,7 @@ import ( "go.podman.io/storage/pkg/ioutils" "go.podman.io/storage/pkg/lockfile" "go.podman.io/storage/pkg/mount" + "go.podman.io/storage/pkg/pools" "go.podman.io/storage/pkg/stringid" "go.podman.io/storage/pkg/system" "go.podman.io/storage/pkg/tarlog" @@ -195,11 +196,53 @@ type DiffOptions struct { Compression *archive.Compression } -// stagedLayerOptions are the options passed to .create to populate a staged +// layerCreationContents are the options passed to .create to populate a staged // layer -type stagedLayerOptions struct { +type layerCreationContents struct { + // These are used via the zstd:chunked pull paths DiffOutput *drivers.DriverWithDifferOutput DiffOptions *drivers.ApplyDiffWithDifferOpts + + // stagedLayerExtraction is used by the normal tar layer extraction. + stagedLayerExtraction *maybeStagedLayerExtraction +} + +// maybeStagedLayerExtraction is a helper to encapsulate details around extracting +// a layer potentially before we even take a look if the driver implements the +// ApplyDiffStaging interface. +// This should be initialized with layerStore.newMaybeStagedLayerExtraction() +type maybeStagedLayerExtraction struct { + // diff contains the tar archive, can be compressed, must be non nil, but can be at EOF when the content was already staged + diff io.Reader + // staging interface of the storage driver, set when the driver supports staging and nil otherwise + staging drivers.ApplyDiffStaging + // result is a placeholder for the applyDiff() result so we can pass that down the stack easily. + // If result is not nil the layer was staged successfully, if this is set stagedTarSplit and + // stagedLayer must be set as well. + result *applyDiffResult + + // stagedTarSplit is the temp file where we staged the tar split file + stagedTarSplit *tempdir.StagedAddition + // stagedLayer is the temp directory where we staged the extracted layer content + stagedLayer *tempdir.StagedAddition + + // cleanupFuncs contains the set of tempdir cleanup function that get executed in cleanup() + cleanupFuncs []tempdir.CleanupTempDirFunc +} + +type applyDiffResult struct { + compressedDigest digest.Digest + compressedSize int64 + compressionType archive.Compression + uncompressedDigest digest.Digest + uncompressedSize int64 + // size of the data, including the full size of sparse files, and excluding all metadata + // It is neither compressedSize nor uncompressedSize. + // The use case for this seems unclear, it gets returned in PutLayer() but in the Podman + // stack at least that value is never used so maybe we can look into removing this. + size int64 + uids []uint32 + gids []uint32 } // roLayerStore wraps a graph driver, adding the ability to refer to layers by @@ -216,6 +259,11 @@ type roLayerStore interface { // stopReading releases locks obtained by startReading. stopReading() + // checkIdOrNameConflict checks if the id or names are already in use and returns an + // error in that case. As Special case if the layer already exists it returns it as + // well together with the error. + checkIdOrNameConflict(id string, names []string) (*Layer, error) + // Exists checks if a layer with the specified name or ID is known. Exists(id string) bool @@ -288,7 +336,7 @@ type rwLayerStore interface { // underlying drivers do not themselves distinguish between writeable // and read-only layers. Returns the new layer structure and the size of the // diff which was applied to its parent to initialize its contents. - create(id string, parent *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, diff io.Reader, slo *stagedLayerOptions) (*Layer, int64, error) + create(id string, parent *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, contents *layerCreationContents) (*Layer, int64, error) // updateNames modifies names associated with a layer based on (op, names). updateNames(id string, names []string, op updateNameOperation) error @@ -354,6 +402,14 @@ type rwLayerStore interface { // Dedup deduplicates layers in the store. dedup(drivers.DedupArgs) (drivers.DedupResult, error) + + // newMaybeStagedLayerExtraction initializes a new maybeStagedLayerExtraction. The caller + // must call maybeStagedLayerExtraction.cleanup() to remove any temporary files. + newMaybeStagedLayerExtraction(diff io.Reader) *maybeStagedLayerExtraction + + // stageWithUnlockedStore stages the layer content without needing the store locked. + // If the driver does not support stage addition then this is a NOP and does nothing. + stageWithUnlockedStore(m *maybeStagedLayerExtraction, parent string, options *LayerOptions) error } type multipleLockFile struct { @@ -1307,13 +1363,8 @@ func (r *layerStore) Status() ([][2]string, error) { // Requires startWriting. func (r *layerStore) PutAdditionalLayer(id string, parentLayer *Layer, names []string, aLayer drivers.AdditionalLayer) (layer *Layer, err error) { - if duplicateLayer, idInUse := r.byid[id]; idInUse { - return duplicateLayer, ErrDuplicateID - } - for _, name := range names { - if _, nameInUse := r.byname[name]; nameInUse { - return nil, ErrDuplicateName - } + if layer, err := r.checkIdOrNameConflict(id, names); err != nil { + return layer, err } parent := "" @@ -1378,8 +1429,25 @@ func (r *layerStore) pickStoreLocation(volatile, writeable bool) layerLocations } } +// checkIdOrNameConflict checks if the id or names are already in use and returns an +// error in that case. As Special case if the layer already exists it returns it as +// well together with the error. +// +// Requires startReading or startWriting. +func (r *layerStore) checkIdOrNameConflict(id string, names []string) (*Layer, error) { + if duplicateLayer, idInUse := r.byid[id]; idInUse { + return duplicateLayer, ErrDuplicateID + } + for _, name := range names { + if _, nameInUse := r.byname[name]; nameInUse { + return nil, ErrDuplicateName + } + } + return nil, nil +} + // Requires startWriting. -func (r *layerStore) create(id string, parentLayer *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, diff io.Reader, slo *stagedLayerOptions) (layer *Layer, size int64, err error) { +func (r *layerStore) create(id string, parentLayer *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, contents *layerCreationContents) (layer *Layer, size int64, err error) { if moreOptions == nil { moreOptions = &LayerOptions{} } @@ -1400,14 +1468,8 @@ func (r *layerStore) create(id string, parentLayer *Layer, names []string, mount _, idInUse = r.byid[id] } } - if duplicateLayer, idInUse := r.byid[id]; idInUse { - return duplicateLayer, -1, ErrDuplicateID - } - names = dedupeStrings(names) - for _, name := range names { - if _, nameInUse := r.byname[name]; nameInUse { - return nil, -1, ErrDuplicateName - } + if layer, err := r.checkIdOrNameConflict(id, names); err != nil { + return layer, -1, err } parent := "" if parentLayer != nil { @@ -1568,18 +1630,31 @@ func (r *layerStore) create(id string, parentLayer *Layer, names []string, mount } size = -1 - if diff != nil { - if size, err = r.applyDiffWithOptions(layer.ID, moreOptions, diff); err != nil { - cleanupFailureContext = "applying layer diff" - return nil, -1, err - } - } else if slo != nil { - if err := r.applyDiffFromStagingDirectory(layer.ID, slo.DiffOutput, slo.DiffOptions); err != nil { - cleanupFailureContext = "applying staged directory diff" - return nil, -1, err + if contents != nil { + if contents.stagedLayerExtraction != nil { + if contents.stagedLayerExtraction.result != nil { + // The layer is staged, just commit it and update the metadata. + if err := contents.stagedLayerExtraction.commitLayer(r, layer.ID); err != nil { + cleanupFailureContext = "committing staged layer diff" + return nil, -1, err + } + r.applyDiffResultToLayer(layer, contents.stagedLayerExtraction.result) + } else { + // The diff was not staged, apply it now here instead. + if size, err = r.applyDiffWithOptions(layer.ID, moreOptions, contents.stagedLayerExtraction.diff); err != nil { + cleanupFailureContext = "applying layer diff" + return nil, -1, err + } + } + } else { + // staging logic for the chunked pull path + if err := r.applyDiffFromStagingDirectory(layer.ID, contents.DiffOutput, contents.DiffOptions); err != nil { + cleanupFailureContext = "applying staged directory diff" + return nil, -1, err + } } } else { - // applyDiffWithOptions() would have updated r.bycompressedsum + // The layer creation content above would have updated r.bycompressedsum // and r.byuncompressedsum for us, but if we used a template // layer, we didn't call it, so add the new layer as candidates // for searches for layers by checksum @@ -2398,37 +2473,118 @@ func (r *layerStore) ApplyDiff(to string, diff io.Reader) (size int64, err error return r.applyDiffWithOptions(to, nil, diff) } +func createTarSplitFile(r *layerStore, layerID string) (*os.File, error) { + if err := os.MkdirAll(filepath.Dir(r.tspath(layerID)), 0o700); err != nil { + return nil, err + } + return os.OpenFile(r.tspath(layerID), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) +} + +// newMaybeStagedLayerExtraction initializes a new maybeStagedLayerExtraction. The caller +// must call maybeStagedLayerExtraction.cleanup() to remove any temporary files. +func (r *layerStore) newMaybeStagedLayerExtraction(diff io.Reader) *maybeStagedLayerExtraction { + m := &maybeStagedLayerExtraction{ + diff: diff, + } + if d, ok := r.driver.(drivers.ApplyDiffStaging); ok { + m.staging = d + } + return m +} + +func (sl *maybeStagedLayerExtraction) cleanup() error { + return tempdir.CleanupTemporaryDirectories(sl.cleanupFuncs...) +} + +// stageWithUnlockedStore stages the layer content without needing the store locked. +// If the driver does not support stage addition then this is a NOP and does nothing. +// This should be done without holding the storage lock, if a parent is given the caller +// must check for existence beforehand while holding a lock. +func (r *layerStore) stageWithUnlockedStore(sl *maybeStagedLayerExtraction, parent string, layerOptions *LayerOptions) (retErr error) { + if sl.staging == nil { + return nil + } + td, err := tempdir.NewTempDir(filepath.Join(r.layerdir, tempDirPath)) + if err != nil { + return err + } + sl.cleanupFuncs = append(sl.cleanupFuncs, td.Cleanup) + + stagedTarSplit, err := td.StageAddition() + if err != nil { + return err + } + sl.stagedTarSplit = stagedTarSplit + + f, err := os.OpenFile(stagedTarSplit.Path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) + if err != nil { + return err + } + // make sure to check for errors on close and return that one. + defer func() { + closeErr := f.Close() + if retErr == nil { + retErr = closeErr + } + }() + + result, err := applyDiff(layerOptions, sl.diff, f, func(payload io.Reader) (int64, error) { + cleanup, stagedLayer, size, err := sl.staging.StartStagingDiffToApply(parent, drivers.ApplyDiffOpts{ + Diff: payload, + Mappings: idtools.NewIDMappingsFromMaps(layerOptions.UIDMap, layerOptions.GIDMap), + // MountLabel is not supported for the unlocked extraction, see the comment in (*store).PutLayer() + MountLabel: "", + }) + sl.cleanupFuncs = append(sl.cleanupFuncs, cleanup) + sl.stagedLayer = stagedLayer + return size, err + }) + if err != nil { + return err + } + + if err := f.Sync(); err != nil { + return fmt.Errorf("sync staged tar-split file: %w", err) + } + + sl.result = result + return nil +} + +// commitLayer() commits the content that was staged in stageWithUnlockedStore() +// // Requires startWriting. -func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, diff io.Reader) (size int64, err error) { - if !r.lockfile.IsReadWrite() { - return -1, fmt.Errorf("not allowed to modify layer contents at %q: %w", r.layerdir, ErrStoreIsReadOnly) - } - - layer, ok := r.lookup(to) - if !ok { - return -1, ErrLayerUnknown +func (sl *maybeStagedLayerExtraction) commitLayer(r *layerStore, layerID string) error { + err := sl.stagedTarSplit.Commit(r.tspath(layerID)) + if err != nil { + return err } + return sl.staging.CommitStagedLayer(layerID, sl.stagedLayer) +} +// applyDiff can be called without holding any store locks so if the supplied +// applyDriverFunc requires locking the caller must ensure proper locking. +func applyDiff(layerOptions *LayerOptions, diff io.Reader, tarSplitFile *os.File, applyDriverFunc func(io.Reader) (int64, error)) (*applyDiffResult, error) { header := make([]byte, 10240) n, err := diff.Read(header) if err != nil && err != io.EOF { - return -1, err + return nil, err } compression := archive.DetectCompression(header[:n]) defragmented := io.MultiReader(bytes.NewReader(header[:n]), diff) - // Decide if we need to compute digests - var compressedDigest, uncompressedDigest digest.Digest // = "" + result := applyDiffResult{} + var compressedDigester, uncompressedDigester digest.Digester // = nil if layerOptions != nil && layerOptions.OriginalDigest != "" && layerOptions.OriginalDigest.Algorithm() == digest.Canonical { - compressedDigest = layerOptions.OriginalDigest + result.compressedDigest = layerOptions.OriginalDigest } else { compressedDigester = digest.Canonical.Digester() } if layerOptions != nil && layerOptions.UncompressedDigest != "" && layerOptions.UncompressedDigest.Algorithm() == digest.Canonical { - uncompressedDigest = layerOptions.UncompressedDigest + result.uncompressedDigest = layerOptions.UncompressedDigest } else if compression != archive.Uncompressed { uncompressedDigester = digest.Canonical.Digester() } @@ -2442,13 +2598,15 @@ func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, compressedCounter := ioutils.NewWriteCounter(compressedWriter) defragmented = io.TeeReader(defragmented, compressedCounter) - tsdata := bytes.Buffer{} + tarSplitWriter := pools.BufioWriter32KPool.Get(tarSplitFile) + defer pools.BufioWriter32KPool.Put(tarSplitWriter) + uidLog := make(map[uint32]struct{}) gidLog := make(map[uint32]struct{}) var uncompressedCounter *ioutils.WriteCounter - size, err = func() (int64, error) { // A scope for defer - compressor, err := pgzip.NewWriterLevel(&tsdata, pgzip.BestSpeed) + size, err := func() (int64, error) { // A scope for defer + compressor, err := pgzip.NewWriterLevel(tarSplitWriter, pgzip.BestSpeed) if err != nil { return -1, err } @@ -2481,62 +2639,108 @@ func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, if err != nil { return -1, err } + + return applyDriverFunc(payload) + }() + if err != nil { + return nil, err + } + + if err := tarSplitWriter.Flush(); err != nil { + return nil, fmt.Errorf("failed to flush tar-split writer buffer: %w", err) + } + + if compressedDigester != nil { + result.compressedDigest = compressedDigester.Digest() + } + if uncompressedDigester != nil { + result.uncompressedDigest = uncompressedDigester.Digest() + } + if result.uncompressedDigest == "" && compression == archive.Uncompressed { + result.uncompressedDigest = result.compressedDigest + } + + if layerOptions != nil && layerOptions.OriginalDigest != "" && layerOptions.OriginalSize != nil { + result.compressedSize = *layerOptions.OriginalSize + } else { + result.compressedSize = compressedCounter.Count + } + result.uncompressedSize = uncompressedCounter.Count + result.compressionType = compression + + result.uids = make([]uint32, 0, len(uidLog)) + for uid := range uidLog { + result.uids = append(result.uids, uid) + } + slices.Sort(result.uids) + result.gids = make([]uint32, 0, len(gidLog)) + for gid := range gidLog { + result.gids = append(result.gids, gid) + } + slices.Sort(result.gids) + + result.size = size + + return &result, err +} + +// Requires startWriting. +func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, diff io.Reader) (_ int64, retErr error) { + if !r.lockfile.IsReadWrite() { + return -1, fmt.Errorf("not allowed to modify layer contents at %q: %w", r.layerdir, ErrStoreIsReadOnly) + } + + layer, ok := r.lookup(to) + if !ok { + return -1, ErrLayerUnknown + } + + tarSplitFile, err := createTarSplitFile(r, layer.ID) + if err != nil { + return -1, err + } + // make sure to check for errors on close and return that one. + defer func() { + closeErr := tarSplitFile.Close() + if retErr == nil { + retErr = closeErr + } + }() + + result, err := applyDiff(layerOptions, diff, tarSplitFile, func(payload io.Reader) (int64, error) { options := drivers.ApplyDiffOpts{ Diff: payload, Mappings: r.layerMappings(layer), MountLabel: layer.MountLabel, } - size, err := r.driver.ApplyDiff(layer.ID, layer.Parent, options) - if err != nil { - return -1, err - } - return size, err - }() + return r.driver.ApplyDiff(layer.ID, options) + }) if err != nil { return -1, err } - if err := os.MkdirAll(filepath.Dir(r.tspath(layer.ID)), 0o700); err != nil { - return -1, err - } - if err := ioutils.AtomicWriteFile(r.tspath(layer.ID), tsdata.Bytes(), 0o600); err != nil { - return -1, err - } - if compressedDigester != nil { - compressedDigest = compressedDigester.Digest() - } - if uncompressedDigester != nil { - uncompressedDigest = uncompressedDigester.Digest() - } - if uncompressedDigest == "" && compression == archive.Uncompressed { - uncompressedDigest = compressedDigest + if err := tarSplitFile.Sync(); err != nil { + return -1, fmt.Errorf("sync tar-split file: %w", err) } - updateDigestMap(&r.bycompressedsum, layer.CompressedDigest, compressedDigest, layer.ID) - layer.CompressedDigest = compressedDigest - if layerOptions != nil && layerOptions.OriginalDigest != "" && layerOptions.OriginalSize != nil { - layer.CompressedSize = *layerOptions.OriginalSize - } else { - layer.CompressedSize = compressedCounter.Count - } - updateDigestMap(&r.byuncompressedsum, layer.UncompressedDigest, uncompressedDigest, layer.ID) - layer.UncompressedDigest = uncompressedDigest - layer.UncompressedSize = uncompressedCounter.Count - layer.CompressionType = compression - layer.UIDs = make([]uint32, 0, len(uidLog)) - for uid := range uidLog { - layer.UIDs = append(layer.UIDs, uid) - } - slices.Sort(layer.UIDs) - layer.GIDs = make([]uint32, 0, len(gidLog)) - for gid := range gidLog { - layer.GIDs = append(layer.GIDs, gid) - } - slices.Sort(layer.GIDs) + r.applyDiffResultToLayer(layer, result) err = r.saveFor(layer) - return size, err + return result.size, err +} + +// Requires startWriting. +func (r *layerStore) applyDiffResultToLayer(layer *Layer, result *applyDiffResult) { + updateDigestMap(&r.bycompressedsum, layer.CompressedDigest, result.compressedDigest, layer.ID) + layer.CompressedDigest = result.compressedDigest + layer.CompressedSize = result.compressedSize + updateDigestMap(&r.byuncompressedsum, layer.UncompressedDigest, result.uncompressedDigest, layer.ID) + layer.UncompressedDigest = result.uncompressedDigest + layer.UncompressedSize = result.uncompressedSize + layer.CompressionType = result.compressionType + layer.UIDs = result.uids + layer.GIDs = result.gids } // Requires (startReading or?) startWriting. @@ -2553,7 +2757,7 @@ func (r *layerStore) DifferTarget(id string) (string, error) { } // Requires startWriting. -func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *drivers.DriverWithDifferOutput, options *drivers.ApplyDiffWithDifferOpts) error { +func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *drivers.DriverWithDifferOutput, options *drivers.ApplyDiffWithDifferOpts) (retErr error) { ddriver, ok := r.driver.(drivers.DriverWithDiffer) if !ok { return ErrNotSupported @@ -2597,10 +2801,23 @@ func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *driver } if diffOutput.TarSplit != nil { - tsdata := bytes.Buffer{} - compressor, err := pgzip.NewWriterLevel(&tsdata, pgzip.BestSpeed) + tarSplitFile, err := createTarSplitFile(r, layer.ID) if err != nil { - compressor = pgzip.NewWriter(&tsdata) + return err + } + // make sure to check for errors on close and return that one. + defer func() { + closeErr := tarSplitFile.Close() + if retErr == nil { + retErr = closeErr + } + }() + tarSplitWriter := pools.BufioWriter32KPool.Get(tarSplitFile) + defer pools.BufioWriter32KPool.Put(tarSplitWriter) + + compressor, err := pgzip.NewWriterLevel(tarSplitWriter, pgzip.BestSpeed) + if err != nil { + compressor = pgzip.NewWriter(tarSplitWriter) } if _, err := diffOutput.TarSplit.Seek(0, io.SeekStart); err != nil { return err @@ -2614,11 +2831,12 @@ func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *driver return err } compressor.Close() - if err := os.MkdirAll(filepath.Dir(r.tspath(layer.ID)), 0o700); err != nil { - return err + + if err := tarSplitWriter.Flush(); err != nil { + return fmt.Errorf("failed to flush tar-split writer buffer: %w", err) } - if err := ioutils.AtomicWriteFile(r.tspath(layer.ID), tsdata.Bytes(), 0o600); err != nil { - return err + if err := tarSplitFile.Sync(); err != nil { + return fmt.Errorf("sync tar-split file: %w", err) } } for k, v := range diffOutput.BigData { diff --git a/vendor/go.podman.io/storage/pkg/lockfile/lockfile.go b/vendor/go.podman.io/storage/pkg/lockfile/lockfile.go index 52b4fe597e..3a8a4bc398 100644 --- a/vendor/go.podman.io/storage/pkg/lockfile/lockfile.go +++ b/vendor/go.podman.io/storage/pkg/lockfile/lockfile.go @@ -420,7 +420,10 @@ func (l *LockFile) tryLock(lType rawfilelock.LockType) error { if !success { return fmt.Errorf("resource temporarily unavailable") } - l.stateMutex.Lock() + if !l.stateMutex.TryLock() { + rwMutexUnlocker() + return fmt.Errorf("resource temporarily unavailable") + } defer l.stateMutex.Unlock() if l.counter == 0 { // If we're the first reference on the lock, we need to open the file again. diff --git a/vendor/go.podman.io/storage/store.go b/vendor/go.podman.io/storage/store.go index 84019a4947..3d8ea50759 100644 --- a/vendor/go.podman.io/storage/store.go +++ b/vendor/go.podman.io/storage/store.go @@ -1449,12 +1449,48 @@ func (s *store) canUseShifting(uidmap, gidmap []idtools.IDMap) bool { return s.graphDriver.SupportsShifting(uidmap, gidmap) } +// On entry: +// - rlstore must be locked for reading or writing +// - rlstores MUST NOT be locked +// Returns an extra unlock function to unlock any potentially read locked rlstores by this function. +// The unlock function is always set and thus must always be called. +func getParentLayer(rlstore roLayerStore, rlstores []roLayerStore, parent string) (*Layer, func(), error) { + // function we return to the caller so the caller gets the right stores locked and can unlock at the proper time themselves + var lockedLayerStores []roLayerStore + unlock := func() { + for _, i := range lockedLayerStores { + i.stopReading() + } + } + for _, l := range append([]roLayerStore{rlstore}, rlstores...) { + lstore := l + if lstore != rlstore { + if err := lstore.startReading(); err != nil { + return nil, unlock, err + } + lockedLayerStores = append(lockedLayerStores, lstore) + } + if l, err := lstore.Get(parent); err == nil && l != nil { + return l, unlock, nil + } + } + + return nil, unlock, ErrLayerUnknown +} + // On entry: // - rlstore must be locked for writing // - rlstores MUST NOT be locked -func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, parent string, names []string, mountLabel string, writeable bool, lOptions *LayerOptions, diff io.Reader, slo *stagedLayerOptions) (*Layer, int64, error) { +// +// Returns the new copied LayerOptions with mappings set, the parent Layer and +// an extra unlock function to unlock any potentially read locked rlstores by this function. +// The unlock function is always set and thus must always be called. +func populateLayerOptions(s *store, rlstore rwLayerStore, rlstores []roLayerStore, parent string, lOptions *LayerOptions) (*LayerOptions, *Layer, func(), error) { + // WARNING: Update also the freshLayer checks in store.PutLayer if adding more logic here. var parentLayer *Layer var options LayerOptions + // make sure we always return a valid func instead of nil so the caller can call it without checking + unlock := func() {} if lOptions != nil { options = *lOptions options.BigData = slices.Clone(lOptions.BigData) @@ -1469,53 +1505,32 @@ func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, pare uidMap := options.UIDMap gidMap := options.GIDMap if parent != "" { - var ilayer *Layer - for _, l := range append([]roLayerStore{rlstore}, rlstores...) { - lstore := l - if lstore != rlstore { - if err := lstore.startReading(); err != nil { - return nil, -1, err - } - defer lstore.stopReading() - } - if l, err := lstore.Get(parent); err == nil && l != nil { - ilayer = l - parent = ilayer.ID - break - } + var err error + parentLayer, unlock, err = getParentLayer(rlstore, rlstores, parent) + if err != nil { + return nil, nil, unlock, err } - if ilayer == nil { - return nil, -1, ErrLayerUnknown - } - parentLayer = ilayer if err := s.containerStore.startWriting(); err != nil { - return nil, -1, err + return nil, nil, unlock, err } defer s.containerStore.stopWriting() containers, err := s.containerStore.Containers() if err != nil { - return nil, -1, err + return nil, nil, unlock, err } for _, container := range containers { if container.LayerID == parent { - return nil, -1, ErrParentIsContainer + return nil, nil, unlock, ErrParentIsContainer } } if !options.HostUIDMapping && len(options.UIDMap) == 0 { - uidMap = ilayer.UIDMap + uidMap = parentLayer.UIDMap } if !options.HostGIDMapping && len(options.GIDMap) == 0 { - gidMap = ilayer.GIDMap + gidMap = parentLayer.GIDMap } } else { - // FIXME? It’s unclear why we are holding containerStore locked here at all - // (and because we are not modifying it, why it is a write lock, not a read lock). - if err := s.containerStore.startWriting(); err != nil { - return nil, -1, err - } - defer s.containerStore.stopWriting() - if !options.HostUIDMapping && len(options.UIDMap) == 0 { uidMap = s.uidMap } @@ -1533,7 +1548,7 @@ func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, pare GIDMap: copySlicePreferringNil(gidMap), } } - return rlstore.create(id, parentLayer, names, mountLabel, nil, &options, writeable, diff, slo) + return &options, parentLayer, unlock, nil } func (s *store) PutLayer(id, parent string, names []string, mountLabel string, writeable bool, lOptions *LayerOptions, diff io.Reader) (*Layer, int64, error) { @@ -1541,11 +1556,92 @@ func (s *store) PutLayer(id, parent string, names []string, mountLabel string, w if err != nil { return nil, -1, err } + + var ( + contents *layerCreationContents + options *LayerOptions + parentLayer *Layer + ) + + if diff != nil { + m := rlstore.newMaybeStagedLayerExtraction(diff) + defer func() { + if err := m.cleanup(); err != nil { + logrus.Errorf("Error cleaning up temporary directories: %v", err) + } + }() + // driver can do unlocked staging so do that without holding the layer lock + // Special case we only support it when no mount label is used. c/image doesn't set it for layers + // and the overlay driver doesn't use it for extract today so it would be safe even when set but + // that is not exactly obvious and if someone would implement the ApplyDiffStaging interface for + // another driver that may be no longer true. So for now simply fall back to the locked extract path + // to ensure we don't cause any weird issues here. + if m.staging != nil && mountLabel == "" { + // func so we have a scope for defer, we don't want to hold the lock for stageWithUnlockedStore() + layer, err := func() (*Layer, error) { + if err := rlstore.startWriting(); err != nil { + return nil, err + } + defer rlstore.stopWriting() + + if layer, err := rlstore.checkIdOrNameConflict(id, names); err != nil { + return layer, err + } + + var unlockLayerStores func() + options, parentLayer, unlockLayerStores, err = populateLayerOptions(s, rlstore, rlstores, parent, lOptions) + unlockLayerStores() + return nil, err + }() + if err != nil { + return layer, -1, err + } + + // make sure to use the resolved full ID if there is a parent + if parentLayer != nil { + parent = parentLayer.ID + } + + if err := rlstore.stageWithUnlockedStore(m, parent, options); err != nil { + return nil, -1, err + } + } + + contents = &layerCreationContents{ + stagedLayerExtraction: m, + } + } + if err := rlstore.startWriting(); err != nil { return nil, -1, err } defer rlstore.stopWriting() - return s.putLayer(rlstore, rlstores, id, parent, names, mountLabel, writeable, lOptions, diff, nil) + + if options == nil { + var unlockLayerStores func() + options, parentLayer, unlockLayerStores, err = populateLayerOptions(s, rlstore, rlstores, parent, lOptions) + defer unlockLayerStores() + if err != nil { + return nil, -1, err + } + } else if parent != "" { + // We used the staged extraction without holding the lock. + // Check again that the parent layer is still valid and exists. + freshLayer, unlockLayerStores, err := getParentLayer(rlstore, rlstores, parent) + defer unlockLayerStores() + if err != nil { + return nil, -1, err + } + // In populateLayerOptions() we get the ID mappings in order to extract correctly, ensure the freshly + // looked up parent Layer still has the same mappings to prevent silent UID/GID corruption. + if !slices.Equal(freshLayer.UIDMap, parentLayer.UIDMap) || !slices.Equal(freshLayer.GIDMap, parentLayer.GIDMap) { + // Fatal problem. Mappings changed so the parent must be considered different now. + // Since we consumed the diff there is no we to recover, return error to caller. The caller would need to retry. + // How likely is that and would need to return a special error so c/image could do the retries? + return nil, -1, fmt.Errorf("error during staged layer apply, parent layer %q changed id mappings while the content was extracted, must retry layer creation", parent) + } + } + return rlstore.create(id, parentLayer, names, mountLabel, nil, options, writeable, contents) } func (s *store) CreateLayer(id, parent string, names []string, mountLabel string, writeable bool, options *LayerOptions) (*Layer, error) { @@ -1753,7 +1849,7 @@ func (s *store) imageTopLayerForMapping(image *Image, ristore roImageStore, rlst } } layerOptions.TemplateLayer = layer.ID - mappedLayer, _, err := rlstore.create("", parentLayer, nil, layer.MountLabel, nil, &layerOptions, false, nil, nil) + mappedLayer, _, err := rlstore.create("", parentLayer, nil, layer.MountLabel, nil, &layerOptions, false, nil) if err != nil { return nil, fmt.Errorf("creating an ID-mapped copy of layer %q: %w", layer.ID, err) } @@ -1924,7 +2020,7 @@ func (s *store) CreateContainer(id string, names []string, image, layer, metadat options.Flags[mountLabelFlag] = mountLabel } - clayer, _, err := rlstore.create(layer, imageTopLayer, nil, mlabel, options.StorageOpt, layerOptions, true, nil, nil) + clayer, _, err := rlstore.create(layer, imageTopLayer, nil, mlabel, options.StorageOpt, layerOptions, true, nil) if err != nil { return nil, err } @@ -3182,11 +3278,16 @@ func (s *store) ApplyStagedLayer(args ApplyStagedLayerOptions) (*Layer, error) { // if the layer doesn't exist yet, try to create it. - slo := stagedLayerOptions{ + contents := layerCreationContents{ DiffOutput: args.DiffOutput, DiffOptions: args.DiffOptions, } - layer, _, err = s.putLayer(rlstore, rlstores, args.ID, args.ParentLayer, args.Names, args.MountLabel, args.Writeable, args.LayerOptions, nil, &slo) + options, parentLayer, unlockLayerStores, err := populateLayerOptions(s, rlstore, rlstores, args.ParentLayer, args.LayerOptions) + defer unlockLayerStores() + if err != nil { + return nil, err + } + layer, _, err = rlstore.create(args.ID, parentLayer, args.Names, args.MountLabel, nil, options, args.Writeable, &contents) return layer, err } diff --git a/vendor/go.podman.io/storage/userns.go b/vendor/go.podman.io/storage/userns.go index f22a1cfe92..86ca8b5938 100644 --- a/vendor/go.podman.io/storage/userns.go +++ b/vendor/go.podman.io/storage/userns.go @@ -197,7 +197,7 @@ outer: // We need to create a temporary layer so we can mount it and lookup the // maximum IDs used. - clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil, nil) + clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil) if err != nil { return 0, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index fcfc12706d..596f105ac6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -475,7 +475,7 @@ github.com/moby/moby/api/types/storage github.com/moby/moby/api/types/swarm github.com/moby/moby/api/types/system github.com/moby/moby/api/types/volume -# github.com/moby/moby/client v0.1.0 +# github.com/moby/moby/client v0.2.1 ## explicit; go 1.24.0 github.com/moby/moby/client github.com/moby/moby/client/internal @@ -575,12 +575,12 @@ github.com/opencontainers/go-digest ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 -# github.com/opencontainers/runc v1.3.3 -## explicit; go 1.23.0 +# github.com/opencontainers/runc v1.4.0 +## explicit; go 1.24.0 +github.com/opencontainers/runc/internal/linux github.com/opencontainers/runc/internal/pathrs github.com/opencontainers/runc/libcontainer/apparmor github.com/opencontainers/runc/libcontainer/devices -github.com/opencontainers/runc/libcontainer/utils # github.com/opencontainers/runtime-spec v1.3.0 ## explicit github.com/opencontainers/runtime-spec/specs-go @@ -773,7 +773,7 @@ go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded go.opentelemetry.io/otel/trace/internal/telemetry go.opentelemetry.io/otel/trace/noop -# go.podman.io/common v0.66.1-0.20251126122123-4fc82df3fdc0 +# go.podman.io/common v0.66.1-0.20251128185259-94e31d2e45ba ## explicit; go 1.24.6 go.podman.io/common/internal go.podman.io/common/internal/attributedstring @@ -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.38.1-0.20251126122123-4fc82df3fdc0 +# go.podman.io/image/v5 v5.38.1-0.20251128185259-94e31d2e45ba ## explicit; go 1.24.6 go.podman.io/image/v5/copy go.podman.io/image/v5/directory @@ -917,7 +917,7 @@ go.podman.io/image/v5/transports go.podman.io/image/v5/transports/alltransports go.podman.io/image/v5/types go.podman.io/image/v5/version -# go.podman.io/storage v1.61.1-0.20251125064110-c4e25180a61d +# go.podman.io/storage v1.61.1-0.20251128185259-94e31d2e45ba ## explicit; go 1.24.0 go.podman.io/storage go.podman.io/storage/drivers