Repository navigation
Description
Wave 3 · 9 finding(s) in 9 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-A02-029 (code, Medium) — staging_path is recorded as an absolute path, so approve/reject/approvals break once the output dir is reached through another path
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. Training runs on an ephemeral CI runner, outputs are archived and restored on the reviewer's host, and the Article 14 decision cannot be recorded.
Where.forgelm/trainer.py:1611(confirmed),forgelm/cli/subcommands/_approve.py:541-550(confirmed),forgelm/cli/subcommands/_approve.py:683-692,forgelm/cli/subcommands/_approvals.py:184-198
Recommended fix. Record staging_path relative to output_dir (or also record its basename). Resolve it against the supplied --output-dir and keep the containment check; fall back to <output_dir>/final_model.staging.<run_id>. -
CR-A06-026 (code, Medium) — Under FSDP2 / DeepSpeed ZeRO-3 the gates' generate() is a collective, but ForgeLM's rank-local OOM/error fallbacks change the number of generate calls on one rank only, so the job hangs (or stalls fo…
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced (plumbing only)
Failure scenario. FSDP2 run with 4 GPUs. During the judge gate one rank OOMs on a batch (its GPU still holds that rank's device_map='auto' guard shards) and falls back to per-prompt generation while the other ranks continue batched. The collectives mismatch and the job hangs until the NCCL watchdog timeout, then aborts, so the run fails as a crash after the full training cost.
Where.forgelm/safety/_generate.py:74-103,forgelm/judge.py:432-454,forgelm/safety/_generate.py:132-136,forgelm/judge.py:478-480
Recommended fix. For sharded models, make the generation loop rank-synchronous: agree on a per-batch OOM fallback across ranks (all_reduce a 'failed' flag before retrying), or gather full parameters once on rank 0 and generate there while other ranks wait at a barrier. -
CR-A08-014 (code, Medium) — safe_post/safe_get have only a per-read timeout and no response-size cap: a slow-drip or huge response stalls or exhausts webhook, judge, teacher and doctor calls
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. The webhook receiver or judge endpoint (or a host DNS points to) drips one byte every few seconds, and the training run never starts because notify_start never returns. Or it returns a multi-GB body and the trainer process is OOM-killed.
Where.forgelm/_http.py:586-607,forgelm/_http.py:722-739,forgelm/_http.py:33-36,forgelm/webhook.py:157
Recommended fix. Use stream=True with a byte cap (for example 1-10 MiB, configurable per caller) and an overall wall-clock deadline (a monotonic check while iterating iter_content, or a watchdog). Webhooks need no body, so close after reading the status. Align the architecture.md timeout description with the code. -
CR-A10-028 (code, Medium) — reverse-pii --type custom arms a fixed 30 s SIGALRM per whole file, so a benign regex on a large file fails with exit 2 as a 'ReDoS'
severity Medium · reachabilitydocumented-opt-in· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. A 4 GB JSONL shard scanned with--type customand a normal regex fails at 30 s with exit 2, although nothing is wrong with the regex; the operator has no flag to raise the budget.
Where.forgelm/cli/subcommands/_reverse_pii.py:83(confirmed),forgelm/cli/subcommands/_reverse_pii.py:428-469(_scan_file_with_alarm; alarm armed at :452, message at :449),forgelm/cli/subcommands/_reverse_pii.py:390-425(_scan_files_with_redos_guard)
Recommended fix. Scale the budget with file size (or make it per-chunk of N lines, re-armed) or expose it as a documented option; low priority. -
CR-A11-032 (code, Medium) — The 65536-char lazy bound on the PGP private-key regex costs about 6.8 s per MB on banner-dense text and fails regex.md's own 1K/5K/10K methodology
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. A crafted or corrupted corpus (scraped PEM dumps, a hostile contributor's JSONL rows, or a document passed toingest --secrets-maskin paragraph mode, where an over-long paragraph becomes one chunk) packed with BEGIN banners slows the always-on scan by about 300-1000x per byte: about 7 s per MB for PGP plus about 1.1 s per MB for OpenSSH. A 100 MB file adds about 13 minutes of CPU and 1 GB adds about 2.2 hours. This stalls a webhook-triggered CI audit, the threat regex.md:153 names. The cost…
Where.forgelm/data_audit/_secrets.py:49-59(comment claiming no O(n^2) reintroduced; constant 65536),forgelm/data_audit/_secrets.py:133-158(mask_secrets),forgelm/data_audit/_secrets.py:126-129,forgelm/data_audit/_secrets.py:151-157
Recommended fix. Replace the two envelope regexes with a linear finder. Find BEGIN markers with a literalfinditer. Collect END positions once. For each BEGIN not already consumed, bisect to the next END and accept it if it falls within the bound, then advance past that END. This keeps the bounded-span semantics while making the cost O(n + m log m), about 10^4x faster on adversarial rows with no change on norma… -
CR-A11-041 (code, Medium) — JWT secret regex is quadratic on 'eyJhbGc-' runs with no '.': a 300 KB row adds 30 s to
forgelm audit
severity Medium · reachabilitydefault· intentcontract-violation· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. A 1 MB crafted row costs about 2.5 minutes of JWT scanning alone (extrapolated). Combined with the email pattern, a handful of rows exhausts a CI job's time budget.
Where.forgelm/data_audit/_secrets.py:83-87,forgelm/data_audit/_secrets.py:127,forgelm/data_audit/_secrets.py:157,forgelm/data_audit/_aggregator.py:159
Recommended fix. Bound the segments (for example {10,4096}) or pre-filter with a cheap'.' in text/ 'eyJ' count check. Add the builder to a secrets linearity test. -
CR-A12-021 (code, Medium) — PDF header/footer stripper is O(P x L^2): every peel pass rebuilds all pages' line lists
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. One crafted PDF in a corpus stalls the ingest job for hours with no timeout.
Where.forgelm/ingestion.py:433-445,forgelm/ingestion.py:310-360,forgelm/ingestion.py:1011,forgelm/ingestion.py:384-447
Recommended fix. Cap the number of passes, or drop all occurrences of a repeating line within the window in one pass, using a deque or index-based scan. Add an STD-069 style linearity test. -
CR-A12-030 (code, Medium) — No per-file or per-run CPU-time bound on pypdf / python-docx / ebooklib extraction (CPU-exhaustion DoS, distinct from the memory gap in M-A12-036)
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · not reproduced (code trace)
Failure scenario. An operator pointsforgelm ingestat a corpus that includes one attacker-authored PDF/DOCX/EPUB (IN-05, untrusted authorship is the stated threat — regex.md:153 / RTP R-01 call adversarial corpora realistic). The document is small on disk (bounded size, so M-A12-036's memory heuristics are irrelevant) but its content stream or markup is crafted so that pypdf/ebooklib/python-docx text extraction takes minutes to hours of CPU per page. Because there is no time bound, no per-file deadline and no…
Where.forgelm/ingestion.py:970-973,forgelm/ingestion.py:982,forgelm/ingestion.py:649-663,forgelm/ingestion.py:883-891
Recommended fix. Add a per-file extraction deadline (POSIX signal.alarm / a watchdog thread, consistent with the strip-pattern guard's own platform caveats) and/or process files with a bounded worker pool so a single pathological file cannot stall the run. Document the bound as an opt-out, mirroring --strip-pattern-no-timeout. -
CR-A16-032 (code, Medium) — No SIGTERM handling: as PID 1 in the shipped image,
docker stopand Kubernetes or spot SIGTERM is ignored until SIGKILL; outside a container SIGTERM kills with no terminal audit event or failure we…
severity Medium · reachabilitydefault· intentintended-but-harmful· confidence confirmed · independently confirmed · reproduced end to end
Failure scenario. In a k8s spot node drain ordocker compose stop, kubelet or docker sends SIGTERM to PID 1forgelm --config job.yaml. Python ignores it (SIG_DFL in PID 1) and keeps training for 10-30 s, then is SIGKILLed mid-step or mid-save. The audit log ends at training.started and no failure webhook is sent. Outside a container (systemd, CI runner cancel), SIGTERM gives rc -15 immediately with the same missing audit and webhook trail.
Where.Dockerfile:112-113,docker-compose.yaml:48,forgelm/cli/_dispatch.py:119-121,forgelm/cli/_dispatch.py:155-170
Recommended fix. Addinit: trueto the compose service and documentdocker run --init(or add tini as the ENTRYPOINT) so SIGTERM is delivered. Install a SIGTERM handler at the CLI seam that raises a dedicated exception, so the trainer's except block emitspipeline.failed(reason=terminated) and notify_failure. Optionally expose transformers' enable_jit_checkpoint, but only together with treating a signal-st…
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