feat: update Landlock sandbox with new ABI support and access rights management

This commit is contained in:
Ivan 2026-07-16 11:59:43 -05:00
parent afa2cdbc33
commit 68d433b58e
No known key found for this signature in database
5 changed files with 417 additions and 42 deletions

Binary file not shown.

View file

@ -6,7 +6,6 @@ from __future__ import annotations
import ctypes
import ctypes.util
import errno
import logging
import os
import site
@ -28,18 +27,39 @@ _LANDLOCK_ACCESS_FS_MAKE_SOCK = 1 << 9
_LANDLOCK_ACCESS_FS_MAKE_FIFO = 1 << 10
_LANDLOCK_ACCESS_FS_MAKE_BLOCK = 1 << 11
_LANDLOCK_ACCESS_FS_MAKE_SYM = 1 << 12
_LANDLOCK_ACCESS_FS_REFER = 1 << 13
_LANDLOCK_ACCESS_FS_TRUNCATE = 1 << 14
_LANDLOCK_ACCESS_FS_IOCTL_DEV = 1 << 15
_LANDLOCK_CREATE_RULESET_VERSION = 1 << 0
_LANDLOCK_RULE_PATH_BENEATH = 1
_PR_SET_NO_NEW_PRIVS = 38
_READ_ACCESS = (
# ABI v1 filesystem rights. Newer rights are added only when the running ABI
# supports them, and only granted on paths that already need write or /dev.
_FS_ACCESS_ABI1 = (
_LANDLOCK_ACCESS_FS_EXECUTE
| _LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
| _LANDLOCK_ACCESS_FS_MAKE_CHAR
| _LANDLOCK_ACCESS_FS_MAKE_DIR
| _LANDLOCK_ACCESS_FS_MAKE_REG
| _LANDLOCK_ACCESS_FS_MAKE_SOCK
| _LANDLOCK_ACCESS_FS_MAKE_FIFO
| _LANDLOCK_ACCESS_FS_MAKE_BLOCK
| _LANDLOCK_ACCESS_FS_MAKE_SYM
)
_READ_ACCESS_BASE = (
_LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_EXECUTE
)
_RW_ACCESS = _READ_ACCESS | (
_RW_ACCESS_BASE = _READ_ACCESS_BASE | (
_LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
@ -74,6 +94,7 @@ class _LandlockPathBeneathAttr(ctypes.Structure):
("parent_fd", ctypes.c_int32),
]
_pack_ = 1
_layout_ = "ms"
def _parse_kernel_version(release: str) -> tuple[int, int, int]:
@ -118,6 +139,7 @@ def _landlock_env_override() -> bool | None:
_landlock_support_cached: bool | None = None
_landlock_abi_cached: int | None = None
def _syscall_numbers():
@ -155,19 +177,74 @@ def _syscall(libc, nr: int, *args):
return rc
def _probe_landlock_create_ruleset() -> bool:
def _handled_access_fs_for_abi(abi: int) -> int:
"""Return handled FS rights for a best-effort sandbox on this ABI.
Intentionally omits network port rules and IPC scoping so mesh traffic,
Unix sockets, and signals keep working. Omits RESOLVE_UNIX for the same
reason. Rights we do handle are also granted on RW roots (including /dev).
"""
if abi < 1:
return 0
handled = _FS_ACCESS_ABI1
if abi >= 2:
handled |= _LANDLOCK_ACCESS_FS_REFER
if abi >= 3:
handled |= _LANDLOCK_ACCESS_FS_TRUNCATE
if abi >= 5:
handled |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return handled
def _ruleset_attr_size(abi: int) -> int:
"""Bytes of landlock_ruleset_attr the running ABI understands."""
if abi >= 6:
return ctypes.sizeof(_LandlockRulesetAttr)
if abi >= 4:
return ctypes.sizeof(ctypes.c_uint64) * 2
return ctypes.sizeof(ctypes.c_uint64)
def _read_access_for_handled(handled: int) -> int:
return _READ_ACCESS_BASE & handled
def _rw_access_for_handled(handled: int) -> int:
access = _RW_ACCESS_BASE
if handled & _LANDLOCK_ACCESS_FS_REFER:
access |= _LANDLOCK_ACCESS_FS_REFER
if handled & _LANDLOCK_ACCESS_FS_TRUNCATE:
access |= _LANDLOCK_ACCESS_FS_TRUNCATE
if handled & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
access |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return access & handled
def _probe_landlock_abi() -> int:
"""Return the Landlock ABI version, or 0 when unavailable."""
global _landlock_abi_cached
if _landlock_abi_cached is not None:
return _landlock_abi_cached
libc = _libc()
nums = _syscall_numbers()
if libc is None or nums is None:
return False
_landlock_abi_cached = 0
return 0
create_nr, _, _ = nums
try:
abi = _syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION)
except OSError as exc:
if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
return False
return False
return abi >= 1
abi = int(_syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION))
except OSError:
_landlock_abi_cached = 0
return 0
if abi < 1:
_landlock_abi_cached = 0
return 0
_landlock_abi_cached = abi
return abi
def _probe_landlock_create_ruleset() -> bool:
return _probe_landlock_abi() >= 1
def _is_android() -> bool:
@ -179,6 +256,15 @@ def _is_android() -> bool:
return hasattr(sys, "getandroidapilevel")
def landlock_abi_version() -> int:
"""Return the probed Landlock ABI version, or 0 if unsupported."""
if sys.platform != "linux" or _is_android():
return 0
if not _kernel_version_meets_minimum():
return 0
return _probe_landlock_abi()
def landlock_kernel_supported() -> bool:
global _landlock_support_cached
if _landlock_support_cached is not None:
@ -296,20 +382,34 @@ def _collect_rw_roots(
return paths
def _file_access_from_dir_access(access: int, handled: int) -> int:
"""Map a directory access mask to rights valid on a non-directory path."""
file_bits = _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
if access & _LANDLOCK_ACCESS_FS_EXECUTE:
file_bits |= _LANDLOCK_ACCESS_FS_EXECUTE
if access & _LANDLOCK_ACCESS_FS_TRUNCATE:
file_bits |= _LANDLOCK_ACCESS_FS_TRUNCATE
if access & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
file_bits |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return file_bits & access & handled
def _add_path_beneath_rule(
libc,
add_rule_nr: int,
ruleset_fd: int,
path: str,
access: int,
handled: int,
) -> None:
if not path or not os.path.exists(path):
return
effective_access = access
if not os.path.isdir(path):
effective_access = (
_LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
)
effective_access = _file_access_from_dir_access(access, handled)
else:
effective_access = access & handled
if effective_access == 0:
return
open_flags = os.O_PATH | os.O_CLOEXEC | os.O_RDONLY
try:
fd = os.open(path, open_flags)
@ -353,13 +453,21 @@ def apply_landlock_sandbox(
logger.warning("Landlock disabled: %s", exc)
return False
attr = _LandlockRulesetAttr(handled_access_fs=_RW_ACCESS)
abi = _probe_landlock_abi()
if abi < 1:
logger.warning("Landlock disabled: ABI probe failed")
return False
handled = _handled_access_fs_for_abi(abi)
read_access = _read_access_for_handled(handled)
rw_access = _rw_access_for_handled(handled)
attr = _LandlockRulesetAttr(handled_access_fs=handled)
try:
ruleset_fd = _syscall(
libc,
create_nr,
ctypes.byref(attr),
ctypes.sizeof(attr),
_ruleset_attr_size(abi),
0,
)
except OSError as exc:
@ -368,13 +476,17 @@ def apply_landlock_sandbox(
try:
for root in _collect_read_roots():
_add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _READ_ACCESS)
_add_path_beneath_rule(
libc, add_rule_nr, ruleset_fd, root, read_access, handled
)
rw_roots = _collect_rw_roots(storage_dir, reticulum_config_dir, log_dir)
public_existing = _existing_dir(public_dir)
if public_existing and public_existing not in rw_roots:
rw_roots.append(public_existing)
for root in rw_roots:
_add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _RW_ACCESS)
_add_path_beneath_rule(
libc, add_rule_nr, ruleset_fd, root, rw_access, handled
)
_syscall(libc, restrict_nr, ruleset_fd, 0)
except OSError as exc:
logger.warning("Landlock disabled while adding rules: %s", exc)
@ -390,7 +502,10 @@ def apply_landlock_sandbox(
pass
if landlock_auto_enabled():
logger.info("Landlock filesystem sandbox enabled (auto-detected on Linux)")
logger.info(
"Landlock filesystem sandbox enabled (auto-detected on Linux, ABI %s)",
abi,
)
else:
logger.info("Landlock filesystem sandbox enabled")
logger.info("Landlock filesystem sandbox enabled (ABI %s)", abi)
return True

View file

@ -107,3 +107,116 @@ def test_collect_read_roots_includes_interpreter_prefix():
assert any(
prefix == root or prefix.startswith(root.rstrip("/") + "/") for root in roots
), f"prefix {prefix!r} not covered by {roots!r}"
def test_handled_access_fs_for_abi_gates_new_rights():
abi1 = ll._handled_access_fs_for_abi(1)
assert abi1 & ll._LANDLOCK_ACCESS_FS_REFER == 0
assert abi1 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
assert abi1 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
assert abi1 & ll._LANDLOCK_ACCESS_FS_WRITE_FILE
abi2 = ll._handled_access_fs_for_abi(2)
assert abi2 & ll._LANDLOCK_ACCESS_FS_REFER
assert abi2 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
abi3 = ll._handled_access_fs_for_abi(3)
assert abi3 & ll._LANDLOCK_ACCESS_FS_REFER
assert abi3 & ll._LANDLOCK_ACCESS_FS_TRUNCATE
assert abi3 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
abi5 = ll._handled_access_fs_for_abi(5)
assert abi5 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
# Network and UNIX-resolve rights stay unhandled on purpose.
assert abi5 == ll._handled_access_fs_for_abi(10)
def test_rw_access_grants_new_rights_when_handled():
handled = ll._handled_access_fs_for_abi(5)
read_access = ll._read_access_for_handled(handled)
rw_access = ll._rw_access_for_handled(handled)
assert read_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
assert read_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
assert read_access & ll._LANDLOCK_ACCESS_FS_REFER == 0
assert rw_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
assert rw_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
assert rw_access & ll._LANDLOCK_ACCESS_FS_REFER
def test_ruleset_attr_size_matches_abi():
assert ll._ruleset_attr_size(1) == 8
assert ll._ruleset_attr_size(3) == 8
assert ll._ruleset_attr_size(4) == 16
assert ll._ruleset_attr_size(5) == 16
assert ll._ruleset_attr_size(6) == 24
def test_file_access_includes_truncate_with_write():
handled = ll._handled_access_fs_for_abi(5)
rw = ll._rw_access_for_handled(handled)
file_access = ll._file_access_from_dir_access(rw, handled)
assert file_access & ll._LANDLOCK_ACCESS_FS_WRITE_FILE
assert file_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
assert file_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
@pytest.mark.skipif(sys.platform != "linux", reason="Landlock probe requires Linux")
def test_landlock_abi_version_on_linux():
ll._landlock_abi_cached = None
ll._landlock_support_cached = None
abi = ll.landlock_abi_version()
assert isinstance(abi, int)
assert abi >= 0
if ll.landlock_kernel_supported():
assert abi >= 1
@pytest.mark.skipif(
sys.platform != "linux" or not ll.landlock_kernel_supported(),
reason="Landlock apply requires a supported Linux kernel",
)
def test_apply_landlock_preserves_storage_write_and_truncate(tmp_path):
"""Apply sandbox in a subprocess and confirm RW + truncate still work."""
import subprocess
import textwrap
from pathlib import Path
storage = tmp_path / "storage"
storage.mkdir()
script = textwrap.dedent(
f"""
import os
import sys
from meshchatx.src.backend.landlock_sandbox import apply_landlock_sandbox
storage = {str(storage)!r}
os.environ["MESHCHAT_LANDLOCK"] = "1"
ok = apply_landlock_sandbox(storage_dir=storage, log_dir=storage)
if not ok:
print("APPLY_FAILED")
sys.exit(2)
path = os.path.join(storage, "landlock-abi-check.txt")
with open(path, "w", encoding="utf-8") as handle:
handle.write("hello")
with open(path, "w", encoding="utf-8") as handle:
handle.write("truncated")
with open(path, encoding="utf-8") as handle:
data = handle.read()
if data != "truncated":
print("TRUNCATE_FAILED", repr(data))
sys.exit(3)
print("OK")
"""
)
result = subprocess.run(
[sys.executable, "-c", script],
cwd=str(Path(__file__).resolve().parents[2]),
capture_output=True,
text=True,
timeout=30,
check=False,
)
if "APPLY_FAILED" in result.stdout:
pytest.skip("Landlock could not be applied in this environment")
assert result.returncode == 0, (result.stdout, result.stderr)
assert "OK" in result.stdout

View file

@ -26,18 +26,39 @@ _LANDLOCK_ACCESS_FS_MAKE_SOCK = 1 << 9
_LANDLOCK_ACCESS_FS_MAKE_FIFO = 1 << 10
_LANDLOCK_ACCESS_FS_MAKE_BLOCK = 1 << 11
_LANDLOCK_ACCESS_FS_MAKE_SYM = 1 << 12
_LANDLOCK_ACCESS_FS_REFER = 1 << 13
_LANDLOCK_ACCESS_FS_TRUNCATE = 1 << 14
_LANDLOCK_ACCESS_FS_IOCTL_DEV = 1 << 15
_LANDLOCK_CREATE_RULESET_VERSION = 1 << 0
_LANDLOCK_RULE_PATH_BENEATH = 1
_PR_SET_NO_NEW_PRIVS = 38
_READ_ACCESS = (
# ABI v1 filesystem rights. Newer rights are added only when the running ABI
# supports them, and only granted on paths that already need write or /dev.
_FS_ACCESS_ABI1 = (
_LANDLOCK_ACCESS_FS_EXECUTE
| _LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
| _LANDLOCK_ACCESS_FS_MAKE_CHAR
| _LANDLOCK_ACCESS_FS_MAKE_DIR
| _LANDLOCK_ACCESS_FS_MAKE_REG
| _LANDLOCK_ACCESS_FS_MAKE_SOCK
| _LANDLOCK_ACCESS_FS_MAKE_FIFO
| _LANDLOCK_ACCESS_FS_MAKE_BLOCK
| _LANDLOCK_ACCESS_FS_MAKE_SYM
)
_READ_ACCESS_BASE = (
_LANDLOCK_ACCESS_FS_READ_FILE
| _LANDLOCK_ACCESS_FS_READ_DIR
| _LANDLOCK_ACCESS_FS_EXECUTE
)
_RW_ACCESS = _READ_ACCESS | (
_RW_ACCESS_BASE = _READ_ACCESS_BASE | (
_LANDLOCK_ACCESS_FS_WRITE_FILE
| _LANDLOCK_ACCESS_FS_REMOVE_DIR
| _LANDLOCK_ACCESS_FS_REMOVE_FILE
@ -72,6 +93,7 @@ class _LandlockPathBeneathAttr(ctypes.Structure):
("parent_fd", ctypes.c_int32),
]
_pack_ = 1
_layout_ = "ms"
def _parse_kernel_version(release: str) -> tuple[int, int, int]:
@ -116,6 +138,7 @@ def _landlock_env_override() -> bool | None:
_landlock_support_cached: bool | None = None
_landlock_abi_cached: int | None = None
def _syscall_numbers():
@ -143,19 +166,83 @@ def _syscall(libc, nr: int, *args):
return rc
def _probe_landlock_create_ruleset() -> bool:
def _handled_access_fs_for_abi(abi: int) -> int:
"""Return handled FS rights for a best-effort sandbox on this ABI.
Intentionally omits network port rules and IPC scoping so mesh traffic,
Unix sockets, and signals keep working. Omits RESOLVE_UNIX for the same
reason. Rights we do handle are also granted on RW roots (including /dev).
"""
if abi < 1:
return 0
handled = _FS_ACCESS_ABI1
if abi >= 2:
handled |= _LANDLOCK_ACCESS_FS_REFER
if abi >= 3:
handled |= _LANDLOCK_ACCESS_FS_TRUNCATE
if abi >= 5:
handled |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return handled
def _ruleset_attr_size(abi: int) -> int:
"""Bytes of landlock_ruleset_attr the running ABI understands."""
if abi >= 6:
return ctypes.sizeof(_LandlockRulesetAttr)
if abi >= 4:
return ctypes.sizeof(ctypes.c_uint64) * 2
return ctypes.sizeof(ctypes.c_uint64)
def _read_access_for_handled(handled: int) -> int:
return _READ_ACCESS_BASE & handled
def _rw_access_for_handled(handled: int) -> int:
access = _RW_ACCESS_BASE
if handled & _LANDLOCK_ACCESS_FS_REFER:
access |= _LANDLOCK_ACCESS_FS_REFER
if handled & _LANDLOCK_ACCESS_FS_TRUNCATE:
access |= _LANDLOCK_ACCESS_FS_TRUNCATE
if handled & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
access |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return access & handled
def _probe_landlock_abi() -> int:
"""Return the Landlock ABI version, or 0 when unavailable."""
global _landlock_abi_cached
if _landlock_abi_cached is not None:
return _landlock_abi_cached
libc = _libc()
nums = _syscall_numbers()
if libc is None or nums is None:
return False
_landlock_abi_cached = 0
return 0
create_nr, _, _ = nums
try:
abi = _syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION)
except OSError as exc:
if exc.errno in (errno.ENOSYS, errno.EOPNOTSUPP):
return False
return False
return abi >= 1
abi = int(_syscall(libc, create_nr, 0, 0, _LANDLOCK_CREATE_RULESET_VERSION))
except OSError:
_landlock_abi_cached = 0
return 0
if abi < 1:
_landlock_abi_cached = 0
return 0
_landlock_abi_cached = abi
return abi
def _probe_landlock_create_ruleset() -> bool:
return _probe_landlock_abi() >= 1
def landlock_abi_version() -> int:
"""Return the probed Landlock ABI version, or 0 if unsupported."""
if sys.platform != "linux":
return 0
if not _kernel_version_meets_minimum():
return 0
return _probe_landlock_abi()
def landlock_kernel_supported() -> bool:
@ -274,20 +361,34 @@ def _collect_rw_roots(
return paths
def _file_access_from_dir_access(access: int, handled: int) -> int:
"""Map a directory access mask to rights valid on a non-directory path."""
file_bits = _LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
if access & _LANDLOCK_ACCESS_FS_EXECUTE:
file_bits |= _LANDLOCK_ACCESS_FS_EXECUTE
if access & _LANDLOCK_ACCESS_FS_TRUNCATE:
file_bits |= _LANDLOCK_ACCESS_FS_TRUNCATE
if access & _LANDLOCK_ACCESS_FS_IOCTL_DEV:
file_bits |= _LANDLOCK_ACCESS_FS_IOCTL_DEV
return file_bits & access & handled
def _add_path_beneath_rule(
libc,
add_rule_nr: int,
ruleset_fd: int,
path: str,
access: int,
handled: int,
) -> None:
if not path or not os.path.exists(path):
return
effective_access = access
if not os.path.isdir(path):
effective_access = (
_LANDLOCK_ACCESS_FS_READ_FILE | _LANDLOCK_ACCESS_FS_WRITE_FILE
)
effective_access = _file_access_from_dir_access(access, handled)
else:
effective_access = access & handled
if effective_access == 0:
return
open_flags = os.O_PATH | os.O_CLOEXEC | os.O_RDONLY
try:
fd = os.open(path, open_flags)
@ -335,13 +436,21 @@ def apply_landlock_sandbox(
logger.warning("Landlock disabled: %s", exc)
return False
attr = _LandlockRulesetAttr(handled_access_fs=_RW_ACCESS)
abi = _probe_landlock_abi()
if abi < 1:
logger.warning("Landlock disabled: ABI probe failed")
return False
handled = _handled_access_fs_for_abi(abi)
read_access = _read_access_for_handled(handled)
rw_access = _rw_access_for_handled(handled)
attr = _LandlockRulesetAttr(handled_access_fs=handled)
try:
ruleset_fd = _syscall(
libc,
create_nr,
ctypes.byref(attr),
ctypes.sizeof(attr),
_ruleset_attr_size(abi),
0,
)
except OSError as exc:
@ -350,7 +459,9 @@ def apply_landlock_sandbox(
try:
for root in _collect_read_roots(extra_read_paths):
_add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _READ_ACCESS)
_add_path_beneath_rule(
libc, add_rule_nr, ruleset_fd, root, read_access, handled
)
for root in _collect_rw_roots(
storage_dir,
reticulum_config_dir,
@ -359,7 +470,9 @@ def apply_landlock_sandbox(
log_dir,
temp_only=temp_only,
):
_add_path_beneath_rule(libc, add_rule_nr, ruleset_fd, root, _RW_ACCESS)
_add_path_beneath_rule(
libc, add_rule_nr, ruleset_fd, root, rw_access, handled
)
_syscall(libc, restrict_nr, ruleset_fd, 0)
except OSError as exc:
logger.warning("Landlock disabled while adding rules: %s", exc)
@ -375,9 +488,12 @@ def apply_landlock_sandbox(
pass
if landlock_auto_enabled(config_enabled):
logger.info("Landlock filesystem sandbox enabled (auto-detected on Linux)")
logger.info(
"Landlock filesystem sandbox enabled (auto-detected on Linux, ABI %s)",
abi,
)
else:
logger.info("Landlock filesystem sandbox enabled")
logger.info("Landlock filesystem sandbox enabled (ABI %s)", abi)
return True
@ -385,7 +501,7 @@ def landlock_status_dict(
*,
active: bool = False,
config_enabled: bool = True,
) -> dict[str, bool]:
) -> dict[str, bool | int]:
"""Return a dict describing Landlock availability and state."""
return {
"landlock_kernel_supported": landlock_kernel_supported(),
@ -393,4 +509,5 @@ def landlock_status_dict(
"landlock_auto_enabled": landlock_auto_enabled(config_enabled),
"landlock_disabled_by_env": landlock_disabled_by_env(),
"landlock_active": active,
"landlock_abi_version": landlock_abi_version(),
}

View file

@ -95,3 +95,33 @@ def test_landlock_status_dict():
assert status["landlock_active"] is True
assert "landlock_kernel_supported" in status
assert "landlock_requested" in status
assert "landlock_abi_version" in status
def test_handled_access_fs_for_abi_gates_new_rights():
abi1 = ll._handled_access_fs_for_abi(1)
assert abi1 & ll._LANDLOCK_ACCESS_FS_REFER == 0
assert abi1 & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
assert abi1 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
abi5 = ll._handled_access_fs_for_abi(5)
assert abi5 & ll._LANDLOCK_ACCESS_FS_REFER
assert abi5 & ll._LANDLOCK_ACCESS_FS_TRUNCATE
assert abi5 & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
assert abi5 == ll._handled_access_fs_for_abi(10)
def test_rw_access_grants_new_rights_when_handled():
handled = ll._handled_access_fs_for_abi(5)
rw_access = ll._rw_access_for_handled(handled)
read_access = ll._read_access_for_handled(handled)
assert read_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE == 0
assert rw_access & ll._LANDLOCK_ACCESS_FS_TRUNCATE
assert rw_access & ll._LANDLOCK_ACCESS_FS_IOCTL_DEV
assert rw_access & ll._LANDLOCK_ACCESS_FS_REFER
def test_ruleset_attr_size_matches_abi():
assert ll._ruleset_attr_size(1) == 8
assert ll._ruleset_attr_size(4) == 16
assert ll._ruleset_attr_size(6) == 24