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.
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)
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
- error creating log file directory: {err:?}
- error creating log file: {err:?}
- error opening log file: {err:?}
- Blocked symlink: ${entry.path}
- Directory path contains potentially unsafe characters: ${dir
AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17).
Data as JSON: /api/errors/bc506cbc9b223192.
Report an issue: GitHub.