mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
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:
parent
8a3350ac8a
commit
3c547e5ed7
@ -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)
|
||||
|
||||
|
||||
|
||||
61
backend/tests/test_lark_cli_atomic_publish.py
Normal file
61
backend/tests/test_lark_cli_atomic_publish.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user