{"record":{"id":"e591712e7c14505b","repo":"xai-org/grok-build","slug":"workflow-artifact-is-not-a-regular-file","errorCode":null,"errorMessage":"workflow artifact is not a regular file: {}","messagePattern":"workflow artifact is not a regular file: (.+?)","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-shell/src/session/workflow/store.rs","lineNumber":310,"sourceCode":"            .bytes()\n            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')\n    {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"invalid workflow run id\",\n        ));\n    }\n    Ok(())\n}\n\npub(crate) fn script_revision_path(run_dir: &Path, revision: u32) -> PathBuf {\n    run_dir.join(\"scripts\").join(format!(\"{revision:04}.rhai\"))\n}\n\npub(crate) fn read_bounded_nofollow(path: &Path, limit: u64) -> io::Result<Vec<u8>> {\n    let metadata = std::fs::symlink_metadata(path)?;\n    if metadata.file_type().is_symlink() || !metadata.is_file() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"workflow artifact is not a regular file: {}\",\n                path.display()\n            ),\n        ));\n    }\n    if metadata.len() > limit {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"workflow artifact exceeds {limit} bytes: {}\",\n                path.display()\n            ),\n        ));\n    }\n    let mut options = std::fs::OpenOptions::new();\n    options.read(true);","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-shell/src/session/workflow/store.rs#L292-L328","documentation":"read_bounded_nofollow performs a TOCTOU-safe, size-bounded read of workflow artifacts. Before opening, it checks symlink_metadata: if the path is a symlink or not a regular file it refuses with InvalidData and this message, protecting against symlink attacks and reading directories/fifos as run state.","triggerScenarios":"load_workflow_runs_sync encounters a run dir where runs.json / scripts/NNNN.rhai is a symlink, a directory, a fifo/device, or was swapped for a non-file between writes.","commonSituations":"Tampered or partially-written run directories; an operator replaced an artifact with a symlink; a malicious workflow created a symlink named like the script; leftover directory at the script path.","solutions":["Remove or restore the offending artifact as a regular file","Regenerate the run state by re-running register/persist so files are written via atomic_write","Audit who/what created symlinks in the runs directory (potential tampering)","Ensure script_copy_path destinations are never symlink targets"],"exampleFix":"// before\nlet bytes = std::fs::read(&script_path)?; // works on symlink, hides tampering\n// after\nlet bytes = store.read_bounded_nofollow(&script_path, MAX_ARTIFACT_BYTES)?; // now rejects symlink -> restore real file","handlingStrategy":"validation","validationCode":"fn artifact_is_plain_file(p: &std::path::Path) -> bool {\n    match std::fs::symlink_metadata(p) {\n        Ok(m) => m.is_file() && !m.file_type().is_symlink(),\n        Err(_) => false,\n    }\n}","typeGuard":null,"tryCatchPattern":"match store.read_bounded_nofollow(&path, LIMIT) {\n    Ok(bytes) => parse(&bytes)?,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().starts_with(\"workflow artifact is not a regular file\") => {\n        // skip/remove the tampered run instead of failing the whole load\n        eprintln!(\"skipping non-regular artifact: {e}\");\n        Vec::new()\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Write workflow artifacts only via atomic_write (temp+rename), never in-place","Keep the runs directory writable only by the shell process","Treat any symlink inside the runs dir as tampering and alert/remove","Load runs with read_bounded_nofollow, never plain fs::read"],"tags":["io","security","symlink","invalid-data"],"backgroundTag":"symlink-artifact-rejected","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}