diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b0d34f829..b8a8976b0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,7 +51,7 @@ jobs: - ubuntu:plucky container: image: ${{ matrix.container }} - options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined + options: --privileged steps: - name: Sanitize container name (for artifact name) run: | diff --git a/data/apport b/data/apport index ad1456389..545fef30a 100755 --- a/data/apport +++ b/data/apport @@ -503,15 +503,29 @@ def is_systemd_watchdog_restart(signum: int, proc_pid: ProcPid) -> bool: def is_same_ns(proc_pid: ProcPid, ns: str) -> bool: if not os.path.exists(f"/proc/self/ns/{ns}") or not proc_pid.exists(f"ns/{ns}"): + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): does not exist (True 1)") # If the namespace doesn't exist, then it's obviously shared return True try: + path = proc_pid.readlink(f"ns/{ns}") + print( + f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): " + f"proc_pid.readlink('ns/{ns}') = {path}" + ) + path = os.readlink(f"/proc/self/ns/{ns}") + print( + f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): " + f"os.readlink('/proc/self/ns/{ns}') = {path}" + ) if proc_pid.readlink(f"ns/{ns}") == os.readlink(f"/proc/self/ns/{ns}"): # Check that the inode for both namespaces is the same + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): same ns (True 2)") return True except OSError as error: + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): OSError: {error}") if error.errno == errno.ENOENT: + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): ENOENT (True 2)") return True raise @@ -519,10 +533,16 @@ def is_same_ns(proc_pid: ProcPid, ns: str) -> bool: with contextlib.suppress(FileNotFoundError): with proc_pid.open("cgroup") as cgroup: for line in cgroup: + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): cgroup line: {line}") fields = line.split(":") if fields[-1].startswith("/system.slice"): + print( + f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}):" + f" system.slice (True 4)" + ) return True + print(f"DEBUG: is_same_ns({proc_pid.pid}, {ns!r}): False") return False diff --git a/tests/integration/test_signal_crashes.py b/tests/integration/test_signal_crashes.py index 0e49ace54..4141fa6b6 100644 --- a/tests/integration/test_signal_crashes.py +++ b/tests/integration/test_signal_crashes.py @@ -90,6 +90,34 @@ } """ +UNSHARE_CODE = """\ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +int main() { + if (unshare(CLONE_NEWNS | CLONE_NEWPID) == -1) + err(EXIT_FAILURE, "unshare"); + + pid_t child = fork(); + if (child < 0) { + err(EXIT_FAILURE, "fork"); + } else if (child > 0) { + int rc; + waitpid(child, &rc, 0); + return rc; + } + + int zero = 0; + printf("42 / 0 = %i\\n", 42 / zero); + return 0; +} +""" + @contextlib.contextmanager def compile_c_code(name: str, c_code: str, tmpdir: str = "/var/tmp") -> Iterator[str]: @@ -785,6 +813,30 @@ def test_crash_setuid_drop(self) -> None: suid_dumpable=2, ) + @unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root") + def test_crash_unshare(self) -> None: + """Report generation for a binary that uses unshare and crashes. + + This unshare test case simulates a container crash that does + not have Apport support. + """ + with ( + self.assertLogs(level="WARNING") as warning_logs, + compile_c_code("unshare", UNSHARE_CODE) as unshare, + ): + resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) + self.do_crash( + command=unshare, + command_dies=True, + follow_fork=True, + expect_report=False, + ) + + self.assertRegex( + warning_logs.output[0], + r"ERROR:root:host pid \d+ crashed in a container without apport support", + ) + @unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root") def test_crash_setuid_unpackaged(self) -> None: """Report generation for unpackaged setuid program.""" @@ -1177,6 +1229,8 @@ def do_crash( via_socket: bool = False, cwd: str | None = None, expected_owner: int | None = None, + command_dies: bool = False, + follow_fork: bool = False, **kwargs: typing.Any, ) -> str: # TODO: Split into smaller functions/methods @@ -1228,7 +1282,7 @@ def do_crash( try: gdb = subprocess.Popen( # pylint: disable=consider-using-with - self.gdb_command(command, args, gdb_core_file, uid), + self.gdb_command(command, args, gdb_core_file, uid, follow_fork), env={"HOME": self.workdir}, stdin=subprocess.PIPE, cwd=cwd, @@ -1243,10 +1297,13 @@ def do_crash( try: command_process = self.wait_for_gdb_child_process( - gdb.pid, expected_command or command + gdb.pid, + expected_command or command, + "tracing-stop" if command_dies else "sleeping", ) - os.kill(command_process.pid, sig) + if not command_dies: + os.kill(command_process.pid, sig) self.wait_for_core_file(gdb.pid, gdb_core_file) if hook_before_apport: @@ -1304,7 +1361,11 @@ def do_crash( @staticmethod def gdb_command( - command: str, args: Iterable[str], core_file: str, uid: int | None + command: str, + args: Iterable[str], + core_file: str, + uid: int | None, + follow_fork: bool = False, ) -> list[str]: """Construct GDB arguments to call the test executable. @@ -1325,6 +1386,7 @@ def gdb_command( f" --reuid={uid} --clear-groups /bin/sh -c 'exec {command}{cmd_args}'" ) command = "/usr/bin/setpriv" + if uid is not None or follow_fork: gdb_args += ["--ex", "set follow-fork-mode child"] gdb_args += [