Skip to content

Commit 5df3dd4

Browse files
authored
Merge pull request #28450 from TomSweeneyRedHat/dev/tsweeney/cve-2025-52881-follow-4.4.1-rhel
[v4.4.1-rhel] do not pass volume-opt as bind mounts options to runtime
2 parents dfe1819 + 2a3418c commit 5df3dd4

5 files changed

Lines changed: 125 additions & 13 deletions

File tree

libpod/container_internal_common.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
312312
if err := c.relabel(m.Source, c.MountLabel(), label.IsShared(o)); err != nil {
313313
return nil, err
314314
}
315-
315+
case "no-dereference":
316+
// crun calls the option `copy-symlink`.
317+
// Podman decided for --no-dereference as many
318+
// bin-utils tools (e..g, touch, chown, cp) do.
319+
options = append(options, "copy-symlink")
320+
case "copy", "nocopy":
321+
// no real OCI runtime bind mount options, these should already be handled by the named volume mount above
316322
default:
317323
options = append(options, o)
318324
}

libpod/runtime_ctr.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,15 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai
479479
_, err := r.state.Volume(vol.Name)
480480
if err == nil {
481481
// The volume exists, we're good
482+
// Make sure to drop all volume-opt options as they only apply to
483+
// the volume create which we don't do again.
484+
var volOpts []string
485+
for _, opts := range vol.Options {
486+
if !strings.HasPrefix(opts, "volume-opt") {
487+
volOpts = append(volOpts, opts)
488+
}
489+
}
490+
vol.Options = volOpts
482491
continue
483492
} else if !errors.Is(err, define.ErrNoSuchVolume) {
484493
return nil, fmt.Errorf("retrieving named volume %s for new container: %w", vol.Name, err)
@@ -504,6 +513,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai
504513
if len(vol.Options) > 0 {
505514
isDriverOpts := false
506515
driverOpts := make(map[string]string)
516+
var volOpts []string
507517
for _, opts := range vol.Options {
508518
if opts == "idmap" {
509519
needsChown = false
@@ -515,8 +525,11 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai
515525
return nil, err
516526
}
517527
driverOpts[driverOptKey] = driverOptValue
528+
} else {
529+
volOpts = append(volOpts, opts)
518530
}
519531
}
532+
vol.Options = volOpts
520533
if isDriverOpts {
521534
parsedOptions := []VolumeCreateOption{WithVolumeOptions(driverOpts)}
522535
volOptions = append(volOptions, parsedOptions...)

test/e2e/common_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ func (s *PodmanSessionIntegration) InspectImageJSON() []inspect.ImageData {
421421
return i
422422
}
423423

424+
// PodmanExitCleanly runs a podman command with args, and expects it to ExitCleanly within the default timeout.
425+
// It returns the session (to allow consuming output if desired).
426+
func (p *PodmanTestIntegration) PodmanExitCleanly(args ...string) *PodmanSessionIntegration {
427+
session := p.Podman(args)
428+
session.WaitWithDefaultTimeout()
429+
Expect(session).Should(ExitCleanly())
430+
return session
431+
}
432+
424433
// InspectContainer returns a container's inspect data in JSON format
425434
func (p *PodmanTestIntegration) InspectContainer(name string) []define.InspectContainerData {
426435
cmd := []string{"inspect", name}
@@ -475,8 +484,15 @@ func (p *PodmanTestIntegration) RunTopContainerWithArgs(name string, args []stri
475484
podmanArgs = append(podmanArgs, "--name", name)
476485
}
477486
podmanArgs = append(podmanArgs, args...)
478-
podmanArgs = append(podmanArgs, "-d", ALPINE, "top")
479-
return p.Podman(podmanArgs)
487+
podmanArgs = append(podmanArgs, "-d", ALPINE, "top", "-b")
488+
session := p.PodmanExitCleanly(podmanArgs...)
489+
cid := session.OutputToString()
490+
// Output indicates that top is running, which means it's safe
491+
// for our caller to invoke `podman stop`
492+
if !WaitContainerReady(p, cid, "Mem:", 20, 1) {
493+
Fail("Could not start a top container")
494+
}
495+
return session
480496
}
481497

482498
// RunLsContainer runs a simple container in the background that

test/e2e/run_volume_test.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package integration
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"os"
67
"os/exec"
78
"os/user"
89
"path/filepath"
10+
"strconv"
911
"strings"
1012

1113
. "github.com/containers/podman/v4/test/utils"
1214
. "github.com/onsi/ginkgo"
1315
. "github.com/onsi/gomega"
1416
. "github.com/onsi/gomega/gexec"
17+
"github.com/opencontainers/runtime-spec/specs-go"
1518
)
1619

1720
// in-container mount point: using a path that is definitely not present
@@ -451,9 +454,27 @@ var _ = Describe("Podman run with volumes", func() {
451454
Expect(separateVolumeSession).Should(Exit(0))
452455
Expect(separateVolumeSession.OutputToString()).To(Equal(baselineOutput))
453456

454-
copySession := podmanTest.Podman([]string{"run", "--rm", "-v", "testvol3:/etc/apk:copy", ALPINE, "stat", "-c", "%h", "/etc/apk/arch"})
455-
copySession.WaitWithDefaultTimeout()
456-
Expect(copySession).Should(Exit(0))
457+
podmanTest.PodmanExitCleanly("run", "--name", "testctr", "-v", "testvol3:/etc/apk:copy", ALPINE, "stat", "-c", "%h", "/etc/apk/arch")
458+
459+
inspect := podmanTest.PodmanExitCleanly("container", "inspect", "testctr", "--format", "{{.OCIConfigPath}}")
460+
461+
// Make extra check that the OCI config does not contain the copy opt, runc 1.3.0 fails on that while crun does not.
462+
// We only test crun upstream so make sure the spec is sane: https://github.com/containers/podman/issues/26938
463+
f, err := os.Open(inspect.OutputToString())
464+
Expect(err).ToNot(HaveOccurred())
465+
defer f.Close()
466+
var spec specs.Spec
467+
err = json.NewDecoder(f).Decode(&spec)
468+
Expect(err).ToNot(HaveOccurred())
469+
470+
found := false
471+
for _, m := range spec.Mounts {
472+
if m.Destination == "/etc/apk" {
473+
found = true
474+
Expect(m.Options).To(Equal([]string{"rprivate", "nosuid", "nodev", "rbind"}))
475+
}
476+
}
477+
Expect(found).To(BeTrue(), "OCI spec contains /etc/apk mount")
457478

458479
noCopySession := podmanTest.Podman([]string{"run", "--rm", "-v", "testvol4:/etc/apk:nocopy", ALPINE, "stat", "-c", "%h", "/etc/apk/arch"})
459480
noCopySession.WaitWithDefaultTimeout()
@@ -875,14 +896,18 @@ VOLUME /test/`, ALPINE)
875896
It("podman run with --mount and named volume with driver-opts", func() {
876897
// anonymous volume mount with driver opts
877898
vol := "type=volume,source=test_vol,dst=/test,volume-opt=type=tmpfs,volume-opt=device=tmpfs,volume-opt=o=nodev"
878-
session := podmanTest.Podman([]string{"run", "--rm", "--mount", vol, ALPINE, "echo", "hello"})
879-
session.WaitWithDefaultTimeout()
880-
Expect(session).Should(Exit(0))
881899

882-
inspectVol := podmanTest.Podman([]string{"volume", "inspect", "test_vol"})
883-
inspectVol.WaitWithDefaultTimeout()
884-
Expect(inspectVol).Should(Exit(0))
885-
Expect(inspectVol.OutputToString()).To(ContainSubstring("nodev"))
900+
// Loop twice to cover both the initial code path that creates the volume and the ones which reuses it.
901+
for i := range 2 {
902+
name := "testctr" + strconv.Itoa(i)
903+
podmanTest.PodmanExitCleanly("run", "--name", name, "--mount", vol, ALPINE, "echo", "hello")
904+
905+
inspectVol := podmanTest.PodmanExitCleanly("volume", "inspect", "test_vol")
906+
Expect(inspectVol.OutputToString()).To(ContainSubstring("nodev"))
907+
908+
inspect := podmanTest.PodmanExitCleanly("container", "inspect", name, "--format", "{{range .Mounts}}{{.Options}}{{end}}")
909+
Expect(inspect.OutputToString()).To(ContainSubstring("[nosuid nodev rbind]"))
910+
}
886911
})
887912

888913
It("volume permissions after run", func() {

test/utils/matchers.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,58 @@ func (matcher *ExitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
168168
return true
169169
}
170170

171+
// ExitCleanly asserts that a PodmanSession exits 0 and with no stderr
172+
// Consider using PodmanTestIntegration.PodmanExitCleanly instead of directly using this matcher.
173+
func ExitCleanly() types.GomegaMatcher {
174+
return &exitCleanlyMatcher{}
175+
}
176+
177+
type exitCleanlyMatcher struct {
178+
msg string
179+
}
180+
181+
type podmanSession interface {
182+
ExitCode() int
183+
ErrorToString() string
184+
}
185+
186+
func (matcher *exitCleanlyMatcher) Match(actual interface{}) (success bool, err error) {
187+
session, ok := actual.(podmanSession)
188+
if !ok {
189+
return false, fmt.Errorf("ExitCleanly must be passed a PodmanSession; Got:\n %+v\n%q", actual, format.Object(actual, 1))
190+
}
191+
192+
exitcode := session.ExitCode()
193+
stderr := session.ErrorToString()
194+
if exitcode != 0 {
195+
matcher.msg = fmt.Sprintf("Command failed with exit status %d", exitcode)
196+
if stderr != "" {
197+
matcher.msg += ". See above for error message."
198+
}
199+
return false, nil
200+
}
201+
202+
// FIXME: #19809, "failed to connect to syslog" warnings on f38
203+
// FIXME: so, until that is fixed, don't check stderr if containerized
204+
if !Containerized() {
205+
if stderr != "" {
206+
matcher.msg = fmt.Sprintf("Unexpected warnings seen on stderr: %q", stderr)
207+
return false, nil
208+
}
209+
}
210+
211+
return true, nil
212+
}
213+
214+
func (matcher *exitCleanlyMatcher) FailureMessage(_ interface{}) (message string) {
215+
return matcher.msg
216+
}
217+
218+
func (matcher *exitCleanlyMatcher) NegatedFailureMessage(_ interface{}) (message string) {
219+
// FIXME - I see no situation in which we could ever want this?
220+
return matcher.msg + " (NOT!)"
221+
}
222+
171223
type ValidJSONMatcher struct {
172224
types.GomegaMatcher
173225
}

0 commit comments

Comments
 (0)