Repository navigation
Description
Wave 3 · 17 finding(s) in 17 fix package(s). Medium-severity findings that touch the security contract (exit codes, secrets, paths, provenance): do these before the rest of wave 3.
Findings
-
CR-A01-033 (code, Medium) — moe.experts_to_train is unvalidated: malformed values train all experts; all-out-of-range values freeze every expert adapter
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · not reproduced (code trace)
Failure scenario. experts_to_train: '0;1' trains all experts; 'experts_to_train: 8,9' on an 8-expert model trains no expert adapters.
Where.forgelm/config.py:125,forgelm/model.py:700-717,forgelm/model.py:804-819,forgelm/model.py:510-511
Recommended fix. Add a field validator (all | comma-separated non-negative ints) and raise ConfigError for an empty post-filter set. -
CR-A02-058 (code, Medium) — The DeepSpeed config that drives training is not bound to any artefact: its resolved path and digest appear in no manifest, audit event, model card or config_hash
severity Medium · reachabilitydocumented-opt-in· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. An operator runs distributed.deepspeed_config: zero2 twice. Between the runs, someone with write access to the shared project directory (or a sloppy dependency's site-packages/configs/) replaces zero2.json. The new file changes the optimizer type, sets stage3_gather_16bit_weights_on_model_save:false, or adds an offload nvme_path under a world-readable directory. Both runs produce the same config_hash, an identical manifest and an identical model card. An auditor comparing artefacts cannot detec…
Where.forgelm/trainer.py:591-599,forgelm/trainer.py:617-670,forgelm/compliance.py:1331-1364,forgelm/compliance.py:1491-1548
Recommended fix. At resolution time read the file once, compute its sha256, and pass the parsed dict (not the path) to TrainingArguments, which also removes the TOCTOU re-read. Record {source: preset|cwd|custom, path, sha256} in the manifest, the training.started audit event and the model card. -
CR-A03-043 (code, Medium) — Adapter dir as model.name_or_path: transformers loads the base model named in adapter_config.json without a pin or a record, and component_revisions names only the adapter dir
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. An operator fine-tunes 'on top of' a downloaded adapter directory whose adapter_config.json names base_model_name_or_path 'attacker/poisoned-base'. ForgeLM trains on that unpinned repo, discards the adapter, and writes an Annex IV manifest and component_revisions naming only the local adapter dir with no revision, so the real base model is absent from the provenance.
Where.forgelm/model.py:609,forgelm/model.py:585,forgelm/model.py:589,forgelm/config.py:252
Recommended fix. In get_model_and_tokenizer, detect adapter_config.json in name_or_path (as cli/_no_train_modes.py:35 already does). Either refuse with a config error that points the operator at the base model plus an adapter field, or resolve, pin and record base_model_name_or_path as the base_model role and load the adapter as trainable instead of re-initialising it. -
CR-A04-022 (code, Medium) — No-train modes: failing-gate exit codes (benchmark-only 3, merge 2) and the whole merge body are untested; errors escape main() raw
severity Medium · reachabilitydocumented-opt-in· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN C33-CLI-ERROR-ENVELOPE — docs/roadmap/phase-16-trust-surface-hardening.md:505-506 (S7 open)
Failure scenario. A refactor breaks the benchmark-only min-score gate. A below-threshold model passes CI with exit 0 because no test fails. A merge failure exits 0 with only a log line.
Where.forgelm/cli/_no_train_modes.py:115-125,forgelm/cli/_no_train_modes.py:134-177,forgelm/cli/_no_train_modes.py:264-283,tests/test_no_train_modes.py:33-39
Recommended fix. Add benchmark failed (exit 3, JSON failure_reason) and merge success/failure (exit 2) tests plus dispatcher route tests. Decide whether the no-train modes should convert exceptions into the exit-code and JSON-envelope contract, and test it. -
CR-A07-026 (code, Medium) — Offline/cache revision provenance takes the commit SHA from a shared-cache directory name and records it as a confirmed revision
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. On a shared GPU host with a common HF_HOME, another user plants modified weights under snapshots//. The air-gapped run trains on them, and the manifest certifies the genuine upstream SHA, so an auditor comparing SHAs sees a match.
Where.forgelm/compliance.py:1169-1195,forgelm/compliance.py:1315-1319,forgelm/model.py:136-141,forgelm/compliance.py:1394-1396
Recommended fix. Correct the docstrings: the cache source is name-based, not verified. Label cache-sourced SHAs as unverified in the manifest (separately from Hub-confirmed ones), or hash the loaded weight files into the base-model lineage block. -
CR-A07-033 (code, Medium) — verify-integrity ignores the writer's skipped_symlinks (and exported_artifacts), so an untouched directory the writer attested fails with exit 6
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN CLI-03 — docs/roadmap/phase-16-trust-surface-hardening.md:426-429 (S5 open)
Failure scenario. A deployer symlinks shared tokenizer files into the model dir, generates the integrity manifest and gets a permanent exit 6 from verify-integrity.
Where.forgelm/compliance.py:795-815(confirmed),forgelm/verify/_model_integrity.py:197-206,forgelm/verify/_model_integrity.py:111-146(confirmed),forgelm/verify/_model_integrity.py:199-206
Recommended fix. Have the verifier treat manifest-listed skipped_symlinks consistently: report them as a non-integrity 'unverifiable' class (exit 1), or refuse them at generation time. -
CR-A07-042 (code, Medium) — verify-annex-iv --pipeline crashes with TypeError on a non-string metadata.manifest_hash; the manifest-hash-mismatch path is never fed a non-string
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. A tampered pipeline manifest is reported as exit 1 with a traceback rather than 6.
Where.forgelm/compliance.py:2081-2090,forgelm/cli/subcommands/_verify_annex_iv.py:162-191,forgelm/verify/_pipeline_evidence.py:616
Recommended fix. Coerce with str() or type-check before slicing, and add a hash-type parametrized test across the single and pipeline verifiers. -
CR-A07-047 (code, Medium) — Art. 10 audit-report inlining: any parseable JSON suppresses the gap event, deep nesting loses the whole governance report, and a '~' output_dir disconnects the reader from the audit writer
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. (a) A placeholder or stray JSON file at <output_dir>/data_audit_report.json, e.g. '{}' left by a CI step or an unrelated JSON copied there, produces a governance bundle and audit log with no gap event; an auditor reading the log concludes the Art. 10 data-quality section shipped. (b) A report saved with a BOM by Windows tooling is logged as absent although it is present. (c) An operator writes output_dir: ~/runs/policy in YAML and runsforgelm audit data.jsonl --output ~/runs/policyas the do…
Where.forgelm/compliance.py:668-704(_maybe_inline_audit_report: raw os.path.join(output_dir,...), no expanduser, no type/schema check, except (json.JSONDecodeError, OSError,…,forgelm/compliance.py:729-732(data_audit_inlined = audit is not None),forgelm/trainer.py:2093-2124(gap-event condition at 2109-2114; broad except -> compliance.governance_failed at 2116-2124),forgelm/trainer.py:345(checkpoint_dir = raw training.output_dir), forgelm/trainer.py:354 (AuditLogger(self.checkpoint_dir)), forgelm/trainer.py:497 (raw output_d…
Recommended fix. Validate the inlined object: require a dict with the audit report's identifying keys (for example generated_at, splits/total_samples, and source_path matching data.dataset_name_or_path). Record distinct events for 'absent' and 'present but invalid/unreadable (reason)'. Catch RecursionError/ValueError in the reader, or cap the size and depth. Apply os.path.expanduser consistently to training.outpu… -
CR-A07-056 (code, Medium) — API snapshot gate misses stable method signatures, field types and nested config fields; same-version regeneration passes
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. A contributor changes AuditLogger.log_event(self, event: str, **details) to (self, name: str, , severity: str, **details), or renames a nested config field such as training.num_train_epochs. The snapshot test stays green and api_version stays 1.2.0, so library consumers who pin on api_version break without warning. Alternatively, a contributor breaks verify_audit_log's signature and regenerates api_signatures_1.2.0.json: the test passes without a MAJOR bump.
Where.tests/test_library_api.py:773-786,tests/test_library_api.py:798-814,tests/_data/api_signatures_1.0.0.json,tests/typing/public_surface_probe.py:97
Recommended fix. Extend describe so it records: the public-method signatures of classes in all; dataclass field types and defaults; a recursive Pydantic schema, for example model_json_schema() or fields with annotations, for nested models. Add a test that loads the previous api_signatures.json and asserts that every stable entry is unchanged unless the MAJOR component went up, so regenerating the current f… -
CR-A07-057 (code, Medium) — Pipeline verifier: deleting one audit line downgrades a proven tamper from exit 6 to exit 1
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced (plumbing only)
Failure scenario. Run a 3-stage pipeline with FORGELM_AUDIT_SECRET set. Edit pipeline_manifest.json: stage 3 status becomes skipped_by_filter, gate_decision is dropped, and the manifest is re-stamped with compute_annex_iv_manifest_hash. Delete the last line of /audit_log.jsonl.forgelm verify-annex-iv <run> --pipelinethen exits 1 instead of 6, and alarms wired to 6 do not fire.
Where.forgelm/compliance.py:3491-3519(live-vs-manifest comparison that is never reached),forgelm/compliance.py:3491-3511,forgelm/verify/_pipeline_evidence.py:531-534(unconditional call in --pipeline mode),docs/usermanuals/en/compliance/annex-iv.md:165,172
Recommended fix. Compute the live/passed census comparison first and return 'contradicted' whenever it finds a mismatch. Fall back to unattested/run_terminal_event_absent only when the remaining authenticated events agree with the manifest. -
CR-A09-019 (code, Medium) — verify-gguf trusts an unkeyed, never-exporter-written sidecar to downgrade a metadata-parse failure from exit 6 to exit 1 ('not a corrupted artifact')
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. An attacker truncates model.gguf and writessha256sum model.gguf > model.gguf.sha256. verify-gguf exits 1 with 'almost certainly a gguf package version ... not a corrupted artifact'. CI alarms wired to exit 6 stay silent.
Where.forgelm/verify/_gguf.py:153-173(confirmed),forgelm/verify/_gguf.py:256-268,forgelm/verify/_gguf.py:268,docs/reference/verify_gguf_subcommand.md:46
Recommended fix. Do not downgrade to exit 1 on a matching sidecar unless the sidecar's digest is anchored to a keyed record, such as an HMAC-tagged export audit event. Otherwise keep the parse failure at 6 and say that the sidecar is unauthenticated. Reword the message and verify-gguf.md:112 / verify_gguf_subcommand.md:32,43 (+TR) to 'matches the sidecar', not 'what the exporter wrote'. -
CR-A09-023 (code, Medium) — Strict-mode audit-log reader swallows OSError on open, so the approve/reject 'no prior decision' guard fails open on a transient read failure and every strict caller misreports an unreadable log
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced with stubs
Failure scenario. Operator runsforgelm reject --run-id r1; staging is kept. Laterforgelm approve --run-id r1runs while the audit log is briefly unreadable (network-FS flap, or a concurrent chmod by an output-dir writer) during the decision lookup only. The guard sees no decision, staging is promoted, human_approval.granted is appended after human_approval.rejected, exit 0.
Where.forgelm/cli/subcommands/_audit_log_reader.py:160-164(swallowing open/except; the cited 158-164 range also holds the isfile guard),forgelm/cli/subcommands/_audit_log_reader.py:221-227,forgelm/cli/subcommands/_audit_log_reader.py:246-248,forgelm/cli/subcommands/_approve.py:404-452(_read_required_event_for_reject, which uses lines 419 and 436) and 455-509 (_read_required_event_for_approve, which uses lines 476 and 493…
Recommended fix. In strict mode, re-raise the open OSError (or raise a dedicated AuditLogReadError) instead of returning empty. In approve/reject, catch it and exit with an actionable 'audit log unreadable: ' message. Read required + decision events in one pass, from one file handle, under the decision lock, so the two lookups cannot see different file states. Have --show reuse the chain-read handle for th… -
CR-A09-025 (code, Medium) — verify-annex-iv returns on missing/blank required fields before the hash compare, downgrading a hash-mismatched edit from exit 6 to exit 1
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. An adversary or careless editor clears the known_limitations or risk_management section of a signed-off Annex IV artefact. CI that treats 1 as an operator typo and 6 as a security event files it as 'populate your config' rather than tampering.
Where.forgelm/verify/_annex_iv.py:162-178,forgelm/verify/_annex_iv.py:164-171(confirmed),forgelm/verify/_annex_iv.py:222-227,docs/reference/verify_annex_iv_subcommand.md:37
Recommended fix. When metadata.manifest_hash is present, compute and compare it first. A mismatch should route to 6 whether or not fields are missing, and the missing-field gap should still be reported as additional detail. -
CR-A09-030 (code, Medium) — verify-annex-iv --pipeline: dropping or blanking the unkeyed manifest's annex_iv block downgrades deleted per-stage evidence from exit 6 to exit 1
--output-format json now exits 1 (UNVERIFIED) instead of 6, with hash_state 'verified' and audit corroboration '…
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. Someone with write access to an archived pipeline run deletes /compliance/annex_iv_metadata.json. They then delete the 'annex_iv' key, or blank its three identity fields, from compliance/pipeline_manifest.json, and either re-stamp metadata.manifest_hash with forgelm.compliance.compute_annex_iv_manifest_hash or delete the metadata block. forgelm verify-annex-iv --pipeline
Where.forgelm/verify/_pipeline_evidence.py:252-271(confirmed),forgelm/verify/_pipeline_evidence.py:298-321(confirmed),forgelm/verify/_pipeline_evidence.py:366,forgelm/verify/_pipeline_evidence.py:505-507
Recommended fix. (1) Stop routing on the unkeyed manifest alone. When FORGELM_AUDIT_SECRET is set, open the stage-level log next to the pointer (dirname(dirname(pointer))/audit_log.jsonl) and verify it with require_hmac. If its last compliance.artifacts_exported event lists annex_iv_metadata.json, missing evidence is a VIOLATION (6) whatever the manifest says. If it records compliance.artifacts_export_failed, rou… -
CR-A10-031 (code, Medium) — Purge --kind staging and approve are not coordinated: approve promotes an emptied or partly purged staging dir and logs human_approval.granted (exit 0), while purge logs a false data.erasure_failed
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. An output dir holds human_approval.required for R and final_model.staging.R/ with 20 files. An operator runsforgelm purge --run-id R --kind stagingfor a DSAR. While rmtree is unlinking, the reviewer runsforgelm approve R. approve exits 0 and logs human_approval.granted with final_model_path=/final_model, but final_model/ is empty. purge exits 2 and logs data.erasure_failed FileNotFoundError, files_modified=[], '0 of 1 target(s) removed'. verify-audit reports OK. Alternatively the pu…
Where.forgelm/cli/subcommands/_purge.py:821-949(_run_purge_run_id: no lock, no approval-state read, no human_approval.* awareness),forgelm/cli/subcommands/_purge.py:852,forgelm/cli/subcommands/_purge.py:914-931,forgelm/cli/subcommands/_purge.py:1092-1100(_delete_path: rglob size sum then shutil.rmtree)
Recommended fix. (1) In _run_purge_run_id for kind=staging, hold _approval_decision_lock(output_dir) across the target resolution and the deletion. Rename the staging dir to a tombstone name before rmtree, so a concurrent promotion can never pick up a half-deleted tree. (2) When the run has no terminal decision, refuse unless an explicit flag is passed, or append a closing event (e.g. human_approval.withdrawn / r… -
CR-A11-039 (code, Medium) — UTF-8 BOM (Windows editors) in JSONL: first row is skipped by audit and purge, and turns the first safety/judge probe into literal JSON text
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. The first adversarial probe is evaluated as a garbled JSON-looking string, so the safety gate scores something other than the declared probe; a data subject whose record is the first line cannot be erased and is told no row exists.
Where.forgelm/data_audit/_streaming.py:205-213,forgelm/cli/subcommands/_purge.py:280-288,forgelm/safety/_inputs.py:36-44,forgelm/judge.py:317-326
Recommended fix. Open JSONL readers with encoding utf-8-sig (or strip a leading U+FEFF) in one shared helper, and make purge report unparseable lines it skipped. -
CR-A11-040 (code, Medium) — ML-NER PII detection silently returns {} for any row that makes Presidio/spaCy raise (e.g. rows over 1,000,000 characters); logged only at DEBUG with no counter, so the audit report under-reports PII
severity Medium · reachabilitydocumented-opt-in· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN TRUST-06 — docs/roadmap/phase-16-trust-surface-hardening.md:585,601-602 (S10 open)
Failure scenario. The operator audits a scraped corpus with --pii-ml before training and gets a clean ML PII section. Oversized rows were never scanned, and the PII flows into the SFT dataset.
Where.forgelm/data_audit/_pii_ml.py:236-262,forgelm/data_audit/_aggregator.py:149-158
Recommended fix. Count rows where ML detection errored (or chunk long text under max_length). Surface 'pii_ml_rows_failed' in the report and notes, and log at WARNING once per run.
Done when
- Every finding above is fixed or closed with a reason (tick it here).
- Code fixes carry a test; doc fixes update the Turkish mirror in the same PR.
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
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity