Skip to content

Commit

Permalink
test_signal_crashes: replace the use of ping with a custom binary
Browse files Browse the repository at this point in the history
The tests were previously using `ping` under the assumption that it was
setuid or setcap. However, Debian has dropped all of those in
3:20240905-1 since those privileges aren't necessary anymore on recent
kernels with the proper sysctl knobs. (LP: #2089387)

V2:
* simplify file handling using pathlib
* skip the calling test if GCC isn't present

Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/apport/+bug/2089387
Forwarded: canonical#406
  • Loading branch information
schopin-pro committed Nov 28, 2024
1 parent 0c16409 commit 49d150d
Showing 1 changed file with 65 additions and 34 deletions.
99 changes: 65 additions & 34 deletions tests/integration/test_signal_crashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from unittest.mock import MagicMock

import psutil
import pytest

import apport.fileutils
from tests.helper import (
Expand All @@ -58,6 +59,50 @@
apport_binary = import_module_from_file(APPORT_PATH)


@contextlib.contextmanager
def create_dropsuid() -> Iterator[str]:
"""Compiles a suid binary that immediately drops privilege then sleeps."""
DROPSUID_SOURCE = """
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
int main() {
int euid = geteuid();
int uid = getuid();
// We need to be suid
if (uid == euid) {
fprintf(stderr, "uid: %d, euid: %d\\n", uid, euid);
return 1;
}
// This call is supposed to succeed?!
if (seteuid(uid)) {
fprintf(stderr, "errno: %d\\n", errno);
return 2;
}
// We actually check that it succeeded.
if (geteuid() != uid)
return 3;
sleep(60);
return 0;
}
"""
if not os.path.exists("/usr/bin/gcc"):
pytest.skip("This test needs GCC available")
with tempfile.TemporaryDirectory(dir="/var/tmp") as d:
tempdir = Path(d)
source = tempdir / "dropsuid.c"
binary = tempdir / "dropsuid"
source.write_text(DROPSUID_SOURCE)
cmd = ["/usr/bin/gcc", "-g", source, "-o", binary]
subprocess.run(cmd, check=True)
# Grant everyone read permission on the directory!
os.chmod(tempdir, 0o755)
os.chmod(binary, 0o4755)
yield str(binary)


@contextlib.contextmanager
def create_suid(tmpdir: str = "/var/tmp") -> Iterator[str]:
"""Creates a `sleep` suid binary in a subdirectory of `tmpdir`."""
Expand Down Expand Up @@ -706,32 +751,23 @@ def test_crash_setuid_keep(self) -> None:
# run test program in /run (which should only be writable to root)
self.do_crash(command=suid, uid=MAIL_UID, suid_dumpable=2, cwd="/run")

@unittest.skipUnless(os.path.exists("/bin/ping"), "this test needs /bin/ping")
@unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root")
def test_crash_suid_dumpable_debug(self) -> None:
"""Report generation for setuid program with suid_dumpable set to 1.
ping has cap_net_raw=ep and therefore do_crash needs root.
"""
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))

"""Report generation for setuid program with suid_dumpable set to 1."""
# if a user can crash a suid root binary, it should not create
# core files if /proc/sys/fs/suid_dumpable is set to 1 ("debug")
self.do_crash(
command="/bin/ping", args=["127.0.0.1"], uid=MAIL_UID, suid_dumpable=1
)
with create_suid() as suid:
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
self.do_crash(command=suid, uid=MAIL_UID, suid_dumpable=1)

@unittest.skipUnless(os.path.exists("/bin/ping"), "this test needs /bin/ping")
@unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root")
def test_crash_setuid_drop(self) -> None:
"""Report generation for setuid program which drops root."""
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))

# if a user can crash a suid root binary, it should not create
# core files
self.do_crash(
command="/bin/ping", args=["127.0.0.1"], uid=MAIL_UID, suid_dumpable=2
)
with create_dropsuid() as dropsuid:
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
# if a user can crash a suid root binary, it should not create
# core files
self.do_crash(command=dropsuid, uid=MAIL_UID, suid_dumpable=2)

@unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root")
def test_crash_setuid_unpackaged(self) -> None:
Expand Down Expand Up @@ -774,25 +810,21 @@ def test_core_dump_packaged_sigquit_via_socket(self) -> None:
via_socket=True,
)

@unittest.skipUnless(os.path.exists("/bin/ping"), "this test needs /bin/ping")
@unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root")
def test_crash_setuid_drop_via_socket(self):
"""Report generation via socket for setuid program which drops root."""
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
self.do_crash(
command="/bin/ping",
args=["127.0.0.1"],
uid=MAIL_UID,
suid_dumpable=2,
via_socket=True,
)
with create_dropsuid() as dropsuid:
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
self.do_crash(
command=dropsuid, uid=MAIL_UID, suid_dumpable=2, via_socket=True
)

# check crash report
report = apport.Report()
with open(self.test_report, "rb") as report_file:
report.load(report_file)
self.assertEqual(report["Signal"], "11")
self.assertEqual(report["ExecutablePath"], os.path.realpath("/bin/ping"))
# check crash report
report = apport.Report()
with open(self.test_report, "rb") as report_file:
report.load(report_file)
self.assertEqual(report["Signal"], "11")
self.assertEqual(report["ExecutablePath"], dropsuid)

@unittest.mock.patch("os.readlink")
def test_is_not_same_ns(self, readlink_mock: MagicMock) -> None:
Expand Down Expand Up @@ -1137,7 +1169,6 @@ def do_crash(
self.gdb_command(command, args, gdb_core_file, uid),
env={"HOME": self.workdir},
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, # ping produces output!
**kwargs,
)
except FileNotFoundError as error:
Expand Down

0 comments on commit 49d150d

Please sign in to comment.