diff --git a/rust/src/nfs/nfs.rs b/rust/src/nfs/nfs.rs index a5d57d052b..3e742e6138 100644 --- a/rust/src/nfs/nfs.rs +++ b/rust/src/nfs/nfs.rs @@ -1504,8 +1504,19 @@ impl NFSState { "REQUEST {} partial v4 COMPOUND rejected: structural scan error", r.hdr.xid ); - // A definitive structural error from the buffered bytes: log a malformed - // tx and skip the record (bounded fallback, no buffering toward the claim). + // Mirror the full record path: register the xid so a matching reply + // is processed like any other v4 request. + let xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0); + self.requestmap.put(r.hdr.xid, xidmap); + self.reject_v4_malformed_request_record(r); + } + + /// Log a malformed tx for a v4 COMPOUND request record that never reached + /// the compound parser and skip to its boundary. An event that does not + /// land on a tx does not exist: set_event marks the newest tx, which is + /// another RPC's when the flow has one, and none on a flow that has not. + /// Callers own the requestmap entry (a GSS record must keep its creds). + pub(crate) fn reject_v4_malformed_request_record(&mut self, r: &RpcPacket<'_>) { let mut tx = self.new_tx(); tx.xid = r.hdr.xid; tx.procedure = r.procedure; @@ -1517,10 +1528,6 @@ impl NFSState { tx.tx_data.0.updated_ts = true; tx.tx_data.0.updated_tc = true; self.transactions.push(tx); - // Mirror the full record path: register the xid so a matching reply - // is processed like any other v4 request. - let xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0); - self.requestmap.put(r.hdr.xid, xidmap); // Skip to the RPC record boundary without buffering the claimed rest // of the record. let pending = record_tail(r.prog_data_size, r.prog_data.len()); @@ -1604,6 +1611,50 @@ impl NFSState { ); } + /// The RPCSEC_GSS unwrap of a v4 COMPOUND reply record failed. The event + /// belongs on the tx that owns the xid; with none fall back to a rejected + /// tx of its own - a compound that opens no request tx (PUTFH+GETATTR, a + /// SEQUENCE-led v4.1 compound) takes that branch on every bad reply, so it + /// must carry the procedure the xidmap recorded. + pub(crate) fn reject_v4_gss_reply_unwrap(&mut self, r: &RpcReplyPacket<'_>, procedure: u32) { + // same tx the reply belongs to (cf. get_tx_by_xid); position first so + // the fallback push is not fighting a borrow + let known = self + .transactions + .iter() + .position(|tx| !tx.is_file_tx && tx.xid == r.hdr.xid); + if let Some(i) = known { + let tx = &mut self.transactions[i]; + tx.tx_data.set_event(NFSEvent::MalformedData as u8); + tx.response_done = true; + tx.tx_data.0.updated_ts = true; + tx.tx_data.0.updated_tc = true; + } else { + let mut tx = self.new_tx(); + tx.xid = r.hdr.xid; + tx.procedure = procedure; + tx.nfs_version = self.nfs_version; + tx.request_done = true; + tx.response_done = true; + tx.is_file_closed = true; + tx.tx_data.set_event(NFSEvent::MalformedData as u8); + tx.tx_data.0.updated_ts = true; + tx.tx_data.0.updated_tc = true; + self.transactions.push(tx); + } + let pending = record_tail(r.prog_data_size, r.prog_data.len()); + self.set_skip(Direction::ToClient, pending); + self.tc_chunk_xid = 0; + self.tc_chunk_left = 0; + self.tc_chunk_tail = 0; + self.tc_chunk_fh.clear(); + SCLogDebug!( + "v4 COMPOUND GSS unwrap failed (record claim {}): skipping {} stream bytes", + r.prog_data_size, + pending + ); + } + fn process_reply_record( &mut self, flow: *mut Flow, stream_slice: &StreamSlice, r: &RpcReplyPacket, ) -> u32 { @@ -4040,6 +4091,142 @@ mod tests { NFS_CFG_MAX_WRITE_SIZE = NFS_MAX_RW_SIZE_DEFAULT; } } + + /// A CALL record with RPCSEC_GSS creds (procedure 0 = RPCSEC_GSS_DATA, the + /// given service) and `envelope` as prog_data. + fn gss_v4_record(xid: u32, service: u32, envelope: &[u8]) -> Vec { + let mut creds: Vec = Vec::new(); + creds.extend_from_slice(&be32(1)); // rpc_gss_version + creds.extend_from_slice(&be32(0)); // procedure: RPCSEC_GSS_DATA + creds.extend_from_slice(&be32(1)); // sequence number + creds.extend_from_slice(&be32(service)); + creds.extend_from_slice(&be32(16)); // context handle length + creds.extend_from_slice(&[0u8; 16]); + let mut body: Vec = Vec::new(); + body.extend_from_slice(&be32(2)); // rpcver + body.extend_from_slice(&be32(100003)); // program: NFS + body.extend_from_slice(&be32(4)); // progver + body.extend_from_slice(&be32(NFSPROC4_COMPOUND)); // procedure + body.extend_from_slice(&be32(6)); // creds flavor: RPCSEC_GSS + body.extend_from_slice(&be32(creds.len() as u32)); + body.extend_from_slice(&creds); + body.extend_from_slice(&be32(0)); // verifier flavor: NULL + body.extend_from_slice(&be32(0)); // verifier length + let frag_len = (4 + 4 + body.len() + envelope.len()) as u32; + let mut buf: Vec = Vec::new(); + buf.extend_from_slice(&(frag_len | 0x8000_0000).to_be_bytes()); + buf.extend_from_slice(&be32(xid)); + buf.extend_from_slice(&be32(1)); // msgtype: CALL + buf.extend_from_slice(&body); + buf.extend_from_slice(envelope); + buf + } + + /// Integrity envelope per RFC 2203 5.3.2.2: length of the wrapped body + /// (sequence number + arguments), sequence number, arguments, checksum. + fn gss_envelope(len: u32, data: &[u8]) -> Vec { + let mut v = Vec::new(); + v.extend_from_slice(&be32(len)); + v.extend_from_slice(&be32(1)); + v.extend_from_slice(data); + v + } + + fn gss_v4_reply_record(xid: u32, envelope: &[u8]) -> Vec { + let mut body: Vec = Vec::new(); + body.extend_from_slice(&be32(xid)); + body.extend_from_slice(&be32(1)); // msgtype: REPLY + body.extend_from_slice(&be32(0)); // reply_state: ACCEPTED + body.extend_from_slice(&be32(0)); // verifier flavor: NULL + body.extend_from_slice(&be32(0)); // verifier length + body.extend_from_slice(&be32(0)); // accept_state + let frag_len = (body.len() + envelope.len()) as u32; + let mut buf: Vec = Vec::new(); + buf.extend_from_slice(&(frag_len | 0x8000_0000).to_be_bytes()); + buf.extend_from_slice(&body); + buf.extend_from_slice(envelope); + buf + } + + #[test] + fn test_v4_gss_envelope_over_bound_rejected() { + // The envelope length fails the credentials bound: a definitive + // malformed record. The arm used to only set_event, which labels the + // newest tx and does nothing at all when the state has none, so the + // record left no trace and the compound was never parsed. + let buf = gss_v4_record( + 0x5101, + 2, + &gss_envelope(RPC_MAX_CREDS_SIZE, &compound_head(0)), + ); + let mut state = NFSState::new(); + let (_, pkt) = parse_rpc(&buf, true).expect("record parses"); + state.process_request_record_v4(&pkt); + + reject_tx(&state, false, NFSPROC4_COMPOUND); + assert_eq!(state.transactions[0].xid, 0x5101); + assert_eq!(state.skip_ts, 0); + // the xid stays registered, with its gss combination, so the reply of + // this call is not taken for a plain compound + assert!(state.requestmap.peek(&0x5101).is_some()); + } + + #[test] + fn test_v4_gss_envelope_past_record_rejected() { + // In range but beyond the record: the unwrap is incomplete and the + // whole record is in hand, so nothing more can arrive for it + let buf = gss_v4_record(0x5102, 2, &gss_envelope(3000, &[])); + let mut state = NFSState::new(); + let (_, pkt) = parse_rpc(&buf, true).expect("record parses"); + state.process_request_record_v4(&pkt); + + reject_tx(&state, false, NFSPROC4_COMPOUND); + assert_eq!(state.transactions[0].xid, 0x5102); + assert_eq!(state.skip_ts, 0); + } + + #[test] + fn test_v4_gss_reply_unwrap_rejects_its_own_tx() { + // An async mount has several txs open at once; set_event marks the last, + // which is a different call than the one being rejected. The shape that + // names the malformed reject (flags set, list not grown) must land on + // the tx owning the xid. + let mut state = NFSState::new(); + for xid in [0x6100u32, 0x6101] { + let mut tx = state.new_tx(); + tx.xid = xid; + state.transactions.push(tx); + } + let buf = gss_v4_reply_record(0x6100, &gss_envelope(RPC_MAX_CREDS_SIZE, &[])); + let (_, pkt) = parse_rpc_reply(&buf, true).expect("reply parses"); + state.reject_v4_gss_reply_unwrap(&pkt, NFSPROC4_COMPOUND); + + assert_eq!(state.transactions.len(), 2, "no extra tx for a known xid"); + assert!( + state.transactions[0].response_done, + "event on the owning tx" + ); + assert!( + !state.transactions[1].response_done, + "other call left alone" + ); + assert_eq!(state.skip_tc, 0); + } + + #[test] + fn test_v4_gss_reply_unwrap_without_tx() { + // a compound that opens no request tx (PUTFH+GETATTR, SEQUENCE-led + // v4.1) takes this branch on every bad reply: the reject tx must carry + // the procedure from the xidmap, not the 0 that reads as NULL + let mut state = NFSState::new(); + let buf = gss_v4_reply_record(0x6200, &gss_envelope(8, &[])); + let (_, pkt) = parse_rpc_reply(&buf, true).expect("reply parses"); + state.reject_v4_gss_reply_unwrap(&pkt, NFSPROC4_COMPOUND); + + reject_tx(&state, false, NFSPROC4_COMPOUND); + assert_eq!(state.transactions[0].xid, 0x6200); + assert_eq!(state.skip_tc, 0); + } } #[test] diff --git a/rust/src/nfs/nfs4.rs b/rust/src/nfs/nfs4.rs index aace76dca7..12b3c1b0a6 100644 --- a/rust/src/nfs/nfs4.rs +++ b/rust/src/nfs/nfs4.rs @@ -362,17 +362,14 @@ impl NFSState { SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec); data = rec.data; } - Err(Err::Incomplete(_n)) => { - SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n); - self.set_event(NFSEvent::MalformedData); - return; - } - Err(Err::Error(_e)) | Err(Err::Failure(_e)) => { + Err(Err::Incomplete(_)) | Err(Err::Error(_)) | Err(Err::Failure(_)) => { SCLogDebug!( - "NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}", - _e + "RPCSEC_GSS: integrity envelope could not be unwrapped (v4 compound call)" ); - self.set_event(NFSEvent::MalformedData); + // Keep the gss creds in the map: the reply of this + // xid is wrapped the same way and must be unwrapped. + self.requestmap.put(r.hdr.xid, xidmap); + self.reject_v4_malformed_request_record(r); return; } } @@ -505,14 +502,11 @@ impl NFSState { SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec); data = rec.data; } - Err(Err::Incomplete(_n)) => { - SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n); - self.set_event(NFSEvent::MalformedData); - return; - } - Err(Err::Error(_e)) | Err(Err::Failure(_e)) => { - SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}", _e); - self.set_event(NFSEvent::MalformedData); + Err(Err::Incomplete(_)) | Err(Err::Error(_)) | Err(Err::Failure(_)) => { + SCLogDebug!( + "RPCSEC_GSS: integrity envelope could not be unwrapped (v4 compound reply)" + ); + self.reject_v4_gss_reply_unwrap(r, xidmap.procedure); return; } }