Skip to content

[Wave 3] Medium — Security-contract fixes: Paths, symlinks and TOCTOU #646

Open

Description

@cemililik

Wave 3 · 4 finding(s) in 4 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-057 (code, Medium) — Config path fields are never ~-expanded: output_dir '/x' splits a run between $HOME (HF checkpoints) and a literal ./ directory (final model, audit log, compliance); --resume misses the checkpoin…
    severity Medium · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. An operator answers the wizard's 'Output directory' prompt with /runs/exp1 and trains. Checkpoints land in $HOME/runs/exp1 while the model, audit log and compliance bundle land in .//runs/exp1. A later --resume auto starts from scratch and checkpoint cleanup targets the wrong tree. A wizard-entered model path '/models/x' passes --dry-run and fails at load with exit 2.
    Where. forgelm/config.py:451-453, forgelm/cli/_training.py:152, forgelm/cli/_resume.py:13-61, forgelm/utils.py:85-92
    Recommended fix. Normalise every path-typed config field with os.path.expanduser in one before-validator, as the wizard save-path and ingestion already do. Alternatively, reject a leading '
    ' with an actionable message. Have the wizard store the expanded value for output_dir and model paths, as BYOD already does. Document path resolution in configuration.md.

  • CR-A07-039 (code, Medium) — Fixed-name temp and artefact writes in output dirs follow pre-planted symlinks (genesis manifest .tmp, pipeline_manifest.json.tmp, bundle .tmp, deployer_instructions.md and others)
    severity Medium · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. Create the symlink shared_out/audit_log.jsonl.manifest.json.tmp -> ../victim_outside.txt, then run AuditLogger(shared_out).log_event('training.started'). victim_outside.txt now holds the genesis manifest JSON, and audit_log.jsonl.manifest.json is a symlink.
    Where. forgelm/compliance.py:466-472 (confirmed), forgelm/compliance.py:3062-3077 (open at 3064, os.replace at 3077), forgelm/compliance.py:2311-2318 (tmp at 2311, ZipFile open at 2313, os.replace at 2318), forgelm/compliance.py:1761-1763
    Recommended fix. Create temp files with tempfile.mkstemp(dir=...) or os.open(O_CREAT|O_EXCL|O_NOFOLLOW), and refuse a symlinked final target.

  • CR-A10-033 (code, Medium) — The approval decision lock opens .approval.lock following symlinks with no file-type or inode check
    severity Medium · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. Output-dir writer runs ln -s audit_log.jsonl <out>/.approval.lock. The approver runs forgelm approve r1 --output-dir <out>. final_model.staging/ is renamed to final_model/, then the process blocks indefinitely in log_event with no output. When the approver kills it, the audit log holds only human_approval.required. The model is live with no granted record, and a retry fails with 'final directory already exists'. reject also hangs, with no rejected event (staging is preserved).
    Where. forgelm/cli/subcommands/_approve.py:85, forgelm/cli/subcommands/_approve.py:84, forgelm/cli/subcommands/_approve.py:589, forgelm/cli/subcommands/_approve.py:614
    Recommended fix. Open the lock with os.open(path, O_RDWR|O_CREAT|O_NOFOLLOW (|O_CLOEXEC), 0o600). fstat it and refuse anything that is not a regular file with st_nlink==1, owned by the euid, and on a different inode than audit_log.jsonl. Alternatively, place the lock on the audit-log handle itself, sharing one file description with log_event. Consider a bounded LOCK_NB retry loop with a message. Rated low: an out…

  • CR-A12-022 (code, Medium) — Input discovery follows file symlinks out of the input tree, pulling arbitrary readable files into the training JSONL
    severity Medium · reachability default · intent intended-but-harmful · confidence confirmed · independently confirmed · reproduced end to end
    Failure scenario. An externally supplied corpus tarball contains 'appendix.txt -> (local review artefact)'. After extraction, 'forgelm ingest ./corpus' copies the credentials into the training data. --secrets-mask catches only some formats.
    Where. forgelm/ingestion.py:1999-2006 (_iter_input_files body, glob at 2003, is_file test at 2005), forgelm/ingestion.py:1997, forgelm/ingestion.py:2005, forgelm/wizard/_byod.py:109-114
    Recommended fix. Skip symlinks (entry.is_symlink()), or require entry.resolve() to stay under input_path.resolve(), and log what was skipped. Document the behaviour.

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: complianceCode area: compliancearea: configCode area: configarea: 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