From 3a021fa60f468da186ff3539c47e176eccb015cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TD=E5=8C=97=E5=B2=B8=E8=8A=B1=E5=9B=AD?= Date: Thu, 25 Dec 2025 17:33:03 +0800 Subject: [PATCH] backport to fix CVE-2025-54410 --- 1021-backport-to-fix-CVE-2025-54410.patch | 443 ++++++++++++++++++++++ moby.spec | 9 +- 2 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 1021-backport-to-fix-CVE-2025-54410.patch diff --git a/1021-backport-to-fix-CVE-2025-54410.patch b/1021-backport-to-fix-CVE-2025-54410.patch new file mode 100644 index 0000000..29ee2d9 --- /dev/null +++ b/1021-backport-to-fix-CVE-2025-54410.patch @@ -0,0 +1,443 @@ +From 41f080df250aef805d34628d1719c369e9ff1585 Mon Sep 17 00:00:00 2001 +From: Rob Murray +Date: Fri, 14 Feb 2025 16:23:49 +0000 +Subject: [PATCH] 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. + +Signed-off-by: Rob Murray +(cherry picked from commit a527e5a546526117a0f3d7a33f3fcb8f4cd87c72) + +Test that firewalld reload doesn't re-create deleted iptables rules + +Signed-off-by: Rob Murray +(cherry picked from commit c3fa7c17794feaf4e27238aa3c6de42c606765fa) + +Signed-off-by: Andrey Epifanov + +--- + integration/network/bridge_test.go | 65 +++++++++++++++++++ + integration/networking/bridge_test.go | 3 + + internal/testutils/networking/iptables.go | 60 +++++++++++++++++ + .../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 | 4 ++ + libnetwork/iptables/firewalld.go | 1 + + 8 files changed, 239 insertions(+), 47 deletions(-) + create mode 100644 internal/testutils/networking/iptables.go + 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 b09ff57..09b7da9 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 1015077..f114637 100644 +--- a/integration/networking/bridge_test.go ++++ b/integration/networking/bridge_test.go +@@ -11,6 +11,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" +@@ -167,6 +168,8 @@ func TestBridgeICC(t *testing.T) { + Force: true, + }) + ++ networking.FirewalldReload(t, d) ++ + pingHost := tc.pingHost + if pingHost == "" { + if tc.isLinkLocal { +diff --git a/internal/testutils/networking/iptables.go b/internal/testutils/networking/iptables.go +new file mode 100644 +index 0000000..2041e50 +--- /dev/null ++++ b/internal/testutils/networking/iptables.go +@@ -0,0 +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 0000000..24a7bbb +--- /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 005c726..2b313c8 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 db78438..0000000 +--- 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 328c58b..a28c8ca 100644 +--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go ++++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go +@@ -130,6 +130,10 @@ 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 dc67240..d3b7642 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.43.0 + diff --git a/moby.spec b/moby.spec index c255e77..bcc0e34 100644 --- a/moby.spec +++ b/moby.spec @@ -7,7 +7,7 @@ Name: moby Version: 25.0.3 -Release: 32 +Release: 33 Summary: The open-source application container engine License: Apache-2.0 URL: https://www.docker.com @@ -44,6 +44,7 @@ Patch1017: 1017-libnetwork-fix-non-constant-format-string-in-call-go.patch Patch1018: 1018-Fix-setup-user-chains-even-if-there-are-running-cont.patch Patch1019: 1019-Dockerd-rootless-make-etc-var-run-cdi-available.patch Patch1020: 1020-seccomp-add-riscv64-mapping-to-seccomp_linux.go.patch +Patch1021: 1021-backport-to-fix-CVE-2025-54410.patch # Patch 2001-2999 for tini Patch2001: 2001-tini.c-a-function-declaration-without-a-prototype-is.patch Requires(meta): %{name}-engine = %{version}-%{release} @@ -235,6 +236,12 @@ fi %systemd_postun_with_restart docker.service %changelog +* Thu Dec 25 2025 zhangbowei - 25.0.3-33 +-Type:cve +-CVE:CVE-2025-54410 +-SUG:NA +-DESC:backport to fix CVE-2025-54410 + * Mon Jun 9 2025 Dingli Zhang - 25.0.3-32 - seccomp: add riscv64 mapping to seccomp_linux.go -- Gitee