|
36 | 36 |
|
37 | 37 | import pytest |
38 | 38 |
|
| 39 | +from omnigent.inner import bwrap_sandbox |
39 | 40 | from omnigent.inner.bwrap_sandbox import ( |
40 | 41 | _ALLOWED_SOCKET_FAMILIES, |
41 | 42 | _CLONE_NEW_FLAG_BITS, |
42 | 43 | _DEFAULT_CWD_ALLOW_HIDDEN, |
| 44 | + _HOST_SANDBOX_BACKEND_ENV, |
| 45 | + _PROC_BIND_HOST_BACKENDS, |
43 | 46 | BwrapSandboxBackend, |
44 | 47 | _bwrap_extra_seccomp_rules, |
| 48 | + _detect_host_sandbox_backend, |
| 49 | + _should_bind_host_proc, |
45 | 50 | ) |
46 | 51 | from omnigent.inner.datamodel import OSEnvSandboxSpec, OSEnvSpec |
47 | 52 | from omnigent.inner.sandbox import SandboxPolicy, with_denied_unix_sockets |
@@ -879,6 +884,124 @@ def _last_index(pred: object) -> int: |
879 | 884 | ) |
880 | 885 |
|
881 | 886 |
|
| 887 | +# --------------------------------------------------------------------------- |
| 888 | +# Nested-sandbox /proc handling (lakebox proc bind) |
| 889 | +# --------------------------------------------------------------------------- |
| 890 | + |
| 891 | + |
| 892 | +def _clear_host_backend_signals(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: |
| 893 | + """ |
| 894 | + Neutralise every host-backend signal so detection returns ``None``. |
| 895 | +
|
| 896 | + Removes the declaration env var and repoints the lakebox marker at a |
| 897 | + path that does not exist, isolating the test from the machine it runs |
| 898 | + on — which must not be assumed to be (or not to be) a lakebox microVM. |
| 899 | + """ |
| 900 | + monkeypatch.delenv(_HOST_SANDBOX_BACKEND_ENV, raising=False) |
| 901 | + monkeypatch.setattr(bwrap_sandbox, "_LAKEBOX_MARKER", tmp_path / "no-such-marker") |
| 902 | + |
| 903 | + |
| 904 | +def test_detect_host_backend_none_without_signals( |
| 905 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 906 | +) -> None: |
| 907 | + """ |
| 908 | + With no env declaration and no marker, no outer backend is detected, |
| 909 | + so the proc bind stays off (fresh procfs is kept everywhere by |
| 910 | + default). |
| 911 | + """ |
| 912 | + _clear_host_backend_signals(monkeypatch, tmp_path) |
| 913 | + assert _detect_host_sandbox_backend() is None |
| 914 | + assert _should_bind_host_proc() is False |
| 915 | + |
| 916 | + |
| 917 | +def test_detect_host_backend_env_declaration_is_normalised( |
| 918 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 919 | +) -> None: |
| 920 | + """ |
| 921 | + ``OMNIGENT_HOST_SANDBOX_BACKEND`` is the explicit, authoritative |
| 922 | + signal; it is trimmed and lower-cased so callers don't have to match |
| 923 | + an exact casing, and ``lakebox`` is on the proc-bind allow-list. |
| 924 | + """ |
| 925 | + _clear_host_backend_signals(monkeypatch, tmp_path) |
| 926 | + monkeypatch.setenv(_HOST_SANDBOX_BACKEND_ENV, " LakeBox ") |
| 927 | + assert _detect_host_sandbox_backend() == "lakebox" |
| 928 | + assert _should_bind_host_proc() is True |
| 929 | + |
| 930 | + |
| 931 | +def test_detect_host_backend_env_non_lakebox_keeps_fresh_proc( |
| 932 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 933 | +) -> None: |
| 934 | + """ |
| 935 | + A declared backend that is NOT on :data:`_PROC_BIND_HOST_BACKENDS` |
| 936 | + must not trigger the proc downgrade — even if the lakebox marker |
| 937 | + happens to exist — because the explicit declaration is authoritative. |
| 938 | + """ |
| 939 | + marker = tmp_path / "run-lakebox" |
| 940 | + marker.mkdir() |
| 941 | + monkeypatch.setattr(bwrap_sandbox, "_LAKEBOX_MARKER", marker) |
| 942 | + monkeypatch.setenv(_HOST_SANDBOX_BACKEND_ENV, "modal") |
| 943 | + assert _detect_host_sandbox_backend() == "modal" |
| 944 | + assert "modal" not in _PROC_BIND_HOST_BACKENDS |
| 945 | + assert _should_bind_host_proc() is False |
| 946 | + |
| 947 | + |
| 948 | +def test_detect_host_backend_marker_autodetects_lakebox( |
| 949 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 950 | +) -> None: |
| 951 | + """ |
| 952 | + When no env var is set, the ``/run/lakebox`` marker directory |
| 953 | + autodetects lakebox. This is the prune-proof fallback the re-exec |
| 954 | + launcher path relies on (the env var may be stripped by the spawn |
| 955 | + allow-list, but the filesystem marker survives). |
| 956 | + """ |
| 957 | + marker = tmp_path / "run-lakebox" |
| 958 | + marker.mkdir() |
| 959 | + monkeypatch.delenv(_HOST_SANDBOX_BACKEND_ENV, raising=False) |
| 960 | + monkeypatch.setattr(bwrap_sandbox, "_LAKEBOX_MARKER", marker) |
| 961 | + assert _detect_host_sandbox_backend() == "lakebox" |
| 962 | + assert _should_bind_host_proc() is True |
| 963 | + |
| 964 | + |
| 965 | +def test_wrap_launcher_argv_fresh_proc_by_default( |
| 966 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 967 | +) -> None: |
| 968 | + """ |
| 969 | + On an ordinary host the wrap emits ``--proc /proc`` (a fresh procfs |
| 970 | + tied to the new PID namespace) and never binds the host ``/proc``. |
| 971 | + """ |
| 972 | + _clear_host_backend_signals(monkeypatch, tmp_path) |
| 973 | + backend = _make_backend() |
| 974 | + policy = _make_policy(tmp_path) |
| 975 | + argv = backend.wrap_launcher_argv([sys.executable, "-c", "pass"], policy, tmp_path) |
| 976 | + assert _has_pair_single_dest(argv, "--proc", "/proc") |
| 977 | + assert not _has_pair(argv, "--bind", "/proc", "/proc") |
| 978 | + |
| 979 | + |
| 980 | +def test_wrap_launcher_argv_binds_proc_on_lakebox( |
| 981 | + monkeypatch: pytest.MonkeyPatch, tmp_path: Path |
| 982 | +) -> None: |
| 983 | + """ |
| 984 | + On a lakebox host the wrap binds the existing ``/proc`` instead of |
| 985 | + mounting a fresh procfs (lakebox's masked ``/proc`` overmounts make |
| 986 | + the fresh mount fail under ``--unshare-pid``). ``/dev`` and ``/tmp`` |
| 987 | + are unaffected. |
| 988 | + """ |
| 989 | + _clear_host_backend_signals(monkeypatch, tmp_path) |
| 990 | + monkeypatch.setenv(_HOST_SANDBOX_BACKEND_ENV, "lakebox") |
| 991 | + backend = _make_backend() |
| 992 | + policy = _make_policy(tmp_path) |
| 993 | + argv = backend.wrap_launcher_argv([sys.executable, "-c", "pass"], policy, tmp_path) |
| 994 | + assert _has_pair(argv, "--bind", "/proc", "/proc"), ( |
| 995 | + "lakebox host must bind the existing /proc; got no `--bind /proc /proc`." |
| 996 | + ) |
| 997 | + assert not _has_pair_single_dest(argv, "--proc", "/proc"), ( |
| 998 | + "lakebox host must NOT also mount a fresh procfs — the two would " |
| 999 | + "conflict at the same mountpoint." |
| 1000 | + ) |
| 1001 | + assert _has_pair_single_dest(argv, "--dev", "/dev") |
| 1002 | + assert "--tmpfs" in argv |
| 1003 | + |
| 1004 | + |
882 | 1005 | # --------------------------------------------------------------------------- |
883 | 1006 | # Dotfile masking + symlink defense |
884 | 1007 | # --------------------------------------------------------------------------- |
|
0 commit comments