Skip to content

Commit a597237

Browse files
committed
fix(evaluation): 禁止非原子报告发布回退
1 parent 94618bc commit a597237

2 files changed

Lines changed: 86 additions & 35 deletions

File tree

examples/optimization/eval_optimize_loop/artifact_writer.py

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
}
4545
_AT_FDCWD = -100
4646
_RENAME_NOREPLACE = 1
47+
_RENAME_EXCL = 0x4
4748
_RENAMEAT2_UNAVAILABLE = {
4849
errno.ENOSYS,
4950
errno.EINVAL,
@@ -177,52 +178,82 @@ def _validate_optimizer_config_for_copy(path: Path) -> None:
177178
_validate_sensitive_config_values(payload)
178179

179180

180-
def _target_exists(path: Path) -> bool:
181-
return path.exists() or path.is_symlink()
182-
183-
184181
def _rename_directory_no_replace(source: Path, target: Path) -> None:
185182
"""Atomically publish a directory without replacing an existing target.
186183
187-
Linux uses renameat2 with RENAME_NOREPLACE. Platforms without that primitive
188-
use a controlled fallback that rechecks the target immediately before rename.
184+
Each supported platform uses an atomic no-replace primitive. Platforms
185+
without that primitive fail closed rather than risking a replacement race.
189186
"""
190187
if sys.platform.startswith("linux"):
191188
try:
192189
libc = ctypes.CDLL(None, use_errno=True)
193190
renameat2 = libc.renameat2
194191
except (AttributeError, OSError):
195-
renameat2 = None
196-
if renameat2 is not None:
197-
renameat2.argtypes = [
198-
ctypes.c_int,
199-
ctypes.c_char_p,
200-
ctypes.c_int,
201-
ctypes.c_char_p,
202-
ctypes.c_uint,
203-
]
204-
renameat2.restype = ctypes.c_int
205-
ctypes.set_errno(0)
206-
result = renameat2(
207-
_AT_FDCWD,
208-
os.fsencode(source),
209-
_AT_FDCWD,
210-
os.fsencode(target),
211-
_RENAME_NOREPLACE,
192+
raise ArtifactWriteError(
193+
"atomic no-replace unavailable: Linux renameat2 is unavailable"
212194
)
213-
if result == 0:
214-
return
215-
error_number = ctypes.get_errno()
216-
if error_number == errno.EEXIST:
217-
raise ArtifactWriteError(
218-
f"report directory already exists: {target}"
219-
)
220-
if error_number not in _RENAMEAT2_UNAVAILABLE:
221-
raise OSError(error_number, os.strerror(error_number), target)
195+
renameat2.argtypes = [
196+
ctypes.c_int,
197+
ctypes.c_char_p,
198+
ctypes.c_int,
199+
ctypes.c_char_p,
200+
ctypes.c_uint,
201+
]
202+
renameat2.restype = ctypes.c_int
203+
ctypes.set_errno(0)
204+
result = renameat2(
205+
_AT_FDCWD,
206+
os.fsencode(source),
207+
_AT_FDCWD,
208+
os.fsencode(target),
209+
_RENAME_NOREPLACE,
210+
)
211+
if result == 0:
212+
return
213+
error_number = ctypes.get_errno()
214+
if error_number == errno.EEXIST:
215+
raise ArtifactWriteError(f"report directory already exists: {target}")
216+
if error_number in _RENAMEAT2_UNAVAILABLE:
217+
raise ArtifactWriteError(
218+
"atomic no-replace unavailable: Linux renameat2 does not support "
219+
f"RENAME_NOREPLACE ({os.strerror(error_number)})"
220+
)
221+
raise OSError(error_number, os.strerror(error_number), target)
222+
223+
if sys.platform.startswith("win"):
224+
try:
225+
os.rename(source, target)
226+
except FileExistsError as exc:
227+
raise ArtifactWriteError(f"report directory already exists: {target}") from exc
228+
return
229+
230+
if sys.platform == "darwin":
231+
try:
232+
libc = ctypes.CDLL(None, use_errno=True)
233+
renamex_np = libc.renamex_np
234+
except (AttributeError, OSError):
235+
raise ArtifactWriteError(
236+
"atomic no-replace unavailable: Darwin renamex_np is unavailable"
237+
)
238+
renamex_np.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint]
239+
renamex_np.restype = ctypes.c_int
240+
ctypes.set_errno(0)
241+
result = renamex_np(os.fsencode(source), os.fsencode(target), _RENAME_EXCL)
242+
if result == 0:
243+
return
244+
error_number = ctypes.get_errno()
245+
if error_number == errno.EEXIST:
246+
raise ArtifactWriteError(f"report directory already exists: {target}")
247+
if error_number in _RENAMEAT2_UNAVAILABLE:
248+
raise ArtifactWriteError(
249+
"atomic no-replace unavailable: Darwin renamex_np does not support "
250+
f"RENAME_EXCL ({os.strerror(error_number)})"
251+
)
252+
raise OSError(error_number, os.strerror(error_number), target)
222253

223-
if _target_exists(target):
224-
raise ArtifactWriteError(f"report directory already exists: {target}")
225-
source.rename(target)
254+
raise ArtifactWriteError(
255+
f"atomic no-replace unavailable: unsupported platform {sys.platform}"
256+
)
226257

227258

228259
def _published_relative_path(root: Path, path: Path) -> str:

tests/evaluation/test_eval_optimize_loop_stage5_artifacts.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,26 @@ def create_competing_report_before_publish(path, content):
233233
assert list(run_dir.glob(".report.tmp-*")) == []
234234

235235

236+
def test_publish_fails_closed_when_atomic_no_replace_is_unsupported(
237+
tmp_path, monkeypatch
238+
):
239+
source = tmp_path / "staging"
240+
target = tmp_path / "report"
241+
source.mkdir()
242+
243+
def unexpected_rename(self, destination):
244+
raise AssertionError("普通 Path.rename 不得作为 no-replace 回退")
245+
246+
monkeypatch.setattr(artifact_writer.sys, "platform", "freebsd")
247+
monkeypatch.setattr(Path, "rename", unexpected_rename)
248+
249+
with pytest.raises(ArtifactWriteError, match="atomic no-replace unavailable"):
250+
artifact_writer._rename_directory_no_replace(source, target)
251+
252+
assert source.is_dir()
253+
assert not target.exists()
254+
255+
236256
@pytest.mark.parametrize("target_inside_run", [False, True])
237257
def test_discovery_rejects_any_file_symlink(tmp_path, target_inside_run):
238258
run_dir = tmp_path / "run"

0 commit comments

Comments
 (0)