Skip to content

Commit de80dfa

Browse files
fix(parser): clamp _safe_int to non-negative for token accounting
Negative usage token values from adversarial JSONL must not reduce session metadata totals; apply max(0, ...) in _safe_int and add regression test.
1 parent e963fcf commit de80dfa

2 files changed

Lines changed: 27 additions & 5 deletions

File tree

tests/test_parser_fuzz.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,28 @@ def test_non_numeric_usage_tokens_do_not_crash(tmp_path: Path) -> None:
266266
assert session["metadata"]["total_ephemeral_5m_tokens"] == 0
267267

268268

269+
def test_negative_usage_tokens_clamp_to_zero(tmp_path: Path) -> None:
270+
"""Negative token counts must not reduce session metadata totals."""
271+
entry = {
272+
"type": "assistant",
273+
"timestamp": "2026-06-11T00:00:00Z",
274+
"message": {
275+
"model": "claude-test",
276+
"content": [{"type": "text", "text": "hi"}],
277+
"usage": {
278+
"input_tokens": -100,
279+
"output_tokens": -1.5,
280+
"cache_creation": {"ephemeral_5m_input_tokens": -50},
281+
},
282+
},
283+
}
284+
path = _write_jsonl(tmp_path / "negative_usage.jsonl", [json.dumps(entry)])
285+
session = parse_session(path)
286+
assert session["metadata"]["total_input_tokens"] == 0
287+
assert session["metadata"]["total_output_tokens"] == 0
288+
assert session["metadata"]["total_ephemeral_5m_tokens"] == 0
289+
290+
269291
def test_non_finite_usage_tokens_do_not_crash(tmp_path: Path) -> None:
270292
"""json.loads accepts NaN/Infinity literals; int(nan)/int(inf) raise, so the
271293
parser must coerce them to 0 rather than propagate ValueError/OverflowError."""

utils/jsonl_parser.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@
4242

4343

4444
def _safe_int(val: Any) -> int:
45-
"""Coerce a value to int for token accounting; non-numeric or non-finite
46-
input becomes 0 so fuzzed/malformed usage fields never raise during
47-
arithmetic. json.loads accepts NaN/Infinity literals, so guard against them."""
45+
"""Coerce a value to a non-negative int for token accounting; non-numeric,
46+
non-finite, or negative input becomes 0 so fuzzed/malformed usage fields
47+
never raise during arithmetic and counters cannot go below zero."""
4848
if isinstance(val, bool):
4949
return 0
5050
if isinstance(val, int):
51-
return val
51+
return max(0, val)
5252
if isinstance(val, float):
53-
return int(val) if math.isfinite(val) else 0
53+
return max(0, int(val)) if math.isfinite(val) else 0
5454
return 0
5555

5656

0 commit comments

Comments
 (0)