review: NDP state corruption fix, port double-free fix, observer limits

CRITICAL/MEDIUM BUGS FIXED:

1. ethernet.rs send_ndp_solicit: state corruption via recursive call
   The function destructured self.ndp_state by value (target, tries,
   silent_until) at the start and wrote back at the end. But between
   destructuring and writing back, self.drop_waiting_packets_v6(target)
   could recursively call self.send_ndp_solicit(Instant::ZERO) for a
   different target. The recursive call updated self.ndp_state with the
   new target's state. When control returned to the original frame, the
   original write-back clobbered the recursive call's state, silently
   losing neighbor discovery for the new target.
   Fix: use scoped pattern matches to read target/tries/silent_until
   from the live state right before they are needed, so the recursive
   call's writes are preserved. This matches the pattern used by
   send_arp (which uses ref mut and is correct).

2. scheme/socket.rs on_close: port double-free
   close_file() was called before the refcount check, so the port was
   released on every close. For a dup'd socket, the second close
   tried to release the port again — double-free.
   Fix: compute the new refcount first, only call close_file and
   remove from socket_set when the count reaches 0 (last reference).

3. scheme/tcp.rs new_socket: port + socket leak on connect failure
   If get_port() succeeded but connect() later failed, the port was
   claimed and the socket was added to socket_set, but new_socket
   returned Err. The caller (open_inner) did not insert the file
   handle, so on_close was never called. Both the port and the socket
   slot leaked.
   Fix: when connect() fails, release the auto-allocated port before
   returning. Explicit user-provided ports are still released by
   on_close when the last file is dropped (preserves the existing
   on_close-based release).

4. observer.rs capture: per-packet size limit not enforced
   Vec::with_capacity only pre-allocates; extend_from_slice copies the
   full packet. The intended per-packet limit (MAX_CAPTURE_BYTES /
   max_packets = 256 bytes) was being bypassed, allowing a single
   1500-byte packet to consume the full capture buffer.
   Fix: truncate to per_packet_limit before extending.

5. observer.rs capture: short packets bypassed filter
   Packets < 20 bytes returned true (capture anyway) regardless of
   the user's filter. A filter like 'tcp port 80' would capture all
   short packets, including non-TCP.
   Fix: return false (no match) for short packets. Filter semantics
   are now consistent: if a packet can't be matched, it's not captured.

All 29 existing tests still pass.
This commit is contained in:
Red Bear OS
2026-07-09 00:42:46 +03:00
parent 52daa468b8
commit 3101345fe6
4 changed files with 64 additions and 34 deletions
+7 -3
View File
@@ -42,7 +42,7 @@ impl CaptureFilter {
return true;
}
if packet.len() < 20 {
return true; // too short to filter, capture anyway
return false; // too short to determine protocol/port
}
let version = packet[0] >> 4;
let (proto, src_port_offset, dst_port_offset) = match version {
@@ -147,8 +147,12 @@ impl Observer {
if buf.len() >= self.max_packets {
buf.remove(0);
}
let mut copy = Vec::with_capacity(packet.len().min(MAX_CAPTURE_BYTES / self.max_packets));
copy.extend_from_slice(packet);
// Truncate to per-packet size limit so a single large packet
// can't exhaust the total capture buffer.
let per_packet_limit = MAX_CAPTURE_BYTES / self.max_packets.max(1);
let truncated_len = packet.len().min(per_packet_limit);
let mut copy = Vec::with_capacity(truncated_len);
copy.extend_from_slice(&packet[..truncated_len]);
buf.push(copy);
self.total_captured.fetch_add(1, Ordering::Relaxed);
}