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
- 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
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
- 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
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
- journal is not a regular file: {}
- path {:?} is outside the memory directory {:?}
- workflow artifact exceeds {limit} bytes: {}
- workflow artifact changed during open: {}
- Marketplace source blocked: {}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/e591712e7c14505b.
Report an issue: GitHub.