diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index ef707238f4e..7aada58032a 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -1455,6 +1455,11 @@ func readConmonPipeData(runtimeName string, pipe *os.File, ociLog string) (int, if ociLog != "" { ociLogData, err := os.ReadFile(ociLog) if err == nil { + if errs, err := parseOCIErrors(ociLogData); err == nil { + if tcpErr := checkOCIErrorsForTCPEstablished(errs); tcpErr != nil { + return -1, tcpErr + } + } var ociErr ociError if err := json.Unmarshal(ociLogData, &ociErr); err == nil { return -1, getOCIRuntimeError(runtimeName, ociErr.Msg) @@ -1468,6 +1473,11 @@ func readConmonPipeData(runtimeName string, pipe *os.File, ociLog string) (int, if ociLog != "" { ociLogData, err := os.ReadFile(ociLog) if err == nil { + if errs, err := parseOCIErrors(ociLogData); err == nil { + if tcpErr := checkOCIErrorsForTCPEstablished(errs); tcpErr != nil { + return ss.si.Data, tcpErr + } + } var ociErr ociError if err := json.Unmarshal(ociLogData, &ociErr); err == nil { return ss.si.Data, getOCIRuntimeError(runtimeName, ociErr.Msg) diff --git a/libpod/oci_util.go b/libpod/oci_util.go index bc316b5a74a..46df9023e65 100644 --- a/libpod/oci_util.go +++ b/libpod/oci_util.go @@ -3,6 +3,7 @@ package libpod import ( + "bytes" "errors" "fmt" "net" @@ -30,6 +31,34 @@ type ociError struct { Msg string `json:"msg,omitempty"` } +func parseOCIErrors(log []byte) ([]ociError, error) { + var errs []ociError + for _, line := range bytes.Split(log, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + var e ociError + if err := json.Unmarshal(line, &e); err != nil { + return nil, err + } + errs = append(errs, e) + } + return errs, nil +} + +// checkOCIErrorsForTCPEstablished tries to match +// an ociError indicating established TCP connections +// and convert it to a user-facing message. +func checkOCIErrorsForTCPEstablished(errs []ociError) error { + for _, e := range errs { + if strings.Contains(e.Msg, "Connected TCP socket in image") { + return fmt.Errorf("checkpoint contains established TCP connections, restore requires --tcp-established or --tcp-close: %w", define.ErrOCIRuntime) + } + } + return nil +} + // Bind ports to keep them closed on the host func bindPorts(ports []types.PortMapping) ([]*os.File, error) { var files []*os.File diff --git a/libpod/oci_util_test.go b/libpod/oci_util_test.go new file mode 100644 index 00000000000..8cd5c7a515c --- /dev/null +++ b/libpod/oci_util_test.go @@ -0,0 +1,53 @@ +//go:build !remote && (linux || freebsd) + +package libpod + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestParseOCIErrors verifies parsing of multiple ociErrors +// from a log file. +func TestParseOCIErrors(t *testing.T) { + log := []byte(`{"msg":"(00.022100) Error (criu/sk-inet.c:654): Connected TCP socket in image","level":"error","time":"2025-01-01T00:00:00Z"} +{"msg":"(00.022200) Error (criu/cr-restore.c:2522): Restoring FAILED.","level":"error","time":"2025-01-01T00:00:01Z"} +{"msg":"(00.022418) Error (criu/cgroup.c:1970): cg: cgroupd: recv req error: No such file or directory","level":"error","time":"2025-01-01T00:00:02Z"} +`) + + errs, err := parseOCIErrors(log) + require.NoError(t, err) + + // Verify that all 3 messages are parsed. + assert.Equal(t, []ociError{ + {Msg: "(00.022100) Error (criu/sk-inet.c:654): Connected TCP socket in image", Level: "error", Time: "2025-01-01T00:00:00Z"}, + {Msg: "(00.022200) Error (criu/cr-restore.c:2522): Restoring FAILED.", Level: "error", Time: "2025-01-01T00:00:01Z"}, + {Msg: "(00.022418) Error (criu/cgroup.c:1970): cg: cgroupd: recv req error: No such file or directory", Level: "error", Time: "2025-01-01T00:00:02Z"}, + }, errs) +} + +// TestCheckOCIErrorsForTCPEstablished tests matching +// of "Connected TCP socket in image" errors in a log file. +func TestCheckOCIErrorsForTCPEstablished(t *testing.T) { + withTCP := []ociError{ + {Msg: "(00.022100) Error (criu/sk-inet.c:654): Connected TCP socket in image", Level: "error"}, + {Msg: "(00.022200) Error (criu/cr-restore.c:2522): Restoring FAILED.", Level: "error"}, + {Msg: "(00.022418) Error (criu/cgroup.c:1970): cg: cgroupd: recv req error: No such file or directory", Level: "error"}, + } + + err := checkOCIErrorsForTCPEstablished(withTCP) + + // Verify that the log message is matched and expected error converted. + require.Error(t, err) + assert.Contains(t, err.Error(), "--tcp-established") + + withoutTCP := []ociError{ + {Msg: "some other OCI runtime error", Level: "error"}, + } + + // Verify that different message is not matched. + err = checkOCIErrorsForTCPEstablished(withoutTCP) + assert.NoError(t, err) +} diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index d162b99cbf9..0bf557e09c1 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -347,9 +347,9 @@ var _ = Describe("Podman checkpoint", func() { result = podmanTest.Podman([]string{"container", "restore", cid}) result.WaitWithDefaultTimeout() - // Some older versions print "CRIU restoring failed: -52" while others - // "Error: crun: (00.054135) Error (criu/cgroup.c:1998): cg: cgroupd: recv req error: No such file or directory: OCI runtime attempted to invoke a command that was not found" - expectStderr := "cg: cgroupd: recv req error|CRIU restoring failed: -52" + // Some older versions print "CRIU restoring failed: -52", + // for newer versions the error message is converted. + expectStderr := "checkpoint contains established TCP connections, restore requires --tcp-established or --tcp-close|CRIU restoring failed: -52" if podmanTest.OCIRuntime == "runc" { expectStderr = "runc: criu failed: type NOTIFY errno 0" } @@ -391,9 +391,9 @@ var _ = Describe("Podman checkpoint", func() { result := podmanTest.Podman([]string{"container", "restore", cid}) result.WaitWithDefaultTimeout() - // Some older versions print "CRIU restoring failed: -52" while others - // "Error: crun: (00.054135) Error (criu/cgroup.c:1998): cg: cgroupd: recv req error: No such file or directory: OCI runtime attempted to invoke a command that was not found" - expectStderr := "cg: cgroupd: recv req error|CRIU restoring failed: -52" + // Some older versions print "CRIU restoring failed: -52", + // for newer versions the error message is converted. + expectStderr := "checkpoint contains established TCP connections, restore requires --tcp-established or --tcp-close|CRIU restoring failed: -52" if podmanTest.OCIRuntime == "runc" { expectStderr = "runc: criu failed: type NOTIFY errno 0" }