pull/16514/merge
Victor Julien 6 hours ago committed by GitHub
commit 2e88aaee0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1910,6 +1910,7 @@ order file data.
enabled: yes
max-read-size: 16mb
max-write-size: 16mb
max-metadata-size: 16mb
max-read-queue-size: 64mb
max-read-queue-cnt: 64
@ -1925,6 +1926,12 @@ The `max-write-size` option can be set to control the max size of accepted
WRITE request records. Events will be raised if a WRITE request claims too
much data. A value of 0 disables the checks.
The `max-metadata-size` option can be set to control how much of a partial
NFSv3 LOOKUP, READDIRPLUS or CREATE reply may be buffered towards the size its
RPC record claims, so the file handle to filename mapping is kept. A value of 0
falls back to the default of 16mb: this limit cannot be disabled. It is
separate from `max-read-size`, which only bounds READ data.
For file tracking the parser queues up out of order file data per file
transaction. To avoid using too much memory the parser allows for limiting
both the size in bytes and the number of queued chunks.

@ -127,6 +127,11 @@ Logging Changes
``client_hello_done`` is now ``client_hello``). See
:doc:`firewall/tls-state-migration` for the old-to-new mapping.
- NFS eve records are now logged in the flow direction, like SMB, HTTP and
DNS records: a record completed on a reply carries the client as
``src_ip``/``src_port`` where 8.x logged the packet's addresses. Filters
that matched responses on ``src_port 2049`` need ``dest_port 2049`` now.
Removals
~~~~~~~~

@ -2135,27 +2135,27 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zstd"
version = "0.13.3"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
version = "2.1.2+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
dependencies = [
"cc",
"pkg-config",

@ -45,7 +45,7 @@ serde = { version = "1.0.219", default-features = false, features = ["std", "der
serde_json = { version = "1.0.140", default-features = false, features = ["std"] }
suricata-sys = { path = "sys", version = "@PACKAGE_VERSION@" }
time = "=0.3.41"
zstd = { version = "~0.13.3", default-features = false, features = ["arrays", "zdict_builder"] }
zstd = { version = "~0.14.3", default-features = false, features = ["arrays", "zdict_builder"] }
[lib]
crate-type = ["staticlib", "rlib"]

@ -19,7 +19,7 @@
use std::{ffi::CString, path::Path};
use suricata_sys::sys::{SCError, SCLogMessage};
use suricata_sys::sys::SCError;
use suricata_sys::sys::{SCFatalErrorOnInitStatic, SCLogLevel};
/// The Suricata log-level.
@ -66,12 +66,13 @@ pub fn sclog(level: SCLogLevel, file: &str, line: u32, function: &str, message:
}
/// SCLogMessage wrapper.
#[cfg(not(any(test, feature = "testing")))]
pub fn log_message(
level: SCLogLevel, filename: &str, line: std::os::raw::c_uint, function: &str, module: &str,
message: &str,
) -> SCError {
unsafe {
SCLogMessage(
suricata_sys::sys::SCLogMessage(
level,
to_safe_cstring(filename).as_ptr(),
line,
@ -82,6 +83,17 @@ pub fn log_message(
}
}
/// Unit tests do not link the C library: print instead of calling
/// SCLogMessage, like the fallback in the suricata crate's debug.rs.
#[cfg(any(test, feature = "testing"))]
pub fn log_message(
level: SCLogLevel, filename: &str, line: std::os::raw::c_uint, _function: &str, _module: &str,
message: &str,
) -> SCError {
println!("{}:{} <{:?}> -- {}", filename, line, level, message);
SCError::SC_OK
}
// Convert a &str into a CString by first stripping NUL bytes.
fn to_safe_cstring(val: &str) -> CString {
let mut safe = Vec::with_capacity(val.len());

@ -91,6 +91,14 @@ impl FileTransferTracker {
!self.file_open
}
pub fn get_chunk_is_last(&self) -> bool {
self.chunk_is_last
}
pub fn is_truncated(&self) -> bool {
self.file_is_truncated
}
pub fn is_initialized(&self) -> bool {
return self.file_open || self.file_is_truncated || self.file_closed;
}
@ -496,6 +504,12 @@ impl FileTransferTracker {
self.fill_bytes
}
/// File bytes still missing for the active chunk (file data, not record
/// bytes: the XDR fill and any record tail are excluded).
pub fn get_chunk_left(&self) -> u32 {
self.chunk_left
}
/// True if an append at `offset` is out of order and grows the OOO queue.
pub fn is_ooo_offset(&self, offset: u64) -> bool {
offset > self.tracked

File diff suppressed because it is too large Load Diff

@ -113,7 +113,7 @@ impl NFSState {
match parse_nfs2_reply_read(r.prog_data) {
Ok((_, ref reply)) => {
SCLogDebug!("NFSv2: READ reply record");
self.process_read_record(flow, r, reply, Some(xidmap));
self.process_read_record(flow, r, reply, Some(xidmap), false);
nfs_status = reply.status;
}
_ => {

@ -237,7 +237,7 @@ impl NFSState {
};
} else if xidmap.procedure == NFSPROC3_READ {
if let Ok((_, rd)) = parse_nfs3_reply_read(r.prog_data, true) {
self.process_read_record(flow, r, &rd, Some(xidmap));
self.process_read_record(flow, r, &rd, Some(xidmap), true);
nfs_status = rd.status;
} else {
self.set_event(NFSEvent::MalformedData);

@ -18,6 +18,7 @@
// written by Victor Julien
use nom7::bytes::streaming::take;
use nom7::error::ErrorKind;
use nom7::number::streaming::be_u32;
use nom7::{Err, IResult};
@ -46,12 +47,6 @@ impl NFSState {
let is_last = w.stable == 2;
SCLogDebug!("is_last {}", is_last);
let mut fill_bytes = 0;
let pad = w.write_len % 4;
if pad != 0 {
fill_bytes = 4 - pad;
}
if w.write_len == 0 {
return;
}
@ -88,7 +83,7 @@ impl NFSState {
tx.tx_data.0.files_opened = 0;
// Skip to the RPC record boundary: claim vs data can disagree, and
// claimed-count math lands mid-padding or in a later record.
let pending = r.prog_data_size.saturating_sub(r.prog_data.len() as u32);
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
self.set_skip(Direction::ToServer, pending);
self.ts_chunk_xid = 0;
self.ts_chunk_left = 0;
@ -121,7 +116,7 @@ impl NFSState {
w.data,
w.offset,
w.write_len,
fill_bytes as u8,
0,
is_last,
&r.hdr.xid,
);
@ -161,7 +156,7 @@ impl NFSState {
w.data,
w.offset,
w.write_len,
fill_bytes as u8,
0,
is_last,
&r.hdr.xid,
);
@ -180,7 +175,7 @@ impl NFSState {
}
if queue_exceeded {
// Don't buffer the rest of the record: skip to its RPC boundary.
let pending = r.prog_data_size.saturating_sub(r.prog_data.len() as u32);
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
self.set_skip(Direction::ToServer, pending);
self.ts_chunk_xid = 0;
self.ts_chunk_left = 0;
@ -190,8 +185,9 @@ impl NFSState {
self.ts_chunk_xid = r.hdr.xid;
debug_validate_bug_on!(w.data.len() as u32 > w.write_len);
self.ts_chunk_left = w.write_len - w.data.len() as u32;
// keep the record-tail bookkeeping in sync with the v3 path
self.ts_chunk_tail = r.prog_data_size.saturating_sub(r.prog_data.len() as u32);
// v4 records only reach the parser complete: nothing unbuffered,
// record tail 0 (no aligned-boundary correction like the v3 path)
self.ts_chunk_tail = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
}
}
@ -349,27 +345,31 @@ 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;
// privacy (0, 3) carries an encrypted compound: nothing to
// parse, and it is not malformed: just keep the request state
if creds.procedure == 0 && creds.service == 3 {
self.requestmap.put(r.hdr.xid, xidmap);
return;
}
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);
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;
}
}
@ -383,11 +383,19 @@ impl NFSState {
}
Err(Err::Incomplete(_n)) => {
SCLogDebug!("NFSPROC4_COMPOUND: INCOMPLETE {:?}", _n);
self.reject_failed_v4_compound_parse(r, data);
}
Err(Err::Error(ref e)) if e.code == ErrorKind::Tag => {
// a defined v4 op this parser does not implement: the compound is
// valid, just not parsed. No tx of its own: keeps base's event on
// the newest tx, so a lock-only flow logs nothing when it has no
// earlier tx.
SCLogDebug!("NFSPROC4_COMPOUND: unsupported op {:?}", e);
self.set_event(NFSEvent::MalformedData);
}
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
SCLogDebug!("NFSPROC4_COMPOUND: Parsing failed: {:?}", _e);
self.set_event(NFSEvent::MalformedData);
self.reject_failed_v4_compound_parse(r, data);
}
};
}
@ -443,7 +451,7 @@ impl NFSState {
data_len: rd.data.len() as u32,
data: rd.data,
};
self.process_read_record(flow, r, &reply, Some(xidmap));
self.process_read_record(flow, r, &reply, Some(xidmap), false);
}
Nfs4ResponseContent::Open(_s, Some(ref _rd)) => {
SCLogDebug!("OPENv4: status {} opendata {:?}", _s, _rd);
@ -481,6 +489,12 @@ impl NFSState {
if xidmap.procedure == NFSPROC4_COMPOUND {
let mut data = r.prog_data;
// privacy (0, 3) carries an encrypted response: not parsed,
// and not malformed (the call logged no tx either)
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 3 {
return;
}
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 2 {
SCLogDebug!("GSS INTEGRITY as set by call: {:?}", xidmap);
match parse_rpc_gssapi_integrity(r.prog_data) {
@ -488,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;
}
}

@ -293,7 +293,10 @@ fn nfs4_req_open_exclusive4(i: &[u8]) -> IResult<&[u8], Nfs4OpenRequestContent<'
}
fn nfs4_req_open_exclusive4_1(i: &[u8]) -> IResult<&[u8], Nfs4OpenRequestContent<'_>> {
map(take(8_usize), Nfs4OpenRequestContent::Exclusive4_1)(i)
// EXCLUSIVE4_1 carries an fattr4 after the verifier (RFC 8881 18.16.1.1)
let (i, verifier) = take(8_usize)(i)?;
let (i, _attrs) = nfs4_parse_attrs(i)?;
Ok((i, Nfs4OpenRequestContent::Exclusive4_1(verifier)))
}
fn nfs4_req_open_type(i: &[u8]) -> IResult<&[u8], Nfs4OpenRequestContent<'_>> {
@ -324,10 +327,33 @@ fn nfs4_req_open(i: &[u8]) -> IResult<&[u8], Nfs4RequestContent<'_>> {
let (i, _client_id) = be_u64(i)?;
let (i, owner_len) = be_u32(i)?;
let (i, _) = cond(owner_len > 0, take(owner_len as usize))(i)?;
let (i, _) = cond(owner_len % 4 != 0, take(4 - (owner_len % 4)))(i)?;
let (i, open_type) = be_u32(i)?;
let (i, open_data) = cond(open_type == 1, nfs4_req_open_type)(i)?;
let (i, _claim_type) = be_u32(i)?;
let (i, filename) = nfs4_parse_nfsstring(i)?;
// open_claim4 (RFC 8881 18.16.1): only some claim types carry a name
let (i, claim_type) = be_u32(i)?;
let (i, filename) = match claim_type {
// CLAIM_NULL / CLAIM_DELEGATE_PREV: the component4 only
0 | 3 => nfs4_parse_nfsstring(i)?,
// CLAIM_PREVIOUS: an open_delegation_type4
1 => {
let (i, _deleg) = be_u32(i)?;
(i, &[][..])
}
// CLAIM_DELEGATE_CUR: stateid4 + component4
2 => {
let (i, _stateid) = nfs4_parse_stateid(i)?;
nfs4_parse_nfsstring(i)?
}
// CLAIM_FH / CLAIM_DELEG_PREV_FH: void
4 | 6 => (i, &[][..]),
// CLAIM_DELEG_CUR_FH: stateid4 only
5 => {
let (i, _stateid) = nfs4_parse_stateid(i)?;
(i, &[][..])
}
_ => return Err(Err::Error(make_error(i, ErrorKind::Switch))),
};
let req = Nfs4RequestContent::Open(Nfs4RequestOpen {
open_type,
filename,
@ -569,6 +595,12 @@ fn nfs4_req_sequence(i: &[u8]) -> IResult<&[u8], Nfs4RequestContent<'_>> {
Ok((i, req))
}
/// The op codes the NFSv4 RFCs (7530, 8881, 7862, 8276) assign all fall in this
/// range: inside it an op may be one we do not parse, outside it is no op.
fn v4_request_op_is_assigned(cmd: u32) -> bool {
matches!(cmd, 3..=75)
}
fn parse_request_compound_command(i: &[u8]) -> IResult<&[u8], Nfs4RequestContent<'_>> {
let (i, cmd) = be_u32(i)?;
let (i, cmd_data) = match cmd {
@ -604,6 +636,11 @@ fn parse_request_compound_command(i: &[u8]) -> IResult<&[u8], Nfs4RequestContent
NFSPROC4_LAYOUTRETURN => nfs4_req_layoutreturn(i)?,
NFSPROC4_DESTROY_SESSION => nfs4_req_destroy_session(i)?,
NFSPROC4_DESTROY_CLIENTID => nfs4_req_destroy_clientid(i)?,
// an op no parser here reads: Tag, not Switch, so the caller does not
// call valid traffic malformed (no request parser else yields Tag)
_ if v4_request_op_is_assigned(cmd) => {
return Err(Err::Error(make_error(i, ErrorKind::Tag)));
}
_ => {
return Err(Err::Error(make_error(i, ErrorKind::Switch)));
}
@ -668,11 +705,9 @@ fn skip_attrlist(i: &[u8]) -> IResult<&[u8], ()> {
skip_len_bytes(i)
}
/// Consume a sattr4/fattr4 bitmap the way the parser reads it
/// (nfs4_parse_attrbits): attr_cnt + mask1 (always) + mask2 (cnt >= 2) + mask3
/// (cnt == 3). Unlike skip_attrbits this mirrors the parser, not the wire: the
/// OPEN requests/replies the parser supports carry the mask words it consumes,
/// and the scan must end where the parser ends.
/// Consume a sattr4/fattr4 bitmap as the parser reads it (nfs4_parse_attrbits):
/// attr_cnt + mask1 (always) + mask2 (cnt >= 2) + mask3 (cnt == 3). Mirrors the
/// parser, not the wire: the scan must end where the parser ends.
fn skip_parser_attrbits(i: &[u8]) -> IResult<&[u8], ()> {
let (i, attr_cnt) = be_u32(i)?;
let (i, ()) = skip_fixed(i, 4)?;
@ -733,19 +768,12 @@ fn skip_request_lead_op(i: &[u8], cmd: u32) -> IResult<&[u8], ()> {
}
// access bits
NFSPROC4_ACCESS => skip_fixed(i, 4),
// open4args as nfs4_req_open parses it: seq_id (4) + share_access +
// share_deny + client_id (8) + owner (len + bytes, no XDR pad) +
// open_type (+ open_data for type 1) + claim_type + name. The parser
// reads no claim payload: payload-bearing claims desync it (their
// stateid/fh is read as the name length), as do the other
// open_types: not skippable.
// open4args (RFC 8881 18.16.1): seq_id + share_access + share_deny +
// open_owner4 (client_id + padded owner) + openflag4 + open_claim4.
NFSPROC4_OPEN => {
let (i, ()) = skip_fixed(i, 20)?;
let (i, owner_len) = be_u32(i)?;
if i.len() < owner_len as usize {
return Err(Err::Incomplete(Needed::new(1)));
}
let i = &i[owner_len as usize..];
let (i, ()) = skip_xdr_bytes(i, owner_len as usize)?;
let (i, open_type) = be_u32(i)?;
let i = if open_type == 1 {
let (i, mode) = be_u32(i)?;
@ -757,10 +785,17 @@ fn skip_request_lead_op(i: &[u8], cmd: u32) -> IResult<&[u8], ()> {
let (i, ()) = skip_attr_fields(i)?;
i
}
2 | 3 => {
2 => {
let (i, ()) = skip_fixed(i, 8)?;
i
}
// EXCLUSIVE4_1: verifier + fattr4
3 => {
let (i, ()) = skip_fixed(i, 8)?;
let (i, ()) = skip_parser_attrbits(i)?;
let (i, ()) = skip_attr_fields(i)?;
i
}
_ => return Err(Err::Error(make_error(i, ErrorKind::Switch))),
}
} else if open_type == 0 {
@ -768,18 +803,20 @@ fn skip_request_lead_op(i: &[u8], cmd: u32) -> IResult<&[u8], ()> {
} else {
return Err(Err::Error(make_error(i, ErrorKind::Switch)));
};
// claim4: the parser reads no claim payload (void claims
// only): payload-bearing claims (FH, DELEGATE, FH_ONLY and the
// v4.2 claims) desync it (the payload is read as the name
// length). Only the claims void in every version (NULL=0,
// UNIQUE=2) are skippable; the rest are a bounded rejection,
// never Incomplete (buffering toward the record claim).
// open_claim4: name for 0/2/3, stateid for 2/5, delegation type
// for 1, void for 4/6 (fh-only claims)
let (i, claim_type) = be_u32(i)?;
match claim_type {
0 | 2 => {}
_ => return Err(Err::Error(make_error(i, ErrorKind::Switch))),
0 | 3 => skip_len_bytes(i),
1 => skip_fixed(i, 4),
2 => {
let (i, ()) = skip_fixed(i, 16)?;
skip_len_bytes(i)
}
4 | 6 => Ok((i, ())),
5 => skip_fixed(i, 16),
_ => Err(Err::Error(make_error(i, ErrorKind::Switch))),
}
skip_len_bytes(i)
}
// name (XDR string)
NFSPROC4_LOOKUP | NFSPROC4_REMOVE => skip_len_bytes(i),
@ -910,31 +947,30 @@ fn skip_response_lead_op(i: &[u8], cmd: u32) -> IResult<&[u8], ()> {
NFSPROC4_ACCESS => skip_fixed(i, 8),
// stateid + change_info + result_flags + fattr4 bitmap + file_delegation4 union
NFSPROC4_OPEN => {
// change_info4 is 20 bytes (bool + 2x changeid4 uint64_t)
let (i, ()) = skip_fixed(i, 16 + 20 + 4)?;
let (i, ()) = skip_parser_attrbits(i)?;
let (i, deleg) = be_u32(i)?;
match deleg {
// stateid + 4 words + who_len, then who (no XDR pad, mirrors the parser)
// open_read_delegation4 (RFC 5661 18.16.2): stateid + recall
// + nfsace4 (type, flag, mask, who)
OPEN_DELEGATE_READ => {
let (i, ()) = skip_fixed(i, 16 + 4 * 4)?;
let (i, who_len) = be_u32(i)?;
if i.len() < who_len as usize {
return Err(Err::Incomplete(Needed::new(1)));
}
Ok((&i[who_len as usize..], ()))
let (i, ()) = skip_fixed(i, 16 + 4 + 12)?;
nfs4_skip_who(i)
}
// stateid + 6 words, then who (nfsstring)
// open_write_delegation4: stateid + recall + nfs_space_limit4
// (12) + nfsace4 (type, flag, mask, who)
OPEN_DELEGATE_WRITE => {
let (i, ()) = skip_fixed(i, 16 + 4 * 6)?;
skip_len_bytes(i)
let (i, ()) = skip_fixed(i, 16 + 4 + 12 + 12)?;
nfs4_skip_who(i)
}
OPEN_DELEGATE_NONE => Ok((i, ())),
_ => Err(Err::Error(make_error(i, ErrorKind::Switch))),
}
}
// nfs41_sequence_ok = ssn4(16) + seqid(4) + slots(12) + flags(4) = 36 bytes.
// A typical NFSv4.1 reply leads with SEQUENCE; skipping its result reaches a
// following READ (status already consumed above).
// SEQUENCE4resok: 36 bytes after status (rfc5661 18.46.2); a typical
// v4.1 reply leads with SEQUENCE: skipping its result reaches the
// following READ (status already consumed above)
NFSPROC4_SEQUENCE => skip_fixed(i, 36),
// setclientid4resok: client_id + verifier
NFSPROC4_SETCLIENTID => skip_fixed(i, 16),
@ -1327,21 +1363,19 @@ pub enum Nfs4ResponseFileDelegation<'a> {
#[derive(Debug, PartialEq, Eq)]
pub struct Nfs4ResponseOpenDelegateWrite<'a> {
pub stateid: Nfs4StateId<'a>,
pub who: &'a [u8],
}
fn nfs4_res_open_ok_delegate_write(i: &[u8]) -> IResult<&[u8], Nfs4ResponseFileDelegation<'_>> {
// open_write_delegation4 (RFC 5661 18.16.2): stateid + recall +
// nfs_space_limit4 + nfsace4 (type, flag, mask, who)
let (i, stateid) = nfs4_parse_stateid(i)?;
let (i, _recall) = be_u32(i)?;
let (i, _space_limit) = be_u32(i)?;
let (i, _filesize) = be_u32(i)?;
let (i, _access_type) = be_u32(i)?;
let (i, _ace_flags) = be_u32(i)?;
let (i, _ace_mask) = be_u32(i)?;
let (i, who) = nfs4_parse_nfsstring(i)?;
let (i, _space_limit) = take(12_usize)(i)?;
let (i, _ace) = take(12_usize)(i)?; // acetype4 + aceflag4 + acemask4
let (i, _who) = nfs4_skip_who(i)?;
Ok((
i,
Nfs4ResponseFileDelegation::DelegateWrite(Nfs4ResponseOpenDelegateWrite { stateid, who }),
Nfs4ResponseFileDelegation::DelegateWrite(Nfs4ResponseOpenDelegateWrite { stateid }),
))
}
@ -1351,19 +1385,27 @@ pub struct Nfs4ResponseOpenDelegateRead<'a> {
}
fn nfs4_res_open_ok_delegate_read(i: &[u8]) -> IResult<&[u8], Nfs4ResponseFileDelegation<'_>> {
// open_read_delegation4 (RFC 5661 18.16.2): stateid + recall + nfsace4
// (type, flag, mask, who)
let (i, stateid) = nfs4_parse_stateid(i)?;
let (i, _recall) = be_u32(i)?;
let (i, _ace_type) = be_u32(i)?;
let (i, _ace_flags) = be_u32(i)?;
let (i, _ace_mask) = be_u32(i)?;
let (i, who_len) = be_u32(i)?;
let (i, _who) = take(who_len as usize)(i)?;
let (i, _ace) = take(12_usize)(i)?; // acetype4 + aceflag4 + acemask4
let (i, _who) = nfs4_skip_who(i)?;
Ok((
i,
Nfs4ResponseFileDelegation::DelegateRead(Nfs4ResponseOpenDelegateRead { stateid }),
))
}
/// Consume a utf8str_mixed (length-prefixed string + XDR padding), the
/// nfsace4.who field of a delegation.
fn nfs4_skip_who(i: &[u8]) -> IResult<&[u8], ()> {
let (i, len) = be_u32(i)?;
let (i, _str) = take(len as usize)(i)?;
let (i, _pad) = take(((4 - len % 4) % 4) as usize)(i)?;
Ok((i, ()))
}
fn nfs4_parse_file_delegation(i: &[u8]) -> IResult<&[u8], Nfs4ResponseFileDelegation<'_>> {
let (i, delegation_type) = be_u32(i)?;
let (i, file_delegation) = match delegation_type {
@ -1382,6 +1424,7 @@ fn nfs4_parse_file_delegation(i: &[u8]) -> IResult<&[u8], Nfs4ResponseFileDelega
fn nfs4_res_open_ok(i: &[u8]) -> IResult<&[u8], Nfs4ResponseOpen<'_>> {
let (i, stateid) = nfs4_parse_stateid(i)?;
// change_info4: bool + changeid4 (uint64_t) + changeid4 = 20 bytes
let (i, _change_info) = take(20_usize)(i)?;
let (i, result_flags) = be_u32(i)?;
let (i, _attrs) = nfs4_parse_attrbits(i)?;
@ -1693,10 +1736,14 @@ pub struct Nfs4ResponseSequence<'a> {
}
fn nfs4_res_sequence_ok(i: &[u8]) -> IResult<&[u8], Nfs4ResponseSequence<'_>> {
// SEQUENCE4resok (rfc5661 18.46.2): sessionid4(16) + sequenceid4 +
// slotid + highest_slotid + target_highest_slotid + status_flags = 36
let (i, ssn_id) = take(16_usize)(i)?;
let (i, _seqid) = be_u32(i)?;
let (i, _slots) = take(12_usize)(i)?;
let (i, _flags) = be_u32(i)?;
let (i, _sequenceid) = be_u32(i)?;
let (i, _slotid) = be_u32(i)?;
let (i, _highest_slotid) = be_u32(i)?;
let (i, _target_highest_slotid) = be_u32(i)?;
let (i, _status_flags) = be_u32(i)?;
Ok((i, Nfs4ResponseSequence { ssn_id }))
}
@ -2269,30 +2316,37 @@ mod tests {
// open_data
0x00, 0x00, 0x00, 0x01, 0x00, 0x82, 0x14, 0xe0, /*stateid*/
0x5b, 0x00, 0x88, 0xd9, 0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x16, 0xf8, 0x2f, 0xd5, /*_change_info*/
0xdb, 0xb7, 0xfe, 0x38, 0x16, 0xf8, 0x2f, 0xdf,
0x21, 0xa8, 0x2a, 0x48, 0x00, 0x00, 0x00, 0x04,
0x00, 0x00, 0x00, 0x03,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x02, /*_attrs*/
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, /*change_info: atomic*/
0x16, 0xf8, 0x2f, 0xd5, 0xdb, 0xb7, 0xfe, 0x38, /*before*/
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*after*/
0x00, 0x00, 0x00, 0x04, /*result_flags*/
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x10, /*_attrs*/
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
// delegate_write
0x00, 0x00, 0x00, 0x02, /*delegation_type*/
0x00, 0x00, 0x00, 0x01, 0x02, 0x82, 0x14, 0xe0,
0x00, 0x00, 0x00, 0x01, 0x02, 0x82, 0x14, 0xe0, /*stateid*/
0x5b, 0x00, 0x89, 0xd9, 0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, /*recall*/
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /*space_limit*/
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, /*nfsace4: acetype4*/
0x00, 0x00, 0x00, 0x00, /*aceflag4*/
0x00, 0x00, 0x00, 0x03, /*acemask4*/
0x00, 0x00, 0x00, 0x05, /*who len*/
0x6e, 0x66, 0x73, 0x72, 0x6f, /*"nfsro"*/
0x00, 0x00, 0x00, /*pad*/
];
let stateid_buf = &buf[8..24];
let (_, res_stateid) = nfs4_parse_stateid(stateid_buf).unwrap();
let delegate_buf = &buf[64..];
let (_, delegate) = nfs4_parse_file_delegation(delegate_buf).unwrap();
let (rest, delegate) = nfs4_parse_file_delegation(delegate_buf).unwrap();
assert!(rest.is_empty());
let open_data_buf = &buf[8..];
let (_, res_open_data) = nfs4_res_open_ok(open_data_buf).unwrap();
let (rest, res_open_data) = nfs4_res_open_ok(open_data_buf).unwrap();
assert!(rest.is_empty());
assert_eq!(res_open_data.stateid, res_stateid);
assert_eq!(res_open_data.result_flags, 4);
assert_eq!(res_open_data.delegate, delegate);
@ -2670,7 +2724,7 @@ mod tests {
0x0, 0x0, 0x0, 0x8, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x6e, 0x67, /*filename*/
];
let (_, filename_buf) = nfs4_parse_nfsstring(&buf[96..]).unwrap();
let (_, filename_buf) = nfs4_parse_nfsstring(&buf[100..]).unwrap();
let (_, request) = nfs4_req_open(&buf[4..]).unwrap();
match request {
@ -2728,9 +2782,9 @@ mod tests {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&be32(NFSPROC4_PUTFH));
v.extend_from_slice(&be32(fh_len));
v.extend(std::iter::repeat_n(0, fh_len as usize));
v.extend(std::iter::repeat_n(0u8, fh_len as usize));
let pad = (4 - (fh_len as usize % 4)) % 4;
v.extend(std::iter::repeat_n(0, pad));
v.extend(std::iter::repeat_n(0u8, pad));
v
}
@ -2755,7 +2809,9 @@ mod tests {
v.extend_from_slice(&be32(NFSPROC4_OPEN));
v.extend_from_slice(&be32(0)); // status: OK
v.extend_from_slice(&[0u8; 16]); // stateid
v.extend_from_slice(&[0u8; 20]); // change_info
// change_info: non-zero and distinct, so an undersized skip lands
// in this region and desyncs (a zero region would mask it)
v.extend_from_slice(&(0u8..20).map(|x| 0xA0 + x).collect::<Vec<u8>>());
v.extend_from_slice(&be32(0)); // result_flags
v.extend_from_slice(&be32(0)); // fattr4 attr_cnt (0)
v.extend_from_slice(&be32(0)); // mask1 (the parser always reads it)
@ -2769,9 +2825,9 @@ mod tests {
v.extend_from_slice(&be32(NFSPROC4_GETFH));
v.extend_from_slice(&be32(0)); // status: OK
v.extend_from_slice(&be32(fh_len));
v.extend(std::iter::repeat_n(0, fh_len as usize));
v.extend(std::iter::repeat_n(0u8, fh_len as usize));
let pad = (4 - (fh_len as usize % 4)) % 4;
v.extend(std::iter::repeat_n(0, pad));
v.extend(std::iter::repeat_n(0u8, pad));
v
}
@ -2845,7 +2901,7 @@ mod tests {
v.extend_from_slice(&be32(name.len() as u32));
v.extend_from_slice(name);
let pad = (4 - (name.len() % 4)) % 4;
v.extend(std::iter::repeat_n(0, pad));
v.extend(std::iter::repeat_n(0u8, pad));
v
}
@ -2905,10 +2961,25 @@ mod tests {
);
}
// a v4 OPEN request op as nfs4_req_open parses it: seq_id + share_access
// + share_deny + client_id + owner (len + bytes, no pad) + open_type
// (+ open_data for type 1) + claim_type (void claim, no payload) + name
fn open_req_op(open_type: u32, mode: u32, claim_type: u32, claim_payload: &[u8]) -> Vec<u8> {
// an XDR string (length + bytes + XDR pad)
fn xdr_name(s: &[u8]) -> Vec<u8> {
let mut v = be32(s.len() as u32).to_vec();
v.extend_from_slice(s);
v.extend(std::iter::repeat_n(0u8, (4 - (s.len() % 4)) % 4));
v
}
// a stateid4 (seqid + 12 data bytes)
fn xdr_stateid() -> Vec<u8> {
let mut v = be32(1).to_vec();
v.extend(std::iter::repeat_n(0u8, 12));
v
}
// a v4 OPEN request op per RFC 8881 18.16.1: seq_id + share_access +
// share_deny + open_owner4 (client_id + owner) + openflag4 (+ open_data
// for type 1) + open_claim4 with the payload its claim type carries
fn open_req_op(open_type: u32, mode: u32, claim_type: u32) -> Vec<u8> {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&be32(NFSPROC4_OPEN));
v.extend_from_slice(&[0u8; 20]); // seq_id + share_access + share_deny + client_id
@ -2922,14 +2993,29 @@ mod tests {
v.extend_from_slice(&be32(0)); // mask1 (the parser always reads it)
v.extend_from_slice(&be32(0)); // fields blob len 0
}
_ => v.extend_from_slice(&[0u8; 8]), // exclusive4 words
2 => v.extend_from_slice(&[0u8; 8]), // verifier4
// EXCLUSIVE4_1: verifier4 + fattr4
3 => {
v.extend_from_slice(&[0u8; 8]);
v.extend_from_slice(&be32(0)); // attr_cnt (0)
v.extend_from_slice(&be32(0)); // mask1
v.extend_from_slice(&be32(0)); // fields blob len 0
}
_ => {}
}
}
v.extend_from_slice(&be32(claim_type)); // claim_type
v.extend_from_slice(claim_payload);
v.extend_from_slice(&be32(3)); // name len
v.extend_from_slice(b"foo");
v.push(0); // XDR pad
v.extend_from_slice(&be32(claim_type));
match claim_type {
0 | 3 => v.extend_from_slice(&xdr_name(b"foo")),
1 => v.extend_from_slice(&be32(OPEN_DELEGATE_NONE)),
2 => {
v.extend_from_slice(&xdr_stateid());
v.extend_from_slice(&xdr_name(b"foo"));
}
4 | 6 => {}
5 => v.extend_from_slice(&xdr_stateid()),
_ => {}
}
v
}
@ -2953,7 +3039,20 @@ mod tests {
// the scanner must skip it (not reject the compound) and reach the
// trailing GETFH.
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, 0, &[]));
buf.extend_from_slice(&open_req_op(0, 0, 0));
buf.extend_from_slice(&getfh_req_op());
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
Nfs4CompoundScan::Clean
);
}
#[test]
fn test_scan_request_compound_open_delegate_prev_claim_skipped() {
// CLAIM_DELEGATE_PREV (3) carries a component4, the layout the parser
// reads: the scanner skips it and reaches the trailing op
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, 3));
buf.extend_from_slice(&getfh_req_op());
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
@ -2962,17 +3061,30 @@ mod tests {
}
#[test]
fn test_scan_request_compound_open_payload_claim_rejected() {
// a payload-bearing claim (FH=1, DELEGATE=3, FH_ONLY=4, the v4.2
// claims 5+) desyncs the parser (the stateid/fh is read as the name
// length): the scanner must reject the compound boundedly
// (Malformed), not buffer (Incomplete) and not report the oversized
// WRITE behind it.
let mut stateid = be32(1).to_vec();
stateid.extend(std::iter::repeat_n(0u8, 12));
for claim_type in [1u32, 3, 4, 8, 9] {
fn test_scan_request_compound_open_payload_claims_reach_write() {
// the claims whose payload is not a bare name (1: delegate_type, 2:
// stateid then a name, 4/6: void, 5: stateid) are skipped per the wire,
// so a following oversized WRITE is reached
for claim_type in [1u32, 2, 4, 5, 6] {
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, claim_type, &stateid));
buf.extend_from_slice(&open_req_op(0, 0, claim_type));
buf.extend_from_slice(&write_op(10 * 1024 * 1024));
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
Nfs4CompoundScan::Oversized(10 * 1024 * 1024),
"claim_type {}",
claim_type
);
}
}
#[test]
fn test_scan_request_compound_open_unknown_claim_rejected() {
// a claim type outside the RFC 8881 union can't be advanced past: the
// compound is rejected boundedly, never skipped on a guess
for claim_type in [7u32, 8, 9] {
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, claim_type));
buf.extend_from_slice(&write_op(10 * 1024 * 1024));
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
@ -2983,12 +3095,29 @@ mod tests {
}
}
#[test]
fn test_scan_request_compound_open_name_claims_reach_write() {
// NULL(0)/DELEGATE_PREV(3) carry the one name the parser consumes:
// the OPEN is skipped, a following oversized WRITE is reached
for claim_type in [0u32, 3] {
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, claim_type));
buf.extend_from_slice(&write_op(10 * 1024 * 1024));
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
Nfs4CompoundScan::Oversized(10 * 1024 * 1024),
"claim_type {}",
claim_type
);
}
}
#[test]
fn test_scan_request_compound_open_hides_oversized_write() {
// the OPEN is skipped so a following oversized WRITE is reached (not
// hidden behind it): the record is rejected, not buffered.
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(0, 0, 0, &[]));
buf.extend_from_slice(&open_req_op(0, 0, 0));
buf.extend_from_slice(&write_op(10 * 1024 * 1024));
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
@ -2998,15 +3127,19 @@ mod tests {
#[test]
fn test_scan_request_compound_open_exclusive_getfh_no_write() {
// open_type 1 with an exclusive open_data (8 words) is skipped the
// same way.
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(1, 2, 0, &[]));
buf.extend_from_slice(&getfh_req_op());
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
Nfs4CompoundScan::Clean
);
// open_type 1 with an exclusive open_data (EXCLUSIVE4 verifier, or the
// EXCLUSIVE4_1 verifier + fattr4) is skipped the same way.
for mode in [2u32, 3] {
let mut buf = compound_head(2);
buf.extend_from_slice(&open_req_op(1, mode, 0));
buf.extend_from_slice(&getfh_req_op());
assert_eq!(
scan_nfs4_request_compound_write_len(&buf, 1024),
Nfs4CompoundScan::Clean,
"mode {}",
mode
);
}
}
#[test]
@ -3029,6 +3162,55 @@ mod tests {
}
}
#[test]
fn test_nfs4_req_open_claim_types_exact_wire_length() {
// every open_claim4 layout must stop at the op boundary: the following
// op tag stays in the remainder instead of being read as a name
for claim_type in 0..7u32 {
let next = be32(NFSPROC4_GETATTR);
let mut args = open_req_op(0, 0, claim_type)[4..].to_vec();
args.extend_from_slice(&next);
let (rem, content) = nfs4_req_open(&args).unwrap();
let filename = match content {
Nfs4RequestContent::Open(ref req) => {
assert_eq!(req.open_type, 0);
assert_eq!(req.open_data, None);
req.filename
}
_ => panic!("unexpected content"),
};
let expected: &[u8] = if claim_type == 0 || claim_type == 2 || claim_type == 3 {
b"foo"
} else {
&[]
};
assert_eq!(filename, expected, "claim_type {}", claim_type);
assert_eq!(rem, &next[..], "claim_type {}", claim_type);
}
}
#[test]
fn test_nfs4_req_open_owner_padding_exact_wire_length() {
// owner4 is an opaque<>: a 5-byte owner consumes 8 bytes on the wire,
// so the following op tag stays at the head of the remainder
let next = be32(NFSPROC4_GETATTR);
let mut args: Vec<u8> = Vec::new();
args.extend_from_slice(&[0u8; 20]); // seq_id + share_access + share_deny + client_id
args.extend_from_slice(&be32(5)); // owner len
args.extend_from_slice(b"owner");
args.extend_from_slice(&[0u8; 3]); // XDR pad
args.extend_from_slice(&be32(0)); // open_type: OPEN4_NOCREATE
args.extend_from_slice(&be32(0)); // claim_type: CLAIM_NULL
args.extend_from_slice(&xdr_name(b"file"));
args.extend_from_slice(&next);
let (rem, content) = nfs4_req_open(&args).unwrap();
match content {
Nfs4RequestContent::Open(ref req) => assert_eq!(req.filename, b"file".as_slice()),
_ => panic!("unexpected content"),
}
assert_eq!(rem, &next[..]);
}
// response compound head: compound status + XDR string tag + op count
// (no minor version field, unlike the request)
fn response_compound_head(ops_cnt: u32) -> Vec<u8> {
@ -3113,13 +3295,13 @@ mod tests {
#[test]
fn test_scan_response_compound_sequence_hides_oversized_read() {
// a SEQUENCE op (the typical NFSv4.1 reply lead) followed by an oversized READ:
// the scanner must skip the 36-byte SEQUENCE result to reach the READ, or it
// the scanner must skip the 36-byte SEQUENCE4resok to reach the READ, or it
// rejects the compound (Malformed) and loses the file inspection.
let mut buf = response_compound_head(2);
// SEQUENCE: tag + status(0) + ssn4(16) + seqid(4) + slots(12) + flags(4)
// SEQUENCE: tag + status(0) + 36-byte resok
buf.extend_from_slice(&be32(NFSPROC4_SEQUENCE));
buf.extend_from_slice(&be32(0)); // status ok
buf.extend_from_slice(&[0x11u8; 36]); // sequence_ok result
buf.extend_from_slice(&[0x11u8; 36]); // sequence resok result
// READ: tag + status(0) + eof + read_len (oversized)
buf.extend_from_slice(&be32(NFSPROC4_READ));
buf.extend_from_slice(&be32(0)); // status ok
@ -3149,6 +3331,60 @@ mod tests {
);
}
// an OPEN response op that grants a delegation of the given type
fn open_res_deleg_op(deleg: u32) -> Vec<u8> {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&be32(NFSPROC4_OPEN));
v.extend_from_slice(&be32(0)); // status: OK
v.extend_from_slice(&[0u8; 16]); // stateid
v.extend_from_slice(&[0u8; 20]); // change_info
v.extend_from_slice(&be32(0)); // result_flags
v.extend_from_slice(&be32(0)); // fattr4 attr_cnt (0)
v.extend_from_slice(&be32(0)); // mask1 (the parser always reads it)
v.extend_from_slice(&be32(deleg)); // file_delegation4 type
match deleg {
OPEN_DELEGATE_READ => {
// open_read_delegation4: stateid + recall + nfsace4
// (type, flag, mask, who = "nfsro" + 3 XDR pad)
v.extend_from_slice(&[0u8; 16 + 4 + 12]);
v.extend_from_slice(&be32(5)); // who len
v.extend_from_slice(b"nfsro");
v.extend_from_slice(&[0u8; 3]); // XDR pad
}
OPEN_DELEGATE_WRITE => {
// open_write_delegation4: stateid + recall + space_limit (12)
// + nfsace4 (type, flag, mask, who = "nfsro" + 3 XDR pad)
v.extend_from_slice(&[0u8; 16 + 4 + 12 + 12]);
v.extend_from_slice(&be32(5)); // who len
v.extend_from_slice(b"nfsro");
v.extend_from_slice(&[0u8; 3]); // XDR pad
}
_ => panic!("unsupported delegation type"),
}
v
}
#[test]
fn test_scan_response_compound_open_delegation_then_within_limit_read() {
// a delegation-granting OPEN (both layouts) + a within-limit READ:
// the scanner must skip the RFC-sized delegation struct and reach the READ
for deleg in [OPEN_DELEGATE_READ, OPEN_DELEGATE_WRITE] {
let mut buf = response_compound_head(2);
buf.extend_from_slice(&open_res_deleg_op(deleg));
buf.extend_from_slice(&be32(NFSPROC4_READ));
buf.extend_from_slice(&be32(0)); // status ok
buf.extend_from_slice(&be32(0)); // eof
buf.extend_from_slice(&be32(8)); // within-limit read_len
buf.extend_from_slice(&[0u8; 8]); // data (padded to 4)
assert_eq!(
scan_nfs4_response_compound_read_len(&buf, 1024),
Nfs4CompoundScan::Clean,
"delegation type {}",
deleg
);
}
}
#[test]
fn test_scan_response_compound_open_getfh_no_read() {
// a leading OPEN is a valid op the full parser supports: the scanner

@ -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
);
}

@ -113,6 +113,7 @@ impl SMBState {
tx.id,
String::from_utf8_lossy(file_name)
);
self.file_tx_count += 1;
self.transactions.push_back(tx);
self.transactions.back_mut()
}
@ -213,18 +214,11 @@ impl SMBState {
} else {
self.file_tc_left
};
let file_handle = if direction == Direction::ToServer {
self.file_ts_guid.to_vec()
} else {
self.file_tc_guid.to_vec()
};
let ssn_gap = self.ts_ssn_gap | self.tc_ssn_gap;
// Backstop: cap the OOO queue on every open file tx (an interleaved
// handle can hide a first file's queue), running even with chunk_left == 0.
if ssn_gap {
// Last-resort cap for OOO data in gapped streams: configured
// size/count (1 GiB / 1024 when limits are off).
// handle can hide a queue); runs when the call feeds data: app-layer
// OOO needs no stream gap to grow a queue, an empty call grows none.
// A queue can only exist inside a held file tx.
if !data.is_empty() && self.file_tx_count > 0 {
let backstop = Self::ooo_queue_backstop(direction);
let cnt_backstop = Self::ooo_queue_backstop_cnt(direction);
for tx in &mut self.transactions {
@ -234,7 +228,7 @@ impl SMBState {
let queued_cnt = tdf.file_tracker.get_inflight_cnt() as u64;
if queued_data > backstop || queued_cnt > cnt_backstop {
SCLogDebug!(
"QUEUED size {} / count {} > backstop {} / {} while we've seen GAPs. Truncating file.",
"QUEUED size {} / count {} > backstop {} / {}. Truncating file.",
queued_data,
queued_cnt,
backstop,
@ -256,6 +250,13 @@ impl SMBState {
return 0;
}
SCLogDebug!("chunk_left {} data {}", chunk_left, data.len());
// the backstop above no longer needs the handle: allocate it only
// when a chunk is in flight
let file_handle = if direction == Direction::ToServer {
self.file_ts_guid.to_vec()
} else {
self.file_tc_guid.to_vec()
};
let data_to_handle_len = if chunk_left as usize >= data.len() {
data.len()
@ -353,12 +354,17 @@ pub(super) unsafe extern "C" fn smb_gettxfiles(
AppLayerGetFileState::err()
}
// Serializes the cfg-static-mutating SMB tests (cargo runs them in parallel).
#[cfg(test)]
pub static SMB_CFG_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ooo_queue_backstop_thresholds() {
let _lock = super::SMB_CFG_TEST_LOCK.lock().unwrap();
// defaults: 64 MiB queue caps in both directions
assert_eq!(SMBState::ooo_queue_backstop(Direction::ToClient), 67108864);
assert_eq!(SMBState::ooo_queue_backstop(Direction::ToServer), 67108864);
@ -395,6 +401,7 @@ mod tests {
#[test]
fn test_ooo_queue_backstop_cnt_thresholds() {
// defaults: 64 chunks in both directions
let _lock = super::SMB_CFG_TEST_LOCK.lock().unwrap();
assert_eq!(SMBState::ooo_queue_backstop_cnt(Direction::ToClient), 64);
assert_eq!(SMBState::ooo_queue_backstop_cnt(Direction::ToServer), 64);

@ -743,6 +743,10 @@ pub struct SMBState {
/// transactions list
pub transactions: VecDeque<SMBTransaction>,
/// file txs held in the transactions list: the OOO backstop scan is a
/// no-op without one, so it is skipped when this is 0
pub(crate) file_tx_count: u32,
/// tx counter for assigning incrementing id's to tx's
tx_id: u64,
@ -814,6 +818,7 @@ impl SMBState {
check_post_gap_file_txs: false,
post_gap_files_checked: false,
transactions: VecDeque::new(),
file_tx_count: 0,
tx_id: 0,
dialect: 0,
dialect_vec: None,
@ -863,6 +868,12 @@ impl SMBState {
}
}
if found {
if matches!(
self.transactions[index].type_data,
Some(SMBTransactionTypeData::FILE(_))
) {
self.file_tx_count = self.file_tx_count.saturating_sub(1);
}
SCLogDebug!(
"freeing TX with ID {} TX.ID {} at index {} left: {} max id: {}",
tx_id,
@ -2918,9 +2929,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-read-size");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_READ_SIZE = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_READ_SIZE = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-read-size");
}
},
Err(_) => {
SCLogError!("Invalid max-read-size value");
}
@ -2929,9 +2945,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-write-size");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_WRITE_SIZE = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_WRITE_SIZE = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-write-size");
}
},
Err(_) => {
SCLogError!("Invalid max-write-size value");
}
@ -2940,9 +2961,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-write-queue-size");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_WRITE_QUEUE_SIZE = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_WRITE_QUEUE_SIZE = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-write-queue-size");
}
},
Err(_) => {
SCLogError!("Invalid max-write-queue-size value");
}
@ -2951,9 +2977,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-write-queue-cnt");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_WRITE_QUEUE_CNT = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_WRITE_QUEUE_CNT = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-write-queue-cnt");
}
},
Err(_) => {
SCLogError!("Invalid max-write-queue-cnt value");
}
@ -2962,9 +2993,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-read-queue-size");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_READ_QUEUE_SIZE = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_READ_QUEUE_SIZE = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-read-queue-size");
}
},
Err(_) => {
SCLogError!("Invalid max-read-queue-size value");
}
@ -2973,9 +3009,14 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
let retval = conf_get("app-layer.protocols.smb.max-read-queue-cnt");
if let Some(val) = retval {
match get_memval(val) {
Ok(retval) => {
SMB_CFG_MAX_READ_QUEUE_CNT = retval as u32;
}
Ok(retval) => match u32::try_from(retval) {
Ok(v) => {
SMB_CFG_MAX_READ_QUEUE_CNT = v;
}
Err(_) => {
SCLogError!("value out of range for smb.max-read-queue-cnt");
}
},
Err(_) => {
SCLogError!("Invalid max-read-queue-cnt value");
}
@ -2983,7 +3024,12 @@ pub unsafe extern "C" fn SCRegisterSmbParser() {
}
if let Some(val) = conf_get("app-layer.protocols.smb.max-tx") {
if let Ok(v) = val.parse::<usize>() {
SMB_MAX_TX = v;
// 0 would evict every transaction on insert
if v == 0 {
SCLogError!("Invalid value for smb.max-tx");
} else {
SMB_MAX_TX = v;
}
} else {
SCLogError!("Invalid value for smb.max-tx");
}

@ -1070,6 +1070,9 @@ pub fn smb1_write_request_record(
None => b"<unknown>".to_vec(),
};
let mut set_event_fileoverlap = false;
// a queue-limit reject arms the skip itself; the trailing
// set_file_left must not claim the same bytes again
let mut rejected = false;
let found =
match state.get_file_tx_by_fuid_with_open_file(&file_fid, Direction::ToServer) {
Some(tx) => {
@ -1089,6 +1092,7 @@ pub fn smb1_write_request_record(
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
state.set_skip(Direction::ToServer, nbss_remaining);
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -1117,6 +1121,8 @@ pub fn smb1_write_request_record(
let hdr = SMBCommonHdr::from1(r, SMBHDR_TYPE_HEADER);
let vercmd = SMBVerCmdStat::new1_with_ntstatus(command, r.nt_status);
smb_write_dcerpc_record(state, vercmd, hdr, rd.data);
// pipe data is DCERPC, not file data: no trailing promise
rejected = true;
} else {
let Some(tx) = state.new_file_tx(&file_fid, &file_name, Direction::ToServer)
else {
@ -1146,8 +1152,7 @@ pub fn smb1_write_request_record(
tx.response_done = true;
tx.tx_data.0.files_opened = 0;
state.set_skip(Direction::ToServer, nbss_remaining);
// The trailing set_file_left also claims these bytes: safe -- records
// only reach here with no chunk armed, and handle_skip runs first.
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -1166,13 +1171,17 @@ pub fn smb1_write_request_record(
state.set_event(SMBEvent::FileOverlap);
}
state.set_file_left(
Direction::ToServer,
rd.len,
rd.data.len() as u32,
file_fid.to_vec(),
);
if !rejected {
state.set_file_left(
Direction::ToServer,
rd.len,
rd.data.len() as u32,
file_fid.to_vec(),
);
}
// The close runs even when the write chunk was rejected: it is
// the terminal command for the file tx.
if command == SMB1_COMMAND_WRITE_AND_CLOSE {
let _name = state.guid2name_cache.pop(&file_fid);
SCLogDebug!("closing FID {:?}", file_fid);
@ -1222,6 +1231,9 @@ pub fn smb1_read_response_record(
Some(n) => (n.is_pipe, n.name.to_vec()),
_ => (false, Vec::new()),
};
// a queue-limit reject arms the skip itself; the trailing
// set_file_left must not claim the same bytes again
let mut rejected = false;
if !is_pipe {
let file_name = match state.guid2name_cache.get(&file_fid) {
Some(n) => n.to_vec(),
@ -1249,6 +1261,7 @@ pub fn smb1_read_response_record(
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
state.set_skip(Direction::ToClient, nbss_remaining);
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -1294,9 +1307,8 @@ pub fn smb1_read_response_record(
tx.request_done = true;
tx.response_done = true;
tx.tx_data.0.files_opened = 0;
// The handler tail below also arms set_file_left for these bytes:
// safe -- reached only with no chunk armed, handle_skip first.
state.set_skip(Direction::ToClient, nbss_remaining);
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -1326,14 +1338,18 @@ pub fn smb1_read_response_record(
&[]
};
smb_read_dcerpc_record(state, vercmd, hdr, pure_fid, rd.data);
// pipe data is DCERPC, not file data: no trailing promise
rejected = true;
}
state.set_file_left(
Direction::ToClient,
rd.len,
rd.data.len() as u32,
file_fid.to_vec(),
);
if !rejected {
state.set_file_left(
Direction::ToClient,
rd.len,
rd.data.len() as u32,
file_fid.to_vec(),
);
}
}
_ => {
events.push(SMBEvent::MalformedData);
@ -1382,3 +1398,99 @@ fn smb1_response_record_generic(state: &mut SMBState, r: &SmbRecord, events: Vec
tx.set_events(events);
}
}
#[cfg(test)]
mod tests {
use super::*;
// WRITE_ANDX payload: declares 20 bytes, carries 10 (the 51-byte shape
// with 20 carried is pinned by the layout test in smb1_records).
const WRITE_ANDX_BLOB: &str =
"0eff000000014000000000ff00000008001400000014003f000000000014004142434445464748494a";
// READ_ANDX response payload: declares 20, carries 18, no padding
// (data_offset 54 == 36 + 2 * wct, bcc == data_len_low).
fn read_response_blob() -> Vec<u8> {
let mut b = vec![9u8, 0xfe, 0x00, 0x00, 0x00];
b.extend_from_slice(&[0u8; 6]);
b.extend_from_slice(&20u16.to_le_bytes());
b.extend_from_slice(&54u16.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&[0u8; 6]);
b.extend_from_slice(&20u16.to_le_bytes());
b.extend_from_slice(b"ABCDEFGHIJKLMNOPQR");
b
}
// Pipe records are DCERPC, not file data: the pipe arms must set rejected
// so the trailing set_file_left guard does not arm a promise for them
// (parity with SMB2).
//
// Each record declares more data than it carries, with nbss_remaining
// covering the gap (write: 20 declared / 10 carried / nbss 10, read:
// 20 / 18 / 2), so both pass the too-large guard and reach the pipe
// arms. Pre-fix (no rejected flag) the guard would arm file_ts_left =
// 10 and file_tc_left = 2; the sentinel asserts catch that.
#[test]
fn test_smb1_pipe_records_arm_no_promise() {
let mut state = SMBState::new();
let wblob = hex::decode(WRITE_ANDX_BLOB).unwrap();
// parse-shape pin: declared 20, carried 10
let (_, wrd) = parse_smb1_write_andx_request_record(&wblob, SMB1_HEADER_SIZE).unwrap();
assert_eq!(wrd.len, 20);
assert_eq!(wrd.data.len(), 10);
let wrec = SmbRecord {
command: SMB1_COMMAND_WRITE_ANDX,
is_dos_error: false,
nt_status: SMB_NTSTATUS_SUCCESS,
flags: 0,
flags2: 0x0140,
tree_id: 1,
user_id: 1,
multiplex_id: 1,
process_id: 1,
ssn_id: 1,
data: &wblob,
};
let tree_key = SMBCommonHdr::from1(&wrec, SMBHDR_TYPE_SHARE);
state
.ssn2tree_cache
.put(tree_key, SMBTree::new(b"IPC".to_vec(), true));
state.file_ts_left = 0x1234;
smb1_write_request_record(
&mut state,
&wrec,
SMB1_HEADER_SIZE,
SMB1_COMMAND_WRITE_ANDX,
10,
);
assert_eq!(state.file_ts_left, 0x1234);
let rdata = read_response_blob();
// parse-shape pin: declared 20, carried 18
let (_, rdrd) = parse_smb_read_andx_response_record(&rdata).unwrap();
assert_eq!(rdrd.len, 20);
assert_eq!(rdrd.data.len(), 18);
let rrec = SmbRecord {
command: SMB1_COMMAND_READ_ANDX,
is_dos_error: false,
nt_status: SMB_NTSTATUS_SUCCESS,
flags: 0x80, // SMB1_FLAGS_RESPONSE
flags2: 0x0140,
tree_id: 1,
user_id: 1,
multiplex_id: 1,
process_id: 1,
ssn_id: 1,
data: &rdata,
};
let off_key = SMBCommonHdr::from1(&rrec, SMBHDR_TYPE_OFFSET);
state.read_offset_cache.put(
off_key,
SMBFileGUIDOffset::new(vec![0x40, 0x01, 0x01, 0x00, 0x00, 0x00], 0),
);
state.file_tc_left = 0x1234;
smb1_read_response_record(&mut state, &rrec, SMB1_HEADER_SIZE, 2);
assert_eq!(state.file_tc_left, 0x1234);
}
}

@ -160,6 +160,8 @@ pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
SCLogDebug!("SMBv2 READ: GUID {:?} offset {}", file_guid, offset);
let mut set_event_fileoverlap = false;
// set_file_left must not claim bytes a skip already consumed
let mut rejected = false;
// look up existing tracker and if we have it update it
let found = if let Some(tx) =
state.get_file_tx_by_fuid_with_open_file(&file_guid, Direction::ToClient)
@ -177,6 +179,7 @@ pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
state.set_skip(Direction::ToClient, nbss_remaining);
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -237,9 +240,11 @@ pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER);
let vercmd = SMBVerCmdStat::new2_with_ntstatus(SMB2_COMMAND_READ, r.nt_status);
smb_read_dcerpc_record(state, vercmd, hdr, &file_guid, rd.data);
rejected = true;
} else if is_pipe {
SCLogDebug!("non-DCERPC pipe");
state.set_skip(Direction::ToClient, nbss_remaining);
rejected = true;
} else {
let file_name = match state.guid2name_cache.get(&file_guid) {
Some(n) => n.to_vec(),
@ -271,8 +276,7 @@ pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
tx.response_done = true;
tx.tx_data.0.files_opened = 0;
state.set_skip(Direction::ToClient, nbss_remaining);
// The trailing set_file_left also claims these bytes: safe -- records
// only reach here with no chunk armed, and handle_skip runs first.
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -291,12 +295,14 @@ pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
if set_event_fileoverlap {
state.set_event(SMBEvent::FileOverlap);
}
state.set_file_left(
Direction::ToClient,
rd.len,
rd.data.len() as u32,
file_guid.to_vec(),
);
if !rejected {
state.set_file_left(
Direction::ToClient,
rd.len,
rd.data.len() as u32,
file_guid.to_vec(),
);
}
}
_ => {
SCLogDebug!("SMBv2: failed to parse read response");
@ -342,6 +348,8 @@ pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
};
let mut set_event_fileoverlap = false;
// set_file_left must not claim bytes a skip already consumed
let mut rejected = false;
let found = if let Some(tx) =
state.get_file_tx_by_fuid_with_open_file(&file_guid, Direction::ToServer)
{
@ -361,6 +369,7 @@ pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
state.set_skip(Direction::ToServer, nbss_remaining);
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -422,9 +431,11 @@ pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER);
let vercmd = SMBVerCmdStat::new2(SMB2_COMMAND_WRITE);
smb_write_dcerpc_record(state, vercmd, hdr, wr.data);
rejected = true;
} else if is_pipe {
SCLogDebug!("non-DCERPC pipe: skip rest of the record");
state.set_skip(Direction::ToServer, nbss_remaining);
rejected = true;
} else {
let Some(tx) = state.new_file_tx(&file_guid, &file_name, Direction::ToServer)
else {
@ -454,8 +465,7 @@ pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
tx.response_done = true;
tx.tx_data.0.files_opened = 0;
state.set_skip(Direction::ToServer, nbss_remaining);
// The trailing set_file_left also claims these bytes: safe -- records
// only reach here with no chunk armed, and handle_skip runs first.
rejected = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
@ -474,12 +484,14 @@ pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_rema
if set_event_fileoverlap {
state.set_event(SMBEvent::FileOverlap);
}
state.set_file_left(
Direction::ToServer,
wr.wr_len,
wr.data.len() as u32,
file_guid.to_vec(),
);
if !rejected {
state.set_file_left(
Direction::ToServer,
wr.wr_len,
wr.data.len() as u32,
file_guid.to_vec(),
);
}
}
_ => {
state.set_event(SMBEvent::MalformedData);
@ -986,3 +998,118 @@ pub fn smb2_response_record(state: &mut SMBState, r: &Smb2Record) {
};
}
}
#[cfg(test)]
mod tests {
use super::*;
// link stub: the C file-flags helper has no Rust definition; the tests
// never rely on its value
#[no_mangle]
pub extern "C" fn SCFileFlowFlagsToFlags(_f: u16, _d: u8) -> u16 {
0
}
// the FFI file ops are no-ops under cfg(test), so a default context is
// safe (the filetracker needs it set to track chunks)
static TEST_SBCFG: StreamingBufferConfig = StreamingBufferConfig {
buf_size: 2048,
max_regions: 8,
region_gap: 0,
Calloc: None,
Realloc: None,
Free: None,
};
static TEST_FC: SuricataFileContext = SuricataFileContext {
files_sbcfg: &TEST_SBCFG,
};
// SMB2 write body (parse_smb2_request_write layout): skip4, len, offset,
// guid, channel, remaining, write_flags, skip4, data.
use crate::core::{StreamingBufferConfig, SuricataFileContext};
fn write_body(offset: u64, guid: &[u8], data: &[u8], declared: u32) -> Vec<u8> {
let mut b = vec![0u8; 4];
b.extend(declared.to_le_bytes());
b.extend(offset.to_le_bytes());
b.extend(guid);
b.extend([0u8; 16]); // channel + remaining + write_flags + skip
b.extend(data);
b
}
fn write_rec(data: &[u8], mid: u64) -> Smb2Record<'_> {
Smb2Record {
direction: 0,
header_len: 64,
nt_status: 0,
command: SMB2_COMMAND_WRITE,
message_id: mid,
tree_id: 1,
async_id: 0,
session_id: 1,
data,
}
}
// A queue-limit-rejected record must not arm the trailing file-data
// promise: its remainder is consumed by the skip, so the promise would
// otherwise claim the following record's bytes.
#[test]
fn test_smb2_write_rejected_partial_arms_no_promise() {
let _lock = crate::smb::files::SMB_CFG_TEST_LOCK.lock().unwrap();
// restores the cfg statics this test mutates; leaking them made later
// write-queue-dependent tests order-dependent
struct CfgGuard;
impl Drop for CfgGuard {
fn drop(&mut self) {
unsafe {
SURICATA_SMB_FILE_CONFIG = None;
}
unsafe {
SMB_CFG_MAX_WRITE_QUEUE_CNT = 64;
}
unsafe {
SMB_CFG_MAX_WRITE_QUEUE_SIZE = 67108864;
}
}
}
let _cfg_guard = CfgGuard;
unsafe {
SURICATA_SMB_FILE_CONFIG = Some(&TEST_FC);
}
let mut state = SMBState::new();
let guid: Vec<u8> = [0xAAu8, 0xBB, 0xCC, 0xDD]
.into_iter()
.chain(std::iter::repeat_n(0, 12))
.collect();
// w1 (200 declared, 100 carried, nbss 0): rejected by the NBSS guard
// (200 - 100 > 0) before the tracker is fed, so it queues nothing.
let d1: Vec<u8> = vec![b'A'; 100];
let b1 = write_body(0, &guid, &d1, 200);
smb2_write_request_record(&mut state, &write_rec(&b1, 1), 0);
// w2 (100 declared, 100 carried at offset 300): creates the file tx
// and queues one OOO chunk (inflight count 1).
let d2: Vec<u8> = vec![b'B'; 100];
let b2 = write_body(300, &guid, &d2, 100);
smb2_write_request_record(&mut state, &write_rec(&b2, 2), 0);
// w3 (700 declared, 444 carried, 256 nbss, OOO at 600): a new OOO
// entry would make the inflight count 1 + 1 > 1, so the queue count
// limit rejects it on the partial path.
unsafe {
SMB_CFG_MAX_WRITE_QUEUE_CNT = 1;
}
unsafe {
SMB_CFG_MAX_WRITE_QUEUE_SIZE = 0;
}
let d3: Vec<u8> = vec![b'X'; 444];
let b3 = write_body(600, &guid, &d3, 700);
// sentinel: a regressed guard would overwrite it with the armed 256
state.file_ts_left = 0x1234;
smb2_write_request_record(&mut state, &write_rec(&b3, 3), 256);
assert_eq!(
state.file_ts_left, 0x1234,
"rejected record must not arm the file-data promise"
);
}
}

@ -79,7 +79,11 @@ static int JsonNFSLogger(ThreadVars *tv, void *thread_data,
if (SCNfsTxLoggingIsFiltered(state, nfstx))
return TM_ECODE_OK;
SCJsonBuilder *jb = CreateEveHeader(p, LOG_DIR_PACKET, "nfs", NULL, thread->ctx);
/* Log in the flow (request) direction, as SMB, HTTP and DNS do. A record
* is stamped once, when the tx ends, so with the packet direction the same
* flow would report its txs in both directions: toclient for the txs a real
* reply completed, toserver for the ones the flow-timeout flush ended. */
SCJsonBuilder *jb = CreateEveHeader(p, LOG_DIR_FLOW, "nfs", NULL, thread->ctx);
if (unlikely(jb == NULL)) {
return TM_ECODE_OK;
}

Loading…
Cancel
Save