diff --git a/README.md b/README.md index fd08045..5471cc9 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,9 @@ replaced; a reported, never run, because an agent may have written it. The pinned program must be an absolute path or a root-owned command on a `PATH` entry that root owns and no group or other user can write, up to `/`, so a binary planted in a -user-writable directory such as a virtualenv never runs. A bare name runs as +user-writable directory such as a virtualenv never runs. The Nix store never +counts: it is group-writable, and any user can have the daemon build +root-owned content there, so pin an absolute path on Nix. A bare name runs as the resolved file that was checked. Inside a user namespace other than the initial one, root means the overflow uid, under which the real root's files show there, as do those of every other unmapped owner. Outside such a diff --git a/tests/unit/pkgmgr/actions/worktree/test_trust.py b/tests/unit/pkgmgr/actions/worktree/test_trust.py index 6799a12..19abc04 100644 --- a/tests/unit/pkgmgr/actions/worktree/test_trust.py +++ b/tests/unit/pkgmgr/actions/worktree/test_trust.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import shutil import tempfile import unittest from types import SimpleNamespace @@ -113,6 +112,13 @@ class TestRootResolved(unittest.TestCase): stats["/usr/bin"] = (0, 0o040757) self.assertIsNone(self._resolve(stats)) + def test_sticky_group_writable_ancestor_like_the_nix_store_is_refused( + self, + ) -> None: + stats = self._tree() + stats["/usr"] = (0, 0o041775) + self.assertIsNone(self._resolve(stats)) + def test_stat_error_is_refused(self) -> None: stats = self._tree() del stats["/usr"] @@ -131,24 +137,32 @@ class TestRootResolved(unittest.TestCase): class TestTrustedPath(unittest.TestCase): + def setUp(self) -> None: + trusted = [ + entry + for entry in map(trust.root_resolved, os.defpath.split(os.pathsep)) + if entry and os.path.isdir(entry) + ] + if not trusted: + self.skipTest("no os.defpath directory is root-controlled on this host") + self.system = trusted[0] + def test_symlinked_entry_is_kept_only_resolved(self) -> None: - system = os.path.dirname(os.path.realpath(shutil.which("true"))) with tempfile.TemporaryDirectory() as tmp: link = os.path.join(tmp, "bin") - os.symlink(system, link) + os.symlink(self.system, link) with patch.dict(os.environ, {"PATH": link}): - self.assertEqual(trust.trusted_path(), system) + self.assertEqual(trust.trusted_path(), self.system) def test_entry_keeps_the_resolution_it_was_checked_under(self) -> None: - system = os.path.dirname(os.path.realpath(shutil.which("true"))) with tempfile.TemporaryDirectory() as tmp: link = os.path.join(tmp, "bin") - os.symlink(system, link) + os.symlink(self.system, link) with ( patch.dict(os.environ, {"PATH": link}), - patch.object(trust.os.path, "realpath", side_effect=[system, tmp]), + patch.object(trust.os.path, "realpath", side_effect=[self.system, tmp]), ): - self.assertEqual(trust.trusted_path(), system) + self.assertEqual(trust.trusted_path(), self.system) def test_no_root_controlled_entry_is_refused(self) -> None: with (