mirror of https://github.com/razor-ai/soup.git
fix(v0.61.0): POSIX CI green — lstat RAW path before realpath in 3 loaders
CI on ubuntu / macOS exposed a missed TOCTOU detail: `os.lstat(realpath(path))` silently resolves symlinks before the lstat, so `S_ISLNK` never trips and a symlinked input path passes the rejection. Windows CI was skipped (POSIX-only symlink test), so the bug didn't surface in local smoke. Three fixes, matching the v0.53.7 #106 project policy of "lstat the RAW path before realpath": - `edit_diff.load_probes` — lstat path first, then realpath after rejection. - `unlearning_eval.load_evidence_file` — same. - `unlearning_eval.get_fixture_path` — lstat raw candidate before realpath. No new tests — the existing `test_symlink_rejected` covers it; it now passes on POSIX where it previously failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
740832e1b4
commit
f2c74040ef
|
|
@ -115,13 +115,17 @@ def load_probes(path: str) -> Tuple[str, ...]:
|
|||
raise ValueError("probe path must not contain null bytes")
|
||||
if not is_under_cwd(path):
|
||||
raise ValueError(f"probe path must stay under cwd: {path!r}")
|
||||
real = os.path.realpath(path)
|
||||
# CRITICAL: lstat the RAW path BEFORE realpath (review-fix from CI)
|
||||
# — realpath resolves symlinks, so `lstat(realpath(path))` always sees
|
||||
# the target file. We need to detect the symlink at the raw path.
|
||||
# Matches v0.53.7 #106 TOCTOU policy.
|
||||
try:
|
||||
st = os.lstat(real)
|
||||
st = os.lstat(path)
|
||||
except FileNotFoundError as exc:
|
||||
raise FileNotFoundError(f"probe file not found: {path!r}") from exc
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
raise ValueError("probe path must not be a symlink")
|
||||
real = os.path.realpath(path)
|
||||
if st.st_size > _MAX_PROBE_BYTES:
|
||||
raise ValueError(
|
||||
f"probe file exceeds {_MAX_PROBE_BYTES} bytes"
|
||||
|
|
|
|||
|
|
@ -470,21 +470,22 @@ def get_fixture_path(benchmark: str) -> Optional[Path]:
|
|||
pkg_root = files("soup_cli")
|
||||
except (ModuleNotFoundError, TypeError):
|
||||
return None
|
||||
candidate = Path(
|
||||
os.path.realpath(
|
||||
os.path.join(str(pkg_root), "data", "_fixtures",
|
||||
"unlearning", fixture_name)
|
||||
)
|
||||
raw_candidate = Path(
|
||||
os.path.join(str(pkg_root), "data", "_fixtures",
|
||||
"unlearning", fixture_name)
|
||||
)
|
||||
if not candidate.is_file():
|
||||
return None
|
||||
# Symlink rejection (TOCTOU defence - matches project policy).
|
||||
# Symlink rejection at the RAW path BEFORE realpath (review-fix —
|
||||
# realpath resolves symlinks so lstat on the resolved target always
|
||||
# sees a regular file). Matches v0.53.7 #106 TOCTOU policy.
|
||||
try:
|
||||
st = os.lstat(candidate)
|
||||
st = os.lstat(raw_candidate)
|
||||
except OSError:
|
||||
return None
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
return None
|
||||
candidate = Path(os.path.realpath(raw_candidate))
|
||||
if not candidate.is_file():
|
||||
return None
|
||||
return candidate
|
||||
|
||||
|
||||
|
|
@ -502,13 +503,15 @@ def load_evidence_file(path: str) -> Mapping[str, Mapping[str, Any]]:
|
|||
raise ValueError("evidence path must not contain null bytes")
|
||||
if not is_under_cwd(path):
|
||||
raise ValueError(f"evidence path must stay under cwd: {path!r}")
|
||||
real = os.path.realpath(path)
|
||||
# CRITICAL: lstat the RAW path BEFORE realpath (review-fix from CI)
|
||||
# — realpath resolves symlinks. Matches v0.53.7 #106 TOCTOU policy.
|
||||
try:
|
||||
st = os.lstat(real)
|
||||
st = os.lstat(path)
|
||||
except FileNotFoundError as exc:
|
||||
raise FileNotFoundError(f"evidence file not found: {path!r}") from exc
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
raise ValueError("evidence path must not be a symlink")
|
||||
real = os.path.realpath(path)
|
||||
if st.st_size > _MAX_EVIDENCE_BYTES:
|
||||
raise ValueError(
|
||||
f"evidence file exceeds {_MAX_EVIDENCE_BYTES} bytes"
|
||||
|
|
|
|||
Loading…
Reference in New Issue