Skip to content

Commit 5da9702

Browse files
authored
harden runtime kv/queue file handling against symlink targets (#41)
1 parent 9f9c33f commit 5da9702

1 file changed

Lines changed: 58 additions & 4 deletions

File tree

cli/provenact-cli/src/runtime_exec.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ use serde_json::json;
44
use std::collections::{BTreeSet, HashMap};
55
use std::env;
66
use std::ffi::OsString;
7-
use std::fs::{self, OpenOptions};
7+
use std::fs::{self, File, OpenOptions};
88
use std::io::{BufRead, BufReader, Read, Write};
99
use std::path::{Path, PathBuf};
1010
use std::sync::atomic::{AtomicU64, Ordering};
1111
use std::thread;
1212
use std::time::Duration;
1313
use std::time::{SystemTime, UNIX_EPOCH};
1414

15+
#[cfg(unix)]
16+
use std::os::unix::fs::OpenOptionsExt;
17+
1518
use getrandom::fill as random_fill_os;
1619
use provenact_verifier::{sha256_prefixed, Capability};
1720
use url::Url;
@@ -511,7 +514,10 @@ fn define_hostcalls(linker: &mut Linker<HostState>) -> Result<(), wasmtime::Erro
511514
Some(guard) => guard,
512515
None => return Ok(-1),
513516
};
514-
if fs::write(path, value).is_err() {
517+
if is_symlink_path(&path) {
518+
return Ok(-1);
519+
}
520+
if write_file_replace_symlink_safe(&path, &value).is_err() {
515521
return Ok(-1);
516522
}
517523
Ok(0)
@@ -546,6 +552,9 @@ fn define_hostcalls(linker: &mut Linker<HostState>) -> Result<(), wasmtime::Erro
546552
Some(guard) => guard,
547553
None => return Ok(-1),
548554
};
555+
if is_symlink_path(&path) {
556+
return Ok(-1);
557+
}
549558
let data = match fs::read(path) {
550559
Ok(v) => v,
551560
Err(_) => return Ok(-1),
@@ -599,11 +608,14 @@ fn define_hostcalls(linker: &mut Linker<HostState>) -> Result<(), wasmtime::Erro
599608
Some(guard) => guard,
600609
None => return Ok(-1),
601610
};
611+
if is_symlink_path(&path) {
612+
return Ok(-1);
613+
}
602614
let current_size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
603615
if current_size > MAX_QUEUE_FILE_BYTES {
604616
return Ok(-1);
605617
}
606-
let mut file = match OpenOptions::new().create(true).append(true).open(path) {
618+
let mut file = match open_append_no_follow(&path) {
607619
Ok(f) => f,
608620
Err(_) => return Ok(-1),
609621
};
@@ -648,6 +660,9 @@ fn define_hostcalls(linker: &mut Linker<HostState>) -> Result<(), wasmtime::Erro
648660
Some(guard) => guard,
649661
None => return Ok(-1),
650662
};
663+
if is_symlink_path(&path) {
664+
return Ok(-1);
665+
}
651666
if fs::metadata(&path).map(|m| m.len()).unwrap_or(0) > MAX_QUEUE_FILE_BYTES {
652667
return Ok(-1);
653668
}
@@ -676,7 +691,7 @@ fn define_hostcalls(linker: &mut Linker<HostState>) -> Result<(), wasmtime::Erro
676691
} else {
677692
format!("{}\n", lines.join("\n"))
678693
};
679-
if fs::write(&path, rewritten.as_bytes()).is_err() {
694+
if write_file_replace_symlink_safe(&path, rewritten.as_bytes()).is_err() {
680695
return Ok(-1);
681696
}
682697
Ok(write_to_memory(&mut caller, out_ptr as usize, &payload).unwrap_or(-1))
@@ -969,6 +984,22 @@ fn write_file_replace_symlink_safe(path: &Path, bytes: &[u8]) -> std::io::Result
969984
))
970985
}
971986

987+
fn is_symlink_path(path: &Path) -> bool {
988+
fs::symlink_metadata(path)
989+
.map(|meta| meta.file_type().is_symlink())
990+
.unwrap_or(false)
991+
}
992+
993+
fn open_append_no_follow(path: &Path) -> std::io::Result<File> {
994+
let mut options = OpenOptions::new();
995+
options.create(true).append(true);
996+
#[cfg(unix)]
997+
{
998+
options.custom_flags(libc::O_NOFOLLOW);
999+
}
1000+
options.open(path)
1001+
}
1002+
9721003
fn default_runtime_root(kind: &str) -> PathBuf {
9731004
if let Some(home) = env::var_os("PROVENACT_HOME") {
9741005
return PathBuf::from(home).join("runtime").join(kind);
@@ -1139,4 +1170,27 @@ mod tests {
11391170

11401171
let _ = fs::remove_dir_all(&base);
11411172
}
1173+
1174+
#[cfg(unix)]
1175+
#[test]
1176+
fn open_append_no_follow_rejects_symlink_targets() {
1177+
let base = std::env::temp_dir().join(format!(
1178+
"provenact-open-append-test-{}-{}",
1179+
std::process::id(),
1180+
SystemTime::now()
1181+
.duration_since(UNIX_EPOCH)
1182+
.map(|d| d.as_nanos())
1183+
.unwrap_or(0)
1184+
));
1185+
fs::create_dir_all(&base).expect("temp dir should exist");
1186+
1187+
let target = base.join("target.log");
1188+
fs::write(&target, b"seed").expect("target write");
1189+
let sink = base.join("sink.log");
1190+
std::os::unix::fs::symlink(&target, &sink).expect("symlink create");
1191+
assert!(is_symlink_path(&sink));
1192+
assert!(open_append_no_follow(&sink).is_err());
1193+
1194+
let _ = fs::remove_dir_all(&base);
1195+
}
11421196
}

0 commit comments

Comments
 (0)