fix(sandbox): platform-aware Lark CLI runtime validation for Windows hosts (#5442)

* fix(sandbox): platform-aware Lark CLI runtime validation for Windows hosts

The managed Lark CLI sandbox runtime validation and its tests assumed POSIX
semantics that Windows hosts cannot satisfy, breaking the focused AIO/Lark CLI
suites (5 failures on current main).

- _validate_lark_cli_sandbox_runtime keeps the strict executable-bit contract
  on POSIX; on Windows it validates the Linux-only artifacts by content
  instead (ELF/PE/Mach-O image magic for linux-*/lark-cli, shebang for the
  bin/lark-cli launcher), since NTFS cannot represent the exec bit.
- The AIO runtime-mounts test now asserts the explicit Windows credential
  contract: an owner-only inheritable DACL (via the existing PowerShell ACL
  resolvers) instead of exact 0o700 modes, which remain asserted on POSIX.
- Accept-path runtime tests stage ELF-prefixed payloads so both platforms
  exercise realistic artifact content; the extractor's mode assertion is
  POSIX-only with a writability check on Windows.

Focused suites: 5 failed / 162 passed -> 167 passed, 3 skipped on Windows 11;
POSIX behavior unchanged (POSIX branches keep the original assertions).

* refactor(tests): share Windows ACL resolvers via a helper module and cover the Windows shebang gate

Review follow-ups on #5442:

- Move the PowerShell ACL resolvers (_windows_acl_env/_windows_acl_sids/
  _windows_acl_protected/_windows_acl_owner_sid) from
  tests/test_lark_cli_integration.py into tests/_windows_acl_helpers.py,
  following the existing shared-helper convention, so the aio suite no longer
  imports the full lark-cli integration module (which drags in app.gateway
  routers and the FastAPI TestClient at collection time).
- Add test_managed_sandbox_runtime_rejects_launcher_without_shebang_on_windows:
  a launcher without a shebang plus ELF-magic binaries, with lark_cli.os
  monkeypatched via the existing Windows stub so the shebang-missing reject
  branch of _runtime_artifact_is_executable is covered on every platform.
- Comment the rejects-non-executable prestaged-binary test to record that on
  Windows the rejection comes from the payload's non-magic content, since
  chmod() cannot clear the exec bit there.

Focused suites: 168 passed / 3 skipped on Windows 11.
This commit is contained in:
xiaodu55 2026-09-15 07:26:08 +08:00 committed by GitHub
parent 3dc895df4d
commit 2536f24f81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 156 additions and 70 deletions

View File

@ -1249,6 +1249,23 @@ def _write_lark_cli_sandbox_launcher(staging: Path) -> None:
launcher.chmod(0o755)
def _runtime_artifact_is_executable(relative: Path, candidate: Path) -> bool:
"""Decide whether a managed runtime artifact is executable, platform-aware.
POSIX keeps the strict executable-bit contract. NTFS cannot represent the
exec bit, so Windows validates these Linux-only artifacts by content
instead: the per-arch binaries must carry an executable image magic and
the ``bin/lark-cli`` launcher must be a script with a shebang.
"""
if os.name != "nt":
return candidate.stat().st_mode & 0o111 != 0
with candidate.open("rb") as handle:
prefix = handle.read(4)
if relative == Path("bin/lark-cli"):
return prefix.startswith(b"#!")
return is_executable_binary_prefix(prefix)
def _validate_lark_cli_sandbox_runtime(root: Path) -> None:
if root.is_symlink() or not root.is_dir():
raise ValueError("Managed Lark CLI sandbox runtime root must be a regular directory, not a symlink.")
@ -1261,7 +1278,7 @@ def _validate_lark_cli_sandbox_runtime(root: Path) -> None:
candidate = root / relative
if not candidate.is_file():
raise ValueError(f"Managed Lark CLI sandbox runtime is missing a regular file: {relative}")
if candidate.stat().st_mode & 0o111 == 0:
if not _runtime_artifact_is_executable(relative, candidate):
raise ValueError(f"Managed Lark CLI sandbox runtime file is not executable: {relative}")

View File

@ -0,0 +1,70 @@
"""Shared Windows ACL inspection helpers for lark-cli credential tests.
These resolvers shell out to Windows PowerShell ``Get-Acl`` and translate ACEs
to raw SIDs so tests can assert the owner-only DACL contract that the
credential-tree hardener establishes on NTFS (where POSIX modes are not
representable). They are Windows-only; callers gate them on ``os.name == "nt"``.
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
def _windows_acl_env() -> dict[str, str]:
"""Return a PowerShell environment with a clean, ordered ``PSModulePath``.
The Codex runtime prepends a bundled PowerShell module path that shadows the
stock ``Microsoft.PowerShell.Security`` module, which makes ``Get-Acl`` fail
to autoload under ``-NoProfile``. Use the stock Windows PowerShell module path
so ACL inspection is reliable on any host.
"""
system_root = os.environ.get("SystemRoot", r"C:\Windows")
program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
modules = f"{system_root}\\system32\\WindowsPowerShell\\v1.0\\Modules;{program_files}\\WindowsPowerShell\\Modules"
return {**os.environ, "PSModulePath": modules}
def _windows_acl_sids(path: Path) -> set[str]:
"""Return the SIDs granted on *path* (Windows-only, PowerShell resolver).
``icacls`` displays localized account names rather than raw SIDs, so we
translate each ACE IdentityReference back to a SID before asserting.
"""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').Access | ForEach-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value }"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return {line.strip() for line in out.stdout.splitlines() if line.strip()}
def _windows_acl_protected(path: Path) -> bool:
"""Return whether *path*'s DACL is protected from inheritance (Windows-only)."""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').AreAccessRulesProtected"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return out.stdout.strip() == "True"
def _windows_acl_owner_sid(path: Path) -> str:
"""Return *path*'s object owner as a raw SID (Windows-only)."""
cmd = "$acl = Get-Acl -LiteralPath $env:DEER_FLOW_TEST_ACL_PATH; $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env={**_windows_acl_env(), "DEER_FLOW_TEST_ACL_PATH": str(path)},
)
return out.stdout.strip()

View File

@ -4,12 +4,14 @@ import asyncio
import contextlib
import hashlib
import importlib
import os
import stat
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from _windows_acl_helpers import _windows_acl_owner_sid, _windows_acl_sids
from deerflow.config.paths import Paths, join_host_path
from deerflow.config.sandbox_config import SandboxConfig
@ -218,9 +220,19 @@ def test_get_lark_cli_runtime_mounts_uses_user_auth_dirs(tmp_path, monkeypatch):
str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data"),
False,
)
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config").stat().st_mode) == 0o700
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config" / "locks").stat().st_mode) == 0o700
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data").stat().st_mode) == 0o700
config_dir = tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config"
locks_dir = config_dir / "locks"
data_dir = tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data"
if os.name == "nt":
# NTFS cannot represent POSIX modes; the contract the credential-tree
# hardener establishes on Windows is an owner-only inheritable DACL.
owner_sid = _windows_acl_owner_sid(config_dir)
for hardened in (config_dir, locks_dir, data_dir):
assert _windows_acl_sids(hardened) == {owner_sid}
else:
assert stat.S_IMODE(config_dir.stat().st_mode) == 0o700
assert stat.S_IMODE(locks_dir.stat().st_mode) == 0o700
assert stat.S_IMODE(data_dir.stat().st_mode) == 0o700
assert container_paths["/mnt/integrations/lark-cli/runtime"] == (
str(runtime_dir),
True,

View File

@ -21,6 +21,7 @@ from uuid import uuid4
import pytest
from _router_auth_helpers import make_authed_test_app
from _windows_acl_helpers import _windows_acl_owner_sid, _windows_acl_protected, _windows_acl_sids
from fastapi.testclient import TestClient
from app.gateway.auth.models import User
@ -92,63 +93,6 @@ def _bootstrap_credential_dirs(monkeypatch, tmp_path, *, config: bool = True, da
return config_dir, data_dir
def _windows_acl_env() -> dict[str, str]:
"""Return a PowerShell environment with a clean, ordered ``PSModulePath``.
The Codex runtime prepends a bundled PowerShell module path that shadows the
stock ``Microsoft.PowerShell.Security`` module, which makes ``Get-Acl`` fail
to autoload under ``-NoProfile``. Use the stock Windows PowerShell module path
so ACL inspection is reliable on any host.
"""
system_root = os.environ.get("SystemRoot", r"C:\Windows")
program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
modules = f"{system_root}\\system32\\WindowsPowerShell\\v1.0\\Modules;{program_files}\\WindowsPowerShell\\Modules"
return {**os.environ, "PSModulePath": modules}
def _windows_acl_sids(path: Path) -> set[str]:
"""Return the SIDs granted on *path* (Windows-only, PowerShell resolver).
``icacls`` displays localized account names rather than raw SIDs, so we
translate each ACE IdentityReference back to a SID before asserting.
"""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').Access | ForEach-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value }"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return {line.strip() for line in out.stdout.splitlines() if line.strip()}
def _windows_acl_protected(path: Path) -> bool:
"""Return whether *path*'s DACL is protected from inheritance (Windows-only)."""
cmd = "(Get-Acl -LiteralPath '" + str(path) + "').AreAccessRulesProtected"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env=_windows_acl_env(),
)
return out.stdout.strip() == "True"
def _windows_acl_owner_sid(path: Path) -> str:
"""Return *path*'s object owner as a raw SID (Windows-only)."""
cmd = "$acl = Get-Acl -LiteralPath $env:DEER_FLOW_TEST_ACL_PATH; $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value"
out = subprocess.run(
["powershell", "-NoProfile", "-Command", cmd],
capture_output=True,
text=True,
check=True,
env={**_windows_acl_env(), "DEER_FLOW_TEST_ACL_PATH": str(path)},
)
return out.stdout.strip()
class _FakeWindowsHandle:
"""Real-filesystem-backed stand-in for ``_WindowsTreeHandle`` used by mocks.
@ -295,8 +239,8 @@ def test_managed_sandbox_runtime_verifies_and_installs_linux_archives(monkeypatc
assert hasattr(lark_cli, "_ensure_managed_sandbox_lark_cli"), "managed sandbox runtime installer is missing"
_patch_paths(monkeypatch, tmp_path / "home")
archives = {
"lark-cli-1.0.65-linux-amd64.tar.gz": _make_lark_cli_binary_tar(b"amd64-binary"),
"lark-cli-1.0.65-linux-arm64.tar.gz": _make_lark_cli_binary_tar(b"arm64-binary"),
"lark-cli-1.0.65-linux-amd64.tar.gz": _make_lark_cli_binary_tar(b"\x7fELF-amd64-payload"),
"lark-cli-1.0.65-linux-arm64.tar.gz": _make_lark_cli_binary_tar(b"\x7fELF-arm64-payload"),
}
checksums = "".join(f"{hashlib.sha256(payload).hexdigest()} {name}\n" for name, payload in archives.items()).encode()
assets = {"checksums.txt": checksums, **archives}
@ -305,9 +249,15 @@ def test_managed_sandbox_runtime_verifies_and_installs_linux_archives(monkeypatc
runtime = lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"amd64-binary"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"arm64-binary"
assert stat.S_IMODE((runtime / "linux-amd64" / "lark-cli").stat().st_mode) == 0o755
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"\x7fELF-amd64-payload"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"\x7fELF-arm64-payload"
installed_mode = stat.S_IMODE((runtime / "linux-amd64" / "lark-cli").stat().st_mode)
if os.name == "nt":
# NTFS cannot represent the exec bit; writability is the strongest
# host-side contract the extractor can establish for the artifact.
assert installed_mode & 0o222
else:
assert installed_mode == 0o755
launcher = (runtime / "bin" / "lark-cli").read_text(encoding="utf-8")
assert "uname -m" in launcher
assert "x86_64" in launcher and "aarch64" in launcher
@ -353,7 +303,7 @@ def test_managed_sandbox_runtime_accepts_prestaged_airgapped_tree(monkeypatch, t
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.write_bytes(b"\x7fELF" + f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
@ -368,8 +318,8 @@ def test_managed_sandbox_runtime_accepts_prestaged_airgapped_tree(monkeypatch, t
runtime = lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"amd64-binary"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"arm64-binary"
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"\x7fELFamd64-binary"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"\x7fELFarm64-binary"
def test_managed_sandbox_runtime_rejects_any_symlink_in_prestaged_tree(monkeypatch, tmp_path) -> None:
@ -401,6 +351,8 @@ def test_managed_sandbox_runtime_rejects_any_symlink_in_prestaged_tree(monkeypat
def test_managed_sandbox_runtime_rejects_non_executable_prestaged_binary(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
# On Windows chmod() cannot clear the exec bit, so rejection there comes
# from the non-magic payload content rather than the 0o644 mode below.
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
@ -419,13 +371,48 @@ def test_managed_sandbox_runtime_rejects_non_executable_prestaged_binary(monkeyp
assert not lark_cli.lark_cli_managed_sandbox_dir().exists()
def test_managed_sandbox_runtime_rejects_launcher_without_shebang_on_windows(monkeypatch, tmp_path) -> None:
"""The Windows content gate must reject a launcher without a shebang.
``test_managed_sandbox_runtime_rejects_non_executable_prestaged_binary``
stages a launcher WITH a shebang, so on Windows its rejection always comes
from the per-arch binary content and this branch is only exercised
positively. Monkeypatching ``lark_cli.os`` with the Windows stub runs the
Windows branch on POSIX CI too; ``candidate.open()`` is a pathlib method,
so the stub is safe here.
"""
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(b"\x7fELF" + f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
launcher.write_text("echo not-a-launcher\n", encoding="utf-8")
launcher.chmod(0o755)
monkeypatch.setenv(lark_cli.LARK_CLI_SANDBOX_RUNTIME_SOURCE_ENV, str(source))
windows_os = _windows_os_stub()
# The installer also touches a few host-neutral os helpers; only os.name
# must read "nt" to drive the validation branch under test.
windows_os.getenv = os.getenv
windows_os.getpid = os.getpid
monkeypatch.setattr(lark_cli, "os", windows_os)
with pytest.raises(ValueError, match="executable"):
lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert not lark_cli.lark_cli_managed_sandbox_dir().exists()
def test_concurrent_managed_sandbox_runtime_installs_serialize_replacement(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.write_bytes(b"\x7fELF" + f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)