Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/turborepo-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ description = "User-facing logging interface for Turborepo"
workspace = true

[dependencies]
libc = "0.2"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem"] }

[dev-dependencies]
tempfile = { workspace = true }
96 changes: 79 additions & 17 deletions crates/turborepo-log/src/sinks/structured.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,30 +102,34 @@
std::fs::create_dir_all(parent)?;
}

// Reject symlinks to prevent symlink-following attacks.
if path.exists() {
let meta = path.symlink_metadata()?;
if meta.file_type().is_symlink() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!(
"refusing to write structured log to symlink: {}",
path.display()
),
));
}
}

let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.write(true).create(true);

#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
opts.custom_flags(libc::O_NOFOLLOW);
}

#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;

use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;

opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}

let mut file = opts
.open(path)
.map_err(|error| map_open_error(path, error))?;
if file.metadata()?.file_type().is_symlink() {
return Err(symlink_error(path));
}

let mut file = opts.open(path)?;
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
file.write_all(b"[\n]\n")?;

Ok(Self {
Expand Down Expand Up @@ -163,6 +167,27 @@
}
}

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 {

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 10/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 2/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 3/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 5/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 7/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 1/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 9/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 6/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 8/10)

unused variable: `path`

Check warning on line 180 in crates/turborepo-log/src/sinks/structured.rs

View workflow job for this annotation

GitHub Actions / Rust testing on windows (partition 4/10)

unused variable: `path`
#[cfg(unix)]
{
if error.raw_os_error() == Some(libc::ELOOP) {
return symlink_error(path);
}
}

error
}

// Messages sent from emit/task_output threads to the writer thread.
enum WriterMsg {
Entry(String),
Expand Down Expand Up @@ -558,11 +583,24 @@
assert!(path.exists());
}

#[test]
fn json_array_file_truncates_existing_regular_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("existing.json");
std::fs::write(&path, r#"[{"stale":true}]"#).unwrap();

let _file = JsonArrayFile::create(&path).unwrap();

let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "[\n]\n");
}

#[cfg(any(unix, windows))]
#[test]
fn json_array_file_rejects_symlinks() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real.json");
std::fs::write(&target, "").unwrap();
std::fs::write(&target, "do not overwrite").unwrap();
let link = dir.path().join("link.json");
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &link).unwrap();
Expand All @@ -573,6 +611,30 @@
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("symlink"));
assert_eq!(
std::fs::read_to_string(&target).unwrap(),
"do not overwrite"
);
}

#[cfg(any(unix, windows))]
#[test]
fn json_array_file_rejects_dangling_symlinks() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("missing.json");
let link = dir.path().join("link.json");
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &link).unwrap();
#[cfg(windows)]
std::os::windows::fs::symlink_file(&target, &link).unwrap();

let result = JsonArrayFile::create(&link);

assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("symlink"));
assert!(!target.exists());
assert!(link.symlink_metadata().unwrap().file_type().is_symlink());
}

#[cfg(unix)]
Expand Down
Loading