@@ -2357,18 +2357,6 @@ impl VirtualFs {
23572357 return Err ( err. to_errno ( ) ) ;
23582358 }
23592359
2360- // A committed channel already sent Finish to the worker
2361- // (flush(), or link() on a dirty source): a racing write
2362- // could enqueue after Finish and be silently dropped. Fail
2363- // loudly instead.
2364- {
2365- let state = channel. state . lock ( ) . expect ( "state poisoned" ) ;
2366- if matches ! ( & * state, CommitState :: Committed | CommitState :: Failed ( _) ) {
2367- debug ! ( "streaming write after commit rejected for ino={}" , handle_ino) ;
2368- return Err ( libc:: EIO ) ;
2369- }
2370- }
2371-
23722360 // Enforce append-only: offset must match bytes written so far
23732361 let expected = channel. bytes_written . load ( Ordering :: Relaxed ) ;
23742362 if offset != expected {
@@ -2380,10 +2368,26 @@ impl VirtualFs {
23802368 }
23812369
23822370 let len = data. len ( ) ;
2383- channel. tx . blocking_send ( WriteMsg :: Data ( data. to_vec ( ) ) ) . map_err ( |_| {
2384- error ! ( "streaming channel closed for ino={}" , handle_ino) ;
2385- libc:: EIO
2386- } ) ?;
2371+ // Enqueue under the state mutex to serialize against
2372+ // streaming_commit(), which flips to Committing under the
2373+ // same mutex before enqueueing Finish: either this Data lands
2374+ // ahead of Finish (and is included in the commit), or the
2375+ // write observes Committing/Committed and fails loudly
2376+ // instead of being silently dropped behind Finish.
2377+ {
2378+ let state = channel. state . lock ( ) . expect ( "state poisoned" ) ;
2379+ match & * state {
2380+ CommitState :: Writing | CommitState :: Deferred => { }
2381+ _ => {
2382+ debug ! ( "streaming write after commit rejected for ino={}" , handle_ino) ;
2383+ return Err ( libc:: EIO ) ;
2384+ }
2385+ }
2386+ channel. tx . blocking_send ( WriteMsg :: Data ( data. to_vec ( ) ) ) . map_err ( |_| {
2387+ error ! ( "streaming channel closed for ino={}" , handle_ino) ;
2388+ libc:: EIO
2389+ } ) ?;
2390+ }
23872391 channel. bytes_written . fetch_add ( len as u64 , Ordering :: Relaxed ) ;
23882392
23892393 let new_size = offset + len as u64 ;
@@ -2424,7 +2428,7 @@ impl VirtualFs {
24242428 debug ! ( "flush: already failed for ino={}: {}" , ino, msg) ;
24252429 return Err ( libc:: EIO ) ;
24262430 }
2427- CommitState :: Writing | CommitState :: Deferred => { }
2431+ CommitState :: Writing | CommitState :: Deferred | CommitState :: Committing => { }
24282432 }
24292433 }
24302434
@@ -2520,7 +2524,12 @@ impl VirtualFs {
25202524 Some ( OpenFile :: Streaming { ino, channel } ) => {
25212525 let needs_commit = {
25222526 let state = channel. state . lock ( ) . expect ( "state poisoned" ) ;
2523- matches ! ( & * state, CommitState :: Writing | CommitState :: Deferred )
2527+ // Committing: an earlier flush()/link() commit failed
2528+ // mid-flight (pending_info parked) — release retries it.
2529+ matches ! (
2530+ & * state,
2531+ CommitState :: Writing | CommitState :: Deferred | CommitState :: Committing
2532+ )
25242533 } ;
25252534 if needs_commit {
25262535 // If flush() didn't defer (e.g. Writing state from a direct
@@ -2656,6 +2665,17 @@ impl VirtualFs {
26562665 return Ok ( ( ) ) ;
26572666 }
26582667
2668+ // Flip to Committing under the state mutex BEFORE enqueueing Finish:
2669+ // write() enqueues Data under the same mutex, so a racing write either
2670+ // lands ahead of Finish or observes Committing and is rejected instead
2671+ // of being silently dropped behind Finish.
2672+ {
2673+ let mut state = channel. state . lock ( ) . expect ( "state poisoned" ) ;
2674+ if matches ! ( & * state, CommitState :: Writing | CommitState :: Deferred ) {
2675+ * state = CommitState :: Committing ;
2676+ }
2677+ }
2678+
26592679 let file_info = {
26602680 let pending = channel. pending_info . lock ( ) . expect ( "pending_info poisoned" ) . take ( ) ;
26612681 if let Some ( info) = pending {
@@ -2990,7 +3010,7 @@ impl VirtualFs {
29903010 debug ! ( "streaming commit already failed for ino={}: {}" , ino, msg) ;
29913011 return Err ( libc:: EIO ) ;
29923012 }
2993- CommitState :: Writing | CommitState :: Deferred => { }
3013+ CommitState :: Writing | CommitState :: Deferred | CommitState :: Committing => { }
29943014 }
29953015 }
29963016 // Install hook before commit so concurrent open() can wait on us.
@@ -3109,6 +3129,11 @@ impl VirtualFs {
31093129 }
31103130 // A concurrent create may have won the race since phase 1; the remote
31113131 // add already landed, but locally the newer entry owns the name.
3132+ // Accepted TOCTOU: in this narrow window a failed link leaves the
3133+ // alias (and, for a dirty source, the source commit) on the Hub —
3134+ // same trade-off as pre-existing clean-source links. Deterministic
3135+ // validation failures (EEXIST/ENOTDIR/ENOENT in phase 1b) commit
3136+ // nothing.
31123137 if inodes. lookup_child ( newparent, newname) . is_some ( ) {
31133138 return Err ( libc:: EEXIST ) ;
31143139 }
@@ -4067,6 +4092,10 @@ enum CommitState {
40674092 Writing ,
40684093 /// flush() deferred commit (dup'd fd or zero writes). release() will handle it.
40694094 Deferred ,
4095+ /// A commit is in flight: Finish is (about to be) enqueued. Writes are
4096+ /// rejected from this point — they would land behind Finish and be
4097+ /// silently dropped by the exiting worker.
4098+ Committing ,
40704099 /// Commit completed successfully.
40714100 Committed ,
40724101 /// Unrecoverable error — inode has been reverted.
0 commit comments