Skip to content

Commit 12849f7

Browse files
Pigbibicodex
andcommitted
fix: close final strict linkage review gaps
Co-Authored-By: Codex <noreply@openai.com>
1 parent 9703b54 commit 12849f7

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

src/research_signal_context_pipelines/latest_linkage.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import errno
77
import os
88
import re
9+
import stat
910
from pathlib import Path
1011
from typing import Any
1112

@@ -15,6 +16,7 @@
1516

1617
_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
1718
_OPEN_SUPPORTS_DIR_FD = os.open in getattr(os, "supports_dir_fd", set())
19+
MAX_JSON_ARTIFACT_BYTES = 4 * 1024 * 1024
1820

1921

2022
def validate_latest_signal(
@@ -93,14 +95,18 @@ def _resolve_source_path(source: str, base: Path | None) -> Path:
9395

9496

9597
def _read_declaration(path: Path, base: Path | None, declared_source: str) -> bytes:
96-
if getattr(os, "O_NOFOLLOW", None) is None or getattr(os, "O_DIRECTORY", None) is None:
98+
if (
99+
getattr(os, "O_NOFOLLOW", None) is None
100+
or getattr(os, "O_DIRECTORY", None) is None
101+
or getattr(os, "O_NONBLOCK", None) is None
102+
):
97103
raise SignalValidationError("secure descriptor-based source reading is unavailable")
98104
if not _OPEN_SUPPORTS_DIR_FD:
99105
raise SignalValidationError("secure openat source reading is unavailable")
100106
fd = -1
101107
try:
102108
if base is None:
103-
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
109+
fd = os.open(Path(declared_source), os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0))
104110
else:
105111
raw_path = Path(declared_source)
106112
try:
@@ -117,11 +123,18 @@ def _read_declaration(path: Path, base: Path | None, declared_source: str) -> by
117123
fd = next_fd
118124
final_fd = -1
119125
try:
120-
final_fd = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW, dir_fd=fd)
126+
final_fd = os.open(
127+
parts[-1], os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0), dir_fd=fd
128+
)
121129
finally:
122130
parent_fd = fd
123131
fd = final_fd
124132
os.close(parent_fd)
133+
metadata = os.fstat(fd)
134+
if not stat.S_ISREG(metadata.st_mode):
135+
raise SignalValidationError("declaration source must be a regular file")
136+
if metadata.st_size > MAX_JSON_ARTIFACT_BYTES:
137+
raise SignalValidationError("declaration source exceeds maximum size")
125138
with os.fdopen(fd, "rb") as stream:
126139
fd = -1
127140
return stream.read()
@@ -181,5 +194,5 @@ def _parse_datetime(value: Any, name: str) -> dt.datetime:
181194
except ValueError as exc:
182195
raise SignalValidationError(f"{name} must be an ISO datetime") from exc
183196
if parsed.tzinfo is None:
184-
return parsed.replace(tzinfo=dt.timezone.utc)
197+
raise SignalValidationError(f"{name} must be an ISO datetime with an explicit timezone")
185198
return parsed.astimezone(dt.timezone.utc)

tests/test_latest_linkage.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ def test_final_symlink_is_rejected_without_following_it(tmp_path: Path) -> None:
107107
validate_latest_signal(signal_payload("alias.json"), signal_base_dir=tmp_path)
108108

109109

110+
def test_absolute_declared_symlink_is_rejected_without_resolving_identity(tmp_path: Path) -> None:
111+
write_snapshot(tmp_path)
112+
alias = tmp_path / "absolute-alias.json"
113+
alias.symlink_to(tmp_path / "theme_momentum_snapshot.json")
114+
115+
with pytest.raises(SignalValidationError, match="no-follow|symlink"):
116+
validate_latest_signal(signal_payload(str(alias)))
117+
118+
110119
def test_descriptor_read_rejects_symlink_swap_after_bounds_check(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
111120
path = write_snapshot(tmp_path)
112121
outside = tmp_path.parent / "outside.json"
@@ -154,6 +163,35 @@ def open_once(*args: object, **kwargs: object) -> int:
154163
assert calls == 2
155164

156165

166+
@pytest.mark.parametrize("generated_at", ["2026-06-27", "2026-06-27T00:00:00"])
167+
def test_generated_at_requires_time_and_explicit_timezone(tmp_path: Path, generated_at: str) -> None:
168+
payload = signal_payload()
169+
payload["generated_at"] = generated_at
170+
write_snapshot(tmp_path)
171+
172+
with pytest.raises(SignalValidationError, match="ISO datetime"):
173+
validate_latest_signal(payload, signal_base_dir=tmp_path)
174+
175+
payload = signal_payload()
176+
write_snapshot(tmp_path, generated_at=generated_at)
177+
with pytest.raises(SignalValidationError, match="ISO datetime"):
178+
validate_latest_signal(payload, signal_base_dir=tmp_path)
179+
180+
181+
def test_non_regular_and_oversized_declarations_fail_closed(tmp_path: Path) -> None:
182+
fifo = tmp_path / "theme_momentum_snapshot.json"
183+
os.mkfifo(fifo)
184+
with pytest.raises(SignalValidationError, match="regular file"):
185+
validate_latest_signal(signal_payload(), signal_base_dir=tmp_path)
186+
187+
fifo.unlink()
188+
fifo.write_bytes(b"{}")
189+
with fifo.open("ab") as stream:
190+
stream.truncate(linkage.MAX_JSON_ARTIFACT_BYTES + 1)
191+
with pytest.raises(SignalValidationError, match="maximum size"):
192+
validate_latest_signal(signal_payload(), signal_base_dir=tmp_path)
193+
194+
157195
def test_bad_hash_and_as_of_mismatch_are_validation_errors(tmp_path: Path) -> None:
158196
path = write_snapshot(tmp_path)
159197
payload = signal_payload()

0 commit comments

Comments
 (0)