From 67b5fb7800f46f87cb2aed0073828f2fb78f669e Mon Sep 17 00:00:00 2001 From: Victor Julien Date: Tue, 20 Oct 2026 16:51:38 +0000 Subject: [PATCH] nfs: scan a GSS record by the combination of its call The record scanners unwrapped the (0, 2) combination and took every other one as a bare compound. A control procedure or a privacy compound was then scanned as if it were a COMPOUND: the envelope length was read as the first tag, so the scan either stayed incomplete and buffered toward the claim, or rejected a record that has nothing to reject. Scan by the combination the call recorded: hand the credential pair to gss_compound_scan_data, which leaves (0, 1) raw because rpc_gss_svc_none wraps nothing, unwraps (0, 2), and reports a privacy (0, 3) compound as opaque. There is nothing to scan in an encrypted compound, so the record is skipped to its boundary and neither parsed nor scored: the request path keeps the call in the request map for its reply, the reply path drops it. Any other combination is one the full record path rejects as well, so it is malformed, boundedly and without buffering. The v4 request path records the combination whatever it is, not only when it unwraps. Ticket: #9263. --- rust/src/nfs/nfs.rs | 76 ++++++++++++++++++++++--------------- rust/src/nfs/nfs4.rs | 7 ++-- rust/src/nfs/rpc_records.rs | 65 +++++++++++++++++++++---------- 3 files changed, 96 insertions(+), 52 deletions(-) diff --git a/rust/src/nfs/nfs.rs b/rust/src/nfs/nfs.rs index c859eee0da..03a91be3b3 100644 --- a/rust/src/nfs/nfs.rs +++ b/rust/src/nfs/nfs.rs @@ -2433,22 +2433,34 @@ impl NFSState { match parse_rpc(cur_i, false) { Ok((_rem, ref hdr)) => { // RPCSEC_GSS integrity compounds arrive wrapped (length, seqnum, data): - // unwrap like the full record path, or the envelope length is read as the - // compound tag and the scan stays incomplete (buffering toward the claim). - let (gss_proc, gss_service) = match &hdr.creds { - RpcRequestCreds::GssApi(ref g) => (g.procedure, g.service), - _ => (0, 0), + // unwrap like the full record path before scanning - the raw + // envelope's length would be read as the compound tag, buffering toward the claim + let gss = match &hdr.creds { + RpcRequestCreds::GssApi(ref g) => Some((g.procedure, g.service)), + _ => None, }; - let scanres = match gss_compound_scan_data( - gss_proc, - gss_service, - hdr.prog_data, - ) { + let scanres = match gss_compound_scan_data(gss, hdr.prog_data) { GssCompoundScan::Compound(data) => { scan_nfs4_request_compound_write_len(data, max_write) } GssCompoundScan::EnvelopeIncomplete => Nfs4CompoundScan::Incomplete, GssCompoundScan::EnvelopeMalformed => Nfs4CompoundScan::Malformed, + // privacy: encrypted; keep the call for its + // reply and skip the record, no tx + GssCompoundScan::Opaque => { + let mut xidmap = + NFSRequestXidMap::new(hdr.progver, hdr.procedure, 0); + if let Some((procedure, service)) = gss { + xidmap.gssapi_proc = procedure; + xidmap.gssapi_service = service; + } + self.requestmap.put(hdr.hdr.xid, xidmap); + return self.skip_partial_record( + Direction::ToServer, + cur_i, + rec_size, + ); + } }; match scanres { Nfs4CompoundScan::Oversized(_write_len) => { @@ -2737,29 +2749,33 @@ impl NFSState { let max_read = unsafe { NFS_CFG_MAX_READ_SIZE }; // a zero limit disables the check if max_read != 0 { - // the request recorded its RPCSEC_GSS credential combination in the - // xidmap; integrity-wrapped compounds (proc 0, service 2) are unwrapped first. - let gss_integrity = self - .requestmap - .get(&phdr.xid) - .map(|xm| xm.gssapi_proc == 0 && xm.gssapi_service == 2) - .unwrap_or(false); + // (0, 0) is the non-GSS default; GSS requests record + // their combination (only (0, 2) is unwrapped) + let gss = self.requestmap.get(&phdr.xid).and_then(|xm| { + if xm.gssapi_proc == 0 && xm.gssapi_service == 0 { + None + } else { + Some((xm.gssapi_proc, xm.gssapi_service)) + } + }); match parse_rpc_reply(cur_i, false) { Ok((_rem, ref hdr)) => { - let scanres = if gss_integrity { - match gss_compound_scan_data(0, 2, hdr.prog_data) { - GssCompoundScan::Compound(data) => { - scan_nfs4_response_compound_read_len(data, max_read) - } - GssCompoundScan::EnvelopeIncomplete => { - Nfs4CompoundScan::Incomplete - } - GssCompoundScan::EnvelopeMalformed => { - Nfs4CompoundScan::Malformed - } + let scanres = match gss_compound_scan_data(gss, hdr.prog_data) { + GssCompoundScan::Compound(data) => { + scan_nfs4_response_compound_read_len(data, max_read) + } + GssCompoundScan::EnvelopeIncomplete => Nfs4CompoundScan::Incomplete, + GssCompoundScan::EnvelopeMalformed => Nfs4CompoundScan::Malformed, + // privacy: encrypted reply; the call is done, + // pop its xid and skip the record + GssCompoundScan::Opaque => { + self.requestmap.pop(&hdr.hdr.xid); + return self.skip_partial_record( + Direction::ToClient, + cur_i, + rec_size, + ); } - } else { - scan_nfs4_response_compound_read_len(hdr.prog_data, max_read) }; match scanres { Nfs4CompoundScan::Oversized(read_len) => { diff --git a/rust/src/nfs/nfs4.rs b/rust/src/nfs/nfs4.rs index 824dac55d1..ae3245ef69 100644 --- a/rust/src/nfs/nfs4.rs +++ b/rust/src/nfs/nfs4.rs @@ -344,15 +344,16 @@ impl NFSState { let mut data = r.prog_data; if let RpcRequestCreds::GssApi(ref creds) = r.creds { + // store proc and serv for the reply; only (0, 2) is + // unwrapped below + xidmap.gssapi_proc = creds.procedure; + xidmap.gssapi_service = creds.service; if creds.procedure == 0 && creds.service == 2 { SCLogDebug!("GSS INTEGRITY: {:?}", creds); match parse_rpc_gssapi_integrity(r.prog_data) { Ok((_rem, rec)) => { SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec); data = rec.data; - // store proc and serv for the reply - xidmap.gssapi_proc = creds.procedure; - xidmap.gssapi_service = creds.service; } Err(Err::Incomplete(_n)) => { SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n); diff --git a/rust/src/nfs/rpc_records.rs b/rust/src/nfs/rpc_records.rs index 31acb9bf2b..fa1630a0c4 100644 --- a/rust/src/nfs/rpc_records.rs +++ b/rust/src/nfs/rpc_records.rs @@ -129,22 +129,28 @@ pub enum GssCompoundScan<'a> { /// The claimed envelope length fails the bounds check: definitive malformed /// (the full record path rejects it the same way). EnvelopeMalformed, + /// RPCSEC_GSS privacy (0, 3): the compound is encrypted, so there is + /// nothing to scan either way; it is opaque, not malformed. + Opaque, } -/// `gss_proc`/`gss_service` carry the request's RPCSEC_GSS credential combination -/// (header on the request side, xidmap on the response). Only the (0, 2) combination -/// the full record path unwraps is unwrapped here, so both always see the same compound. -pub fn gss_compound_scan_data( - gss_proc: u32, gss_service: u32, prog_data: &[u8], -) -> GssCompoundScan<'_> { - if gss_proc == 0 && gss_service == 2 { - match parse_rpc_gssapi_integrity(prog_data) { +/// `gss` is the request's RPCSEC_GSS credential combination (header on the +/// request side, xidmap on the response; None: non-GSS). Only (0, 2) is +/// unwrapped, (0, 3) is opaque; another GSS combination is definitive +/// malformed, never Incomplete +pub fn gss_compound_scan_data(gss: Option<(u32, u32)>, prog_data: &[u8]) -> GssCompoundScan<'_> { + match gss { + None => GssCompoundScan::Compound(prog_data), + // rpc_gss_svc_none (RFC 2203): no integrity/privacy wrap + Some((0, 1)) => GssCompoundScan::Compound(prog_data), + Some((0, 2)) => match parse_rpc_gssapi_integrity(prog_data) { Ok((_rem, ref rec)) => GssCompoundScan::Compound(rec.data), Err(Err::Incomplete(_)) => GssCompoundScan::EnvelopeIncomplete, Err(_) => GssCompoundScan::EnvelopeMalformed, - } - } else { - GssCompoundScan::Compound(prog_data) + }, + // rpc_gss_svc_privacy: the payload is encrypted, in both directions + Some((0, 3)) => GssCompoundScan::Opaque, + Some(_) => GssCompoundScan::EnvelopeMalformed, } } @@ -504,26 +510,47 @@ mod tests { envelope.extend_from_slice(&((compound.len()) as u32).to_be_bytes()); // len envelope.extend_from_slice(&1u32.to_be_bytes()); // seqnum envelope.extend_from_slice(compound); // data - let r = gss_compound_scan_data(0, 2, &envelope[..]); + let r = gss_compound_scan_data(Some((0, 2)), &envelope[..]); match r { GssCompoundScan::Compound(d) => assert_eq!(d, compound), _ => panic!("unexpected {:?}", r), } } + #[test] + fn test_gss_compound_scan_data_service_none() { + // (0, 1): rpc_gss_svc_none (RFC 2203) -- the data is not + // wrapped, the raw compound is scanned as-is + let prog: &[u8] = &[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02]; + assert_eq!( + gss_compound_scan_data(Some((0, 1)), prog), + GssCompoundScan::Compound(prog) + ); + } + + #[test] + fn test_gss_compound_scan_data_privacy() { + // rpc_gss_svc_privacy: encrypted, opaque to the scan + let prog: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0xde, 0xad, 0xbe, 0xef]; + assert_eq!( + gss_compound_scan_data(Some((0, 3)), prog), + GssCompoundScan::Opaque + ); + } + #[test] fn test_gss_compound_scan_data_no_wrapper() { // non-GSS requests: prog_data is scanned as-is let prog: &[u8] = &[0x00, 0x00, 0x00, 0x02]; assert_eq!( - gss_compound_scan_data(0, 0, prog), + gss_compound_scan_data(None, prog), GssCompoundScan::Compound(prog) ); - // other GSS combinations are not unwrapped (the full record path - // does not unwrap them either) + // a GSS combination the full record path does not unwrap: definitive + // malformed (the full path rejects it the same way), never Incomplete assert_eq!( - gss_compound_scan_data(1, 0, prog), - GssCompoundScan::Compound(prog) + gss_compound_scan_data(Some((1, 0)), prog), + GssCompoundScan::EnvelopeMalformed ); } @@ -535,7 +562,7 @@ mod tests { envelope.extend_from_slice(&100u32.to_be_bytes()); envelope.extend_from_slice(&1u32.to_be_bytes()); assert_eq!( - gss_compound_scan_data(0, 2, &envelope[..]), + gss_compound_scan_data(Some((0, 2)), &envelope[..]), GssCompoundScan::EnvelopeIncomplete ); } @@ -549,7 +576,7 @@ mod tests { envelope.extend_from_slice(&1u32.to_be_bytes()); envelope.extend_from_slice(&[0u8; 16]); assert_eq!( - gss_compound_scan_data(0, 2, &envelope[..]), + gss_compound_scan_data(Some((0, 2)), &envelope[..]), GssCompoundScan::EnvelopeMalformed ); }