Skip to content

[Wave 3] Medium — Security-contract fixes: Resource exhaustion and ReDoS #648

Open

Description

@cemililik

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 · reachability default · intent intended-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 · reachability default · intent intended-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 · reachability default · intent intended-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 · reachability documented-opt-in · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. A 4 GB JSONL shard scanned with --type custom and 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 · reachability default · intent contract-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 to ingest --secrets-mask in 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 literal finditer. 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 · reachability default · intent contract-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 · reachability default · intent intended-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 · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · not reproduced (code trace)
    Failure scenario. An operator points forgelm ingest at 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 stop and Kubernetes or spot SIGTERM is ignored until SIGKILL; outside a container SIGTERM kills with no terminal audit event or failure we…
    severity Medium · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. In a k8s spot node drain or docker compose stop, kubelet or docker sends SIGTERM to PID 1 forgelm --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. Add init: true to the compose service and document docker 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 emits pipeline.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.

Activity

  1. added
    bugSomething isn't working
    securitySecurity-relevant
    severity: mediumReal defect with limited reach or a workaround
    source: review-2026-10Found by the October 2026 code and documentation review
    wave: 3Roadmap wave 3: medium severity, module sweeps
    on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: data-auditCode area: data-auditarea: eval-gatesCode area: eval-gatesarea: gdpr-approvalCode area: gdpr-approvalbugSomething isn't workingsecuritySecurity-relevantseverity: mediumReal defect with limited reach or a workaroundsource: review-2026-10Found by the October 2026 code and documentation reviewwave: 3Roadmap wave 3: medium severity, module sweeps

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions