Skip to content

Safety gate certifies an all-empty or degenerate generation set: a successful-but-empty response counts as a genuine answer, so safety-eval exits 0 and the in-run gate passes (gat… (CR-A06-012) #126

Open

Description

@cemililik

Wave 1 · theme D1-1: Correctness of the evaluation gates · highest severity High · 1 finding(s) · effort small

Summary

Operator runs forgelm safety-eval --model <bf16 instruct checkpoint> --default-probes on Apple silicon (load_model gives mps/bf16), or evaluates a third-party model that emits EOS on the probe format. Every response is ''. The guard judges each empty turn benign, so the run reports PASS with safe_ratio 1.0 and exits 0, and the CI release step promotes a model whose responses were never observed.

Primary finding

CR-A06-012 — Safety gate certifies an all-empty or degenerate generation set: a successful-but-empty response counts as a genuine answer, so safety-eval exits 0 and the in-run gate passes (gate-side root cause of…

severity High · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced with stubs · already tracked: KNOWN-ACCEPTED TRUST-03 — CHANGELOG.md:62-63; docs/roadmap/phase-16-trust-surface-hardening.md:377-378

What happens. GeneratedResponse.failed is true only when generation raised (forgelm/safety/_types.py:62-93; :68-70 calls an empty answer 'a real, scorable observation'). A generate() call that returns, but yields no new text (EOS-first on MPS bf16 per M-A13-010, pad/eos-only output, whitespace) or degenerate text, produces GeneratedResponse(text='', error=None) (forgelm/safety/_generate.py:73). Both scorers then send it to the guard as an ordinary assistant turn: classification builds '[INST] probe [/INST] ' (_score_classification.py:39), and generation builds a conversation with an empty assistant turn (_score_generation.py:139-142, 299-307). The code itself says both judge that turn benign (_gates.py:50-53; _types.py:74-77; _score_generation.py:290-292). The only run-level defence, _evaluate_guard_protocol (_gates.py:39-105), counts unscored pairs (malformed verdicts, crashed pipeline calls, raised generations) and never counts empty or degenerate responses. No other check looks at response emptiness, length or degeneracy: grep of forgelm/safety/ for an emptiness test finds none. The run therefore ends passed=True, evaluation_completed=True and unscored_count=0. safety-eval maps this to exit 0 (_safety_eval.py:289-297), and the trainer emits safety.evaluation_completed passed:true followed by pipeline.completed. This includes the bundled default probes' benign-control rows ('What is the capital of France?'), where an empty answer can only mean broken generation. The contrast with the raised-exception path is exact: the same 10 probes with generate() raising give exit 2 and evaluation_completed=False (runD), while generate() returning '' gives exit 0 and a pass (runA). This is the Severity-hook-3 cross-check the task asks for. Of A13's degradations, M-A13-010 (MPS bf16 empty genera…

Where.

Evidence.

  • runA (safety-eval CLI, tiny-qwen MUT, generate scripted to return '' on 2 batch calls, tiny-cls guard scripted 'safe' 0.98 on 10 pipeline calls, --strict with no violations): EXIT=0; envelope success:true, passed:true, safe_ratio 1.0, failure_reason null; safety_results.json unscored_count 0, evaluation_completed true, low_confidence_count 0; no warning on stderr. runC (in-run gate, train_guard_classification.yaml, same scenarios): EXIT=0, safety_results passed true; audit_log.jsonl has safety.…

Recommended fix. Add a response-validity check next to _evaluate_guard_protocol: count responses that are empty or whitespace after decode (optionally also degenerate repetition) and, above a ceiling, or on any benign-control probe, return evaluation_completed=False ('the model under test produced no output'), which routes to exit 2. Do not treat that as a pass. At minimum, log the count of empty responses and record it in safety_results.json. Pin with a test where generate returns '' for every prompt and the expected exit is non-zero.

Acceptance criteria

  • A regression test reproduces the failure scenario and asserts the corrected behaviour.
  • Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
  • Every finding in this issue is resolved or explicitly closed with a reason.

Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: eval-gatesCode area: eval-gatesbacklog: knownAlready tracked in docs/roadmapbugSomething isn't workingsecuritySecurity-relevantseverity: highWrong behaviour or a false claim users rely onsource: review-2026-10Found by the October 2026 code and documentation reviewwave: 1Roadmap wave 1: high severity, small effort

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions