forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_part_f_hardening.py
More file actions
232 lines (178 loc) · 9.48 KB
/
Copy pathtest_part_f_hardening.py
File metadata and controls
232 lines (178 loc) · 9.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""Part F — Standalone hardening (v0.33.0).
Tests for:
- #21 RLVR code_exec_reward: OS-level isolation strategy detection +
Linux unshare attempt + macOS sandbox-exec wrapper detection.
- #22 checkpoint_intelligence.prune_checkpoints: TOCTOU-safe symlink
handling via os.lstat + S_ISLNK and onerror abort on rmtree walk.
"""
from __future__ import annotations
import os
import stat
import sys
import pytest
# ---------------------------------------------------------------------------
# #22 — prune_checkpoints TOCTOU hardening
# ---------------------------------------------------------------------------
class TestPruneCheckpointsTOCTOU:
def test_prune_skips_top_level_symlink_via_lstat(self, tmp_path):
"""Top-level symlink masquerading as a checkpoint dir must be skipped
without following the link target."""
from soup_cli.eval.checkpoint_intelligence import CheckpointTracker
# Real checkpoint to keep
(tmp_path / "checkpoint-100").mkdir()
# Decoy target outside the prune root
outside = tmp_path.parent / "outside_target_dir"
outside.mkdir(exist_ok=True)
(outside / "sentinel.txt").write_text("must-not-delete", encoding="utf-8")
link = tmp_path / "checkpoint-200"
try:
os.symlink(str(outside), str(link), target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip("symlink creation not permitted (Windows non-admin)")
tracker = CheckpointTracker(metric="composite", keep_top=1)
tracker.record(step=100, score=0.9)
tracker.record(step=200, score=0.5)
removed = tracker.prune_checkpoints(tmp_path)
# The symlink must NOT be followed — sentinel survives
assert (outside / "sentinel.txt").exists()
# Symlink itself was skipped (not in removed list)
assert 200 not in removed
def test_prune_aborts_on_symlink_inside_checkpoint(self, tmp_path):
"""If rmtree encounters a symlink mid-walk inside a doomed checkpoint,
it must abort instead of following it (defence-in-depth)."""
from soup_cli.eval.checkpoint_intelligence import CheckpointTracker
# Two checkpoints; we'll keep the top one
ckpt_keep = tmp_path / "checkpoint-100"
ckpt_keep.mkdir()
ckpt_doomed = tmp_path / "checkpoint-200"
ckpt_doomed.mkdir()
# Plant a symlink INSIDE the doomed checkpoint pointing outside
outside = tmp_path.parent / "siblings_must_survive"
outside.mkdir(exist_ok=True)
(outside / "secret.txt").write_text("keep-me", encoding="utf-8")
nested_link = ckpt_doomed / "linked"
try:
os.symlink(str(outside), str(nested_link), target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip("symlink creation not permitted (Windows non-admin)")
tracker = CheckpointTracker(metric="composite", keep_top=1)
tracker.record(step=100, score=0.9)
tracker.record(step=200, score=0.5)
# Should not follow the nested symlink
tracker.prune_checkpoints(tmp_path)
assert (outside / "secret.txt").exists(), "rmtree followed a symlink"
def test_prune_uses_lstat_for_symlink_check(self, tmp_path, monkeypatch):
"""Verify prune uses os.lstat-based check, not Path.is_symlink, so a
broken symlink (target removed mid-walk) is still rejected."""
from soup_cli.eval import checkpoint_intelligence as ci
# Create a broken symlink as 'checkpoint-300'
broken = tmp_path / "checkpoint-300"
try:
os.symlink(str(tmp_path / "_nonexistent_"), str(broken))
except (OSError, NotImplementedError):
pytest.skip("symlink creation not permitted")
tracker = ci.CheckpointTracker(metric="composite", keep_top=1)
tracker.record(step=100, score=0.9)
tracker.record(step=300, score=0.5)
removed = tracker.prune_checkpoints(tmp_path)
assert 300 not in removed
# ---------------------------------------------------------------------------
# #21 — RLVR code_exec_reward OS-level isolation strategy
# ---------------------------------------------------------------------------
class TestCodeExecIsolationStrategy:
def test_get_isolation_strategy_returns_known_value(self):
from soup_cli.trainer.rewards import _get_isolation_strategy
strategy = _get_isolation_strategy()
assert strategy in {"namespaces", "sandbox-exec", "best-effort"}
def test_isolation_strategy_linux_with_unshare(self, monkeypatch):
from soup_cli.trainer import rewards
# Use _compute_isolation_strategy (uncached) like the other tests so
# the sys.platform patch actually takes effect — _get_isolation_strategy
# may return a cached value populated on a different platform during
# earlier tests / on the macOS / Windows CI runner.
monkeypatch.setattr(sys, "platform", "linux")
# Inject a fake os.unshare so the namespaces branch is reachable
# regardless of the host kernel.
monkeypatch.setattr(os, "unshare", lambda *_a, **_k: None,
raising=False)
if hasattr(rewards, "_ISOLATION_STRATEGY_CACHE"):
rewards._ISOLATION_STRATEGY_CACHE = None
strategy = rewards._compute_isolation_strategy()
# On a Linux-shaped host with os.unshare present, the strategy must
# be "namespaces". The "best-effort" branch is covered by the
# separate "linux_unshare_unavailable" test.
assert strategy == "namespaces"
def test_isolation_strategy_macos_with_sandbox_exec(self, monkeypatch):
import shutil as shutil_mod
from soup_cli.trainer import rewards
# Force fresh evaluation
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(shutil_mod, "which", lambda name: (
"/usr/bin/sandbox-exec" if name == "sandbox-exec" else None
))
# Bypass any module-level cache
if hasattr(rewards, "_ISOLATION_STRATEGY_CACHE"):
rewards._ISOLATION_STRATEGY_CACHE = None
strategy = rewards._compute_isolation_strategy()
assert strategy == "sandbox-exec"
def test_isolation_strategy_macos_without_sandbox_exec(self, monkeypatch):
import shutil as shutil_mod
from soup_cli.trainer import rewards
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(shutil_mod, "which", lambda _name: None)
if hasattr(rewards, "_ISOLATION_STRATEGY_CACHE"):
rewards._ISOLATION_STRATEGY_CACHE = None
strategy = rewards._compute_isolation_strategy()
assert strategy == "best-effort"
def test_isolation_strategy_windows(self, monkeypatch):
from soup_cli.trainer import rewards
monkeypatch.setattr(sys, "platform", "win32")
if hasattr(rewards, "_ISOLATION_STRATEGY_CACHE"):
rewards._ISOLATION_STRATEGY_CACHE = None
strategy = rewards._compute_isolation_strategy()
assert strategy == "best-effort"
def test_isolation_strategy_linux_unshare_unavailable(self, monkeypatch):
from soup_cli.trainer import rewards
monkeypatch.setattr(sys, "platform", "linux")
# Pretend os.unshare doesn't exist
if hasattr(os, "unshare"):
monkeypatch.delattr(os, "unshare", raising=False)
if hasattr(rewards, "_ISOLATION_STRATEGY_CACHE"):
rewards._ISOLATION_STRATEGY_CACHE = None
strategy = rewards._compute_isolation_strategy()
assert strategy == "best-effort"
def test_macos_sandbox_profile_blocks_network_and_writes(self):
"""The macOS sandbox profile must deny network and writes outside /tmp."""
from soup_cli.trainer.rewards import MACOS_SANDBOX_PROFILE
# Profile must default-deny then explicitly allow narrow process needs
assert "(deny default)" in MACOS_SANDBOX_PROFILE
assert "network" in MACOS_SANDBOX_PROFILE
# Must be a single-line or properly formatted scheme expression
assert "(version 1)" in MACOS_SANDBOX_PROFILE
# ---------------------------------------------------------------------------
# Smoke-test that existing code_exec_reward path still works on this host
# ---------------------------------------------------------------------------
class TestCodeExecRewardSmoke:
def test_correct_code_still_scores_one(self):
from soup_cli.trainer.rewards import code_exec_reward
completions = [[{"role": "assistant", "content": "```python\nprint(2+2)\n```"}]]
scores = code_exec_reward(completions, expected=["4"])
assert scores == [1.0]
def test_wrong_code_still_scores_zero(self):
from soup_cli.trainer.rewards import code_exec_reward
completions = [[{"role": "assistant", "content": "```python\nprint(3)\n```"}]]
scores = code_exec_reward(completions, expected=["4"])
assert scores == [0.0]
# ---------------------------------------------------------------------------
# Helper: confirm S_ISLNK lstat-style check works
# ---------------------------------------------------------------------------
def test_lstat_islnk_detects_symlink(tmp_path):
target = tmp_path / "real"
target.mkdir()
link = tmp_path / "link"
try:
os.symlink(str(target), str(link))
except (OSError, NotImplementedError):
pytest.skip("symlinks not supported")
assert stat.S_ISLNK(os.lstat(str(link)).st_mode)
assert not stat.S_ISLNK(os.lstat(str(target)).st_mode)