vercel/turborepo · error · std::io::Error
refusing to write structured log to symlink
Error message
refusing to write structured log to symlink: {} What it means
The structured-log sink (JsonArrayFile::create, structured.rs:100-129) opens its JSON log file with O_NOFOLLOW on Unix and FILE_FLAG_OPEN_REPARSE_POINT on Windows, then rejects the open when metadata says the target is a symlink (map_open_error also maps ELOOP to this). Writing through a symlink would let an attacker redirect or swap the log, so the sink refuses with AlreadyExists 'refusing to write structured log to symlink' instead of following it.
Solutions
- Point the structured log output at a real directory (remove the symlink, `mkdir -p` a physical dir)
- Mount tmpfs directly at the log dir instead of symlinking to it
- Fix or remove looping symlinks on the log path (the ELOOP case)
Example fix
# before ln -s /dev/shm/logs ~/.turbo/logs # -> refusal at startup # after sudo mount -t tmpfs -o size=64m,mode=700 tmpfs ~/.turbo/logs
Defensive patterns
Strategy: validation
Validate before calling
// before configuring structured logging
let m = std::fs::symlink_metadata(log_path)?;
if m.file_type().is_symlink() {
anyhow::bail!("{log_path} is a symlink; refusing to configure structured log");
} Type guard
fn is_safe_log_target(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink()).unwrap_or(true)
} Try / catch
// AlreadyExists with this message is fatal by design — fix the path, do not retry
if let Err(e) = sink_create(path) {
if e.kind() == std::io::ErrorKind::AlreadyExists && e.to_string().contains("symlink") {
path = next_physical_log_dir();
}
} Prevention
- Mount tmpfs at the log dir instead of symlinking to it
- Audit dotfile-manager-managed dirs used for logs
- Avoid symlink chains anywhere on the log path (ELOOP maps here too)
When it happens
Trigger: Structured logging enabled and the configured log path (or its final component) is a symlink — users symlinking the log dir into tmpfs/shared storage, dotfile managers, or CI images that symlink log locations. On Unix, symlink loops surface as ELOOP mapped to the same message.
Common situations: `ln -s /dev/shm/turbo-logs ~/.turbo/logs` style setups for speed Shared/network log directories via symlink in containers Symlink chains that loop (ELOOP)
Related errors
- Blocked symlink
- error creating log file directory
- error creating log file
- error opening log file
- [33m"turbo-ignore" is deprecated. Use "turbo query…
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/bc506cbc9b223192.
Report an issue: GitHub.
Appendix: source
Thrown at crates/turborepo-log/src/sinks/structured.rs:171
if self.has_entries {
buf.extend_from_slice(b",\n");
}
buf.extend_from_slice(json.as_bytes());
self.has_entries = true;
}
buf.extend_from_slice(b"\n]\n");
// The file always ends with `]\n` (2 bytes). Seek there and
// overwrite with the new entries + fresh closing bracket.
self.file.seek(SeekFrom::End(-2))?;
self.file.write_all(&buf)?;
Ok(())
}
}
fn symlink_error(path: &Path) -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!(
"refusing to write structured log to symlink: {}",
path.display()
),
)
}
fn map_open_error(path: &Path, error: std::io::Error) -> std::io::Error {
#[cfg(unix)]
{
if error.raw_os_error() == Some(libc::ELOOP) {
return symlink_error(path);
}
}
error
}View on GitHub (pinned to f9245100cf)