diff --git a/backend/packages/harness/deerflow/integrations/lark_cli.py b/backend/packages/harness/deerflow/integrations/lark_cli.py index f218499908..b6a854765e 100644 --- a/backend/packages/harness/deerflow/integrations/lark_cli.py +++ b/backend/packages/harness/deerflow/integrations/lark_cli.py @@ -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) diff --git a/backend/tests/test_lark_cli_atomic_publish.py b/backend/tests/test_lark_cli_atomic_publish.py new file mode 100644 index 0000000000..d874b89f52 --- /dev/null +++ b/backend/tests/test_lark_cli_atomic_publish.py @@ -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