fix(lark): publish the extracted runtime and launcher atomically (#6671)

_write_lark_cli_runtime_from_archive and _write_lark_cli_sandbox_launcher wrote
their file with a truncating open and chmod 0o755 afterwards, so a crash mid-write
left an executable fragment behind - for the extracted binary that file can be
large. Publish through a temporary file plus os.replace, as the flow-state writer
in the same module already does.
This commit is contained in:
IronMurphy 2026-10-11 15:16:52 +08:00 committed by GitHub
parent 8a3350ac8a
commit 3c547e5ed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 2 deletions

View File

@ -1248,14 +1248,35 @@ def _extract_lark_cli_runtime_binary(archive: bytes, destination: Path) -> None:
if not candidate:
raise ValueError("Lark CLI runtime archive does not contain a lark-cli executable.")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(candidate)
_publish_bytes_atomically(destination, candidate)
destination.chmod(0o755)
def _publish_bytes_atomically(path: Path, content: bytes) -> None:
"""Publish *content* at *path* without exposing a partial file.
Both callers chmod the result 0o755 and the sandbox execs it, so a crash
between the truncating open and the final write left an executable fragment
behind - for the extracted runtime that file can be large. The flow-state
writer in this module already publishes through a temporary file plus
``os.replace``.
"""
fd, temporary = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
try:
with os.fdopen(fd, "wb") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
except BaseException:
Path(temporary).unlink(missing_ok=True)
raise
def _write_lark_cli_sandbox_launcher(staging: Path) -> None:
launcher = staging / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True, exist_ok=True)
launcher.write_text(LARK_CLI_SANDBOX_LAUNCHER_SCRIPT, encoding="utf-8")
_publish_bytes_atomically(launcher, LARK_CLI_SANDBOX_LAUNCHER_SCRIPT.encode("utf-8"))
launcher.chmod(0o755)

View File

@ -0,0 +1,61 @@
# SPDX-License-Identifier: MIT
"""The Lark CLI runtime binary must never be observable half written."""
from __future__ import annotations
import os
import pytest
from deerflow.integrations.lark_cli import _publish_bytes_atomically
def test_publishes_the_content_and_leaves_no_temp_file(tmp_path):
target = tmp_path / "lark-cli"
_publish_bytes_atomically(target, b"#!/bin/sh\necho hi\n")
assert target.read_bytes() == b"#!/bin/sh\necho hi\n"
assert list(tmp_path.glob("*.tmp")) == []
def test_a_failed_publish_keeps_the_previous_binary(tmp_path, monkeypatch):
target = tmp_path / "lark-cli"
target.write_bytes(b"old runtime")
def _boom(*args, **kwargs):
raise OSError("read-only filesystem")
monkeypatch.setattr(os, "replace", _boom)
with pytest.raises(OSError):
_publish_bytes_atomically(target, b"new runtime")
assert target.read_bytes() == b"old runtime"
assert list(tmp_path.glob("*.tmp")) == []
def test_truncating_write_would_have_destroyed_it(tmp_path):
"""Control: the previous implementation loses the binary in the same failure."""
target = tmp_path / "lark-cli"
target.write_bytes(b"old runtime")
try:
with open(target, "wb") as handle:
handle.write(b"new ")
raise OSError("read-only filesystem")
except OSError:
pass
assert target.read_bytes() != b"old runtime"
def test_the_install_path_uses_the_atomic_publish():
import inspect
from deerflow.integrations import lark_cli
source = inspect.getsource(lark_cli)
assert "destination.write_bytes(" not in source
assert "_publish_bytes_atomically(destination" in source