Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package docker-compose for openSUSE:Factory 
checked in at 2025-11-02 22:33:34
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/docker-compose (Old)
 and      /work/SRC/openSUSE:Factory/.docker-compose.new.1980 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "docker-compose"

Sun Nov  2 22:33:34 2025 rev:82 rq:1315062 version:2.40.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/docker-compose/docker-compose.changes    
2025-10-23 16:38:26.724923894 +0200
+++ /work/SRC/openSUSE:Factory/.docker-compose.new.1980/docker-compose.changes  
2025-11-02 22:33:49.345815417 +0100
@@ -1,0 +2,21 @@
+Sun Nov 02 06:45:19 UTC 2025 - Johannes Kastl 
<[email protected]>
+
+- Update to version 2.40.3:
+  * Fixes
+    - Fix OCI compose override support by @ndeloof #13311
+    - Fix help output for "exec --no-tty" option by @tonyo #13314
+    - Prompt default implementation to prevent a panic by @ndeloof
+      #13317
+    - Run hooks on restart by @ndeloof #13321
+    - Fix(run): Ensure images exist only for the target service in
+      run command by @idsulik #13325
+    - Fix(git): Fix path traversal vulnerability in git remote
+      loader by @idsulik #13331
+  * Internal
+    - Test to check writeComposeFile detects invalid OCI artifact
+      by @ndeloof #13309
+    - Code Cleanup by @ndeloof #13315
+  * Dependencies
+    - Bump compose-go to version v2.9.1 by @glours #13332
+
+-------------------------------------------------------------------

Old:
----
  docker-compose-2.40.2.obscpio

New:
----
  docker-compose-2.40.3.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ docker-compose.spec ++++++
--- /var/tmp/diff_new_pack.QOqKEa/_old  2025-11-02 22:33:50.325856498 +0100
+++ /var/tmp/diff_new_pack.QOqKEa/_new  2025-11-02 22:33:50.325856498 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           docker-compose
-Version:        2.40.2
+Version:        2.40.3
 Release:        0
 Summary:        Define and run multi-container applications with Docker
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.QOqKEa/_old  2025-11-02 22:33:50.365858176 +0100
+++ /var/tmp/diff_new_pack.QOqKEa/_new  2025-11-02 22:33:50.369858343 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/docker/compose</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v2.40.2</param>
+    <param name="revision">v2.40.3</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.QOqKEa/_old  2025-11-02 22:33:50.393859349 +0100
+++ /var/tmp/diff_new_pack.QOqKEa/_new  2025-11-02 22:33:50.393859349 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/docker/compose</param>
-              <param 
name="changesrevision">6007d4c7e7b7cffdd280c9617954d5086b2a7e79</param></service></servicedata>
+              <param 
name="changesrevision">49b1c1e932aecd3a4843b392162d4bd56021935e</param></service></servicedata>
 (No newline at EOF)
 

++++++ docker-compose-2.40.2.obscpio -> docker-compose-2.40.3.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/cmd/compose/exec.go 
new/docker-compose-2.40.3/cmd/compose/exec.go
--- old/docker-compose-2.40.2/cmd/compose/exec.go       2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/cmd/compose/exec.go       2025-10-30 
10:14:58.000000000 +0100
@@ -82,7 +82,7 @@
        runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container 
if service has multiple replicas")
        runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, 
"Give extended privileges to the process")
        runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command 
as this user")
-       runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", 
!dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default 
`docker compose exec` allocates a TTY.")
+       runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", 
!dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default 
'docker compose exec' allocates a TTY.")
        runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path 
to workdir directory for this command")
 
        runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, 
"Keep STDIN open even if not attached")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/cmd/formatter/shortcut_windows.go 
new/docker-compose-2.40.3/cmd/formatter/shortcut_windows.go
--- old/docker-compose-2.40.2/cmd/formatter/shortcut_windows.go 2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/cmd/formatter/shortcut_windows.go 2025-10-30 
10:14:58.000000000 +0100
@@ -22,4 +22,4 @@
 func handleCtrlZ() {
        // Windows doesn't support SIGSTOP/SIGCONT signals
        // Ctrl+Z behavior is handled differently by the Windows terminal
-}
\ No newline at end of file
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/docs/reference/compose_exec.md 
new/docker-compose-2.40.3/docs/reference/compose_exec.md
--- old/docker-compose-2.40.2/docs/reference/compose_exec.md    2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/docs/reference/compose_exec.md    2025-10-30 
10:14:58.000000000 +0100
@@ -20,7 +20,7 @@
 | `--dry-run`       | `bool`        |         | Execute command in dry run 
mode                                                  |
 | `-e`, `--env`     | `stringArray` |         | Set environment variables      
                                                  |
 | `--index`         | `int`         | `0`     | Index of the container if 
service has multiple replicas                          |
-| `-T`, `--no-tty`  | `bool`        | `true`  | Disable pseudo-TTY allocation. 
By default `docker compose exec` allocates a TTY. |
+| `-T`, `--no-tty`  | `bool`        | `true`  | Disable pseudo-TTY allocation. 
By default 'docker compose exec' allocates a TTY. |
 | `--privileged`    | `bool`        |         | Give extended privileges to 
the process                                          |
 | `-u`, `--user`    | `string`      |         | Run the command as this user   
                                                  |
 | `-w`, `--workdir` | `string`      |         | Path to workdir directory for 
this command                                       |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/docs/reference/docker_compose_exec.yaml 
new/docker-compose-2.40.3/docs/reference/docker_compose_exec.yaml
--- old/docker-compose-2.40.2/docs/reference/docker_compose_exec.yaml   
2025-10-22 19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/docs/reference/docker_compose_exec.yaml   
2025-10-30 10:14:58.000000000 +0100
@@ -63,7 +63,7 @@
       value_type: bool
       default_value: "true"
       description: |
-        Disable pseudo-TTY allocation. By default `docker compose exec` 
allocates a TTY.
+        Disable pseudo-TTY allocation. By default 'docker compose exec' 
allocates a TTY.
       deprecated: false
       hidden: false
       experimental: false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/go.mod 
new/docker-compose-2.40.3/go.mod
--- old/docker-compose-2.40.2/go.mod    2025-10-22 19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/go.mod    2025-10-30 10:14:58.000000000 +0100
@@ -8,7 +8,7 @@
        github.com/Microsoft/go-winio v0.6.2
        github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
        github.com/buger/goterm v1.0.4
-       github.com/compose-spec/compose-go/v2 v2.9.0
+       github.com/compose-spec/compose-go/v2 v2.9.1
        github.com/containerd/containerd/v2 v2.1.4
        github.com/containerd/errdefs v1.0.0
        github.com/containerd/platforms v1.0.0-rc.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/go.sum 
new/docker-compose-2.40.3/go.sum
--- old/docker-compose-2.40.2/go.sum    2025-10-22 19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/go.sum    2025-10-30 10:14:58.000000000 +0100
@@ -78,8 +78,8 @@
 github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod 
h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb 
h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
 github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod 
h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
-github.com/compose-spec/compose-go/v2 v2.9.0 
h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM=
-github.com/compose-spec/compose-go/v2 v2.9.0/go.mod 
h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
+github.com/compose-spec/compose-go/v2 v2.9.1 
h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs=
+github.com/compose-spec/compose-go/v2 v2.9.1/go.mod 
h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
 github.com/containerd/cgroups/v3 v3.0.5 
h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
 github.com/containerd/cgroups/v3 v3.0.5/go.mod 
h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
 github.com/containerd/console v1.0.5 
h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/internal/oci/resolver.go 
new/docker-compose-2.40.3/internal/oci/resolver.go
--- old/docker-compose-2.40.2/internal/oci/resolver.go  2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/internal/oci/resolver.go  2025-10-30 
10:14:58.000000000 +0100
@@ -20,6 +20,7 @@
        "context"
        "io"
        "net/url"
+       "os"
        "strings"
 
        "github.com/containerd/containerd/v2/core/remotes"
@@ -50,6 +51,11 @@
                                        return auth.Username, auth.Password, nil
                                }),
                        )),
+                       docker.WithPlainHTTP(func(s string) (bool, error) {
+                               // Used for testing **only**
+                               _, b := 
os.LookupEnv("__TEST__INSECURE__REGISTRY__")
+                               return b, nil
+                       }),
                ),
        })
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/apiSocket.go 
new/docker-compose-2.40.3/pkg/compose/apiSocket.go
--- old/docker-compose-2.40.2/pkg/compose/apiSocket.go  2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/apiSocket.go  2025-10-30 
10:14:58.000000000 +0100
@@ -45,7 +45,7 @@
                return nil, errors.New("use_api_socket can't be used with a 
Windows Docker Engine")
        }
 
-       creds, err := s.dockerCli.ConfigFile().GetAllCredentials()
+       creds, err := s.configFile().GetAllCredentials()
        if err != nil {
                return nil, fmt.Errorf("resolving credentials failed: %w", err)
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/commit.go 
new/docker-compose-2.40.3/pkg/compose/commit.go
--- old/docker-compose-2.40.2/pkg/compose/commit.go     2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/commit.go     2025-10-30 
10:14:58.000000000 +0100
@@ -40,7 +40,7 @@
                return err
        }
 
-       clnt := s.dockerCli.Client()
+       clnt := s.apiClient()
 
        w := progress.ContextWriter(ctx)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/compose.go 
new/docker-compose-2.40.3/pkg/compose/compose.go
--- old/docker-compose-2.40.2/pkg/compose/compose.go    2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/compose.go    2025-10-30 
10:14:58.000000000 +0100
@@ -37,6 +37,7 @@
        "github.com/docker/docker/api/types/volume"
        "github.com/docker/docker/client"
        "github.com/jonboulle/clockwork"
+       "github.com/sirupsen/logrus"
 
        "github.com/docker/compose/v2/pkg/api"
 )
@@ -63,6 +64,13 @@
        for _, option := range options {
                option(s)
        }
+       if s.prompt == nil {
+               s.prompt = func(message string, defaultValue bool) (bool, 
error) {
+                       fmt.Println(message)
+                       logrus.Warning("Compose is running without a 'prompt' 
component to interact with user")
+                       return defaultValue, nil
+               }
+       }
        return s
 }
 
@@ -92,7 +100,7 @@
 func (s *composeService) Close() error {
        var errs []error
        if s.dockerCli != nil {
-               errs = append(errs, s.dockerCli.Client().Close())
+               errs = append(errs, s.apiClient().Close())
        }
        return errors.Join(errs...)
 }
@@ -323,7 +331,7 @@
 
 func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
        runtimeVersion.once.Do(func() {
-               version, err := s.dockerCli.Client().ServerVersion(ctx)
+               version, err := s.apiClient().ServerVersion(ctx)
                if err != nil {
                        runtimeVersion.err = err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/export.go 
new/docker-compose-2.40.3/pkg/compose/export.go
--- old/docker-compose-2.40.2/pkg/compose/export.go     2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/export.go     2025-10-30 
10:14:58.000000000 +0100
@@ -50,7 +50,7 @@
                return fmt.Errorf("failed to export container: %w", err)
        }
 
-       clnt := s.dockerCli.Client()
+       clnt := s.apiClient()
 
        w := progress.ContextWriter(ctx)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/publish.go 
new/docker-compose-2.40.3/pkg/compose/publish.go
--- old/docker-compose-2.40.2/pkg/compose/publish.go    2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/publish.go    2025-10-30 
10:14:58.000000000 +0100
@@ -90,8 +90,7 @@
                        return err
                }
 
-               config := s.dockerCli.ConfigFile()
-               resolver := oci.NewResolver(config)
+               resolver := oci.NewResolver(s.configFile())
 
                descriptor, err := oci.PushManifest(ctx, resolver, named, 
layers, options.OCIVersion)
                if err != nil {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/publish_test.go 
new/docker-compose-2.40.3/pkg/compose/publish_test.go
--- old/docker-compose-2.40.2/pkg/compose/publish_test.go       2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/publish_test.go       2025-10-30 
10:14:58.000000000 +0100
@@ -77,7 +77,8 @@
                        MediaType: "application/vnd.docker.compose.file+yaml",
                        Annotations: map[string]string{
                                "com.docker.compose.file":    "compose.yaml",
-                               "com.docker.compose.version": internal.Version},
+                               "com.docker.compose.version": internal.Version,
+                       },
                },
                {
                        MediaType: "application/vnd.docker.compose.file+yaml",
@@ -98,5 +99,4 @@
        assert.DeepEqual(t, expectedLayers, layers, cmp.FilterPath(func(path 
cmp.Path) bool {
                return !slices.Contains([]string{".Data", ".Digest", ".Size"}, 
path.String())
        }, cmp.Ignore()))
-
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/pull.go 
new/docker-compose-2.40.3/pkg/compose/pull.go
--- old/docker-compose-2.40.2/pkg/compose/pull.go       2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/pull.go       2025-10-30 
10:14:58.000000000 +0100
@@ -116,7 +116,7 @@
 
                idx := i
                eg.Go(func() error {
-                       _, err := s.pullServiceImage(ctx, service, 
s.configFile(), w, opts.Quiet, project.Environment["DOCKER_DEFAULT_PLATFORM"])
+                       _, err := s.pullServiceImage(ctx, service, w, 
opts.Quiet, project.Environment["DOCKER_DEFAULT_PLATFORM"])
                        if err != nil {
                                pullErrors[idx] = err
                                if service.Build != nil {
@@ -177,9 +177,7 @@
        return err.Error()
 }
 
-func (s *composeService) pullServiceImage(ctx context.Context, service 
types.ServiceConfig,
-       configFile driver.Auth, w progress.Writer, quietPull bool, 
defaultPlatform string,
-) (string, error) {
+func (s *composeService) pullServiceImage(ctx context.Context, service 
types.ServiceConfig, w progress.Writer, quietPull bool, defaultPlatform string) 
(string, error) {
        w.Event(progress.Event{
                ID:     service.Name,
                Status: progress.Working,
@@ -190,7 +188,7 @@
                return "", err
        }
 
-       encodedAuth, err := encodedAuth(ref, configFile)
+       encodedAuth, err := encodedAuth(ref, s.configFile())
        if err != nil {
                return "", err
        }
@@ -330,7 +328,7 @@
                var mutex sync.Mutex
                for name, service := range needPull {
                        eg.Go(func() error {
-                               id, err := s.pullServiceImage(ctx, service, 
s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
+                               id, err := s.pullServiceImage(ctx, service, w, 
quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
                                mutex.Lock()
                                defer mutex.Unlock()
                                pulledImages[name] = api.ImageSummary{
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/push.go 
new/docker-compose-2.40.3/pkg/compose/push.go
--- old/docker-compose-2.40.2/pkg/compose/push.go       2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/push.go       2025-10-30 
10:14:58.000000000 +0100
@@ -27,7 +27,6 @@
 
        "github.com/compose-spec/compose-go/v2/types"
        "github.com/distribution/reference"
-       "github.com/docker/buildx/driver"
        "github.com/docker/docker/api/types/image"
        "github.com/docker/docker/pkg/jsonmessage"
        "golang.org/x/sync/errgroup"
@@ -70,7 +69,7 @@
 
                for _, tag := range tags {
                        eg.Go(func() error {
-                               err := s.pushServiceImage(ctx, tag, 
s.configFile(), w, options.Quiet)
+                               err := s.pushServiceImage(ctx, tag, w, 
options.Quiet)
                                if err != nil {
                                        if !options.IgnoreFailures {
                                                return err
@@ -84,13 +83,13 @@
        return eg.Wait()
 }
 
-func (s *composeService) pushServiceImage(ctx context.Context, tag string, 
configFile driver.Auth, w progress.Writer, quietPush bool) error {
+func (s *composeService) pushServiceImage(ctx context.Context, tag string, w 
progress.Writer, quietPush bool) error {
        ref, err := reference.ParseNormalizedNamed(tag)
        if err != nil {
                return err
        }
 
-       authConfig, err := 
configFile.GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
+       authConfig, err := 
s.configFile().GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
        if err != nil {
                return err
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/restart.go 
new/docker-compose-2.40.3/pkg/compose/restart.go
--- old/docker-compose-2.40.2/pkg/compose/restart.go    2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/restart.go    2025-10-30 
10:14:58.000000000 +0100
@@ -34,7 +34,7 @@
        }, s.stdinfo(), "Restarting")
 }
 
-func (s *composeService) restart(ctx context.Context, projectName string, 
options api.RestartOptions) error {
+func (s *composeService) restart(ctx context.Context, projectName string, 
options api.RestartOptions) error { //nolint:gocyclo
        containers, err := s.getContainers(ctx, projectName, oneOffExclude, 
true)
        if err != nil {
                return err
@@ -86,6 +86,13 @@
                eg, ctx := errgroup.WithContext(ctx)
                for _, ctr := range containers.filter(isService(service)) {
                        eg.Go(func() error {
+                               def := project.Services[service]
+                               for _, hook := range def.PreStop {
+                                       err = s.runHook(ctx, ctr, def, hook, 
nil)
+                                       if err != nil {
+                                               return err
+                                       }
+                               }
                                eventName := getContainerProgressName(ctr)
                                w.Event(progress.RestartingEvent(eventName))
                                timeout := 
utils.DurationSecondToInt(options.Timeout)
@@ -94,6 +101,12 @@
                                        return err
                                }
                                w.Event(progress.StartedEvent(eventName))
+                               for _, hook := range def.PostStart {
+                                       err = s.runHook(ctx, ctr, def, hook, 
nil)
+                                       if err != nil {
+                                               return err
+                                       }
+                               }
                                return nil
                        })
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/run.go 
new/docker-compose-2.40.3/pkg/compose/run.go
--- old/docker-compose-2.40.2/pkg/compose/run.go        2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/run.go        2025-10-30 
10:14:58.000000000 +0100
@@ -97,7 +97,9 @@
                Add(api.SlugLabel, slug).
                Add(api.OneoffLabel, "True")
 
-       if err := s.ensureImagesExists(ctx, project, opts.Build, 
opts.QuietPull); err != nil { // all dependencies already checked, but might 
miss service img
+       // Only ensure image exists for the target service, dependencies were 
already handled by startDependencies
+       buildOpts := prepareBuildOptions(opts)
+       if err := s.ensureImagesExists(ctx, project, buildOpts, 
opts.QuietPull); err != nil { // all dependencies already checked, but might 
miss service img
                return "", err
        }
 
@@ -147,6 +149,16 @@
        return created.ID, err
 }
 
+func prepareBuildOptions(opts api.RunOptions) *api.BuildOptions {
+       if opts.Build == nil {
+               return nil
+       }
+       // Create a copy of build options and restrict to only the target 
service
+       buildOptsCopy := *opts.Build
+       buildOptsCopy.Services = []string{opts.Service}
+       return &buildOptsCopy
+}
+
 func applyRunOptions(project *types.Project, service *types.ServiceConfig, 
opts api.RunOptions) {
        service.Tty = opts.Tty
        service.StdinOpen = opts.Interactive
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/compose/transform/replace.go 
new/docker-compose-2.40.3/pkg/compose/transform/replace.go
--- old/docker-compose-2.40.2/pkg/compose/transform/replace.go  2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/transform/replace.go  2025-10-30 
10:14:58.000000000 +0100
@@ -104,7 +104,6 @@
        } else {
                return replace(in, envFile.Line, envFile.Column, value), nil
        }
-
 }
 
 func getMapping(root *yaml.Node, key string) (*yaml.Node, error) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/compose/wait.go 
new/docker-compose-2.40.3/pkg/compose/wait.go
--- old/docker-compose-2.40.2/pkg/compose/wait.go       2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/compose/wait.go       2025-10-30 
10:14:58.000000000 +0100
@@ -38,7 +38,7 @@
        for _, ctr := range containers {
                eg.Go(func() error {
                        var err error
-                       resultC, errC := 
s.dockerCli.Client().ContainerWait(waitCtx, ctr.ID, "")
+                       resultC, errC := s.apiClient().ContainerWait(waitCtx, 
ctr.ID, "")
 
                        select {
                        case result := <-resultC:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/compose_run_build_once_test.go 
new/docker-compose-2.40.3/pkg/e2e/compose_run_build_once_test.go
--- old/docker-compose-2.40.2/pkg/e2e/compose_run_build_once_test.go    
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/compose_run_build_once_test.go    
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,101 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   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 e2e
+
+import (
+       "crypto/rand"
+       "encoding/hex"
+       "fmt"
+       "strings"
+       "testing"
+
+       "gotest.tools/v3/assert"
+       "gotest.tools/v3/icmd"
+)
+
+// TestRunBuildOnce tests that services with pull_policy: build are only built 
once
+// when using 'docker compose run', even when they are dependencies.
+// This addresses a bug where dependencies were built twice: once in 
startDependencies
+// and once in ensureImagesExists.
+func TestRunBuildOnce(t *testing.T) {
+       c := NewParallelCLI(t)
+
+       t.Run("dependency with pull_policy build is built only once", func(t 
*testing.T) {
+               projectName := randomProjectName("build-once")
+               res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once.yaml", "down", "--rmi", "local", 
"--remove-orphans")
+               res.Assert(t, icmd.Success)
+
+               res = c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once.yaml", "run", "--build", "--rm", "curl")
+               res.Assert(t, icmd.Success)
+
+               // Count how many times nginx was built by looking for its 
unique RUN command output
+               nginxBuilds := strings.Count(res.Combined(), "Building nginx 
at")
+
+               // nginx should build exactly once, not twice
+               assert.Equal(t, nginxBuilds, 1, "nginx dependency should build 
once, but built %d times", nginxBuilds)
+               assert.Assert(t, strings.Contains(res.Combined(), "curl 
service"))
+
+               c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once.yaml", "down", "--remove-orphans")
+       })
+
+       t.Run("nested dependencies build only once each", func(t *testing.T) {
+               projectName := randomProjectName("build-nested")
+               res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-nested.yaml", "down", "--rmi", "local", 
"--remove-orphans")
+               res.Assert(t, icmd.Success)
+
+               res = c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-nested.yaml", "run", "--build", "--rm", "app")
+               res.Assert(t, icmd.Success)
+
+               output := res.Combined()
+
+               // Each service should build exactly once
+               dbBuilds := strings.Count(output, "DB built at")
+               apiBuilds := strings.Count(output, "API built at")
+               appBuilds := strings.Count(output, "App built at")
+
+               assert.Equal(t, dbBuilds, 1, "db should build once, built %d 
times", dbBuilds)
+               assert.Equal(t, apiBuilds, 1, "api should build once, built %d 
times", apiBuilds)
+               assert.Equal(t, appBuilds, 1, "app should build once, built %d 
times", appBuilds)
+               assert.Assert(t, strings.Contains(output, "App running"))
+
+               c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-nested.yaml", "down", "--remove-orphans")
+       })
+
+       t.Run("service with no dependencies builds once", func(t *testing.T) {
+               projectName := randomProjectName("build-simple")
+               res := c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-no-deps.yaml", "down", "--rmi", "local", 
"--remove-orphans")
+               res.Assert(t, icmd.Success)
+
+               res = c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-no-deps.yaml", "run", "--build", "--rm", 
"simple")
+               res.Assert(t, icmd.Success)
+
+               // Should build exactly once
+               simpleBuilds := strings.Count(res.Combined(), "Simple service 
built at")
+               assert.Equal(t, simpleBuilds, 1, "simple should build once, 
built %d times", simpleBuilds)
+               assert.Assert(t, strings.Contains(res.Combined(), "Simple 
service"))
+
+               c.RunDockerComposeCmd(t, "-p", projectName, "-f", 
"./fixtures/run-test/build-once-no-deps.yaml", "down", "--remove-orphans")
+       })
+}
+
+// randomProjectName generates a unique project name for parallel test 
execution
+// Format: prefix-<8 random hex chars> (e.g., "build-once-3f4a9b2c")
+func randomProjectName(prefix string) string {
+       b := make([]byte, 4) // 4 bytes = 8 hex chars
+       rand.Read(b)         //nolint:errcheck
+       return fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(b))
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/compose-override.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/compose-override.yaml
--- 
old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/compose-override.yaml    
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/compose-override.yaml    
    2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,3 @@
+services:
+  app:
+    env_file: test.env
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/compose.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/compose.yaml
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/compose.yaml 
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/compose.yaml 
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,5 @@
+services:
+  app:
+    extends:
+      file: extends.yaml
+      service: test
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/extends.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/extends.yaml
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/extends.yaml 
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/extends.yaml 
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,3 @@
+services:
+  test:
+    image: alpine
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/test.env 
new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/test.env
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/publish/oci/test.env     
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/publish/oci/test.env     
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1 @@
+HELLO=WORLD
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once-nested.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once-nested.yaml
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once-nested.yaml  
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once-nested.yaml  
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,32 @@
+services:
+  # Database service with build
+  db:
+    pull_policy: build
+    build:
+      dockerfile_inline: |
+        FROM alpine
+        RUN echo "DB built at $(date)" > /db-build.txt
+        CMD sleep 3600
+
+  # API service that depends on db
+  api:
+    pull_policy: build
+    build:
+      dockerfile_inline: |
+        FROM alpine
+        RUN echo "API built at $(date)" > /api-build.txt
+        CMD sleep 3600
+    depends_on:
+      - db
+
+  # App service that depends on api (which depends on db)
+  app:
+    pull_policy: build
+    build:
+      dockerfile_inline: |
+        FROM alpine
+        RUN echo "App built at $(date)" > /app-build.txt
+        CMD echo "App running"
+    depends_on:
+      - api
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once-no-deps.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once-no-deps.yaml
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once-no-deps.yaml 
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once-no-deps.yaml 
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,10 @@
+services:
+  # Simple service with no dependencies
+  simple:
+    pull_policy: build
+    build:
+      dockerfile_inline: |
+        FROM alpine
+        RUN echo "Simple service built at $(date)" > /build.txt
+        CMD echo "Simple service"
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once.yaml 
new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once.yaml
--- old/docker-compose-2.40.2/pkg/e2e/fixtures/run-test/build-once.yaml 
1970-01-01 01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/e2e/fixtures/run-test/build-once.yaml 
2025-10-30 10:14:58.000000000 +0100
@@ -0,0 +1,18 @@
+services:
+  # Service with pull_policy: build to ensure it always rebuilds
+  # This is the key to testing the bug - without the fix, this would build 
twice
+  nginx:
+    pull_policy: build
+    build:
+      dockerfile_inline: |
+        FROM alpine
+        RUN echo "Building nginx at $(date)" > /build-time.txt
+        CMD sleep 3600
+
+  # Service that depends on nginx
+  curl:
+    image: alpine
+    depends_on:
+      - nginx
+    command: echo "curl service"
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/e2e/publish_test.go 
new/docker-compose-2.40.3/pkg/e2e/publish_test.go
--- old/docker-compose-2.40.2/pkg/e2e/publish_test.go   2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/e2e/publish_test.go   2025-10-30 
10:14:58.000000000 +0100
@@ -17,6 +17,7 @@
 package e2e
 
 import (
+       "fmt"
        "strings"
        "testing"
 
@@ -173,3 +174,43 @@
                assert.Assert(t, strings.Contains(output, "Private Key\n\"\": 
-----BEGIN DSA PRIVATE KEY-----\nwxyz+ABC=\n-----END DSA PRIVATE KEY-----"), 
output)
        })
 }
+
+func TestPublish(t *testing.T) {
+       c := NewParallelCLI(t)
+       const projectName = "compose-e2e-publish"
+       const registryName = projectName + "-registry"
+       c.RunDockerCmd(t, "run", "--name", registryName, "-P", "-d", 
"registry:3")
+       port := c.RunDockerCmd(t, "inspect", "--format", `{{ (index (index 
.NetworkSettings.Ports "5000/tcp") 0).HostPort }}`, registryName).Stdout()
+       registry := "localhost:" + strings.TrimSpace(port)
+       t.Cleanup(func() {
+               c.RunDockerCmd(t, "rm", "--force", registryName)
+       })
+
+       cmd := c.NewDockerComposeCmd(t, "-f", 
"./fixtures/publish/oci/compose.yaml", "-f", 
"./fixtures/publish/oci/compose-override.yaml",
+               "-p", projectName, "publish", "--with-env", "--yes", 
registry+"/test:test")
+       icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
+               cmd.Env = append(cmd.Env, "__TEST__INSECURE__REGISTRY__=true")
+       }).Assert(t, icmd.Expected{ExitCode: 0})
+
+       // docker exec -it compose-e2e-publish-registry tree 
/var/lib/registry/docker/registry/v2/
+
+       cmd = c.NewDockerComposeCmd(t, "--verbose", "--project-name=oci", "-f", 
fmt.Sprintf("oci://%s/test:test", registry), "config")
+       res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
+               cmd.Env = append(cmd.Env,
+                       "XDG_CACHE_HOME="+t.TempDir(),
+                       "__TEST__INSECURE__REGISTRY__=true")
+       })
+       res.Assert(t, icmd.Expected{ExitCode: 0})
+       assert.Equal(t, res.Stdout(), `name: oci
+services:
+  app:
+    environment:
+      HELLO: WORLD
+    image: alpine
+    networks:
+      default: null
+networks:
+  default:
+    name: oci_default
+`)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/remote/git.go 
new/docker-compose-2.40.3/pkg/remote/git.go
--- old/docker-compose-2.40.2/pkg/remote/git.go 2025-10-22 19:32:16.000000000 
+0200
+++ new/docker-compose-2.40.3/pkg/remote/git.go 2025-10-30 10:14:58.000000000 
+0100
@@ -26,6 +26,7 @@
        "path/filepath"
        "regexp"
        "strconv"
+       "strings"
 
        "github.com/compose-spec/compose-go/v2/cli"
        "github.com/compose-spec/compose-go/v2/loader"
@@ -113,6 +114,9 @@
                g.known[path] = local
        }
        if ref.SubDir != "" {
+               if err := validateGitSubDir(local, ref.SubDir); err != nil {
+                       return "", err
+               }
                local = filepath.Join(local, ref.SubDir)
        }
        stat, err := os.Stat(local)
@@ -129,6 +133,41 @@
        return g.known[path]
 }
 
+// validateGitSubDir ensures a subdirectory path is contained within the base 
directory
+// and doesn't escape via path traversal. Unlike validatePathInBase for OCI 
artifacts,
+// this allows nested directories but prevents traversal outside the base.
+func validateGitSubDir(base, subDir string) error {
+       cleanSubDir := filepath.Clean(subDir)
+
+       if filepath.IsAbs(cleanSubDir) {
+               return fmt.Errorf("git subdirectory must be relative, got: %s", 
subDir)
+       }
+
+       if cleanSubDir == ".." || strings.HasPrefix(cleanSubDir, "../") || 
strings.HasPrefix(cleanSubDir, "..\\") {
+               return fmt.Errorf("git subdirectory path traversal detected: 
%s", subDir)
+       }
+
+       if len(cleanSubDir) >= 2 && cleanSubDir[1] == ':' {
+               return fmt.Errorf("git subdirectory must be relative, got: %s", 
subDir)
+       }
+
+       targetPath := filepath.Join(base, cleanSubDir)
+       cleanBase := filepath.Clean(base)
+       cleanTarget := filepath.Clean(targetPath)
+
+       // Ensure the target starts with the base path
+       relPath, err := filepath.Rel(cleanBase, cleanTarget)
+       if err != nil {
+               return fmt.Errorf("invalid git subdirectory path: %w", err)
+       }
+
+       if relPath == ".." || strings.HasPrefix(relPath, "../") || 
strings.HasPrefix(relPath, "..\\") {
+               return fmt.Errorf("git subdirectory escapes base directory: 
%s", subDir)
+       }
+
+       return nil
+}
+
 func (g gitRemoteLoader) resolveGitRef(ctx context.Context, path string, ref 
*gitutil.GitRef) error {
        if !commitSHA.MatchString(ref.Ref) {
                cmd := exec.CommandContext(ctx, "git", "ls-remote", 
"--exit-code", ref.Remote, ref.Ref)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/remote/git_test.go 
new/docker-compose-2.40.3/pkg/remote/git_test.go
--- old/docker-compose-2.40.2/pkg/remote/git_test.go    1970-01-01 
01:00:00.000000000 +0100
+++ new/docker-compose-2.40.3/pkg/remote/git_test.go    2025-10-30 
10:14:58.000000000 +0100
@@ -0,0 +1,175 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   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 remote
+
+import (
+       "testing"
+
+       "gotest.tools/v3/assert"
+)
+
+func TestValidateGitSubDir(t *testing.T) {
+       base := "/tmp/cache/compose/abc123def456"
+
+       tests := []struct {
+               name    string
+               subDir  string
+               wantErr bool
+       }{
+               {
+                       name:    "valid simple directory",
+                       subDir:  "examples",
+                       wantErr: false,
+               },
+               {
+                       name:    "valid nested directory",
+                       subDir:  "examples/nginx",
+                       wantErr: false,
+               },
+               {
+                       name:    "valid deeply nested directory",
+                       subDir:  "examples/web/frontend/config",
+                       wantErr: false,
+               },
+               {
+                       name:    "valid current directory",
+                       subDir:  ".",
+                       wantErr: false,
+               },
+               {
+                       name:    "valid directory with redundant separators",
+                       subDir:  "examples//nginx",
+                       wantErr: false,
+               },
+               {
+                       name:    "valid directory with dots in name",
+                       subDir:  "examples/nginx.conf.d",
+                       wantErr: false,
+               },
+               {
+                       name:    "path traversal - parent directory",
+                       subDir:  "..",
+                       wantErr: true,
+               },
+               {
+                       name:    "path traversal - multiple parent directories",
+                       subDir:  "../../../etc/passwd",
+                       wantErr: true,
+               },
+               {
+                       name:    "path traversal - deeply nested escape",
+                       subDir:  "../../../../../../../tmp/pwned",
+                       wantErr: true,
+               },
+               {
+                       name:    "path traversal - mixed with valid path",
+                       subDir:  "examples/../../etc/passwd",
+                       wantErr: true,
+               },
+               {
+                       name:    "path traversal - at the end",
+                       subDir:  "examples/..",
+                       wantErr: false, // This resolves to "." which is the 
current directory, safe
+               },
+               {
+                       name:    "path traversal - in the middle",
+                       subDir:  "examples/../../../etc/passwd",
+                       wantErr: true,
+               },
+               {
+                       name:    "path traversal - windows style",
+                       subDir:  "..\\..\\..\\windows\\system32",
+                       wantErr: true,
+               },
+               {
+                       name:    "absolute unix path",
+                       subDir:  "/etc/passwd",
+                       wantErr: true,
+               },
+               {
+                       name:    "absolute windows path",
+                       subDir:  "C:\\windows\\system32\\config\\sam",
+                       wantErr: true,
+               },
+               {
+                       name:    "absolute path with home directory",
+                       subDir:  "/home/user/.ssh/id_rsa",
+                       wantErr: true,
+               },
+               {
+                       name:    "normalized path that would escape",
+                       subDir:  "./../../etc/passwd",
+                       wantErr: true,
+               },
+               {
+                       name:    "directory name with three dots",
+                       subDir:  ".../config",
+                       wantErr: false,
+               },
+               {
+                       name:    "directory name with four dots",
+                       subDir:  "..../config",
+                       wantErr: false,
+               },
+               {
+                       name:    "directory name with five dots",
+                       subDir:  "...../etc/passwd",
+                       wantErr: false, // ".....'' is a valid directory name, 
not path traversal
+               },
+               {
+                       name:    "directory name starting with two dots and 
letter",
+                       subDir:  "..foo/bar",
+                       wantErr: false,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := validateGitSubDir(base, tt.subDir)
+                       if (err != nil) != tt.wantErr {
+                               t.Errorf("validateGitSubDir(%q, %q) error = %v, 
wantErr %v",
+                                       base, tt.subDir, err, tt.wantErr)
+                       }
+               })
+       }
+}
+
+// TestValidateGitSubDirSecurityScenarios tests specific security scenarios
+func TestValidateGitSubDirSecurityScenarios(t *testing.T) {
+       base := "/var/cache/docker-compose/git/1234567890abcdef"
+
+       // Test the exact vulnerability scenario from the issue
+       t.Run("CVE scenario - /tmp traversal", func(t *testing.T) {
+               maliciousPath := "../../../../../../../tmp/pwned"
+               err := validateGitSubDir(base, maliciousPath)
+               assert.ErrorContains(t, err, "path traversal")
+       })
+
+       // Test variations of the attack
+       t.Run("CVE scenario - /etc traversal", func(t *testing.T) {
+               maliciousPath := "../../../../../../../../etc/passwd"
+               err := validateGitSubDir(base, maliciousPath)
+               assert.ErrorContains(t, err, "path traversal")
+       })
+
+       // Test that legitimate nested paths still work
+       t.Run("legitimate nested path", func(t *testing.T) {
+               validPath := "examples/docker-compose/nginx/config"
+               err := validateGitSubDir(base, validPath)
+               assert.NilError(t, err)
+       })
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/remote/oci.go 
new/docker-compose-2.40.3/pkg/remote/oci.go
--- old/docker-compose-2.40.2/pkg/remote/oci.go 2025-10-22 19:32:16.000000000 
+0200
+++ new/docker-compose-2.40.3/pkg/remote/oci.go 2025-10-30 10:14:58.000000000 
+0100
@@ -179,7 +179,7 @@
        return g.known[path]
 }
 
-func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, 
manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error { 
//nolint:gocyclo
+func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, 
manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error {
        err := os.MkdirAll(local, 0o700)
        if err != nil {
                return err
@@ -223,7 +223,7 @@
                        return err
                }
        }
-       f, err := os.Create(filepath.Join(local, file))
+       f, err := os.OpenFile(filepath.Join(local, file), 
os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
        if err != nil {
                return err
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/docker-compose-2.40.2/pkg/remote/oci_test.go 
new/docker-compose-2.40.3/pkg/remote/oci_test.go
--- old/docker-compose-2.40.2/pkg/remote/oci_test.go    2025-10-22 
19:32:16.000000000 +0200
+++ new/docker-compose-2.40.3/pkg/remote/oci_test.go    2025-10-30 
10:14:58.000000000 +0100
@@ -19,6 +19,9 @@
 import (
        "path/filepath"
        "testing"
+
+       spec "github.com/opencontainers/image-spec/specs-go/v1"
+       "gotest.tools/v3/assert"
 )
 
 func TestValidatePathInBase(t *testing.T) {
@@ -85,11 +88,6 @@
                        wantErr:    true,
                },
                {
-                       name:       "current directory reference",
-                       unsafePath: "./file.yaml",
-                       wantErr:    false, // ./ resolves to base dir
-               },
-               {
                        name:       "mixed separators",
                        unsafePath: "config/sub\\file.yaml",
                        wantErr:    true,
@@ -104,11 +102,6 @@
                        unsafePath: "file-name_v1.2.3.yaml",
                        wantErr:    false,
                },
-               {
-                       name:       "single parent then back",
-                       unsafePath: "../compose/file.yaml",
-                       wantErr:    false, // Resolves back to base dir, which 
is fine
-               },
        }
 
        for _, tt := range tests {
@@ -123,3 +116,24 @@
                })
        }
 }
+
+func TestWriteComposeFileWithExtendsPathTraversal(t *testing.T) {
+       tmpDir := t.TempDir()
+
+       // Create a layer with com.docker.compose.extends=true and a path 
traversal attempt
+       layer := spec.Descriptor{
+               MediaType: "application/vnd.docker.compose.file.v1+yaml",
+               Digest:    "sha256:test123",
+               Size:      100,
+               Annotations: map[string]string{
+                       "com.docker.compose.extends": "true",
+                       "com.docker.compose.file":    "../other",
+               },
+       }
+
+       content := []byte("services:\n  test:\n    image: nginx\n")
+
+       // writeComposeFile should return an error due to path traversal
+       err := writeComposeFile(layer, 0, tmpDir, content)
+       assert.Error(t, err, "invalid OCI artifact")
+}

++++++ docker-compose.obsinfo ++++++
--- /var/tmp/diff_new_pack.QOqKEa/_old  2025-11-02 22:33:51.089888526 +0100
+++ /var/tmp/diff_new_pack.QOqKEa/_new  2025-11-02 22:33:51.101889029 +0100
@@ -1,5 +1,5 @@
 name: docker-compose
-version: 2.40.2
-mtime: 1761154336
-commit: 6007d4c7e7b7cffdd280c9617954d5086b2a7e79
+version: 2.40.3
+mtime: 1761815698
+commit: 49b1c1e932aecd3a4843b392162d4bd56021935e
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/docker-compose/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.docker-compose.new.1980/vendor.tar.gz differ: char 
130, line 2

Reply via email to