Skip to content

SCTP decoder and sticky buffers v5 - #15497

Closed
glongo wants to merge 9 commits into
OISF:mainfrom
glongo:dev-4251-sctp-v5
Closed

SCTP decoder and sticky buffers v5#15497
glongo wants to merge 9 commits into
OISF:mainfrom
glongo:dev-4251-sctp-v5

Conversation

@glongo

@glongo glongo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Link to ticket: https://redmine.openinfosecfoundation.org/issues/4251

Describe changes:

  • sctp.has_init, sctp.has_data, sctp.has_abort keywords removed

Previous PR: #15482

SV_BRANCH=OISF/suricata-verify#2999

glongo added 9 commits June 1, 2026 17:49
Extend the SCTP decoder to parse chunk headers after the 12-byte common
header. Each chunk is validated for minimum header size and length
consistency per RFC 4960 sec 3.2.

Add SCTPChunkHdr and SCTPVars structs to track per-packet chunk metadata

Add five new decoder events for protocol violations:
- SCTP_CHUNK_TOO_SMALL: insufficient data for a chunk header
- SCTP_CHUNK_LEN_INVALID: chunk length < 4 or exceeds packet
- SCTP_INIT_CHUNK_NOT_ALONE: INIT/INIT_ACK bundled (RFC 4960 sec 6.10)
- SCTP_INIT_WITH_NON_ZERO_VTAG: INIT with vtag != 0 (RFC 4960 sec 8.5.1)
- SCTP_DATA_WITH_ZERO_VTAG: DATA chunk with vtag == 0

Ticket OISF#4251
Implement a sticky buffer to match the raw SCTP header
(common header + chunks)

Ticket OISF#4251
Add a U8 numeric keyword to match the first SCTP chunk type in a packet
with prefilter support.

Ticket OISF#4251
Add a U8 numeric keyword to match the number of SCTP chunks
parsed in a packet with prefilter support.

Ticket OISF#4251
Add a U32 numeric keyword to match the SCTP verification tag
from the common header with prefilter support.

Ticket OISF#4251
Log SCTP-specific fields in the EVE JSON "sctp" object for alert events.

Ticket OISF#4251
Track the first DATA chunk's data offset and length during chunk iteration,
then reassign p->payload to point at the user data.

When no DATA chunk is present (INIT, SACK, HEARTBEAT, etc.),
payload_len is set to 0 since there is no application data.

Ticket OISF#4251
Add a sctp.data sticky multi-buffer that allows content matching on the bytes
inside any of the SCTP DATA chunks extracted.

Ticket OISF#4251
Add documentation for all sctp keywords.

Ticket OISF#4251
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.18626% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.76%. Comparing base (736f506) to head (bd1f229).

Additional details and impacted files
@@           Coverage Diff            @@
##             main   #15497    +/-   ##
========================================
  Coverage   82.75%   82.76%            
========================================
  Files         998     1005     +7     
  Lines      272550   273301   +751     
========================================
+ Hits       225562   226195   +633     
- Misses      46988    47106   +118     
Flag Coverage Δ
fuzzcorpus 61.42% <40.87%> (+<0.01%) ⬆️
livemode 18.28% <14.84%> (+<0.01%) ⬆️
netns 22.71% <14.84%> (-0.09%) ⬇️
pcap 44.91% <35.76%> (-0.08%) ⬇️
suricata-verify 66.45% <72.74%> (-0.03%) ⬇️
unittests 58.46% <65.25%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@suricata-qa

Copy link
Copy Markdown

WARNING:

field baseline test %
SURI_TLPW2_single_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -
SURI_TLPW2_autofp_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -
SURI_TLPW1_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -
SURI_TLPR1_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -
IPS_AFP_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -
TREX_GENERIC_stats_chk
.decoder.event.sctp.chunk_too_small - 0 -
.decoder.event.sctp.chunk_len_invalid - 0 -
.decoder.event.sctp.init_chunk_not_alone - 0 -
.decoder.event.sctp.init_with_non_zero_vtag - 0 -
.decoder.event.sctp.data_with_zero_vtag - 0 -
.sctp.init - 0 -
.sctp.init_ack - 0 -
.sctp.data - 0 -
.sctp.abort - 0 -
.sctp.shutdown - 0 -

Pipeline = 31783

@victorjulien victorjulien left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there I think, see inline comments.

Comment thread rules/sctp-events.rules
@@ -0,0 +1,8 @@
# SCTP decoder event rules.
# SID's fall in the 2239000-2239999 range. See http://doc.emergingthreats.net/bin/view/Main/SidAllocation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we recently updated these for other files

Comment thread src/decode-sctp.c
break;
}

if (chunk_cnt < SCTP_MAX_TRACKED_CHUNKS) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be an event when there are more chunks than we track?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful for sure.

Comment thread src/decode-sctp.c
has_init_ack = true;
break;
case SCTP_CHUNK_TYPE_DATA:
if (data_chunk_cnt < SCTP_MAX_DATA_CHUNKS && chunk_len >= SCTP_DATA_CHUNK_HDR_LEN) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, should there be an event if max is exceeded?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Comment thread src/decode-sctp.h
#define SCTP_CHUNK_HDR_LEN 4

/** max number of chunks tracked per packet for detection/logging */
#define SCTP_MAX_TRACKED_CHUNKS 16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how was 16 chosen? Add as a comment in the code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a choice I made to keep per-packet overhead low while still allowing some room to track chunks.

SCTP has no hard limit on the number of chunks per packet. A packet can carry as many chunks as fit within the MTU, though in practice most packets contain only a few chunks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about the maximum packet size, which is probably close to 64k, how many iterations of the loop will there be at max? So max size / smallest chunk record size I suppose?

Comment thread src/decode-sctp.h
#define SCTP_MAX_TRACKED_CHUNKS 16

/** max number of DATA chunk payloads tracked per packet */
#define SCTP_MAX_DATA_CHUNKS 16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

*/
static InspectionBuffer *GetBuffer(DetectEngineThreadCtx *det_ctx,
const DetectEngineTransforms *transforms, Packet *p, const int list_id,
const uint32_t local_id, const uint8_t chunk_idx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since local_id == chunk_idx every time, could just pass one

}

const DetectU8Data *data = (const DetectU8Data *)ctx;
const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has been validated by the decoder already, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the decoder stores the raw count at line 146:
p->l4.vars.sctp.chunk_cnt = chunk_cnt;

And chunk_cnt is incremented unconditionally for every chunk, even after SCTP_MAX_TRACKED_CHUNKS is reached.

}

const PrefilterPacketU8HashCtx *h = pectx;
const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

uint32_t seen[8] = { 0 };
for (uint8_t i = 0; i < cnt; i++) {
const uint8_t val = p->l4.vars.sctp.chunk_types[i];
if (seen[val >> 5] & (1U << (val & 0x1F))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain the shift & mask logic here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose is to skip calling PrefilterAddSids for duplicate chunk types within the same packet.
If two chunks both have type DATA, the sigs for that type are only added once.

So, line 119 is used to check if val has been seen.

Comment thread src/output-json.c
SCJbOpenObject(js, "sctp");
SCJbSetUint(js, "vtag", SCTP_GET_RAW_VTAG(PacketGetSCTP(p)));
SCJbSetUint(js, "chunk_cnt", p->l4.vars.sctp.chunk_cnt);
const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, I think we should be able to trust the decoder. To double check we can add debug validate bug on's

Comment thread src/decode-sctp.c
break;
}

const SCTPChunkHdr *chunk = (const SCTPChunkHdr *)(pkt + offset);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be an unaligned access? Should probably be a memcpy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this line following the same pattern as, for instance, PacketSetTCP?
buf is cast directly to a TCPHdr *:

p->l4.hdrs.tcph = (TCPHdr *)buf;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we're slowing going to update these.

Comment thread src/decode-sctp.c
p->proto = IPPROTO_SCTP;

if (p->payload_len > 0) {
if (DecodeSCTPChunks(p, p->payload, p->payload_len) < 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we pass in payload_len, which is uint16_t

Comment thread src/decode-sctp.c
* \retval 0 on success (even if some events were set)
* \retval -1 on fatal error (packet should be rejected)
*/
static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint32_t len)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to use uint32_t here? We pass in uint16_t

Comment thread src/decode-sctp.c
}

/* chunk must not extend beyond available data */
if (chunk_len > (len - offset)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also have an absolute max value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to use UINT16_MAX as absolute value but, as expected, the compiler is complaining:

decode-sctp.c:91:23: warning: comparison is always false due to limited range of data type [-Wtype-limits]
   91 |         if (chunk_len > UINT16_MAX || chunk_len > (len - offset)) {

I wonder if I should use a different value or I can remove this check.

Comment thread src/decode-sctp.c
p->l4.vars.sctp.data_lens[data_chunk_cnt] = chunk_len - SCTP_DATA_CHUNK_HDR_LEN;
data_chunk_cnt++;
}
has_data = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a scenario where we set this but didn't track the chunks? If so, what would happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two cases:

  1. chunk_len < SCTP_DATA_CHUNK_HDR_LEN (Malformed data chunk)
  2. data_chunk_cnt >= SCTP_MAX_DATA_CHUNKS) (more DATA chunks than the array can hold)

The only effect is that counter_sctp_data is incremented, see DecodeSCTP line 210:

if (p->l4.vars.sctp.has_data) {
    StatsCounterIncr(&tv->stats, dtv->counter_sctp_data);
}

Comment thread src/decode-sctp.c
if (padded_len < SCTP_CHUNK_HDR_LEN) {
padded_len = SCTP_CHUNK_HDR_LEN;
}
offset += padded_len;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a DEBUG_VALIDATE_BUG_ON(offset > len) here, as we shouldn't advance offset beyond len

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but note that offset can legitimately exceed len after padding.
The padding rounds up to a 4-byte boundary, so if the last chunk ends at len but isn't 4-byte aligned, padded_len > chunk_len and offset overshoots len.

We'd need to check only for the case where offset exceeds len by more than 3 bytes (the max padding).

@glongo

glongo commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Replaced with #15534

@glongo glongo closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants