@@ -4,14 +4,17 @@ use serde_json::json;
44use std:: collections:: { BTreeSet , HashMap } ;
55use std:: env;
66use std:: ffi:: OsString ;
7- use std:: fs:: { self , OpenOptions } ;
7+ use std:: fs:: { self , File , OpenOptions } ;
88use std:: io:: { BufRead , BufReader , Read , Write } ;
99use std:: path:: { Path , PathBuf } ;
1010use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
1111use std:: thread;
1212use std:: time:: Duration ;
1313use std:: time:: { SystemTime , UNIX_EPOCH } ;
1414
15+ #[ cfg( unix) ]
16+ use std:: os:: unix:: fs:: OpenOptionsExt ;
17+
1518use getrandom:: fill as random_fill_os;
1619use provenact_verifier:: { sha256_prefixed, Capability } ;
1720use 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+
9721003fn 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