From d31582843a92268b94751387998e34b0180a5441 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Wed, 14 Oct 2026 14:48:14 -0600 Subject: [PATCH] nfs: keep partial FILE_SYNC write tx open until its data is done A NFSv3 WRITE with stable set to FILE_SYNC is assumed to be the last chunk of its file, and its file transaction was closed as soon as the record was seen. If the record was only partially received, the continuation could no longer find the transaction by its file handle. Its file data was then not consumed but parsed as a new RPC record, which failed and disabled the parser for the flow, and the file was truncated. Like the READ reply path already does, only close the transaction right away if the record holds all of its file data. Otherwise filetracker_update closes it once the file tracker is done or, as a truncated tracker is never done, once the data of the last chunk is complete. The latter also applies to a partial READ reply with eof set. The NFSv4 WRITE path makes the same FILE_SYNC assumption, but it only handles complete records, so it is not affected. This is also the root cause of Redmine #8009. Ticket: #9196 --- rust/src/nfs/nfs.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/rust/src/nfs/nfs.rs b/rust/src/nfs/nfs.rs index 6cb5d05bf7..df5899f467 100644 --- a/rust/src/nfs/nfs.rs +++ b/rust/src/nfs/nfs.rs @@ -1158,6 +1158,9 @@ impl NFSState { // for now assume that stable FILE_SYNC flags means a single chunk let is_last = w.stable == 2; + // a partial record's file tx must stay open (found by handle) until + // filetracker_update has received the rest of its data + let is_partial = w.file_data.len() < w.file_len as usize; let file_handle = w.handle.value.to_vec(); let file_name = if let Some(name) = self.namemap.get(w.handle.value) { SCLogDebug!("WRITE name {:?}", name); @@ -1243,7 +1246,9 @@ impl NFSState { tdf.file_last_xid = r.hdr.xid; tx.is_last = true; tx.response_done = true; - tx.is_file_closed = true; + if !is_partial { + tx.is_file_closed = true; + } sc_app_layer_parser_trigger_raw_stream_inspection( flow, Direction::ToClient as i32, @@ -1289,12 +1294,16 @@ impl NFSState { if is_last && !queue_exceeded { tdf.file_last_xid = r.hdr.xid; tx.is_last = true; - tx.request_done = true; - tx.is_file_closed = true; - sc_app_layer_parser_trigger_raw_stream_inspection( - flow, - Direction::ToServer as i32, - ); + /* if this is a partial record we will close the tx + * when we've received the final data */ + if !is_partial { + tx.request_done = true; + tx.is_file_closed = true; + sc_app_layer_parser_trigger_raw_stream_inspection( + flow, + Direction::ToServer as i32, + ); + } } } tx.procedure = NFSPROC3_WRITE; @@ -1706,8 +1715,10 @@ impl NFSState { tdf.chunk_count += 1; let cs = filetracker_update(&mut tdf.file_tracker, data, gap_size); - /* see if we need to close the tx */ - if tdf.file_tracker.is_done() { + /* see if we need to close the tx. A truncated tracker is + * never done, so also close it when the data of its last + * chunk is complete. */ + if tdf.file_tracker.is_done() || (tx.is_last && chunk_left == 0) { if direction == Direction::ToClient { tx.response_done = true; tx.is_file_closed = true;