Project

General

Profile

Bug #9342

Updated by Jeff Lucovsky 2 days ago

When pcap-log runs with `conditional: alerts` or `conditional: tag`, Suricata keeps a small pcap header next to every TCP segment. The memory for that header gets added to the stream reassembly memory counter twice, but it's only subtracted once when the segment is freed. Any config like this triggers it: 

 <pre> 
 outputs: 
   - pcap-log: 
       enabled: yes 
       conditional: alerts 
 </pre> 

 Each segment freed by the segment pool frees leaves 88 bytes behind in the counter (24 bytes for the header struct plus a 64 byte buffer). The memory itself is freed fine, so you won't see it in RSS. But the counter keeps creeping up as the pool frees segments, and since the reassembly memcap check uses that counter, Suricata can eventually start refusing new segments (tcp.segment_memcap_drop goes up) even though real usage is below stream.reassembly.memcap. 

 The two places that add it are TcpSegmentPoolAlloc() and TcpSegmentPoolInit(). TcpSegmentPoolCleanup() subtracts it once. The extra add in Alloc() came in with 2bf3172dd ("stream: memcap tracking for TcpSegment alloc"), first released in 7.0.0, so main and 8.0.x are both affected. 

 There's also a leak on the same path. Alloc() allocates the header, but when Init() fails its memcap check, the pool frees the segment without calling Cleanup(), so the header is never freed. Because Alloc() has already added its 88 bytes, Init() fails whenever usage is within 88 bytes of the memcap, so on main this happens near the memcap even with one thread. Each failure leaks 88 bytes and leaves another 88 in the counter. 

 Proposed fix: drop the add in TcpSegmentPoolAlloc() so Init() and Cleanup() pair up, the same way sizeof(TcpSegment) is already handled, and free the header in Init() before it returns failure. After that, Init() can only fail if another thread raises usage or the memcap is lowered between the two calls. Two handled. A unit tests cover this: test runs one checks that an Alloc()/Init()/Cleanup() cycle leaves the counter unchanged, and the other forces Init() to fail and checks that the header is freed. counter ends where it started. A suricata-verify test reuses the nfs3-lookup-overclaim-skip pcap with segment-prealloc set to 0, so each of the pool frees all 725 segments the pool allocates is freed through it. On segments; on x86_64 the final tcp.reassembly_memuse is 76040 bytes before the fix and 12240 after. 

 The same split causes a leak when TcpSegmentPoolInit() fails its memcap check. Alloc() has already allocated the pcap header and its buffer, but the pool frees the segment without calling Cleanup(), so that memory is never freed. The fix frees the header in Init() before it returns failure, and a second unit test forces that failure and checks the header is gone.

Back