Add Debian, Ubuntu and Mint candidate profiles with scoped systemd 255 credentials

This commit is contained in:
FrameBridge compatibility build committed 2026-10-27 10:49:56 -04:00
1 parent b23819fff1
commit a48a20c936
28 files changed
+2527 -274

No files matched your search

+14 -2
View File
@@ -120,9 +120,13 @@ class AdaptiveCoreTests(unittest.TestCase):
def tearDown(self): self.temp.cleanup()
def doctor(self,missing):
def doctor(self,missing,*,compatibility=None):
def which(name,path=None): return None if name in missing else '/fixture/bin/'+name
with patch.object(runtime,'environment',return_value=dict(self.env)),patch.object(runtime.shutil,'which',side_effect=which),patch('framebridge_expanded.network.inspect',return_value={}):
# Graphical-capability tests use an eligible artifact fixture. Source
# checkouts do not contain the installed vendor layout or manifest.
eligibility = compatibility if compatibility is not None else {
'ok': True, 'profile_id': 'debian13-amd64-py313', 'issues': [], 'warnings': []}
with patch.object(runtime,'environment',return_value=dict(self.env)),patch.object(runtime.shutil,'which',side_effect=which),patch('framebridge_expanded.network.inspect',return_value={}),patch('framebridge_expanded.compatibility.assess',return_value=eligibility):
return runtime.doctor(self.state)
def test_optional_gui_dependencies_do_not_block_text_tools(self):
@@ -135,6 +139,14 @@ class AdaptiveCoreTests(unittest.TestCase):
self.assertFalse(result['capabilities']['graphical_cua'])
self.assertTrue(result['optional_missing_commands']['graphical_cua'])
def test_text_mode_does_not_bypass_incompatible_artifact(self):
rejected = {'ok': False, 'profile_id': None, 'issues': ['Fixture artifact ABI mismatch'], 'warnings': []}
result = self.doctor(set(dc.GRAPHICAL_COMMANDS + dc.VIRTUAL_COMMANDS), compatibility=rejected)
self.assertFalse(result['ok'])
self.assertIn('Fixture artifact ABI mismatch', result['issues'])
self.assertEqual([], result['missing_commands'])
self.assertFalse(result['capabilities']['graphical_cua'])
def test_missing_core_command_still_blocks_startup(self):
result=self.doctor({'bash'})
self.assertFalse(result['ok']);self.assertIn('bash',result['missing_commands'])
+75
View File
@@ -0,0 +1,75 @@
"""Candidate archive input selection; no host package or runtime changes."""
import contextlib
import io
import json
from pathlib import Path
import sys
import tarfile
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
import archive_sources
class CandidateArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.base = self.root / "source"
self.base.mkdir()
self.input = self.base / "example.py"
self.input.write_text("# candidate source\n")
def run_archive(self, args):
evidence = {"vendor_sha256": "a" * 64, "packages": [], "native_extensions": []}
with mock.patch.object(archive_sources, "BASE", self.base), \
mock.patch.object(archive_sources, "source_files", return_value={"source/example.py": self.input}), \
mock.patch.object(archive_sources, "vendor_versions", return_value={}), \
mock.patch.object(archive_sources, "validate_vendor", return_value=evidence), \
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as stderr:
archive_sources.main(args)
return self.base / "release/candidate-source" / archive_sources.ARCHIVE_NAME, stderr.getvalue()
def test_default_archive_declares_missing_alternate_abis(self):
output, warning = self.run_archive([])
self.assertIn("cp312", warning)
with tarfile.open(output) as archive:
data = json.load(archive.extractfile(archive_sources.ARCHIVE_ROOT + "/TARGET-VENDOR-PROVENANCE.json"))
readme = archive.extractfile(archive_sources.ARCHIVE_ROOT + "/README-SOURCE-ARCHIVE.md").read().decode()
self.assertEqual(data["omitted_vendor_abis"], ["cp312", "cp314"])
self.assertIn("--target debian13-amd64-py313", readme)
self.assertIn("not a signed public release", readme)
self.assertEqual(data["validation_status"], "candidate")
def test_explicit_vendors_included_without_private_markers_or_bin(self):
args = []
for abi in ("cp312", "cp314"):
vendor = self.root / abi; vendor.mkdir()
(vendor / "module.py").write_text("# reviewed input\n")
(vendor / "private.log").write_text("private canary\n")
(vendor / "FB-ABI-PREPARED.json").write_text('{"private_path":"/home/example"}')
(vendor / "bin").mkdir(); (vendor / "bin/tool").write_text("#!/tmp/private/python\n")
args.extend(["--vendor-" + abi, str(vendor)])
output, warning = self.run_archive(args)
self.assertFalse(warning)
with tarfile.open(output) as archive:
names = archive.getnames()
data = json.load(archive.extractfile(archive_sources.ARCHIVE_ROOT + "/TARGET-VENDOR-PROVENANCE.json"))
self.assertIn(archive_sources.ARCHIVE_ROOT + "/source/target-vendors/cp312/module.py", names)
self.assertIn(archive_sources.ARCHIVE_ROOT + "/source/target-vendors/cp314/module.py", names)
self.assertFalse(any("FB-ABI-PREPARED" in name or name.endswith("bin/tool") or name.endswith("private.log") for name in names))
self.assertEqual(set(data["vendors"]), {"cp312", "cp313", "cp314"})
self.assertEqual(data["omitted_vendor_abis"], [])
def test_alternate_vendor_symlink_is_rejected(self):
vendor = self.root / "cp312"; vendor.mkdir()
(vendor / "unsafe.py").symlink_to(self.input)
with self.assertRaisesRegex(RuntimeError, "symlink rejected"):
self.run_archive(["--vendor-cp312", str(vendor)])
if __name__ == "__main__":
unittest.main()
+238
View File
@@ -0,0 +1,238 @@
"""Synthetic backend/lifecycle tests. No host credentials or sudo are used."""
import hashlib
import io
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, config, connection_background
ID = "tunnel_0123456789abcdef0123456789abcdef"
KEY = "SYNTHETIC-FRAMEBRIDGE-HOST-BACKEND-NOT-A-REAL-KEY"
class HostCredentialAutostartTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(); self.addCleanup(self.temp.cleanup)
self.home = Path(self.temp.name) / "home"; self.home.mkdir(mode=0o700)
self.state = self.home / "profile"
value = config.default_config(); value.update(tunnel_id=ID, headless="no")
config.save_config(self.state, value)
self.settings = {"account_label": "owner@example.test", "tunnel_id": ID,
"control_plane_organization_id": "", "control_plane_url_path": ""}
self.calls = []; self.system_calls = []; self.generation = 0
self.addCleanup(mock.patch.stopall)
mock.patch.object(Path, "home", return_value=self.home).start()
self.command = mock.patch.object(autostart, "_command", side_effect=self.fake_command).start()
self.systemctl = mock.patch.object(autostart, "_systemctl", side_effect=self.fake_systemctl).start()
self.helper = mock.patch.object(autostart, "_helper_ready").start()
mock.patch.object(autostart, "_linger", return_value=False).start()
mock.patch.object(autostart, "_enabled_units", return_value=[]).start()
mock.patch.object(autostart.os, "geteuid", return_value=1000).start()
mock.patch.object(autostart.resource, "setrlimit").start()
mock.patch("builtins.input", return_value="y").start()
self.prompt = mock.patch.object(autostart.getpass, "getpass", return_value=KEY).start()
mock.patch.object(autostart, "status", return_value={"configured": True}).start()
mock.patch.object(connection_background, "status", return_value={"alive": False}).start()
mock.patch.object(connection_background, "disconnect", return_value={"ok": True, "alive": False}).start()
mock.patch("sys.stdout", new_callable=io.StringIO).start()
def fake_command(self, args, **kwargs):
self.calls.append((args, kwargs))
output = b""
if args[:2] == ["systemd-creds", "--version"]: output = b"systemd 255\n"
elif "protect" in args or "encrypt" in args:
self.generation += 1
output = ("SYNTHETIC-HOST-CIPHER-%03d-" % self.generation).encode() * 8
elif "decrypt" in args: output = KEY.encode()
return subprocess.CompletedProcess(args, 0, output, b"")
def fake_systemctl(self, *args, **kwargs):
self.system_calls.append(args)
return subprocess.CompletedProcess(args, 0, b"", b"")
def operations(self, name):
return [(args, options) for args, options in self.calls if name in args]
def test_255_setup_uses_helper_stdin_and_preserves_user_unit(self):
autostart.configure_connection(self.state, self.settings, KEY)
_, directory, unit = autostart._paths(self.state)
record = autostart._manifest(directory)
self.assertEqual(autostart.HOST_BACKEND, record["credential_backend"])
args, options = self.operations("protect")[0]
self.assertEqual(["sudo", "-n", "--", str(autostart.HELPER), "protect", autostart._profile_id(self.state)], args)
self.assertEqual(KEY.encode(), options["data"])
self.assertFalse(self.operations("encrypt"))
self.assertNotIn(KEY, " ".join(args))
self.assertNotIn("User=", unit.read_text())
self.assertNotIn("LoadCredentialEncrypted", unit.read_text())
for path in (unit, directory / "credential.cred", directory / "installation.json", self.state / "config.json"):
self.assertNotIn(KEY.encode(), path.read_bytes())
def test_helper_missing_fails_before_privileges_or_key_prompt(self):
self.helper.side_effect = RuntimeError("Install the matching package before connecting.")
with self.assertRaisesRegex(RuntimeError, "Install"):
autostart.enable(self.state)
self.prompt.assert_not_called()
self.assertFalse(any(args[0] == "sudo" for args, _ in self.calls))
self.assertEqual([], self.system_calls)
def test_node_255_startup_does_not_need_a_credential_backend(self):
config.save_config(self.state, config.default_config(role="node"))
self.helper.side_effect = RuntimeError("missing helper")
autostart._enable(self.state, confirm=False, start_now=False)
self.helper.assert_not_called(); self.prompt.assert_not_called()
self.assertFalse(self.operations("protect"))
self.assertFalse(any(args[:2] == ["systemd-creds", "--version"] for args, _ in self.calls))
self.assertTrue(autostart._manifest(autostart._paths(self.state)[1])["node"])
def test_255_decrypt_is_owner_bound_by_helper_not_owner_supplied_cipher(self):
autostart.configure_connection(self.state, self.settings, KEY)
self.calls.clear()
self.assertEqual(KEY, autostart._credential(self.state))
args, options = self.operations("decrypt")[0]
record = autostart._manifest(autostart._paths(self.state)[1])
self.assertEqual(["sudo", "-n", "--", str(autostart.HELPER), "decrypt", autostart._profile_id(self.state), record["credential_sha256"]], args)
self.assertNotIn("data", options)
self.assertFalse(any(a.startswith("--uid") for a in args))
def test_legacy_manifest_keeps_256_decryption_without_autodetection(self):
_, directory, _ = autostart._paths(self.state)
cipher = b"LEGACY-CIPHERTEXT-" * 8
autostart._write(directory / "credential.cred", cipher)
autostart._json(directory / "installation.json", {"schema": 1, "node": False, "credential_sha256": hashlib.sha256(cipher).hexdigest()})
self.assertEqual(KEY, autostart._credential(self.state))
args, options = self.operations("decrypt")[0]
self.assertEqual(["systemd-creds", "decrypt", "--user", "--name=chatgpt-runtime-key", "-", "-"], args)
self.assertEqual(cipher, options["data"])
self.helper.assert_not_called()
def test_changed_cipher_fails_before_any_privileged_decrypt(self):
autostart.configure_connection(self.state, self.settings, KEY)
(autostart._paths(self.state)[1] / "credential.cred").write_bytes(b"tampered")
self.calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
autostart._credential(self.state)
self.assertEqual([], self.calls)
def test_failed_service_setup_revokes_new_helper_grant(self):
def fail(*args, **kwargs):
if args[0] == "enable": raise RuntimeError("synthetic service failure")
return self.fake_systemctl(*args, **kwargs)
self.systemctl.side_effect = fail
with self.assertRaises(RuntimeError):
autostart.configure_connection(self.state, self.settings, KEY)
self.assertEqual(1, len(self.operations("revoke")))
self.assertIsNone(autostart._manifest(autostart._paths(self.state)[1]))
def test_input_race_after_protect_revokes_before_stopping_any_service(self):
def changed(args, **kwargs):
result = self.fake_command(args, **kwargs)
if "protect" in args:
settings = config.load_config(self.state); settings["name"] = "changed-during-protect"
config.save_config(self.state, settings)
return result
self.command.side_effect = changed
with self.assertRaisesRegex(RuntimeError, "changed during"):
autostart.configure_connection(self.state, self.settings, KEY)
self.assertEqual(1, len(self.operations("revoke")))
self.assertFalse(any(args[0] == "stop" for args in self.system_calls))
def test_rotation_revokes_old_grant_only_after_commit(self):
autostart.configure_connection(self.state, self.settings, KEY)
directory = autostart._paths(self.state)[1]
old = autostart._manifest(directory)["credential_sha256"]
self.calls.clear(); self.system_calls.clear()
autostart.configure_connection(self.state, self.settings, KEY)
current = autostart._manifest(directory)
self.assertNotEqual(old, current["credential_sha256"])
self.assertEqual([], current["retired_helper_credentials"])
self.assertEqual(old, self.operations("revoke")[0][0][-1])
def test_retired_limit_rejects_rotation_before_protect_or_service_change(self):
autostart.configure_connection(self.state, self.settings, KEY)
directory = autostart._paths(self.state)[1]
record = autostart._manifest(directory)
record["retired_helper_credentials"] = [format(n, "064x") for n in range(128)]
autostart._json(directory / "installation.json", record, replace=True)
before = (directory / "installation.json").read_bytes()
self.calls.clear(); self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "must be cleaned"):
autostart.configure_connection(self.state, self.settings, KEY)
self.assertFalse(self.operations("protect"))
self.assertEqual([], self.system_calls)
self.assertEqual(before, (directory / "installation.json").read_bytes())
def test_retirement_failure_is_durable_and_disable_revokes_both_versions(self):
autostart.configure_connection(self.state, self.settings, KEY)
directory = autostart._paths(self.state)[1]
old = autostart._manifest(directory)["credential_sha256"]
def fail_revoke(args, **kwargs):
if "revoke" in args: raise RuntimeError("synthetic revocation failure")
return self.fake_command(args, **kwargs)
self.command.side_effect = fail_revoke
autostart.configure_connection(self.state, self.settings, KEY)
record = autostart._manifest(directory)
self.assertEqual([old], record["retired_helper_credentials"])
self.command.side_effect = self.fake_command; self.calls.clear()
autostart.disable(self.state)
self.assertEqual("-", self.operations("prune")[0][0][-1])
self.assertIsNone(autostart._manifest(directory))
def test_failed_revoke_keeps_manifest_for_retry_after_service_stop(self):
autostart.configure_connection(self.state, self.settings, KEY)
directory = autostart._paths(self.state)[1]
def fail_revoke(args, **kwargs):
if "prune" in args: raise RuntimeError("synthetic revocation failure")
return self.fake_command(args, **kwargs)
self.command.side_effect = fail_revoke
with self.assertRaises(RuntimeError): autostart.disable(self.state)
self.assertIsNotNone(autostart._manifest(directory))
self.assertTrue(autostart._paths(self.state)[2].exists())
def test_disable_recovers_protect_without_an_owner_manifest(self):
installed = self.home / "installed-helper"
installed.write_text("synthetic trusted helper placeholder")
with mock.patch.object(autostart, "HELPER", installed):
result = autostart.disable(self.state)
self.assertTrue(result["orphan_credential_cleanup_verified"])
self.assertEqual("-", self.operations("prune")[0][0][-1])
self.assertTrue(any(args[:2] == ["sudo", "-v"] for args, _ in self.calls))
def test_disable_without_installed_helper_does_not_claim_verified_cleanup(self):
with mock.patch.object(autostart, "HELPER", self.home / "missing-helper"):
result = autostart.disable(self.state)
self.assertFalse(result["orphan_credential_cleanup_verified"])
def test_256_setup_and_disable_reconcile_orphans_after_os_upgrade(self):
installed = self.home / "installed-helper"
installed.write_text("synthetic trusted helper placeholder")
def systemd256(args, **kwargs):
if args[:2] == ["systemd-creds", "--version"]:
self.calls.append((args, kwargs))
return subprocess.CompletedProcess(args, 0, b"systemd 256\n", b"")
return self.fake_command(args, **kwargs)
self.command.side_effect = systemd256
with mock.patch.object(autostart, "HELPER", installed):
autostart.configure_connection(self.state, self.settings, KEY)
self.assertEqual(1, len(self.operations("prune")))
self.assertTrue(self.operations("encrypt")); self.assertFalse(self.operations("protect"))
autostart.disable(self.state)
self.assertEqual(2, len(self.operations("prune")))
def test_unknown_backend_never_invokes_helper_or_decrypt(self):
_, directory, _ = autostart._paths(self.state)
cipher = b"CIPHERTEXT-" * 10
autostart._write(directory / "credential.cred", cipher)
autostart._json(directory / "installation.json", {"schema": 1, "node": False, "credential_backend": "unexpected", "credential_sha256": hashlib.sha256(cipher).hexdigest()})
self.calls.clear()
with self.assertRaisesRegex(RuntimeError, "Unsupported"):
autostart._credential(self.state)
self.assertEqual([], self.calls)
if __name__ == "__main__":
unittest.main()
+111
View File
@@ -0,0 +1,111 @@
"""Negative build-input tests; no package installation or target imports."""
import base64
import hashlib
from pathlib import Path
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
from build_profiles import target_profile, validate_vendor
class VendorValidationTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.profile = target_profile("mint22-amd64-py312")
self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so")
self.make_package("pydantic_core", "pydantic_core/_pydantic_core.cpython-312-x86_64-linux-gnu.so")
self.make_package("rpds_py", "rpds/rpds.cpython-312-x86_64-linux-gnu.so")
def make_package(self, name, relative, tag="cp312-cp312-manylinux_2_28_x86_64", abi_extra=b""):
native = self.root / relative
native.parent.mkdir(parents=True, exist_ok=True)
native.write_bytes(b"\x7fELF\x02\x01" + b"\0" * 12 + (62).to_bytes(2, "little") + abi_extra)
info = self.root / (name + "-1.0.dist-info")
info.mkdir(exist_ok=True)
(info / "METADATA").write_text("Name: " + name + "\nVersion: 1.0\nRequires-Python: >=3.10\n")
(info / "WHEEL").write_text("Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: " + tag + "\n")
self.write_record(info, native)
return info, native
def write_record(self, info, native):
rows = []
for path in (native, info / "METADATA", info / "WHEEL"):
digest = base64.urlsafe_b64encode(hashlib.sha256(path.read_bytes()).digest()).decode().rstrip("=")
rows.append(path.relative_to(self.root).as_posix() + ",sha256=" + digest + "," + str(path.stat().st_size))
rows.append((info / "RECORD").relative_to(self.root).as_posix() + ",,")
(info / "RECORD").write_text("\n".join(rows) + "\n")
def test_valid_target_and_abi3(self):
self.make_package("cryptography", "cryptography/_rust.abi3.so", "cp311-abi3-manylinux_2_34_x86_64")
result = validate_vendor(self.root, self.profile)
self.assertEqual(len(result["native_extensions"]), 4)
self.assertEqual(self.profile["validation_status"], "candidate")
def test_wrong_wheel_abi_rejected(self):
self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so", "cp313-cp313-manylinux_2_28_x86_64")
with self.assertRaisesRegex(RuntimeError, "wheel ABI"):
validate_vendor(self.root, self.profile)
def test_wrong_extension_abi_rejected(self):
old = self.root / "_cffi_backend.cpython-312-x86_64-linux-gnu.so"
old.unlink()
self.make_package("cffi", "_cffi_backend.cpython-313-x86_64-linux-gnu.so")
with self.assertRaisesRegex(RuntimeError, "extension ABI"):
validate_vendor(self.root, self.profile)
def test_wrong_elf_cpu_rejected(self):
info, path = self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so")
data = bytearray(path.read_bytes()); data[18:20] = (183).to_bytes(2, "little")
path.write_bytes(data); self.write_record(info, path)
with self.assertRaisesRegex(RuntimeError, "ELF amd64"):
validate_vendor(self.root, self.profile)
def test_new_glibc_rejected(self):
self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so", abi_extra=b"GLIBC_2.35")
with self.assertRaisesRegex(RuntimeError, "glibc 2.34"):
validate_vendor(self.root, self.profile)
def test_requires_python_rejected(self):
info, path = self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so")
with (info / "METADATA").open("a") as stream: stream.write("Requires-Python: >=3.13\n")
# Replace the original field to avoid duplicate-header ambiguity.
(info / "METADATA").write_text("Name: cffi\nVersion: 1.0\nRequires-Python: >=3.13\n")
self.write_record(info, path)
with self.assertRaisesRegex(RuntimeError, "Requires-Python"):
validate_vendor(self.root, self.profile)
def test_version_set_change_rejected(self):
with self.assertRaisesRegex(RuntimeError, "names/versions"):
validate_vendor(self.root, self.profile, {"cffi": "9.9"})
def test_modified_native_record_rejected(self):
with (self.root / "_cffi_backend.cpython-312-x86_64-linux-gnu.so").open("ab") as stream:
stream.write(b"altered")
with self.assertRaisesRegex(RuntimeError, "RECORD digest"):
validate_vendor(self.root, self.profile)
def test_missing_wheel_rejected(self):
(self.root / "cffi-1.0.dist-info/WHEEL").unlink()
with self.assertRaisesRegex(RuntimeError, "WHEEL/RECORD"):
validate_vendor(self.root, self.profile)
def test_native_record_requires_digest(self):
record = self.root / "cffi-1.0.dist-info/RECORD"
rows = record.read_text().splitlines()
rows[0] = rows[0].split(",")[0] + ",,"
record.write_text("\n".join(rows) + "\n")
with self.assertRaisesRegex(RuntimeError, "RECORD digest missing"):
validate_vendor(self.root, self.profile)
def test_pure_metadata_on_native_rejected(self):
self.make_package("cffi", "_cffi_backend.cpython-312-x86_64-linux-gnu.so", "py3-none-any")
with self.assertRaisesRegex(RuntimeError, "native WHEEL"):
validate_vendor(self.root, self.profile)
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -7,7 +7,7 @@ import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import cli
from framebridge_expanded import cli, __version__
from framebridge_expanded.config import default_config, save_config
@@ -67,7 +67,7 @@ class CommunityCliContractTests(unittest.TestCase):
value=json.loads(text)
self.assertEqual(code,0)
self.assertEqual(value["schema_version"],1)
self.assertEqual(value["version"],"0.3.1")
self.assertEqual(value["version"], __version__)
self.assertEqual(value["result"]["desired_connection"],"disconnected")
self.assertFalse(value["result"]["connection"]["verified"])
+256
View File
@@ -0,0 +1,256 @@
"""Synthetic, unprivileged tests; never run sudo, visudo or systemd-creds."""
import hashlib
import io
import json
import os
from pathlib import Path
import stat
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import credential_helper as helper
KEY = b"SYNTHETIC-FRAMEBRIDGE-TEST-NOT-A-REAL-KEY"
PROFILE = "a" * 64
class CredentialHelperTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.registry = self.root / "registry"
self.policies = self.root / "sudoers"
self.registry.mkdir(mode=0o700)
self.policies.mkdir(mode=0o700)
self.owner = SimpleNamespace(pw_uid=1234, pw_gid=1234, pw_name="testowner", pw_dir="/home/testowner")
self.calls = []
self.generation = 0
self.addCleanup(mock.patch.stopall)
mock.patch.object(helper, "ROOT", self.registry).start()
mock.patch.object(helper, "SUDOERS", self.policies).start()
# Functional tests run as an ordinary user. Trust-path guards have
# dedicated stat-driven tests below rather than requiring real root.
mock.patch.object(helper, "_root_path", side_effect=lambda p, **kw: Path(p)).start()
original_fstat = os.fstat
def root_stat(fd):
values = list(original_fstat(fd)); values[4] = 0
return os.stat_result(values)
mock.patch.object(helper.os, "fstat", side_effect=root_stat).start()
self.run_patcher = mock.patch.object(helper, "_run", side_effect=self.fake_run)
self.run = self.run_patcher.start()
def fake_run(self, args, data=None):
self.calls.append((args, data))
if args[1] == "encrypt":
self.generation += 1
return ("SYNTHETIC-CIPHERTEXT-%03d-" % self.generation).encode() * 6
if args[1] == "decrypt":
return KEY
return b""
def protect(self):
cipher = helper.protect(self.owner, PROFILE, KEY)
return cipher, hashlib.sha256(cipher).hexdigest()
def test_protect_has_literal_owner_rule_and_no_plaintext_in_files_or_argv(self):
cipher, digest = self.protect()
record, policy = helper._paths(self.owner.pw_uid, PROFILE, digest)
self.assertEqual(0o600, stat.S_IMODE(record.stat().st_mode))
self.assertEqual(0o440, stat.S_IMODE(policy.stat().st_mode))
text = policy.read_text()
self.assertIn("testowner ALL=(root) NOSETENV: NOPASSWD:", text)
self.assertTrue(text.endswith(" decrypt " + PROFILE + " " + digest + "\n"))
self.assertNotIn("*", text)
self.assertNotIn("systemd-creds", text)
for path in (record, policy):
self.assertNotIn(KEY, path.read_bytes())
encryption = self.calls[0]
self.assertEqual(helper.CREDS, encryption[0][0])
self.assertEqual(KEY, encryption[1])
self.assertIn("--with-key=host", encryption[0])
self.assertFalse(any(a.startswith("--uid") or a == "--user" for a in encryption[0]))
self.assertNotIn(KEY.decode(), " ".join(encryption[0]))
self.assertEqual(KEY, helper.decrypt(self.owner, PROFILE, digest))
def test_wrong_owner_or_profile_never_decrypts(self):
_, digest = self.protect()
self.calls.clear()
other = SimpleNamespace(pw_uid=5678, pw_gid=5678, pw_name="another", pw_dir="/home/another")
for owner, profile in ((other, PROFILE), (self.owner, "b" * 64)):
with self.subTest(owner=owner.pw_uid, profile=profile):
with self.assertRaises(Exception):
helper.decrypt(owner, profile, digest)
self.assertEqual([], self.calls)
def test_reused_uid_with_different_username_or_home_is_rejected(self):
_, digest = self.protect()
for field in ("pw_name", "pw_dir"):
values = vars(self.owner).copy(); values[field] = "changed"
with self.subTest(field=field), self.assertRaises(RuntimeError):
helper.decrypt(SimpleNamespace(**values), PROFILE, digest)
def test_cipher_or_name_or_principal_tampering_is_rejected_before_decrypt(self):
_, digest = self.protect()
record, _ = helper._paths(self.owner.pw_uid, PROFILE, digest)
original = json.loads(record.read_bytes())
for field, value in (("cipher", "AAAA"), ("name", "another-secret"), ("uid", 0), ("profile", "b" * 64)):
changed = dict(original); changed[field] = value
record.write_text(json.dumps(changed)); self.calls.clear()
with self.subTest(field=field), self.assertRaises(RuntimeError):
helper.decrypt(self.owner, PROFILE, digest)
self.assertEqual([], self.calls)
def test_symlink_hardlink_and_broad_permissions_are_rejected(self):
_, digest = self.protect()
record, _ = helper._paths(self.owner.pw_uid, PROFILE, digest)
backup = record.with_suffix(".backup"); record.rename(backup)
record.symlink_to(backup)
with self.assertRaises(OSError): helper.decrypt(self.owner, PROFILE, digest)
record.unlink(); os.link(backup, record)
with self.assertRaises(RuntimeError): helper.decrypt(self.owner, PROFILE, digest)
record.unlink(); backup.rename(record); record.chmod(0o644)
with self.assertRaises(RuntimeError): helper.decrypt(self.owner, PROFILE, digest)
def test_invalid_policy_does_not_grant_or_leave_a_record(self):
def fail_visudo(args, data=None):
if args[0] == helper.VISUDO: raise RuntimeError("invalid policy")
return self.fake_run(args, data)
self.run.side_effect = fail_visudo
with self.assertRaises(RuntimeError): self.protect()
self.assertEqual([], list(self.registry.iterdir()))
self.assertEqual([], list(self.policies.iterdir()))
def test_rotation_revoke_and_idempotent_partial_cleanup(self):
_, first = self.protect(); _, second = self.protect()
self.assertNotEqual(first, second)
helper.revoke(self.owner, PROFILE, first)
helper.revoke(self.owner, PROFILE, first)
self.assertEqual(KEY, helper.decrypt(self.owner, PROFILE, second))
_, policy = helper._paths(self.owner.pw_uid, PROFILE, second)
policy.unlink() # simulate interruption after authorization removal
helper.revoke(self.owner, PROFILE, second)
self.assertEqual([], list(self.registry.iterdir()))
self.assertEqual([], list(self.policies.iterdir()))
def test_purge_prevalidates_all_policies_before_deleting_any(self):
_, first = self.protect(); _, second = self.protect()
record, policy = helper._paths(self.owner.pw_uid, PROFILE, second)
policy.chmod(0o600); policy.write_text("unrelated rule\n")
with self.assertRaises(RuntimeError): helper.purge_all()
self.assertTrue(helper._paths(self.owner.pw_uid, PROFILE, first)[0].exists())
self.assertTrue(record.exists())
def test_purge_handles_orphan_rule_after_interrupted_revoke(self):
_, digest = self.protect()
record, policy = helper._paths(self.owner.pw_uid, PROFILE, digest)
record.unlink()
helper.purge_all()
self.assertFalse(policy.exists())
def test_prune_recovers_lost_protect_receipt_and_preserves_committed_cipher(self):
_, current = self.protect(); _, lost_receipt = self.protect()
helper.prune(self.owner, PROFILE, {current})
self.assertEqual(KEY, helper.decrypt(self.owner, PROFILE, current))
self.assertFalse(helper._paths(self.owner.pw_uid, PROFILE, lost_receipt)[0].exists())
helper.prune(self.owner, PROFILE, set())
self.assertEqual([], list(self.registry.iterdir()))
self.assertEqual([], list(self.policies.iterdir()))
def test_revoke_persists_authorization_removal_before_deleting_cipher(self):
_, digest = self.protect()
record, policy = helper._paths(self.owner.pw_uid, PROFILE, digest)
observed = []
with mock.patch.object(helper, "_sync", side_effect=lambda p: observed.append((p, record.exists(), policy.exists()))):
helper.revoke(self.owner, PROFILE, digest)
self.assertEqual([(self.policies, True, False), (self.registry, False, False)], observed)
def test_purge_recovers_only_recognized_root_owned_staging_files(self):
_, digest = self.protect()
record, _ = helper._paths(self.owner.pw_uid, PROFILE, digest)
temporary = record.with_name("." + record.name + "." + "d" * 24)
temporary.write_bytes(b"incomplete-staging-json"); temporary.chmod(0o600)
helper.purge_all()
self.assertFalse(temporary.exists())
self.assertEqual([], list(self.registry.iterdir()))
def test_staging_hardlink_after_publish_is_recovered_before_reading_record(self):
_, digest = self.protect()
record, _ = helper._paths(self.owner.pw_uid, PROFILE, digest)
temporary = record.with_name("." + record.name + "." + "d" * 24)
os.link(record, temporary)
original_lstat = Path.lstat
def root_lstat(path):
values = list(original_lstat(path)); values[4] = 0
return os.stat_result(values)
with mock.patch.object(Path, "lstat", root_lstat):
helper._cleanup_temporaries()
self.assertFalse(temporary.exists())
self.assertEqual(KEY, helper.decrypt(self.owner, PROFILE, digest))
def test_broken_receipt_pipe_rolls_back_published_protect_grant(self):
stdout = mock.Mock(); stdout.buffer.write.side_effect = BrokenPipeError("synthetic closed pipe")
with mock.patch.object(helper, "_installation"), mock.patch.object(helper, "_caller", return_value=self.owner), mock.patch.object(helper, "_prepare_root"), mock.patch.object(helper.resource, "setrlimit"), mock.patch.object(helper.os, "umask"), mock.patch("sys.stdin", SimpleNamespace(buffer=io.BytesIO(KEY))), mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr:
self.assertEqual(1, helper.main(["protect", PROFILE]))
self.assertNotIn(KEY.decode(), stderr.getvalue())
self.assertEqual([".lock"], [p.name for p in self.registry.iterdir()])
self.assertEqual([], list(self.policies.iterdir()))
def test_ids_reject_paths_flags_and_extra_arguments(self):
for value in ("../secret", "--uid=0", "A" * 64, "a" * 63, PROFILE + "\n"):
with self.subTest(value=value), self.assertRaises(RuntimeError):
helper.protect(self.owner, value, KEY)
for key in (b"", b"space key", b"\x00", b"a" * (helper.MAX_KEY + 1)):
with self.subTest(size=len(key)), self.assertRaises(RuntimeError):
helper.protect(self.owner, PROFILE, key)
self.assertEqual([], self.calls)
def test_caller_is_derived_from_sudo_identity_not_a_uid_argument(self):
with mock.patch.object(helper.pwd, "getpwuid", return_value=self.owner):
with mock.patch.dict(os.environ, {"SUDO_UID": "1234", "SUDO_GID": "1234", "SUDO_USER": "testowner"}, clear=True):
self.assertEqual(self.owner, helper._caller())
for bad in ({}, {"SUDO_UID": "0"}, {"SUDO_UID": "01234"}, {"SUDO_UID": "1234", "SUDO_GID": "1234", "SUDO_USER": "another"}):
with mock.patch.dict(os.environ, bad, clear=True), self.assertRaises(RuntimeError):
helper._caller()
def test_subprocess_has_fixed_environment_and_no_shell(self):
self.run_patcher.stop()
with mock.patch.object(helper.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, b"cipher", b"")) as run:
self.assertEqual(b"cipher", helper._run([helper.CREDS, "encrypt", "-", "-"], KEY))
_, kwargs = run.call_args
self.assertEqual(helper.ENV, kwargs["env"])
self.assertEqual("/", kwargs["cwd"])
self.assertTrue(kwargs["close_fds"])
self.assertNotIn("shell", kwargs)
self.assertNotIn(KEY.decode(), repr(kwargs["env"]))
def test_main_redacts_failures_and_rejects_extra_arguments(self):
with mock.patch.object(helper, "_installation"), mock.patch.object(helper.resource, "setrlimit"), mock.patch.object(helper.os, "umask"), mock.patch("sys.stderr", new_callable=io.StringIO) as stderr, mock.patch("sys.stdout", new_callable=io.StringIO) as stdout:
self.assertEqual(1, helper.main(["decrypt", PROFILE, "b" * 64, "--uid=0"]))
self.assertEqual("", stdout.getvalue())
self.assertNotIn(KEY.decode(), stderr.getvalue())
class CredentialTrustGuardTests(unittest.TestCase):
def test_root_path_rejects_each_untrusted_component(self):
target = Path("/rootowned/helper")
def info(mode, uid=0, links=1):
return SimpleNamespace(st_mode=mode, st_uid=uid, st_nlink=links)
for bad in (info(stat.S_IFDIR | 0o777), info(stat.S_IFDIR | 0o755, uid=1000), info(stat.S_IFLNK | 0o777)):
def fake(path):
if path == Path("/rootowned"): return bad
return info((stat.S_IFREG | 0o755) if path == target else (stat.S_IFDIR | 0o755))
with mock.patch.object(Path, "lstat", fake), self.assertRaises(RuntimeError):
helper._root_path(target)
def test_installed_helper_requires_isolated_no_site_interpreter(self):
with mock.patch.object(helper.os, "geteuid", return_value=0):
for isolated, no_site in ((0, 1), (1, 0)):
with mock.patch.object(helper.sys, "flags", SimpleNamespace(isolated=isolated, no_site=no_site)), self.assertRaises(RuntimeError):
helper._installation()
if __name__ == "__main__":
unittest.main()
+100
View File
@@ -0,0 +1,100 @@
"""A stalled unprivileged stdout reader must not retain the registry lock.
The subprocess disables installation/caller/crypto operations and uses only its
own temporary directory. No sudo, host files, accounts or credentials are used.
"""
import array
import fcntl
import os
from pathlib import Path
import select
import subprocess
import sys
import tempfile
import termios
import time
import unittest
from framebridge_expanded import credential_helper
CHILD = r"""
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
spec = importlib.util.spec_from_file_location("pipe_canary_helper", sys.argv[1])
helper = importlib.util.module_from_spec(spec)
spec.loader.exec_module(helper)
helper.ROOT = Path(sys.argv[2])
helper._installation = lambda: None
helper._prepare_root = lambda: None
helper._caller = lambda: SimpleNamespace(pw_uid=12345, pw_name="synthetic", pw_dir="/synthetic-only")
helper.decrypt = lambda *args: b"X" * 16384
# The only file is our own temporary lock; simulate its expected root metadata.
actual_fstat = helper.os.fstat
helper.os.fstat = lambda fd: SimpleNamespace(st_mode=actual_fstat(fd).st_mode,
st_uid=0, st_nlink=actual_fstat(fd).st_nlink)
raise SystemExit(helper.main(["decrypt", "a" * 64, "b" * 64]))
"""
class CredentialHelperPipeTests(unittest.TestCase):
@unittest.skipUnless(hasattr(fcntl, "F_SETPIPE_SZ"), "Linux pipe-size controls required")
def test_stalled_stdout_reader_does_not_hold_global_registry_lock(self):
with tempfile.TemporaryDirectory(prefix="framebridge-pipe-canary-") as directory:
read_fd, write_fd = os.pipe()
child = None
lock_fd = None
try:
capacity = fcntl.fcntl(write_fd, fcntl.F_SETPIPE_SZ, 4096)
child = subprocess.Popen([sys.executable, "-I", "-S", "-c", CHILD,
str(Path(credential_helper.__file__).resolve()), directory],
stdin=subprocess.DEVNULL, stdout=write_fd,
stderr=subprocess.PIPE, close_fds=True)
os.close(write_fd)
write_fd = None
deadline = time.monotonic() + 5
buffered = array.array("i", [0])
while time.monotonic() < deadline:
fcntl.ioctl(read_fd, termios.FIONREAD, buffered, True)
if buffered[0] == capacity:
break
if child.poll() is not None:
self.fail("Synthetic helper exited before filling the canary pipe: " + child.stderr.read().decode())
time.sleep(0.01)
self.assertEqual(capacity, buffered[0], "Synthetic pipe did not fill")
self.assertIsNone(child.poll(), "Helper should be waiting for the synthetic stdout reader")
lock_fd = os.open(Path(directory) / ".lock", os.O_RDWR)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
self.fail("Helper retained the registry lock while stdout was blocked")
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
received = bytearray()
deadline = time.monotonic() + 5
while True:
remaining = deadline - time.monotonic()
self.assertGreater(remaining, 0, "Synthetic helper pipe did not finish")
ready, _, _ = select.select([read_fd], [], [], remaining)
self.assertTrue(ready, "Synthetic helper pipe timed out")
block = os.read(read_fd, 4096)
if not block:
break
received.extend(block)
self.assertEqual(b"X" * 16384, bytes(received))
self.assertEqual(0, child.wait(timeout=3), child.stderr.read().decode())
finally:
if child is not None:
if child.poll() is None:
child.kill()
child.wait(timeout=3)
child.stderr.close()
os.close(read_fd)
if write_fd is not None:
os.close(write_fd)
if lock_fd is not None:
os.close(lock_fd)
if __name__ == "__main__":
unittest.main()
+229
View File
@@ -0,0 +1,229 @@
"""Candidate profile/ABI selection; tests use inert native-file fixtures."""
import hashlib
import json
import os
from pathlib import Path
import tempfile
import stat
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from framebridge_expanded import compatibility as compat
from framebridge_expanded import runtime, uninstall
from framebridge_expanded import display_context, network
class LinuxCompatibilityTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.prefix = Path(self.temp.name)
def artifact(self, profile_id):
_distro, _release, minor, abi = compat.PROFILES[profile_id]
vendor = self.prefix / compat.VENDOR_RELATIVE
vendor.mkdir(parents=True, exist_ok=True)
for old in vendor.rglob("*.so"):
old.unlink()
# These inert files test selection only; package tests inspect real ELF.
suffix = ".cpython-" + abi[2:] + "-x86_64-linux-gnu.so"
for rel in ["_cffi_backend", "pydantic_core/_pydantic_core", "rpds/rpds"]:
path = vendor / (rel + suffix)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"synthetic ABI filename fixture, never imported")
manifest = dict(schema=1, profile_id=profile_id, architecture="amd64",
python_minor=minor, python_abi=abi, validation_status="candidate")
(self.prefix / "release-profile.json").write_text(json.dumps(manifest))
return manifest
def check(self, distro, version, python, **options):
return compat.assess(self.prefix, os_release={"ID": distro, "VERSION_ID": version},
machine=options.pop("machine", "x86_64"), python_version=python,
libc=options.pop("libc", ("glibc", "2.39")), **options)
def test_explicit_candidate_profiles_require_matching_artifacts(self):
targets = [("debian13-amd64-py313", "debian", "13", (3, 13)),
("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)),
("ubuntu26.04-amd64-py314", "ubuntu", "26.04", (3, 14)),
("mint22-amd64-py312", "linuxmint", "22.3", (3, 12)),
("lmde7-amd64-py313", "linuxmint", "7", (3, 13))]
for name, distro, version, python in targets:
with self.subTest(name=name):
self.artifact(name)
result = self.check(distro, version, python)
self.assertTrue(result["ok"], result)
self.assertEqual(name, result["profile_id"])
self.assertEqual("candidate", result["validation_status"])
self.assertTrue(result["warnings"])
def test_version_and_derivative_identity_do_not_silently_opt_in(self):
self.artifact("ubuntu24.04-amd64-py312")
for distro, version in [("ubuntu", "22.04"), ("ubuntu", "24.10"),
("linuxmint", "21.3"), ("debian", "12"), ("pop", "24.04")]:
with self.subTest(distro=distro, version=version):
self.assertFalse(self.check(distro, version, (3, 12))["ok"])
self.assertIsNone(compat.profile_for({"ID": "other", "ID_LIKE": "ubuntu", "VERSION_ID": "24.04"}))
def test_interpreter_artifact_and_cpu_must_all_match(self):
self.artifact("ubuntu24.04-amd64-py312")
self.assertFalse(self.check("ubuntu", "24.04", (3, 13))["ok"])
self.assertFalse(self.check("linuxmint", "22.1", (3, 12))["ok"])
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), machine="aarch64")["ok"])
def test_nonstandard_interpreter_abi_is_rejected(self):
self.artifact("ubuntu24.04-amd64-py312")
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), implementation="PyPy")["ok"])
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), abi_flags="t")["ok"])
def test_mixed_native_abi_and_missing_extensions_are_rejected(self):
self.artifact("ubuntu24.04-amd64-py312")
vendor = self.prefix / compat.VENDOR_RELATIVE
path = next(vendor.glob("_cffi_backend*.so"))
path.rename(path.with_name(path.name.replace("cpython-312", "cpython-313")))
result = self.check("ubuntu", "24.04", (3, 12))
self.assertFalse(result["ok"])
self.assertTrue(any("native extension does not match" in issue for issue in result["issues"]))
self.artifact("ubuntu24.04-amd64-py312")
path = next(vendor.glob("_cffi_backend*.so"))
path.rename(path.with_name(path.name.replace("cpython-312", "cpython-312t")))
self.assertFalse(self.check("ubuntu", "24.04", (3, 12))["ok"])
self.artifact("ubuntu24.04-amd64-py312")
next((vendor / "rpds").glob("*.so")).unlink()
self.assertFalse(self.check("ubuntu", "24.04", (3, 12))["ok"])
def test_libc_floor_and_libc_family_are_checked(self):
self.artifact("ubuntu24.04-amd64-py312")
for libc in [("glibc", "2.33"), ("musl", "1.2.5"), ("", "")]:
with self.subTest(libc=libc):
self.assertFalse(self.check("ubuntu", "24.04", (3, 12), libc=libc)["ok"])
self.assertTrue(self.check("ubuntu", "24.04", (3, 12), libc=("glibc", "2.34"))["ok"])
def test_invalid_or_inconsistent_manifest_fails_closed(self):
original = self.artifact("ubuntu24.04-amd64-py312")
path = self.prefix / "release-profile.json"
for value in ["{", "[]", json.dumps({**original, "schema": True}),
json.dumps({**original, "python_abi": "cp313"}),
json.dumps({**original, "validation_status": "unknown"})]:
with self.subTest(value=value):
path.write_text(value)
result = self.check("ubuntu", "24.04", (3, 12))
self.assertFalse(result["ok"])
self.assertTrue(any("Invalid release-profile" in issue for issue in result["issues"]))
def test_missing_manifest_cannot_silently_relabel_legacy_vendor(self):
self.artifact("debian13-amd64-py313")
(self.prefix / "release-profile.json").unlink()
self.assertFalse(self.check("linuxmint", "22.1", (3, 13))["ok"])
self.assertTrue(self.check("debian", "13", (3, 13))["ok"])
with patch.object(compat, "INSTALLED_PREFIX", self.prefix):
self.assertFalse(self.check("debian", "13", (3, 13))["ok"])
def test_wayland_diagnostic_never_claims_native_desktop_control(self):
result = compat.display_support({"XDG_SESSION_TYPE": "wayland", "DISPLAY": ":0"}, {"kind": "text"})
self.assertFalse(result["native_wayland_cua"])
self.assertFalse(result["physical_x11_verified"])
self.assertIn("XWayland", result["notes"][0])
virtual = compat.display_support({"WAYLAND_DISPLAY": "wayland-0"}, {"kind": "virtual-x11"})
self.assertTrue(virtual["private_virtual_x11"])
self.assertFalse(virtual["native_wayland_cua"])
def test_doctor_uses_profile_result_and_start_stops_before_creating_files(self):
rejected = dict(ok=False, profile_id="ubuntu24.04-amd64-py312", issues=["ABI mismatch"], warnings=[])
with patch.object(runtime, "state_path", return_value=self.prefix), \
patch.object(runtime, "load_config", return_value={"role": "node", "headless": "auto"}), \
patch.object(runtime, "environment", return_value={"XDG_SESSION_TYPE": "wayland"}), \
patch.object(runtime.shutil, "which", return_value="/fixture/command"), \
patch.object(compat, "assess", return_value=rejected), \
patch.object(display_context, "missing_graphical_commands", return_value=[]), \
patch.object(display_context, "describe", return_value={"kind": "text"}), \
patch.object(network, "inspect", return_value={}), \
patch.object(runtime, "status", return_value={"running": False}):
doctor = runtime.doctor(self.prefix)
self.assertFalse(doctor["ok"])
self.assertIn("ABI mismatch", doctor["issues"])
self.assertFalse(doctor["compatible_debian_13_amd64"])
self.assertEqual("wayland", doctor["display_support"]["session_type"])
self.assertFalse(runtime.start(self.prefix)["ok"])
self.assertFalse((self.prefix / "logs").exists())
class ManagedInterpreterValidationTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.state = "/home/fixture/profile"
self.unit = self.root / ("framebridge-autostart-" + hashlib.sha256(self.state.encode()).hexdigest()[:16] + ".service")
def unit_text(self, interpreter):
return '\n'.join(["# FrameBridge managed autostart v1", 'Environment="PYTHONPATH=' + uninstall.MODULE_ROOT + '"',
"ExecStart=" + interpreter + " -B -m framebridge_expanded.autostart _run --state " + self.state])
def test_matching_minor_system_interpreters_preserve_cleanup(self):
self.assertEqual(compat.SYSTEM_PYTHONS, uninstall.SYSTEM_PYTHONS)
for interpreter in sorted(compat.SYSTEM_PYTHONS):
with self.subTest(interpreter=interpreter):
self.unit.write_text(self.unit_text(interpreter))
self.assertTrue(uninstall.managed_unit(self.unit, os.getuid()))
def test_arbitrary_interpreters_duplicate_commands_and_links_remain_rejected(self):
for interpreter in ["/tmp/python3.12", "/usr/bin/python3.11", "/usr/local/bin/python3.14"]:
self.unit.write_text(self.unit_text(interpreter))
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
self.unit.write_text(self.unit_text("/usr/bin/python3.12") + "\nExecStart=/bin/true")
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
real = self.root / "real"
real.write_text(self.unit_text("/usr/bin/python3.12"))
self.unit.unlink()
self.unit.symlink_to(real)
self.assertFalse(uninstall.managed_unit(self.unit, os.getuid()))
class RootHelperCleanupTests(unittest.TestCase):
def test_root_purge_uses_only_fixed_helper_and_bounded_environment(self):
calls = []
def run(args, **options):
calls.append((args, options))
return SimpleNamespace(returncode=0)
with patch.object(uninstall.os, "getuid", return_value=0), \
patch.object(uninstall.os.path, "lexists", return_value=True), \
patch.object(uninstall, "_trusted_root_file", return_value=True):
uninstall.purge_credential_helpers(run=run)
self.assertEqual([str(uninstall.CREDENTIAL_HELPER), "purge-all"], calls[0][0])
self.assertEqual({"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, calls[0][1]["env"])
self.assertEqual(30, calls[0][1]["timeout"])
def test_nonroot_or_unsafe_helper_never_executes(self):
calls = []
with patch.object(uninstall.os, "getuid", return_value=1000):
with self.assertRaises(RuntimeError):
uninstall.purge_credential_helpers(run=lambda *a, **kw: calls.append(a))
with patch.object(uninstall.os, "getuid", return_value=0), \
patch.object(uninstall.os.path, "lexists", return_value=True), \
patch.object(uninstall, "_trusted_root_file", return_value=False):
with self.assertRaises(RuntimeError):
uninstall.purge_credential_helpers(run=lambda *a, **kw: calls.append(a))
self.assertEqual([], calls)
def test_failure_to_purge_pauses_removal(self):
with patch.object(uninstall.os, "getuid", return_value=0), \
patch.object(uninstall.os.path, "lexists", return_value=True), \
patch.object(uninstall, "_trusted_root_file", return_value=True):
with self.assertRaises(RuntimeError):
uninstall.purge_credential_helpers(run=lambda *a, **kw: SimpleNamespace(returncode=1))
def test_fixed_helper_trust_rejects_links_nonroot_and_writable_parents(self):
target = uninstall.CREDENTIAL_HELPER
for mode, owner in [(stat.S_IFLNK | 0o777, 0), (stat.S_IFREG | 0o755, 1000),
(stat.S_IFREG | 0o777, 0)]:
with self.subTest(mode=mode, owner=owner), \
patch.object(Path, "lstat", return_value=SimpleNamespace(st_mode=mode, st_uid=owner)):
self.assertFalse(uninstall._trusted_root_file(target))
def info(path):
return SimpleNamespace(st_uid=0, st_mode=(stat.S_IFREG | 0o755) if path == target else (stat.S_IFDIR | 0o775))
with patch.object(Path, "lstat", autospec=True, side_effect=info):
self.assertFalse(uninstall._trusted_root_file(target))
if __name__ == "__main__":
unittest.main()