All of lore.kernel.org
 help / color / mirror / Atom feed
* [meta-virtualization] [scarthgap] [PATCH] docker-moby: Fix CVE-2025-54410
@ 2025-09-08  7:09 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
  2025-09-19  2:34 ` Bruce Ashfield
  0 siblings, 1 reply; 2+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2025-09-08  7:09 UTC (permalink / raw)
  To: meta-virtualization; +Cc: vchavda, bruce.ashfield

From: Deepak Rathore <deeratho@cisco.com>

Upstream Repository: https://github.com/moby/moby.git

Bug Details: https://nvd.nist.gov/vuln/detail/CVE-2025-54410
Type: Security Fix
CVE: CVE-2025-54410
Score: 3.3
Patch:
[1] https://github.com/moby/moby/commit/a012739c2cf3
[2] https://github.com/moby/moby/commit/c761353e7c75
[3] https://github.com/moby/moby/commit/41f080df250a
[4] https://github.com/moby/moby/commit/651b2feb2731

Note:
- The main fix is implemented in [4], which is part of version 25.0.
- Commits [1], [2], and [3] are dependent patches also included in v25.0.
- The FirewalldReload() function, essential for the fix in [4], was
introduced in [3].
- Commit [1] adds the iptables.go file, enabling the implementation of
FirewalldReload() in [3].
- The TestInternalNwConnectivity() test was first introduced in [2] and
later updated in [3] to validate the firewall reload behavior.

Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
 recipes-containers/docker/docker-moby_git.bb  |   4 +
 .../files/CVE-2025-54410-dependent_p1.patch   |  38 ++
 .../files/CVE-2025-54410-dependent_p2.patch   | 143 ++++++
 .../files/CVE-2025-54410-dependent_p3.patch   | 451 ++++++++++++++++++
 .../docker/files/CVE-2025-54410.patch         |  50 ++
 5 files changed, 686 insertions(+)
 create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
 create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
 create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
 create mode 100644 recipes-containers/docker/files/CVE-2025-54410.patch

diff --git a/recipes-containers/docker/docker-moby_git.bb b/recipes-containers/docker/docker-moby_git.bb
index d274b002..e4791319 100644
--- a/recipes-containers/docker/docker-moby_git.bb
+++ b/recipes-containers/docker/docker-moby_git.bb
@@ -58,6 +58,10 @@ SRC_URI = "\
         file://0001-dynbinary-use-go-cross-compiler.patch;patchdir=src/import \
         file://CVE-2024-36620.patch;patchdir=src/import \
         file://CVE-2024-36621.patch;patchdir=src/import \
+        file://CVE-2025-54410-dependent_p1.patch;patchdir=src/import \
+        file://CVE-2025-54410-dependent_p2.patch;patchdir=src/import \
+        file://CVE-2025-54410-dependent_p3.patch;patchdir=src/import \
+        file://CVE-2025-54410.patch;patchdir=src/import \
 	"
 
 DOCKER_COMMIT = "${SRCREV_moby}"
diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
new file mode 100644
index 00000000..f1a7db3d
--- /dev/null
+++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
@@ -0,0 +1,38 @@
+From 4ff88c2b9d85e162676fd54b773a4a3957b5c622 Mon Sep 17 00:00:00 2001
+From: Rob Murray <rob.murray@docker.com>
+Date: Wed, 26 Mar 2025 14:32:15 +0000
+Subject: [PATCH 1/4] Add test util "FirewalldRunning"
+
+CVE: CVE-2025-54410
+Upstream-Status: Backport [https://github.com/moby/moby/commit/a012739c2cf3]
+
+Signed-off-by: Rob Murray <rob.murray@docker.com>
+(cherry picked from commit b8cacdf324c8309fb0555381889aa867391ba9fb)
+Signed-off-by: Andrey Epifanov <aepifanov@mirantis.com>
+(cherry picked from commit a012739c2cf347847d83ea77e30333528602c8a6)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ internal/testutils/networking/iptables.go | 12 ++++++++++++
+ 1 file changed, 12 insertions(+)
+ create mode 100644 internal/testutils/networking/iptables.go
+
+diff --git a/internal/testutils/networking/iptables.go b/internal/testutils/networking/iptables.go
+new file mode 100644
+index 0000000000..2e6ada8363
+--- /dev/null
++++ b/internal/testutils/networking/iptables.go
+@@ -0,0 +1,12 @@
++package networking
++
++import (
++	"os/exec"
++	"strings"
++	"testing"
++)
++
++func FirewalldRunning() bool {
++	state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()
++	return err == nil && strings.TrimSpace(string(state)) == "running"
++}
+--
+2.44.3
diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
new file mode 100644
index 00000000..e376338f
--- /dev/null
+++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
@@ -0,0 +1,143 @@
+From 11125e7362281772f6d8cebaee3d8a1c07456e31 Mon Sep 17 00:00:00 2001
+From: Rob Murray <rob.murray@docker.com>
+Date: Wed, 7 Feb 2024 18:53:27 +0000
+Subject: [PATCH 2/4] Make 'internal' bridge networks accessible from host
+
+Prior to release 25.0.0, the bridge in an internal network was assigned
+an IP address - making the internal network accessible from the host,
+giving containers on the network access to anything listening on the
+bridge's address (or INADDR_ANY on the host).
+
+This change restores that behaviour. It does not restore the default
+route that was configured in the container, because packets sent outside
+the internal network's subnet have always been dropped. So, a 'connect()'
+to an address outside the subnet will still fail fast.
+
+CVE: CVE-2025-54410
+Upstream-Status: Backport [https://github.com/moby/moby/commit/c761353e7c75]
+
+Signed-off-by: Rob Murray <rob.murray@docker.com>
+(cherry picked from commit 419f5a637230570ec44c32f889f981f67ce15793)
+Signed-off-by: Albin Kerouanton <albinker@gmail.com>
+(cherry picked from commit c761353e7c75e4ffa90b80f3b5df40473d59620a)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ integration/networking/bridge_test.go         | 57 +++++++++++++++++++
+ libnetwork/drivers/bridge/setup_ipv4_linux.go | 10 ++--
+ .../drivers/bridge/setup_verify_linux.go      |  4 +-
+ 3 files changed, 63 insertions(+), 8 deletions(-)
+
+diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
+index e3d1fe2a36..8f54234406 100644
+--- a/integration/networking/bridge_test.go
++++ b/integration/networking/bridge_test.go
+@@ -477,3 +477,60 @@ func TestDefaultBridgeAddresses(t *testing.T) {
+		})
+	}
+ }
++
++// Test that a container on an 'internal' network has IP connectivity with
++// the host (on its own subnet, because the n/w bridge has an address on that
++// subnet, and it's in the host's namespace).
++// Regression test for https://github.com/moby/moby/issues/47329
++func TestInternalNwConnectivity(t *testing.T) {
++	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
++
++	ctx := setupTest(t)
++
++	d := daemon.New(t)
++	d.StartWithBusybox(ctx, t, "-D", "--experimental", "--ip6tables")
++	defer d.Stop(t)
++
++	c := d.NewClientT(t)
++	defer c.Close()
++
++	const bridgeName = "intnw"
++	const gw4 = "172.30.0.1"
++	const gw6 = "fda9:4130:4715::1234"
++	network.CreateNoError(ctx, t, c, bridgeName,
++		network.WithInternal(),
++		network.WithIPv6(),
++		network.WithIPAM("172.30.0.0/24", gw4),
++		network.WithIPAM("fda9:4130:4715::/64", gw6),
++		network.WithDriver("bridge"),
++		network.WithOption("com.docker.network.bridge.name", bridgeName),
++	)
++	defer network.RemoveNoError(ctx, t, c, bridgeName)
++
++	const ctrName = "intctr"
++	id := container.Run(ctx, t, c,
++		container.WithName(ctrName),
++		container.WithImage("busybox:latest"),
++		container.WithCmd("top"),
++		container.WithNetworkMode(bridgeName),
++	)
++	defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
++
++	execCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
++	defer cancel()
++
++	res := container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", gw4})
++	assert.Check(t, is.Equal(res.ExitCode, 0))
++	assert.Check(t, is.Equal(res.Stderr(), ""))
++	assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received"))
++
++	res = container.ExecT(execCtx, t, c, id, []string{"ping6", "-c1", "-W3", gw6})
++	assert.Check(t, is.Equal(res.ExitCode, 0))
++	assert.Check(t, is.Equal(res.Stderr(), ""))
++	assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received"))
++
++	// Addresses outside the internal subnet must not be accessible.
++	res = container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", "8.8.8.8"})
++	assert.Check(t, is.Equal(res.ExitCode, 1))
++	assert.Check(t, is.Contains(res.Stderr(), "Network is unreachable"))
++}
+diff --git a/libnetwork/drivers/bridge/setup_ipv4_linux.go b/libnetwork/drivers/bridge/setup_ipv4_linux.go
+index 0940745c23..6b925190ce 100644
+--- a/libnetwork/drivers/bridge/setup_ipv4_linux.go
++++ b/libnetwork/drivers/bridge/setup_ipv4_linux.go
+@@ -32,10 +32,6 @@ func setupBridgeIPv4(config *networkConfiguration, i *bridgeInterface) error {
+	//             are decoupled, we should assign it only when it's really needed.
+	i.bridgeIPv4 = config.AddressIPv4
+
+-	if config.Internal {
+-		return nil
+-	}
+-
+	if !config.InhibitIPv4 {
+		addrv4List, err := i.addresses(netlink.FAMILY_V4)
+		if err != nil {
+@@ -57,8 +53,10 @@ func setupBridgeIPv4(config *networkConfiguration, i *bridgeInterface) error {
+		}
+	}
+
+-	// Store the default gateway
+-	i.gatewayIPv4 = config.AddressIPv4.IP
++	if !config.Internal {
++		// Store the default gateway
++		i.gatewayIPv4 = config.AddressIPv4.IP
++	}
+
+	return nil
+ }
+diff --git a/libnetwork/drivers/bridge/setup_verify_linux.go b/libnetwork/drivers/bridge/setup_verify_linux.go
+index a39d750346..b7ddbcf814 100644
+--- a/libnetwork/drivers/bridge/setup_verify_linux.go
++++ b/libnetwork/drivers/bridge/setup_verify_linux.go
+@@ -20,12 +20,12 @@ func setupVerifyAndReconcileIPv4(config *networkConfiguration, i *bridgeInterfac
+	addrv4, _ := selectIPv4Address(addrsv4, config.AddressIPv4)
+
+	// Verify that the bridge has an IPv4 address.
+-	if !config.Internal && addrv4.IPNet == nil {
++	if addrv4.IPNet == nil {
+		return &ErrNoIPAddr{}
+	}
+
+	// Verify that the bridge IPv4 address matches the requested configuration.
+-	if config.AddressIPv4 != nil && addrv4.IPNet != nil && !addrv4.IP.Equal(config.AddressIPv4.IP) {
++	if config.AddressIPv4 != nil && !addrv4.IP.Equal(config.AddressIPv4.IP) {
+		return &IPv4AddrNoMatchError{IP: addrv4.IP, CfgIP: config.AddressIPv4.IP}
+	}
+
+--
+2.44.3
diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
new file mode 100644
index 00000000..16d4768f
--- /dev/null
+++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
@@ -0,0 +1,451 @@
+From 211e9acdef48b1229591724ba83fe4b6ad86c7b6 Mon Sep 17 00:00:00 2001
+From: Rob Murray <rob.murray@docker.com>
+Date: Fri, 14 Feb 2025 16:23:49 +0000
+Subject: [PATCH 3/4] Restore iptables for current networks on firewalld reload
+
+Using iptables.OnReloaded to restore individual per-network rules
+on firewalld reload means rules for deleted networks pop back in
+to existence (because there was no way to delete the callbacks on
+network-delete).
+
+So, on firewalld reload, walk over current networks and ask them
+to restore their iptables rules.
+
+CVE: CVE-2025-54410
+Upstream-Status: Backport [https://github.com/moby/moby/commit/41f080df250a]
+
+Signed-off-by: Rob Murray <rob.murray@docker.com>
+(cherry picked from commit a527e5a546526117a0f3d7a33f3fcb8f4cd87c72)
+
+Test that firewalld reload doesn't re-create deleted iptables rules
+
+Signed-off-by: Rob Murray <rob.murray@docker.com>
+(cherry picked from commit c3fa7c17794feaf4e27238aa3c6de42c606765fa)
+
+Signed-off-by: Andrey Epifanov <aepifanov@mirantis.com>
+(cherry picked from commit 41f080df250aef805d34628d1719c369e9ff1585)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ integration/network/bridge_test.go            | 65 +++++++++++++++++++
+ integration/networking/bridge_test.go         |  4 ++
+ internal/testutils/networking/iptables.go     | 48 ++++++++++++++
+ .../testutils/networking/iptables_test.go     | 47 ++++++++++++++
+ libnetwork/drivers/bridge/bridge_linux.go     | 65 +++++++++++++++++--
+ libnetwork/drivers/bridge/setup_firewalld.go  | 41 ------------
+ .../drivers/bridge/setup_ip_tables_linux.go   |  3 +
+ libnetwork/iptables/firewalld.go              |  1 +
+ 8 files changed, 227 insertions(+), 47 deletions(-)
+ create mode 100644 internal/testutils/networking/iptables_test.go
+ delete mode 100644 libnetwork/drivers/bridge/setup_firewalld.go
+
+diff --git a/integration/network/bridge_test.go b/integration/network/bridge_test.go
+index b09ff5766c..09b7da9c2b 100644
+--- a/integration/network/bridge_test.go
++++ b/integration/network/bridge_test.go
+@@ -6,11 +6,17 @@ import (
+	"testing"
+	"time"
+
++	containertypes "github.com/docker/docker/api/types/container"
+	networktypes "github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/api/types/versions"
+	ctr "github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/integration/internal/network"
++	"github.com/docker/docker/internal/testutils/networking"
++	"github.com/docker/docker/libnetwork/drivers/bridge"
++	"github.com/docker/docker/testutil/daemon"
++	"github.com/docker/go-connections/nat"
+	"gotest.tools/v3/assert"
++	"gotest.tools/v3/icmd"
+	"gotest.tools/v3/skip"
+ )
+
+@@ -43,3 +49,62 @@ func TestCreateWithMultiNetworks(t *testing.T) {
+	ifacesWithAddress := strings.Count(res.Stdout.String(), "\n")
+	assert.Equal(t, ifacesWithAddress, 3)
+ }
++
++// TestFirewalldReloadNoZombies checks that when firewalld is reloaded, rules
++// belonging to deleted networks/containers do not reappear.
++func TestFirewalldReloadNoZombies(t *testing.T) {
++	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
++	skip.If(t, !networking.FirewalldRunning(), "firewalld is not running")
++	skip.If(t, testEnv.IsRootless, "no firewalld in rootless netns")
++
++	ctx := setupTest(t)
++	d := daemon.New(t)
++	d.StartWithBusybox(ctx, t)
++	defer d.Stop(t)
++	c := d.NewClientT(t)
++
++	const bridgeName = "br-fwdreload"
++	removed := false
++	nw := network.CreateNoError(ctx, t, c, "testnet",
++		network.WithOption(bridge.BridgeName, bridgeName))
++	defer func() {
++		if !removed {
++			network.RemoveNoError(ctx, t, c, nw)
++		}
++	}()
++
++	cid := ctr.Run(ctx, t, c,
++		ctr.WithExposedPorts("80/tcp", "81/tcp"),
++		ctr.WithPortMap(nat.PortMap{"80/tcp": {{HostPort: "8000"}}}))
++	defer func() {
++		if !removed {
++			ctr.Remove(ctx, t, c, cid, containertypes.RemoveOptions{Force: true})
++		}
++	}()
++
++	iptablesSave := icmd.Command("iptables-save")
++	resBeforeDel := icmd.RunCmd(iptablesSave)
++	assert.NilError(t, resBeforeDel.Error)
++	assert.Check(t, strings.Contains(resBeforeDel.Combined(), bridgeName),
++		"With container: expected rules for %s in: %s", bridgeName, resBeforeDel.Combined())
++
++	// Delete the container and its network.
++	ctr.Remove(ctx, t, c, cid, containertypes.RemoveOptions{Force: true})
++	network.RemoveNoError(ctx, t, c, nw)
++	removed = true
++
++	// Check the network does not appear in iptables rules.
++	resAfterDel := icmd.RunCmd(iptablesSave)
++	assert.NilError(t, resAfterDel.Error)
++	assert.Check(t, !strings.Contains(resAfterDel.Combined(), bridgeName),
++		"After deletes: did not expect rules for %s in: %s", bridgeName, resAfterDel.Combined())
++
++	// firewall-cmd --reload, and wait for the daemon to restore rules.
++	networking.FirewalldReload(t, d)
++
++	// Check that rules for the deleted container/network have not reappeared.
++	resAfterReload := icmd.RunCmd(iptablesSave)
++	assert.NilError(t, resAfterReload.Error)
++	assert.Check(t, !strings.Contains(resAfterReload.Combined(), bridgeName),
++		"After deletes: did not expect rules for %s in: %s", bridgeName, resAfterReload.Combined())
++}
+diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
+index 8f54234406..8cdcf02096 100644
+--- a/integration/networking/bridge_test.go
++++ b/integration/networking/bridge_test.go
+@@ -10,6 +10,7 @@ import (
+	containertypes "github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/integration/internal/network"
++	"github.com/docker/docker/internal/testutils/networking"
+	"github.com/docker/docker/testutil"
+	"github.com/docker/docker/testutil/daemon"
+	"gotest.tools/v3/assert"
+@@ -160,6 +161,8 @@ func TestBridgeICC(t *testing.T) {
+				Force: true,
+			})
+
++			networking.FirewalldReload(t, d)
++
+			pingHost := tc.pingHost
+			if pingHost == "" {
+				if tc.linkLocal {
+@@ -515,6 +518,7 @@ func TestInternalNwConnectivity(t *testing.T) {
+		container.WithNetworkMode(bridgeName),
+	)
+	defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
++	networking.FirewalldReload(t, d)
+
+	execCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
+	defer cancel()
+diff --git a/internal/testutils/networking/iptables.go b/internal/testutils/networking/iptables.go
+index 2e6ada8363..2041e507c3 100644
+--- a/internal/testutils/networking/iptables.go
++++ b/internal/testutils/networking/iptables.go
+@@ -1,12 +1,60 @@
+ package networking
+
+ import (
++	"fmt"
+	"os/exec"
++	"regexp"
+	"strings"
+	"testing"
++	"time"
++
++	"github.com/docker/docker/testutil/daemon"
++	"golang.org/x/net/context"
++	"gotest.tools/v3/assert"
++	"gotest.tools/v3/icmd"
++	"gotest.tools/v3/poll"
+ )
+
+ func FirewalldRunning() bool {
+	state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()
+	return err == nil && strings.TrimSpace(string(state)) == "running"
+ }
++
++func extractLogTime(s string) (time.Time, error) {
++	// time="2025-07-15T13:46:13.414214418Z" level=info msg=""
++	re := regexp.MustCompile(`time="([^"]+)"`)
++	matches := re.FindStringSubmatch(s)
++	if len(matches) < 2 {
++		return time.Time{}, fmt.Errorf("timestamp not found in log line: %s, matches: %+v", s, matches)
++	}
++
++	return time.Parse(time.RFC3339Nano, matches[1])
++}
++
++// FirewalldReload reloads firewalld and waits for the daemon to re-create its rules.
++// It's a no-op if firewalld is not running, and the test fails if the reload does
++// not complete.
++func FirewalldReload(t *testing.T, d *daemon.Daemon) {
++	t.Helper()
++	if !FirewalldRunning() {
++		return
++	}
++	timeBeforeReload := time.Now()
++	res := icmd.RunCommand("firewall-cmd", "--reload")
++	assert.NilError(t, res.Error)
++
++	ctx := context.Background()
++	poll.WaitOn(t, d.PollCheckLogs(ctx, func(s string) bool {
++		if !strings.Contains(s, "Firewalld reload completed") {
++			return false
++		}
++		lastReload, err := extractLogTime(s)
++		if err != nil {
++			return false
++		}
++		if lastReload.After(timeBeforeReload) {
++			return true
++		}
++		return false
++	}))
++}
+diff --git a/internal/testutils/networking/iptables_test.go b/internal/testutils/networking/iptables_test.go
+new file mode 100644
+index 0000000000..24a7bbb49d
+--- /dev/null
++++ b/internal/testutils/networking/iptables_test.go
+@@ -0,0 +1,47 @@
++package networking
++
++import (
++	"reflect"
++	"testing"
++	"time"
++)
++
++func Test_getTimeFromLogMsg(t *testing.T) {
++	tests := []struct {
++		name    string
++		s       string
++		want    time.Time
++		wantErr bool
++	}{
++		{
++			name:    "valid time",
++			s:       `time="2025-07-15T13:46:13.414214418Z" level=info msg=""`,
++			want:    time.Date(2025, 7, 15, 13, 46, 13, 414214418, time.UTC),
++			wantErr: false,
++		},
++		{
++			name:    "invalid format",
++			s:       `time="invalid-time-format" level=info msg=""`,
++			want:    time.Time{},
++			wantErr: true,
++		},
++		{
++			name:    "missing time",
++			s:       `level=info msg=""`,
++			want:    time.Time{},
++			wantErr: true,
++		},
++	}
++	for _, tt := range tests {
++		t.Run(tt.name, func(t *testing.T) {
++			got, err := extractLogTime(tt.s)
++			if (err != nil) != tt.wantErr {
++				t.Errorf("getTimeFromLogMsg() error = %v, wantErr %v", err, tt.wantErr)
++				return
++			}
++			if !reflect.DeepEqual(got, tt.want) {
++				t.Errorf("getTimeFromLogMsg() got = %v, want %v", got, tt.want)
++			}
++		})
++	}
++}
+diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go
+index 005c726f2d..2b313c85bf 100644
+--- a/libnetwork/drivers/bridge/bridge_linux.go
++++ b/libnetwork/drivers/bridge/bridge_linux.go
+@@ -483,6 +483,8 @@ func (d *driver) configure(option map[string]interface{}) error {
+	d.config = config
+	d.Unlock()
+
++	iptables.OnReloaded(d.handleFirewalldReload)
++
+	return d.initStore(option)
+ }
+
+@@ -800,12 +802,6 @@ func (d *driver) createNetwork(config *networkConfiguration) (err error) {
+		// Setup IP6Tables.
+		{config.EnableIPv6 && d.config.EnableIP6Tables, network.setupIP6Tables},
+
+-		// We want to track firewalld configuration so that
+-		// if it is started/reloaded, the rules can be applied correctly
+-		{d.config.EnableIPTables, network.setupFirewalld},
+-		// same for IPv6
+-		{config.EnableIPv6 && d.config.EnableIP6Tables, network.setupFirewalld6},
+-
+		// Setup DefaultGatewayIPv4
+		{config.DefaultGatewayIPv4 != nil, setupGatewayIPv4},
+
+@@ -1297,6 +1293,11 @@ func (d *driver) Leave(nid, eid string) error {
+ }
+
+ func (d *driver) ProgramExternalConnectivity(nid, eid string, options map[string]interface{}) error {
++	// Make sure the network isn't deleted, or the in middle of a firewalld reload, while
++	// updating its iptables rules.
++	d.configNetwork.Lock()
++	defer d.configNetwork.Unlock()
++
+	network, err := d.getNetwork(nid)
+	if err != nil {
+		return err
+@@ -1348,6 +1349,11 @@ func (d *driver) ProgramExternalConnectivity(nid, eid string, options map[string
+ }
+
+ func (d *driver) RevokeExternalConnectivity(nid, eid string) error {
++	// Make sure this function isn't deleting iptables rules while handleFirewalldReloadNw
++	// is restoring those same rules.
++	d.configNetwork.Lock()
++	defer d.configNetwork.Unlock()
++
+	network, err := d.getNetwork(nid)
+	if err != nil {
+		return err
+@@ -1381,6 +1387,53 @@ func (d *driver) RevokeExternalConnectivity(nid, eid string) error {
+	return nil
+ }
+
++func (d *driver) handleFirewalldReload() {
++	if !d.config.EnableIPTables && !d.config.EnableIP6Tables {
++		return
++	}
++
++	d.Lock()
++	nids := make([]string, 0, len(d.networks))
++	for _, nw := range d.networks {
++		nids = append(nids, nw.id)
++	}
++	d.Unlock()
++
++	for _, nid := range nids {
++		d.handleFirewalldReloadNw(nid)
++	}
++}
++
++func (d *driver) handleFirewalldReloadNw(nid string) {
++	// Make sure the network isn't being deleted, and ProgramExternalConnectivity/RevokeExternalConnectivity
++	// aren't modifying iptables rules, while restoring the rules.
++	d.configNetwork.Lock()
++	defer d.configNetwork.Unlock()
++
++	nw, err := d.getNetwork(nid)
++	if err != nil {
++		return
++	}
++	if d.config.EnableIPTables {
++		if err := nw.setupIP4Tables(nw.config, nw.bridge); err != nil {
++			log.G(context.TODO()).WithFields(log.Fields{
++				"network": nw.id,
++				"error":   err,
++			}).Warn("Failed to restore IPv4 per-port iptables rules on firewalld reload")
++		}
++	}
++	if d.config.EnableIP6Tables {
++		if err := nw.setupIP6Tables(nw.config, nw.bridge); err != nil {
++			log.G(context.TODO()).WithFields(log.Fields{
++				"network": nw.id,
++				"error":   err,
++			}).Warn("Failed to restore IPv6 per-port iptables rules on firewalld reload")
++		}
++	}
++	nw.portMapper.ReMapAll()
++	log.G(context.TODO()).Info("Restored iptables rules on firewalld reload")
++}
++
+ func (d *driver) link(network *bridgeNetwork, endpoint *bridgeEndpoint, enable bool) (retErr error) {
+	cc := endpoint.containerConfig
+	ec := endpoint.extConnConfig
+diff --git a/libnetwork/drivers/bridge/setup_firewalld.go b/libnetwork/drivers/bridge/setup_firewalld.go
+deleted file mode 100644
+index db7843847c..0000000000
+--- a/libnetwork/drivers/bridge/setup_firewalld.go
++++ /dev/null
+@@ -1,41 +0,0 @@
+-//go:build linux
+-
+-package bridge
+-
+-import (
+-	"errors"
+-
+-	"github.com/docker/docker/libnetwork/iptables"
+-)
+-
+-func (n *bridgeNetwork) setupFirewalld(config *networkConfiguration, i *bridgeInterface) error {
+-	d := n.driver
+-	d.Lock()
+-	driverConfig := d.config
+-	d.Unlock()
+-
+-	// Sanity check.
+-	if !driverConfig.EnableIPTables {
+-		return errors.New("no need to register firewalld hooks, iptables is disabled")
+-	}
+-
+-	iptables.OnReloaded(func() { n.setupIP4Tables(config, i) })
+-	iptables.OnReloaded(n.portMapper.ReMapAll)
+-	return nil
+-}
+-
+-func (n *bridgeNetwork) setupFirewalld6(config *networkConfiguration, i *bridgeInterface) error {
+-	d := n.driver
+-	d.Lock()
+-	driverConfig := d.config
+-	d.Unlock()
+-
+-	// Sanity check.
+-	if !driverConfig.EnableIP6Tables {
+-		return errors.New("no need to register firewalld hooks, ip6tables is disabled")
+-	}
+-
+-	iptables.OnReloaded(func() { n.setupIP6Tables(config, i) })
+-	iptables.OnReloaded(n.portMapperV6.ReMapAll)
+-	return nil
+-}
+diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
+index 328c58bced..42039e24ee 100644
+--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go
++++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
+@@ -130,6 +130,9 @@ func (n *bridgeNetwork) setupIP6Tables(config *networkConfiguration, i *bridgeIn
+		return errors.New("Cannot program chains, EnableIP6Tables is disabled")
+	}
+
++	if i.bridgeIPv6 == nil {
++		return nil
++	}
+	maskedAddrv6 := &net.IPNet{
+		IP:   i.bridgeIPv6.IP.Mask(i.bridgeIPv6.Mask),
+		Mask: i.bridgeIPv6.Mask,
+diff --git a/libnetwork/iptables/firewalld.go b/libnetwork/iptables/firewalld.go
+index dc67240da6..d3b76426e5 100644
+--- a/libnetwork/iptables/firewalld.go
++++ b/libnetwork/iptables/firewalld.go
+@@ -132,6 +132,7 @@ func reloaded() {
+	for _, pf := range onReloaded {
+		(*pf)()
+	}
++	log.G(context.TODO()).Info("Firewalld reload completed")
+ }
+
+ // OnReloaded add callback
+--
+2.44.3
diff --git a/recipes-containers/docker/files/CVE-2025-54410.patch b/recipes-containers/docker/files/CVE-2025-54410.patch
new file mode 100644
index 00000000..b1aa211c
--- /dev/null
+++ b/recipes-containers/docker/files/CVE-2025-54410.patch
@@ -0,0 +1,50 @@
+From a4b2c5a774213eb6594cdece17680e16961c8fcb Mon Sep 17 00:00:00 2001
+From: Rob Murray <rob.murray@docker.com>
+Date: Fri, 14 Feb 2025 16:50:43 +0000
+Subject: [PATCH 4/4] Restore INC iptables rules on firewalld reload
+
+CVE: CVE-2025-54410
+Upstream-Status: Backport [https://github.com/moby/moby/commit/651b2feb2731]
+
+Signed-off-by: Rob Murray <rob.murray@docker.com>
+(cherry picked from commit 651b2feb27316cf907173c2a76cc6eb85f763663)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ integration/networking/bridge_test.go     | 1 +
+ libnetwork/drivers/bridge/bridge_linux.go | 9 +++++++++
+ 2 files changed, 10 insertions(+)
+
+diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
+index 8cdcf02096..7c88cd8b43 100644
+--- a/integration/networking/bridge_test.go
++++ b/integration/networking/bridge_test.go
+@@ -294,6 +294,7 @@ func TestBridgeINC(t *testing.T) {
+			defer c.ContainerRemove(ctx, id1, containertypes.RemoveOptions{
+				Force: true,
+			})
++			networking.FirewalldReload(t, d)
+
+			ctr1Info := container.Inspect(ctx, t, c, id1)
+			targetAddr := ctr1Info.NetworkSettings.Networks[bridge1].IPAddress
+diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go
+index 2b313c85bf..a8a12aebe3 100644
+--- a/libnetwork/drivers/bridge/bridge_linux.go
++++ b/libnetwork/drivers/bridge/bridge_linux.go
+@@ -1431,6 +1431,15 @@ func (d *driver) handleFirewalldReloadNw(nid string) {
+		}
+	}
+	nw.portMapper.ReMapAll()
++
++	// Restore the inter-network connectivity (INC) rules.
++	if err := nw.isolateNetwork(true); err != nil {
++		log.G(context.TODO()).WithFields(log.Fields{
++			"network": nw.id,
++			"error":   err,
++		}).Warn("Failed to restore inter-network iptables rules on firewalld reload")
++	}
++
+	log.G(context.TODO()).Info("Restored iptables rules on firewalld reload")
+ }
+
+--
+2.44.3
-- 
2.44.3



^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [meta-virtualization] [scarthgap] [PATCH] docker-moby: Fix CVE-2025-54410
  2025-09-08  7:09 [meta-virtualization] [scarthgap] [PATCH] docker-moby: Fix CVE-2025-54410 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2025-09-19  2:34 ` Bruce Ashfield
  0 siblings, 0 replies; 2+ messages in thread
From: Bruce Ashfield @ 2025-09-19  2:34 UTC (permalink / raw)
  To: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
  Cc: meta-virtualization, vchavda

In message: [meta-virtualization] [scarthgap] [PATCH] docker-moby: Fix CVE-2025-54410
on 08/09/2025 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) wrote:

> From: Deepak Rathore <deeratho@cisco.com>
> 
> Upstream Repository: https://github.com/moby/moby.git
> 
> Bug Details: https://nvd.nist.gov/vuln/detail/CVE-2025-54410
> Type: Security Fix
> CVE: CVE-2025-54410
> Score: 3.3
> Patch:
> [1] https://github.com/moby/moby/commit/a012739c2cf3
> [2] https://github.com/moby/moby/commit/c761353e7c75
> [3] https://github.com/moby/moby/commit/41f080df250a
> [4] https://github.com/moby/moby/commit/651b2feb2731
> 
> Note:
> - The main fix is implemented in [4], which is part of version 25.0.
> - Commits [1], [2], and [3] are dependent patches also included in v25.0.
> - The FirewalldReload() function, essential for the fix in [4], was
> introduced in [3].
> - Commit [1] adds the iptables.go file, enabling the implementation of
> FirewalldReload() in [3].
> - The TestInternalNwConnectivity() test was first introduced in [2] and
> later updated in [3] to validate the firewall reload behavior.

This isn't answering the right question. scarthgap is on the
25.x series.

Are these fixes not integrated into that stable release ?

A very quick check shows they are:

% git tag --contains a012739c2cf3
v25.0.13

So we should not be doing these as patches, we should be
bumping the version of docker-moby.

Bruce

> 
> Signed-off-by: Deepak Rathore <deeratho@cisco.com>
> ---
>  recipes-containers/docker/docker-moby_git.bb  |   4 +
>  .../files/CVE-2025-54410-dependent_p1.patch   |  38 ++
>  .../files/CVE-2025-54410-dependent_p2.patch   | 143 ++++++
>  .../files/CVE-2025-54410-dependent_p3.patch   | 451 ++++++++++++++++++
>  .../docker/files/CVE-2025-54410.patch         |  50 ++
>  5 files changed, 686 insertions(+)
>  create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
>  create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
>  create mode 100644 recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
>  create mode 100644 recipes-containers/docker/files/CVE-2025-54410.patch
> 
> diff --git a/recipes-containers/docker/docker-moby_git.bb b/recipes-containers/docker/docker-moby_git.bb
> index d274b002..e4791319 100644
> --- a/recipes-containers/docker/docker-moby_git.bb
> +++ b/recipes-containers/docker/docker-moby_git.bb
> @@ -58,6 +58,10 @@ SRC_URI = "\
>          file://0001-dynbinary-use-go-cross-compiler.patch;patchdir=src/import \
>          file://CVE-2024-36620.patch;patchdir=src/import \
>          file://CVE-2024-36621.patch;patchdir=src/import \
> +        file://CVE-2025-54410-dependent_p1.patch;patchdir=src/import \
> +        file://CVE-2025-54410-dependent_p2.patch;patchdir=src/import \
> +        file://CVE-2025-54410-dependent_p3.patch;patchdir=src/import \
> +        file://CVE-2025-54410.patch;patchdir=src/import \
>  	"
>  
>  DOCKER_COMMIT = "${SRCREV_moby}"
> diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
> new file mode 100644
> index 00000000..f1a7db3d
> --- /dev/null
> +++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p1.patch
> @@ -0,0 +1,38 @@
> +From 4ff88c2b9d85e162676fd54b773a4a3957b5c622 Mon Sep 17 00:00:00 2001
> +From: Rob Murray <rob.murray@docker.com>
> +Date: Wed, 26 Mar 2025 14:32:15 +0000
> +Subject: [PATCH 1/4] Add test util "FirewalldRunning"
> +
> +CVE: CVE-2025-54410
> +Upstream-Status: Backport [https://github.com/moby/moby/commit/a012739c2cf3]
> +
> +Signed-off-by: Rob Murray <rob.murray@docker.com>
> +(cherry picked from commit b8cacdf324c8309fb0555381889aa867391ba9fb)
> +Signed-off-by: Andrey Epifanov <aepifanov@mirantis.com>
> +(cherry picked from commit a012739c2cf347847d83ea77e30333528602c8a6)
> +Signed-off-by: Deepak Rathore <deeratho@cisco.com>
> +---
> + internal/testutils/networking/iptables.go | 12 ++++++++++++
> + 1 file changed, 12 insertions(+)
> + create mode 100644 internal/testutils/networking/iptables.go
> +
> +diff --git a/internal/testutils/networking/iptables.go b/internal/testutils/networking/iptables.go
> +new file mode 100644
> +index 0000000000..2e6ada8363
> +--- /dev/null
> ++++ b/internal/testutils/networking/iptables.go
> +@@ -0,0 +1,12 @@
> ++package networking
> ++
> ++import (
> ++	"os/exec"
> ++	"strings"
> ++	"testing"
> ++)
> ++
> ++func FirewalldRunning() bool {
> ++	state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()
> ++	return err == nil && strings.TrimSpace(string(state)) == "running"
> ++}
> +--
> +2.44.3
> diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
> new file mode 100644
> index 00000000..e376338f
> --- /dev/null
> +++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p2.patch
> @@ -0,0 +1,143 @@
> +From 11125e7362281772f6d8cebaee3d8a1c07456e31 Mon Sep 17 00:00:00 2001
> +From: Rob Murray <rob.murray@docker.com>
> +Date: Wed, 7 Feb 2024 18:53:27 +0000
> +Subject: [PATCH 2/4] Make 'internal' bridge networks accessible from host
> +
> +Prior to release 25.0.0, the bridge in an internal network was assigned
> +an IP address - making the internal network accessible from the host,
> +giving containers on the network access to anything listening on the
> +bridge's address (or INADDR_ANY on the host).
> +
> +This change restores that behaviour. It does not restore the default
> +route that was configured in the container, because packets sent outside
> +the internal network's subnet have always been dropped. So, a 'connect()'
> +to an address outside the subnet will still fail fast.
> +
> +CVE: CVE-2025-54410
> +Upstream-Status: Backport [https://github.com/moby/moby/commit/c761353e7c75]
> +
> +Signed-off-by: Rob Murray <rob.murray@docker.com>
> +(cherry picked from commit 419f5a637230570ec44c32f889f981f67ce15793)
> +Signed-off-by: Albin Kerouanton <albinker@gmail.com>
> +(cherry picked from commit c761353e7c75e4ffa90b80f3b5df40473d59620a)
> +Signed-off-by: Deepak Rathore <deeratho@cisco.com>
> +---
> + integration/networking/bridge_test.go         | 57 +++++++++++++++++++
> + libnetwork/drivers/bridge/setup_ipv4_linux.go | 10 ++--
> + .../drivers/bridge/setup_verify_linux.go      |  4 +-
> + 3 files changed, 63 insertions(+), 8 deletions(-)
> +
> +diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
> +index e3d1fe2a36..8f54234406 100644
> +--- a/integration/networking/bridge_test.go
> ++++ b/integration/networking/bridge_test.go
> +@@ -477,3 +477,60 @@ func TestDefaultBridgeAddresses(t *testing.T) {
> +		})
> +	}
> + }
> ++
> ++// Test that a container on an 'internal' network has IP connectivity with
> ++// the host (on its own subnet, because the n/w bridge has an address on that
> ++// subnet, and it's in the host's namespace).
> ++// Regression test for https://github.com/moby/moby/issues/47329
> ++func TestInternalNwConnectivity(t *testing.T) {
> ++	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
> ++
> ++	ctx := setupTest(t)
> ++
> ++	d := daemon.New(t)
> ++	d.StartWithBusybox(ctx, t, "-D", "--experimental", "--ip6tables")
> ++	defer d.Stop(t)
> ++
> ++	c := d.NewClientT(t)
> ++	defer c.Close()
> ++
> ++	const bridgeName = "intnw"
> ++	const gw4 = "172.30.0.1"
> ++	const gw6 = "fda9:4130:4715::1234"
> ++	network.CreateNoError(ctx, t, c, bridgeName,
> ++		network.WithInternal(),
> ++		network.WithIPv6(),
> ++		network.WithIPAM("172.30.0.0/24", gw4),
> ++		network.WithIPAM("fda9:4130:4715::/64", gw6),
> ++		network.WithDriver("bridge"),
> ++		network.WithOption("com.docker.network.bridge.name", bridgeName),
> ++	)
> ++	defer network.RemoveNoError(ctx, t, c, bridgeName)
> ++
> ++	const ctrName = "intctr"
> ++	id := container.Run(ctx, t, c,
> ++		container.WithName(ctrName),
> ++		container.WithImage("busybox:latest"),
> ++		container.WithCmd("top"),
> ++		container.WithNetworkMode(bridgeName),
> ++	)
> ++	defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
> ++
> ++	execCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
> ++	defer cancel()
> ++
> ++	res := container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", gw4})
> ++	assert.Check(t, is.Equal(res.ExitCode, 0))
> ++	assert.Check(t, is.Equal(res.Stderr(), ""))
> ++	assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received"))
> ++
> ++	res = container.ExecT(execCtx, t, c, id, []string{"ping6", "-c1", "-W3", gw6})
> ++	assert.Check(t, is.Equal(res.ExitCode, 0))
> ++	assert.Check(t, is.Equal(res.Stderr(), ""))
> ++	assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received"))
> ++
> ++	// Addresses outside the internal subnet must not be accessible.
> ++	res = container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", "8.8.8.8"})
> ++	assert.Check(t, is.Equal(res.ExitCode, 1))
> ++	assert.Check(t, is.Contains(res.Stderr(), "Network is unreachable"))
> ++}
> +diff --git a/libnetwork/drivers/bridge/setup_ipv4_linux.go b/libnetwork/drivers/bridge/setup_ipv4_linux.go
> +index 0940745c23..6b925190ce 100644
> +--- a/libnetwork/drivers/bridge/setup_ipv4_linux.go
> ++++ b/libnetwork/drivers/bridge/setup_ipv4_linux.go
> +@@ -32,10 +32,6 @@ func setupBridgeIPv4(config *networkConfiguration, i *bridgeInterface) error {
> +	//             are decoupled, we should assign it only when it's really needed.
> +	i.bridgeIPv4 = config.AddressIPv4
> +
> +-	if config.Internal {
> +-		return nil
> +-	}
> +-
> +	if !config.InhibitIPv4 {
> +		addrv4List, err := i.addresses(netlink.FAMILY_V4)
> +		if err != nil {
> +@@ -57,8 +53,10 @@ func setupBridgeIPv4(config *networkConfiguration, i *bridgeInterface) error {
> +		}
> +	}
> +
> +-	// Store the default gateway
> +-	i.gatewayIPv4 = config.AddressIPv4.IP
> ++	if !config.Internal {
> ++		// Store the default gateway
> ++		i.gatewayIPv4 = config.AddressIPv4.IP
> ++	}
> +
> +	return nil
> + }
> +diff --git a/libnetwork/drivers/bridge/setup_verify_linux.go b/libnetwork/drivers/bridge/setup_verify_linux.go
> +index a39d750346..b7ddbcf814 100644
> +--- a/libnetwork/drivers/bridge/setup_verify_linux.go
> ++++ b/libnetwork/drivers/bridge/setup_verify_linux.go
> +@@ -20,12 +20,12 @@ func setupVerifyAndReconcileIPv4(config *networkConfiguration, i *bridgeInterfac
> +	addrv4, _ := selectIPv4Address(addrsv4, config.AddressIPv4)
> +
> +	// Verify that the bridge has an IPv4 address.
> +-	if !config.Internal && addrv4.IPNet == nil {
> ++	if addrv4.IPNet == nil {
> +		return &ErrNoIPAddr{}
> +	}
> +
> +	// Verify that the bridge IPv4 address matches the requested configuration.
> +-	if config.AddressIPv4 != nil && addrv4.IPNet != nil && !addrv4.IP.Equal(config.AddressIPv4.IP) {
> ++	if config.AddressIPv4 != nil && !addrv4.IP.Equal(config.AddressIPv4.IP) {
> +		return &IPv4AddrNoMatchError{IP: addrv4.IP, CfgIP: config.AddressIPv4.IP}
> +	}
> +
> +--
> +2.44.3
> diff --git a/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch b/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
> new file mode 100644
> index 00000000..16d4768f
> --- /dev/null
> +++ b/recipes-containers/docker/files/CVE-2025-54410-dependent_p3.patch
> @@ -0,0 +1,451 @@
> +From 211e9acdef48b1229591724ba83fe4b6ad86c7b6 Mon Sep 17 00:00:00 2001
> +From: Rob Murray <rob.murray@docker.com>
> +Date: Fri, 14 Feb 2025 16:23:49 +0000
> +Subject: [PATCH 3/4] Restore iptables for current networks on firewalld reload
> +
> +Using iptables.OnReloaded to restore individual per-network rules
> +on firewalld reload means rules for deleted networks pop back in
> +to existence (because there was no way to delete the callbacks on
> +network-delete).
> +
> +So, on firewalld reload, walk over current networks and ask them
> +to restore their iptables rules.
> +
> +CVE: CVE-2025-54410
> +Upstream-Status: Backport [https://github.com/moby/moby/commit/41f080df250a]
> +
> +Signed-off-by: Rob Murray <rob.murray@docker.com>
> +(cherry picked from commit a527e5a546526117a0f3d7a33f3fcb8f4cd87c72)
> +
> +Test that firewalld reload doesn't re-create deleted iptables rules
> +
> +Signed-off-by: Rob Murray <rob.murray@docker.com>
> +(cherry picked from commit c3fa7c17794feaf4e27238aa3c6de42c606765fa)
> +
> +Signed-off-by: Andrey Epifanov <aepifanov@mirantis.com>
> +(cherry picked from commit 41f080df250aef805d34628d1719c369e9ff1585)
> +Signed-off-by: Deepak Rathore <deeratho@cisco.com>
> +---
> + integration/network/bridge_test.go            | 65 +++++++++++++++++++
> + integration/networking/bridge_test.go         |  4 ++
> + internal/testutils/networking/iptables.go     | 48 ++++++++++++++
> + .../testutils/networking/iptables_test.go     | 47 ++++++++++++++
> + libnetwork/drivers/bridge/bridge_linux.go     | 65 +++++++++++++++++--
> + libnetwork/drivers/bridge/setup_firewalld.go  | 41 ------------
> + .../drivers/bridge/setup_ip_tables_linux.go   |  3 +
> + libnetwork/iptables/firewalld.go              |  1 +
> + 8 files changed, 227 insertions(+), 47 deletions(-)
> + create mode 100644 internal/testutils/networking/iptables_test.go
> + delete mode 100644 libnetwork/drivers/bridge/setup_firewalld.go
> +
> +diff --git a/integration/network/bridge_test.go b/integration/network/bridge_test.go
> +index b09ff5766c..09b7da9c2b 100644
> +--- a/integration/network/bridge_test.go
> ++++ b/integration/network/bridge_test.go
> +@@ -6,11 +6,17 @@ import (
> +	"testing"
> +	"time"
> +
> ++	containertypes "github.com/docker/docker/api/types/container"
> +	networktypes "github.com/docker/docker/api/types/network"
> +	"github.com/docker/docker/api/types/versions"
> +	ctr "github.com/docker/docker/integration/internal/container"
> +	"github.com/docker/docker/integration/internal/network"
> ++	"github.com/docker/docker/internal/testutils/networking"
> ++	"github.com/docker/docker/libnetwork/drivers/bridge"
> ++	"github.com/docker/docker/testutil/daemon"
> ++	"github.com/docker/go-connections/nat"
> +	"gotest.tools/v3/assert"
> ++	"gotest.tools/v3/icmd"
> +	"gotest.tools/v3/skip"
> + )
> +
> +@@ -43,3 +49,62 @@ func TestCreateWithMultiNetworks(t *testing.T) {
> +	ifacesWithAddress := strings.Count(res.Stdout.String(), "\n")
> +	assert.Equal(t, ifacesWithAddress, 3)
> + }
> ++
> ++// TestFirewalldReloadNoZombies checks that when firewalld is reloaded, rules
> ++// belonging to deleted networks/containers do not reappear.
> ++func TestFirewalldReloadNoZombies(t *testing.T) {
> ++	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
> ++	skip.If(t, !networking.FirewalldRunning(), "firewalld is not running")
> ++	skip.If(t, testEnv.IsRootless, "no firewalld in rootless netns")
> ++
> ++	ctx := setupTest(t)
> ++	d := daemon.New(t)
> ++	d.StartWithBusybox(ctx, t)
> ++	defer d.Stop(t)
> ++	c := d.NewClientT(t)
> ++
> ++	const bridgeName = "br-fwdreload"
> ++	removed := false
> ++	nw := network.CreateNoError(ctx, t, c, "testnet",
> ++		network.WithOption(bridge.BridgeName, bridgeName))
> ++	defer func() {
> ++		if !removed {
> ++			network.RemoveNoError(ctx, t, c, nw)
> ++		}
> ++	}()
> ++
> ++	cid := ctr.Run(ctx, t, c,
> ++		ctr.WithExposedPorts("80/tcp", "81/tcp"),
> ++		ctr.WithPortMap(nat.PortMap{"80/tcp": {{HostPort: "8000"}}}))
> ++	defer func() {
> ++		if !removed {
> ++			ctr.Remove(ctx, t, c, cid, containertypes.RemoveOptions{Force: true})
> ++		}
> ++	}()
> ++
> ++	iptablesSave := icmd.Command("iptables-save")
> ++	resBeforeDel := icmd.RunCmd(iptablesSave)
> ++	assert.NilError(t, resBeforeDel.Error)
> ++	assert.Check(t, strings.Contains(resBeforeDel.Combined(), bridgeName),
> ++		"With container: expected rules for %s in: %s", bridgeName, resBeforeDel.Combined())
> ++
> ++	// Delete the container and its network.
> ++	ctr.Remove(ctx, t, c, cid, containertypes.RemoveOptions{Force: true})
> ++	network.RemoveNoError(ctx, t, c, nw)
> ++	removed = true
> ++
> ++	// Check the network does not appear in iptables rules.
> ++	resAfterDel := icmd.RunCmd(iptablesSave)
> ++	assert.NilError(t, resAfterDel.Error)
> ++	assert.Check(t, !strings.Contains(resAfterDel.Combined(), bridgeName),
> ++		"After deletes: did not expect rules for %s in: %s", bridgeName, resAfterDel.Combined())
> ++
> ++	// firewall-cmd --reload, and wait for the daemon to restore rules.
> ++	networking.FirewalldReload(t, d)
> ++
> ++	// Check that rules for the deleted container/network have not reappeared.
> ++	resAfterReload := icmd.RunCmd(iptablesSave)
> ++	assert.NilError(t, resAfterReload.Error)
> ++	assert.Check(t, !strings.Contains(resAfterReload.Combined(), bridgeName),
> ++		"After deletes: did not expect rules for %s in: %s", bridgeName, resAfterReload.Combined())
> ++}
> +diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
> +index 8f54234406..8cdcf02096 100644
> +--- a/integration/networking/bridge_test.go
> ++++ b/integration/networking/bridge_test.go
> +@@ -10,6 +10,7 @@ import (
> +	containertypes "github.com/docker/docker/api/types/container"
> +	"github.com/docker/docker/integration/internal/container"
> +	"github.com/docker/docker/integration/internal/network"
> ++	"github.com/docker/docker/internal/testutils/networking"
> +	"github.com/docker/docker/testutil"
> +	"github.com/docker/docker/testutil/daemon"
> +	"gotest.tools/v3/assert"
> +@@ -160,6 +161,8 @@ func TestBridgeICC(t *testing.T) {
> +				Force: true,
> +			})
> +
> ++			networking.FirewalldReload(t, d)
> ++
> +			pingHost := tc.pingHost
> +			if pingHost == "" {
> +				if tc.linkLocal {
> +@@ -515,6 +518,7 @@ func TestInternalNwConnectivity(t *testing.T) {
> +		container.WithNetworkMode(bridgeName),
> +	)
> +	defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
> ++	networking.FirewalldReload(t, d)
> +
> +	execCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
> +	defer cancel()
> +diff --git a/internal/testutils/networking/iptables.go b/internal/testutils/networking/iptables.go
> +index 2e6ada8363..2041e507c3 100644
> +--- a/internal/testutils/networking/iptables.go
> ++++ b/internal/testutils/networking/iptables.go
> +@@ -1,12 +1,60 @@
> + package networking
> +
> + import (
> ++	"fmt"
> +	"os/exec"
> ++	"regexp"
> +	"strings"
> +	"testing"
> ++	"time"
> ++
> ++	"github.com/docker/docker/testutil/daemon"
> ++	"golang.org/x/net/context"
> ++	"gotest.tools/v3/assert"
> ++	"gotest.tools/v3/icmd"
> ++	"gotest.tools/v3/poll"
> + )
> +
> + func FirewalldRunning() bool {
> +	state, err := exec.Command("firewall-cmd", "--state").CombinedOutput()
> +	return err == nil && strings.TrimSpace(string(state)) == "running"
> + }
> ++
> ++func extractLogTime(s string) (time.Time, error) {
> ++	// time="2025-07-15T13:46:13.414214418Z" level=info msg=""
> ++	re := regexp.MustCompile(`time="([^"]+)"`)
> ++	matches := re.FindStringSubmatch(s)
> ++	if len(matches) < 2 {
> ++		return time.Time{}, fmt.Errorf("timestamp not found in log line: %s, matches: %+v", s, matches)
> ++	}
> ++
> ++	return time.Parse(time.RFC3339Nano, matches[1])
> ++}
> ++
> ++// FirewalldReload reloads firewalld and waits for the daemon to re-create its rules.
> ++// It's a no-op if firewalld is not running, and the test fails if the reload does
> ++// not complete.
> ++func FirewalldReload(t *testing.T, d *daemon.Daemon) {
> ++	t.Helper()
> ++	if !FirewalldRunning() {
> ++		return
> ++	}
> ++	timeBeforeReload := time.Now()
> ++	res := icmd.RunCommand("firewall-cmd", "--reload")
> ++	assert.NilError(t, res.Error)
> ++
> ++	ctx := context.Background()
> ++	poll.WaitOn(t, d.PollCheckLogs(ctx, func(s string) bool {
> ++		if !strings.Contains(s, "Firewalld reload completed") {
> ++			return false
> ++		}
> ++		lastReload, err := extractLogTime(s)
> ++		if err != nil {
> ++			return false
> ++		}
> ++		if lastReload.After(timeBeforeReload) {
> ++			return true
> ++		}
> ++		return false
> ++	}))
> ++}
> +diff --git a/internal/testutils/networking/iptables_test.go b/internal/testutils/networking/iptables_test.go
> +new file mode 100644
> +index 0000000000..24a7bbb49d
> +--- /dev/null
> ++++ b/internal/testutils/networking/iptables_test.go
> +@@ -0,0 +1,47 @@
> ++package networking
> ++
> ++import (
> ++	"reflect"
> ++	"testing"
> ++	"time"
> ++)
> ++
> ++func Test_getTimeFromLogMsg(t *testing.T) {
> ++	tests := []struct {
> ++		name    string
> ++		s       string
> ++		want    time.Time
> ++		wantErr bool
> ++	}{
> ++		{
> ++			name:    "valid time",
> ++			s:       `time="2025-07-15T13:46:13.414214418Z" level=info msg=""`,
> ++			want:    time.Date(2025, 7, 15, 13, 46, 13, 414214418, time.UTC),
> ++			wantErr: false,
> ++		},
> ++		{
> ++			name:    "invalid format",
> ++			s:       `time="invalid-time-format" level=info msg=""`,
> ++			want:    time.Time{},
> ++			wantErr: true,
> ++		},
> ++		{
> ++			name:    "missing time",
> ++			s:       `level=info msg=""`,
> ++			want:    time.Time{},
> ++			wantErr: true,
> ++		},
> ++	}
> ++	for _, tt := range tests {
> ++		t.Run(tt.name, func(t *testing.T) {
> ++			got, err := extractLogTime(tt.s)
> ++			if (err != nil) != tt.wantErr {
> ++				t.Errorf("getTimeFromLogMsg() error = %v, wantErr %v", err, tt.wantErr)
> ++				return
> ++			}
> ++			if !reflect.DeepEqual(got, tt.want) {
> ++				t.Errorf("getTimeFromLogMsg() got = %v, want %v", got, tt.want)
> ++			}
> ++		})
> ++	}
> ++}
> +diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go
> +index 005c726f2d..2b313c85bf 100644
> +--- a/libnetwork/drivers/bridge/bridge_linux.go
> ++++ b/libnetwork/drivers/bridge/bridge_linux.go
> +@@ -483,6 +483,8 @@ func (d *driver) configure(option map[string]interface{}) error {
> +	d.config = config
> +	d.Unlock()
> +
> ++	iptables.OnReloaded(d.handleFirewalldReload)
> ++
> +	return d.initStore(option)
> + }
> +
> +@@ -800,12 +802,6 @@ func (d *driver) createNetwork(config *networkConfiguration) (err error) {
> +		// Setup IP6Tables.
> +		{config.EnableIPv6 && d.config.EnableIP6Tables, network.setupIP6Tables},
> +
> +-		// We want to track firewalld configuration so that
> +-		// if it is started/reloaded, the rules can be applied correctly
> +-		{d.config.EnableIPTables, network.setupFirewalld},
> +-		// same for IPv6
> +-		{config.EnableIPv6 && d.config.EnableIP6Tables, network.setupFirewalld6},
> +-
> +		// Setup DefaultGatewayIPv4
> +		{config.DefaultGatewayIPv4 != nil, setupGatewayIPv4},
> +
> +@@ -1297,6 +1293,11 @@ func (d *driver) Leave(nid, eid string) error {
> + }
> +
> + func (d *driver) ProgramExternalConnectivity(nid, eid string, options map[string]interface{}) error {
> ++	// Make sure the network isn't deleted, or the in middle of a firewalld reload, while
> ++	// updating its iptables rules.
> ++	d.configNetwork.Lock()
> ++	defer d.configNetwork.Unlock()
> ++
> +	network, err := d.getNetwork(nid)
> +	if err != nil {
> +		return err
> +@@ -1348,6 +1349,11 @@ func (d *driver) ProgramExternalConnectivity(nid, eid string, options map[string
> + }
> +
> + func (d *driver) RevokeExternalConnectivity(nid, eid string) error {
> ++	// Make sure this function isn't deleting iptables rules while handleFirewalldReloadNw
> ++	// is restoring those same rules.
> ++	d.configNetwork.Lock()
> ++	defer d.configNetwork.Unlock()
> ++
> +	network, err := d.getNetwork(nid)
> +	if err != nil {
> +		return err
> +@@ -1381,6 +1387,53 @@ func (d *driver) RevokeExternalConnectivity(nid, eid string) error {
> +	return nil
> + }
> +
> ++func (d *driver) handleFirewalldReload() {
> ++	if !d.config.EnableIPTables && !d.config.EnableIP6Tables {
> ++		return
> ++	}
> ++
> ++	d.Lock()
> ++	nids := make([]string, 0, len(d.networks))
> ++	for _, nw := range d.networks {
> ++		nids = append(nids, nw.id)
> ++	}
> ++	d.Unlock()
> ++
> ++	for _, nid := range nids {
> ++		d.handleFirewalldReloadNw(nid)
> ++	}
> ++}
> ++
> ++func (d *driver) handleFirewalldReloadNw(nid string) {
> ++	// Make sure the network isn't being deleted, and ProgramExternalConnectivity/RevokeExternalConnectivity
> ++	// aren't modifying iptables rules, while restoring the rules.
> ++	d.configNetwork.Lock()
> ++	defer d.configNetwork.Unlock()
> ++
> ++	nw, err := d.getNetwork(nid)
> ++	if err != nil {
> ++		return
> ++	}
> ++	if d.config.EnableIPTables {
> ++		if err := nw.setupIP4Tables(nw.config, nw.bridge); err != nil {
> ++			log.G(context.TODO()).WithFields(log.Fields{
> ++				"network": nw.id,
> ++				"error":   err,
> ++			}).Warn("Failed to restore IPv4 per-port iptables rules on firewalld reload")
> ++		}
> ++	}
> ++	if d.config.EnableIP6Tables {
> ++		if err := nw.setupIP6Tables(nw.config, nw.bridge); err != nil {
> ++			log.G(context.TODO()).WithFields(log.Fields{
> ++				"network": nw.id,
> ++				"error":   err,
> ++			}).Warn("Failed to restore IPv6 per-port iptables rules on firewalld reload")
> ++		}
> ++	}
> ++	nw.portMapper.ReMapAll()
> ++	log.G(context.TODO()).Info("Restored iptables rules on firewalld reload")
> ++}
> ++
> + func (d *driver) link(network *bridgeNetwork, endpoint *bridgeEndpoint, enable bool) (retErr error) {
> +	cc := endpoint.containerConfig
> +	ec := endpoint.extConnConfig
> +diff --git a/libnetwork/drivers/bridge/setup_firewalld.go b/libnetwork/drivers/bridge/setup_firewalld.go
> +deleted file mode 100644
> +index db7843847c..0000000000
> +--- a/libnetwork/drivers/bridge/setup_firewalld.go
> ++++ /dev/null
> +@@ -1,41 +0,0 @@
> +-//go:build linux
> +-
> +-package bridge
> +-
> +-import (
> +-	"errors"
> +-
> +-	"github.com/docker/docker/libnetwork/iptables"
> +-)
> +-
> +-func (n *bridgeNetwork) setupFirewalld(config *networkConfiguration, i *bridgeInterface) error {
> +-	d := n.driver
> +-	d.Lock()
> +-	driverConfig := d.config
> +-	d.Unlock()
> +-
> +-	// Sanity check.
> +-	if !driverConfig.EnableIPTables {
> +-		return errors.New("no need to register firewalld hooks, iptables is disabled")
> +-	}
> +-
> +-	iptables.OnReloaded(func() { n.setupIP4Tables(config, i) })
> +-	iptables.OnReloaded(n.portMapper.ReMapAll)
> +-	return nil
> +-}
> +-
> +-func (n *bridgeNetwork) setupFirewalld6(config *networkConfiguration, i *bridgeInterface) error {
> +-	d := n.driver
> +-	d.Lock()
> +-	driverConfig := d.config
> +-	d.Unlock()
> +-
> +-	// Sanity check.
> +-	if !driverConfig.EnableIP6Tables {
> +-		return errors.New("no need to register firewalld hooks, ip6tables is disabled")
> +-	}
> +-
> +-	iptables.OnReloaded(func() { n.setupIP6Tables(config, i) })
> +-	iptables.OnReloaded(n.portMapperV6.ReMapAll)
> +-	return nil
> +-}
> +diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
> +index 328c58bced..42039e24ee 100644
> +--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go
> ++++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
> +@@ -130,6 +130,9 @@ func (n *bridgeNetwork) setupIP6Tables(config *networkConfiguration, i *bridgeIn
> +		return errors.New("Cannot program chains, EnableIP6Tables is disabled")
> +	}
> +
> ++	if i.bridgeIPv6 == nil {
> ++		return nil
> ++	}
> +	maskedAddrv6 := &net.IPNet{
> +		IP:   i.bridgeIPv6.IP.Mask(i.bridgeIPv6.Mask),
> +		Mask: i.bridgeIPv6.Mask,
> +diff --git a/libnetwork/iptables/firewalld.go b/libnetwork/iptables/firewalld.go
> +index dc67240da6..d3b76426e5 100644
> +--- a/libnetwork/iptables/firewalld.go
> ++++ b/libnetwork/iptables/firewalld.go
> +@@ -132,6 +132,7 @@ func reloaded() {
> +	for _, pf := range onReloaded {
> +		(*pf)()
> +	}
> ++	log.G(context.TODO()).Info("Firewalld reload completed")
> + }
> +
> + // OnReloaded add callback
> +--
> +2.44.3
> diff --git a/recipes-containers/docker/files/CVE-2025-54410.patch b/recipes-containers/docker/files/CVE-2025-54410.patch
> new file mode 100644
> index 00000000..b1aa211c
> --- /dev/null
> +++ b/recipes-containers/docker/files/CVE-2025-54410.patch
> @@ -0,0 +1,50 @@
> +From a4b2c5a774213eb6594cdece17680e16961c8fcb Mon Sep 17 00:00:00 2001
> +From: Rob Murray <rob.murray@docker.com>
> +Date: Fri, 14 Feb 2025 16:50:43 +0000
> +Subject: [PATCH 4/4] Restore INC iptables rules on firewalld reload
> +
> +CVE: CVE-2025-54410
> +Upstream-Status: Backport [https://github.com/moby/moby/commit/651b2feb2731]
> +
> +Signed-off-by: Rob Murray <rob.murray@docker.com>
> +(cherry picked from commit 651b2feb27316cf907173c2a76cc6eb85f763663)
> +Signed-off-by: Deepak Rathore <deeratho@cisco.com>
> +---
> + integration/networking/bridge_test.go     | 1 +
> + libnetwork/drivers/bridge/bridge_linux.go | 9 +++++++++
> + 2 files changed, 10 insertions(+)
> +
> +diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
> +index 8cdcf02096..7c88cd8b43 100644
> +--- a/integration/networking/bridge_test.go
> ++++ b/integration/networking/bridge_test.go
> +@@ -294,6 +294,7 @@ func TestBridgeINC(t *testing.T) {
> +			defer c.ContainerRemove(ctx, id1, containertypes.RemoveOptions{
> +				Force: true,
> +			})
> ++			networking.FirewalldReload(t, d)
> +
> +			ctr1Info := container.Inspect(ctx, t, c, id1)
> +			targetAddr := ctr1Info.NetworkSettings.Networks[bridge1].IPAddress
> +diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go
> +index 2b313c85bf..a8a12aebe3 100644
> +--- a/libnetwork/drivers/bridge/bridge_linux.go
> ++++ b/libnetwork/drivers/bridge/bridge_linux.go
> +@@ -1431,6 +1431,15 @@ func (d *driver) handleFirewalldReloadNw(nid string) {
> +		}
> +	}
> +	nw.portMapper.ReMapAll()
> ++
> ++	// Restore the inter-network connectivity (INC) rules.
> ++	if err := nw.isolateNetwork(true); err != nil {
> ++		log.G(context.TODO()).WithFields(log.Fields{
> ++			"network": nw.id,
> ++			"error":   err,
> ++		}).Warn("Failed to restore inter-network iptables rules on firewalld reload")
> ++	}
> ++
> +	log.G(context.TODO()).Info("Restored iptables rules on firewalld reload")
> + }
> +
> +--
> +2.44.3
> -- 
> 2.44.3
> 


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2025-09-19  2:34 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-09-08  7:09 [meta-virtualization] [scarthgap] [PATCH] docker-moby: Fix CVE-2025-54410 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2025-09-19  2:34 ` Bruce Ashfield

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.