Skip to content

baseline-compare.py:驗證 metric + render 邊界跳脫控制字元 (#117) - #132

Merged
kiki830621 merged 3 commits into
mainfrom
idd/117-compare-stdout-escaping
Aug 1, 2026
Merged

baseline-compare.py:驗證 metric + render 邊界跳脫控制字元 (#117)#132
kiki830621 merged 3 commits into
mainfrom
idd/117-compare-stdout-escaping

Conversation

@kiki830621

Copy link
Copy Markdown
Member

Refs #117

Summary

scripts/lib/baseline-compare.py interpolated corpus / language / metric straight into the pass/fail lines it prints — and that stdout is the log a human reads to decide whether a release is safe.

metric was the one rendered field validated nowhere: the gate's worklist stage covers corpus and language, and unlike golden / tolerance / error_rate it never passes through float(). So a hostile value arrived intact.

Reproduced (no timing window needed — a plain committed baseline suffices):

metric = "cer\x1b[32m\rFAKE ALL-PASS"
→ ✓ c1 [en] cer<ESC>[32m<CR>FAKE ALL-PASS: golden 0.1000 → measured 0.1100

The CR returns the cursor to the start of the line and the SGR turns it green, so the CI log shows a forged pass over the real verdict.

Two layers, because they fail differently

  1. metric must be cer | wer — a bounded vocabulary makes a bad value a loud gate error instead of a decorative label. Same vocabulary the bench validator enforces for metric_kind.
  2. Every rendered value is escaped at the render boundary. Validation lives in the worklist stage, but this script is a separate entry point that re-validates nothing it is handed. Escaping here means a field added to these messages later is covered without the author having to remember.

Escaped, not stripped — \x1b stays visible so the offending bytes remain diagnosable.

Verification

  • RED with true bytes: the first harness used subprocess(text=True), whose universal-newline translation silently turned the CR into \n and understated the finding. Re-run in bytes mode, the real output carries both 0x1b and 0x0d.
  • GREEN, 4 cases: PoC now rejected (exit 1, no control bytes); a hostile language is neutralised even when metric is legal — proving the two layers are independent; a normal baseline's output is unchanged; a real regression still fails correctly.
  • Both layers mutation-locked: deleting safe() reddens 3 assertions; deleting the METRICS check reddens 2. Neither test is vacuous.
  • swift test391 tests / 83 suites, green.

Checklist


Generated by /idd-all. Do NOT add a GitHub close trailer — IDD discipline requires manual /idd-close after merge.

… CI log (#117)

baseline-compare.py printed corpus / language / metric verbatim, and that
stdout IS the log a human reads to approve a release. metric was the one
rendered field validated NOWHERE — the worklist stage covers corpus and
language, and unlike golden/tolerance/error_rate it never passes through
float(), so a hostile string arrived intact.

Two layers, because they fail differently:

- metric must now be cer|wer. A bounded vocabulary makes a bad value a loud
  gate error instead of a decorative label.
- every rendered value is escaped at the RENDER boundary. Validation lives in
  the gate's worklist stage, but this script is a separate entry point that
  re-validates nothing it is handed — so a field added to these messages later
  is covered without the author having to remember.

Verified independently: the reproduced payload 'cer\x1b[32m\rFAKE ALL-PASS'
needs no timing window, only a committed baseline. Both layers are locked by
mutation — deleting safe() reddens 3 assertions, deleting the metric check
reddens 2.

Escaped, not stripped: \x1b stays visible so the offending bytes remain
diagnosable.

Refs #117
Verify found the PR asserting more than the code does — four times, in a change
whose whole subject is a log that must not mislead. No behaviour changes; the
14/14 byte-identical certification stands.

- safe()'s docstring said a later-added field is 'covered without the author
  having to remember'. It is not: the wrapping is explicit at 11 sites and a new
  f-string is unprotected until someone wraps it. The docstring now states that,
  plus the scope it actually has (C0/DEL/C1, so no ANSI sequence and no cursor
  return) and the scope it does NOT (U+2028, bidi overrides — no control byte
  needed; unreachable here only because corpus and language are whitelisted
  upstream).

- 'keeps the offending bytes diagnosable' overstated it: backslash is not
  escaped, so a real ESC renders identically to a benign literal '\x1b'. Now
  'stays visible'.

- The CHANGELOG's 'unlike golden/tolerance/error_rate it never passes through
  float()' implied float() was validating those. It is a conversion — it accepts
  NaN and Infinity, and the gate then reports exit 0 on a real regression. Filed
  as #134 rather than fixed here: the suggested three-line fix was tested and
  does not close the class (a JSON string "NaN" never reaches parse_constant).

- The headline claimed baseline values no longer reach the CI log unescaped,
  but regression-gate.sh still echoes model unescaped. Scoped to 'the compare
  stage'. Not fixed here because PR #126 deletes the exact heredoc involved and
  already rejects ESC/CR via MODEL_RE — a fix here would collide.

Also records the risk no leg had named: METRICS here, MetricKind in Swift and
metric_kind in the bench validator are three copies with no coupling test, and
because the check returns early, one legitimate future metric would suppress the
verdict for every corpus.

Refs #117
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #132 (#117)

Engine

manual fan-out — 2 lens Agents(security / requirements+regression 合併,model: opus)

  • sequenced Devil's Advocate + Codex(gpt-5.6-sol xhigh)。4 legs,比例配合 Simple 單檔。

Aggregate

PASS — 0 blocking。DA 裁定 零 code 變更:4 處文件修正(03c7801)+ 2 個 merge 前 file 的 issue。

要求覆蓋率

4 / 4 Strategy items FULLY。

Findings

# Sev Finding Source Action
1 PASS _CONTROL_CHARS 是 65 個 int-keyed 條目,C1 的 update() 未覆蓋 0x7F;全部 65 個碼位 round-trip 成 \xNN,零洩漏。U+0085 NEL = 0x85 落在 C1 內,已涵蓋 agents:security+codex
2 PASS 9 個 print() 全數稽核,每個 baseline/measured 值都被包住,無漏 sink,此檔不寫 stderr agents:security+codex
3 PASS measured 非獨立注入通道——gate 從 baseline 組出 measured.corpus(已白名單)、measured.metric 從不渲染、error_ratefloat()。惡意音檔名到不了 agents:security
4 PASS 14/14 情境 OLD vs NEW byte-identical(含 pass/fail/gate-error/summary 與合法 CJK corpus 名);5 個 exit code 全保留 agents:reqreg
5 PASS 真實 baseline.json實際 gate 組裝 heredoc 驗證:12 entries、rc=0、輸出 925 B byte-identical agents:reqreg
6 PASS 意外的硬化:舊 code 對 null / "CER" / " cer" / 1 這些 metric exit 0 通過 gate,現在全擋;缺 metric 從未捕捉的 KeyError traceback 變成乾淨 gate error agents:reqreg
7 PASS 兩層 mutation-locked:拿掉 safe() → 恰好 3 個 issue;拿掉 metric 檢查 → 恰好 2且兩個 mutation 互不影響對方的測試——真正防禦縱深該有的簽名 agents:reqreg
8 HIGH gate 對真實 regression 回報 exit 0(fail-OPEN)。security 找到 NaN 路徑(json.load 收 NaN/Infinity → diff > tol 恆 False);DA 另找到一條不涉及非有限數的:pass 行渲染 golden/measured/diff 但**不渲染 tolerance``**——閾值只在失敗時顯示,恰好在稽核「通過」時隱藏。tolerance: 1e308 通過 Swift schema test、JSONSerializationparse_constantmath.isfinite()` agents:security(F3)+devils-advocate Follow-up → #134(merge 前已 file)
9 MEDIUM regression-gate.sh 自己 echo baseline model 未跳脫未驗證(已重現 raw ESC+CR)——同 class、隔一個檔案 agents:security(F1) ✅ CHANGELOG headline 縮限;不在此修——DA 讀過 #126 的 diff,該 PR 刪掉 F1 針對的那段 heredoc 並以 MODEL_RE fullmatch 拒絕 ESC/CR,在此修會衝突
10 MEDIUM baseline-meta.json 的 key 名未跳脫印到 stderr(24 個 PR-editable key,該檔零 schema 測試) agents:security(F2) Noted(屬 #127 家族)
11 LOW U+2028/U+2029 存活。security 建出可用的偽造行splitlines() 看到 5 行含假的 ✓ regression gate: all 9 corpora within tolerance),且 PR 的四個新斷言全數通過 agents:security(F4)+codex Residue(DA 裁決)—— 見下
12 LOW bidi 存活(fribidi 確認 RLO 反轉行尾),但 ✗ REGRESSION 恆在攻擊者資料之前且 / 從不被替換 → 只能遮蔽,不能偽造 agents:security(F5) Residue
13 LOW metric 為 list/dict → TypeError: unhashable traceback 而非乾淨 gate error。但 fail-closed(exit 1、不 echo 值),屬 UX 非 bypass agents:security(F6)+codex Noted
14 orchestrator 的四處過度宣稱(見下) codex+agents:reqreg+devils-advocate ✅ Fixed 03c7801

DA 的三項裁決

① 提早 return → 維持,但 req·reg 的理由有兩條不成立:Codex 的提案本身就是那個調和方案
(它同樣跳過語意未定義的 corpus),所以「拒絕判斷更誠實」駁的是稻草人;且 duplicate 檢查
早退是結構性理由,metric 是語意且局部的——層級不同。維持的真正代價 DA 也點名了:
METRICS(Python)/ MetricKind(Swift)/ metric_kind(bench)三份複本零耦合測試
未來新增第三個 metric 會讓一筆合法 entry 抑制所有 corpus 的判定。已寫入 code 註解。

② bidi/U+2028 → code 層 residue(gate 上游對 corpus/language 做 fullmatchset -e
metric 現已有界;sink 是終端非網頁 viewer,ci.yml 明確排除 gate)。測試套件不算不誠實——
U+2028 屬 category ZlCc,「control characters」的措辭準確。但 safe() docstring 原本寫
「one-line log message」是假的,已縮限。DA 另驗證 isprintable() 對合法 CJK/重音/emoji/空白
是安全的——這是決定 Codex 修法可否採用的關鍵事實,四個 leg 都沒測

③ F3 → 獨立 issue 而非 in-scope。DA 實測 security 建議的修法並否決之
parse_constant 不能關閉該 class(JSON 字串 "NaN" 根本不觸發它,float("NaN") 仍產生 nan)。
「categorically worse + 三行可修」的例外論證在自己的前提上失敗。但 F3 的文字在 scope 內
且必修——CHANGELOG 原本斷言 float() 在做驗證。

四處過度宣稱(本 PR 自己犯的,已全修)

在一個主題就是「log 不能誤導人」的改動裡,PR 對自己的宣稱超過實作四次

  1. safe() docstring:「a field added later is covered without the author having to remember」——
    假的,11 個 site 手動呼叫,新 f-string 不會被自動保護
  2. 「keeps the offending bytes diagnosable」——過譽,\ 未跳脫故真實 ESC 與良性字面 \x1b byte-identical
  3. CHANGELOG:「unlike golden/tolerance/error_rate it never passes through float()」——
    暗示 float() 在驗證。它是轉換不是驗證,收 NaN/Infinity(→ regression gate 對真實 regression 回報 exit 0(fail-open):非有限數 + tolerance 從不被渲染 #134
  4. CHANGELOG headline:「Baseline values no longer reach the CI log unescaped」——
    regression-gate.sh 仍 echo model 未跳脫。已縮限為「the compare stage」

Scope Check

零 code 變更(03c7801 僅動註解/docstring/CHANGELOG)。req·reg 的 14/14 byte-identical
認證因此未被花掉——修完後重驗仍 156 B、11 測試綠。

Process Gaps

無。Codex leg 正常完成。

Follow-up Findings Filed

誠實的範圍聲明

本 PR 交付的是 compare stage 的 metric 值域 + C0/DEL/C1 render 跳脫
不宣稱:(1) 所有 baseline 值都不會未跳脫進 log(regression-gate.shmodel 仍會,見 #126);
(2) Unicode 層級的視覺偽造被擋(U+2028/bidi 在範圍外,見 residue);
(3) gate 不會誤放行(它會 —— 見 #134,且那與本 PR 無關地既已存在)。

@kiki830621
kiki830621 merged commit db4f181 into main Aug 1, 2026
1 check passed
@kiki830621
kiki830621 deleted the idd/117-compare-stdout-escaping branch August 1, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant