From b4924b12a1c284fb814c3079189dfe63ce8eea96 Mon Sep 17 00:00:00 2001 From: Alpamys Date: Sun, 10 May 2026 19:28:02 +0500 Subject: [PATCH] fix(v0.44.0): write_trigger lstat unresolved path so symlink at trigger is detected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trigger_path() returns realpath-resolved path, which FOLLOWS symlinks. write_trigger then lstat'd the resolved path, which is the symlink's TARGET (a regular file), so S_ISLNK was always False — the symlink rejection silently no-op'd. Linux CI caught this: test_write_trigger_rejects_pre_existing_symlink failed with "DID NOT RAISE OSError". Fix: lstat the unresolved os.path.join(output_dir, TRIGGER_FILENAME) before opening. If it's a symlink, raise OSError without following. Windows: test still skipped (no POSIX symlink permissions). Co-Authored-By: Claude Opus 4.7 (1M context) --- soup_cli/utils/checkpoint_trigger.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/soup_cli/utils/checkpoint_trigger.py b/soup_cli/utils/checkpoint_trigger.py index 5ca4a9d..a169f90 100644 --- a/soup_cli/utils/checkpoint_trigger.py +++ b/soup_cli/utils/checkpoint_trigger.py @@ -79,15 +79,18 @@ def write_trigger(output_dir: str, *, contents: Optional[str] = None) -> str: parent = os.path.dirname(path) if parent and not os.path.isdir(parent): os.makedirs(parent, exist_ok=True) - # TOCTOU defence: refuse to write through a pre-existing symlink at the - # trigger path (matches v0.33.0 #22 / v0.43.0 Part C policy). + # TOCTOU defence: lstat the UNRESOLVED join path (not the realpath'd one, + # which would follow the symlink and miss it). Matches v0.33.0 #22 / + # v0.43.0 Part C policy. + unresolved = os.path.join(output_dir, TRIGGER_FILENAME) try: - link_stat = os.lstat(path) + link_stat = os.lstat(unresolved) except FileNotFoundError: link_stat = None if link_stat is not None and stat.S_ISLNK(link_stat.st_mode): raise OSError( - f"refusing to write through symlink at {os.path.basename(path)}" + f"refusing to write through symlink at " + f"{os.path.basename(unresolved)}" ) with open(path, "w", encoding="utf-8") as fh: fh.write(body)