xai-org/grok-build · error · std::io::Error

workflow artifact is not a regular file: {}

Error message

workflow artifact is not a regular file: {}

What it means

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.

Source

Thrown at crates/codegen/xai-grok-shell/src/session/workflow/store.rs:310

            .bytes()
            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
    {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "invalid workflow run id",
        ));
    }
    Ok(())
}

pub(crate) fn script_revision_path(run_dir: &Path, revision: u32) -> PathBuf {
    run_dir.join("scripts").join(format!("{revision:04}.rhai"))
}

pub(crate) fn read_bounded_nofollow(path: &Path, limit: u64) -> io::Result<Vec<u8>> {
    let metadata = std::fs::symlink_metadata(path)?;
    if metadata.file_type().is_symlink() || !metadata.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "workflow artifact is not a regular file: {}",
                path.display()
            ),
        ));
    }
    if metadata.len() > limit {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "workflow artifact exceeds {limit} bytes: {}",
                path.display()
            ),
        ));
    }
    let mut options = std::fs::OpenOptions::new();
    options.read(true);

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Remove or restore the offending artifact as a regular file
  2. Regenerate the run state by re-running register/persist so files are written via atomic_write
  3. Audit who/what created symlinks in the runs directory (potential tampering)
  4. Ensure script_copy_path destinations are never symlink targets

Example fix

// before
let bytes = std::fs::read(&script_path)?; // works on symlink, hides tampering
// after
let bytes = store.read_bounded_nofollow(&script_path, MAX_ARTIFACT_BYTES)?; // now rejects symlink -> restore real file
Defensive patterns

Strategy: validation

Validate before calling

fn artifact_is_plain_file(p: &std::path::Path) -> bool {
    match std::fs::symlink_metadata(p) {
        Ok(m) => m.is_file() && !m.file_type().is_symlink(),
        Err(_) => false,
    }
}

Try / catch

match store.read_bounded_nofollow(&path, LIMIT) {
    Ok(bytes) => parse(&bytes)?,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData
        && e.to_string().starts_with("workflow artifact is not a regular file") => {
        // skip/remove the tampered run instead of failing the whole load
        eprintln!("skipping non-regular artifact: {e}");
        Vec::new()
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/e591712e7c14505b. Report an issue: GitHub.