Skip to content

Commit c078b47

Browse files
committed
Reject trailing NetIPC frame bytes
1 parent 04f8b09 commit c078b47

11 files changed

Lines changed: 349 additions & 6 deletions

File tree

.agents/sow/current/SOW-0015-20260605-codacy-scope-and-maintainability.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,64 @@ Validation completed for this duplication-reduction increment:
738738
- `cppcheck_missingIncludeSystem` appears non-actionable in Codacy because it cannot see ordinary system headers in the analyzer environment.
739739
- `cppcheck_unusedStructMember` is noisy for NetIPC wire-layout structs and internal callback tables, but disabling it affects the whole Netdata repository unless a narrower suppression path is chosen.
740740

741+
### 2026-06-07 - PR 22649 latest review-thread pass
742+
743+
Netdata PR #22649 was rechecked after vendoring SDK commit `04f8b09` into Netdata commit `d60e6bc19a`.
744+
745+
Facts from GitHub, SonarCloud, and CI:
746+
747+
- GitHub review threads: ten total, eight resolved, two open.
748+
- SonarCloud PR issue API: zero unresolved issues.
749+
- SonarCloud PR hotspot API: zero unresolved hotspots.
750+
- Netdata CI: zero failed checks at the snapshot; 103 checks still running and one CLA check pending.
751+
752+
Open GitHub review threads:
753+
754+
- `src/go/pkg/netipc/transport/internal/framing/handshake.go`: cubic claimed the negotiated request payload should follow `min(max(client, server), 256MB)`.
755+
- Current public spec evidence says otherwise: `docs/level1-wire-envelope.md` defines proposals above `1 MiB` as `LIMIT_EXCEEDED` and says accepted request payload limits are echoed unchanged.
756+
- Same-language evidence: C POSIX, C Windows, Rust POSIX, Rust Windows, and Go all implement the same `1 MiB` reject plus echo behavior.
757+
- Decision for this increment: do not change the handshake contract as a PR-comment side effect. Add source clarification if needed and report the thread as stale/contract-conflicting unless the user explicitly chooses to revise the protocol across docs, C, Rust, Go, and tests.
758+
- `src/go/pkg/netipc/transport/internal/framing/receive.go`: cubic claimed packets whose received length exceeds the declared frame length should be rejected instead of accepted.
759+
- Local verification found the finding is real and same-pattern across all packet transports:
760+
- Go shared framing accepted `n >= totalMsg`.
761+
- C POSIX UDS accepted `(size_t)n >= total_msg`.
762+
- C Windows Named Pipe accepted `n >= total_msg`.
763+
- Rust POSIX UDS accepted `n >= total_msg`.
764+
- Rust Windows Named Pipe accepted `n >= total_msg`.
765+
- Decision for this increment: fix the whole class so exact-length packets are accepted, shorter packets enter the chunked path, and longer packets fail as framing/protocol errors.
766+
767+
Implemented SDK follow-up:
768+
769+
- Added a Go shared-framing comment clarifying the current Level 1 handshake contract: request payload limits are client-proposed, proposals above the wire cap are rejected, and accepted values are echoed unchanged.
770+
- Changed non-chunked receive completion in Go shared framing, C POSIX UDS, C Windows Named Pipe, Rust POSIX UDS, and Rust Windows Named Pipe from `received_len >= declared_total` to:
771+
- reject `received_len > declared_total` as a protocol/framing error.
772+
- accept only `received_len == declared_total` as a complete non-chunked message.
773+
- keep `received_len < declared_total` as the chunked-message path.
774+
- Added regression tests for packets that declare one payload byte but contain two bytes in the same packet:
775+
- Go shared framing unit test.
776+
- POSIX C UDS integration test.
777+
- Windows C Named Pipe integration test.
778+
- POSIX Rust UDS socketpair test.
779+
- Windows Rust Named Pipe test.
780+
781+
Validation completed for this increment:
782+
783+
- `cd src/go && go test -count=1 ./pkg/netipc/transport/internal/framing`: passed.
784+
- `cd src/go && go test -count=1 ./pkg/netipc/transport/internal/framing ./pkg/netipc/transport/posix`: passed.
785+
- `cd src/go && go test -count=1 ./pkg/netipc/...`: passed.
786+
- `cd src/go && GOOS=windows GOARCH=amd64 go test -c -o /tmp/netipc-transport-windows.test.exe ./pkg/netipc/transport/windows`: passed.
787+
- `cargo test --manifest-path src/crates/netipc/Cargo.toml test_receive_packet_longer_than_declared_payload -- --test-threads=1`: passed.
788+
- `cargo test --manifest-path src/crates/netipc/Cargo.toml transport::posix -- --test-threads=1`: 60 tests passed.
789+
- `cargo test --manifest-path src/crates/netipc/Cargo.toml -- --test-threads=1`: 333 tests passed.
790+
- `cmake --build build --target test_uds`: passed.
791+
- `/usr/bin/ctest --test-dir build --output-on-failure -R '^test_uds$'`: passed.
792+
- `cmake --build build`: passed.
793+
- `/usr/bin/ctest --test-dir build --output-on-failure`: 46/46 tests passed.
794+
- Win11 MSYS C validation: `test_named_pipe` built and passed.
795+
- Win11 Go validation: `cd src/go && MSYSTEM=MSYS CGO_ENABLED=0 go test -count=1 ./pkg/netipc/transport/internal/framing ./pkg/netipc/transport/windows`: passed.
796+
- Win11 Rust validation: `MSYSTEM=MSYS cargo test --manifest-path src/crates/netipc/Cargo.toml transport::windows -- --test-threads=1`: 26 Windows transport tests passed.
797+
- `git diff --check`: passed.
798+
741799
## Validation
742800

743801
Acceptance criteria evidence:

src/crates/netipc/src/transport/posix.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,14 @@ impl UdsSession {
448448

449449
let total_msg = HEADER_SIZE + hdr.payload_len as usize;
450450

451+
if n > total_msg {
452+
return Err(UdsError::Protocol(
453+
"packet exceeds declared payload_len".into(),
454+
));
455+
}
456+
451457
// Non-chunked: entire message in one packet
452-
if n >= total_msg {
458+
if n == total_msg {
453459
let payload = &buf[HEADER_SIZE..HEADER_SIZE + hdr.payload_len as usize];
454460

455461
// Validate batch directory

src/crates/netipc/src/transport/posix_tests.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,40 @@ fn test_receive_packet_too_short_for_header() {
14611461
unsafe { libc::close(fd1) };
14621462
}
14631463

1464+
#[test]
1465+
fn test_receive_packet_longer_than_declared_payload() {
1466+
let (fd0, fd1) = socketpair_seqpacket();
1467+
let mut session = test_session(fd0, Role::Server, 4096);
1468+
1469+
let payload = [0xBE, 0xEF];
1470+
let mut pkt = [0u8; HEADER_SIZE + 2];
1471+
let hdr = Header {
1472+
magic: MAGIC_MSG,
1473+
version: VERSION,
1474+
header_len: protocol::HEADER_LEN,
1475+
kind: KIND_REQUEST,
1476+
code: 1,
1477+
flags: 0,
1478+
transport_status: protocol::STATUS_OK,
1479+
payload_len: 1,
1480+
item_count: 1,
1481+
message_id: 1,
1482+
};
1483+
hdr.encode(&mut pkt[..HEADER_SIZE]);
1484+
pkt[HEADER_SIZE..].copy_from_slice(&payload);
1485+
1486+
raw_send(fd1, &pkt).expect("send packet with trailing bytes");
1487+
1488+
let mut buf = [0u8; 128];
1489+
let err = session
1490+
.receive(&mut buf)
1491+
.expect_err("trailing bytes should be rejected");
1492+
assert!(matches!(err, UdsError::Protocol(ref msg)
1493+
if msg.contains("exceeds declared payload_len")));
1494+
1495+
unsafe { libc::close(fd1) };
1496+
}
1497+
14641498
#[test]
14651499
fn test_receive_batch_directory_too_short_nonchunked() {
14661500
let (fd0, fd1) = socketpair_seqpacket();

src/crates/netipc/src/transport/windows.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,8 +849,14 @@ impl NpSession {
849849

850850
let total_msg = HEADER_SIZE + hdr.payload_len as usize;
851851

852+
if n > total_msg {
853+
return Err(NpError::Protocol(
854+
"packet exceeds declared payload_len".into(),
855+
));
856+
}
857+
852858
// Non-chunked
853-
if n >= total_msg {
859+
if n == total_msg {
854860
let payload = &buf[HEADER_SIZE..HEADER_SIZE + hdr.payload_len as usize];
855861

856862
// Validate batch directory
@@ -1794,6 +1800,60 @@ mod tests {
17941800
server.join().expect("server join");
17951801
}
17961802

1803+
#[cfg(windows)]
1804+
#[test]
1805+
fn test_receive_packet_longer_than_declared_payload() {
1806+
ensure_run_dir();
1807+
let svc = unique_service("rs_trailing");
1808+
1809+
let mut listener =
1810+
NpListener::bind(TEST_RUN_DIR, &svc, default_server_config()).expect("bind");
1811+
let server = thread::spawn(move || {
1812+
let mut session = listener.accept().expect("accept");
1813+
let mut buf = [0u8; 256];
1814+
let (req_hdr, _) = session.receive(&mut buf).expect("recv request");
1815+
1816+
let mut pkt = [0u8; HEADER_SIZE + 2];
1817+
let resp_hdr = Header {
1818+
magic: MAGIC_MSG,
1819+
version: VERSION,
1820+
header_len: protocol::HEADER_LEN,
1821+
kind: KIND_RESPONSE,
1822+
code: req_hdr.code,
1823+
flags: 0,
1824+
transport_status: protocol::STATUS_OK,
1825+
payload_len: 1,
1826+
item_count: 1,
1827+
message_id: req_hdr.message_id,
1828+
};
1829+
resp_hdr.encode(&mut pkt[..HEADER_SIZE]);
1830+
pkt[HEADER_SIZE..].copy_from_slice(&[0xBE, 0xEF]);
1831+
raw_write(session.handle, &pkt).expect("raw trailing packet write");
1832+
session.close();
1833+
});
1834+
1835+
let mut session =
1836+
NpSession::connect(TEST_RUN_DIR, &svc, &default_client_config()).expect("connect");
1837+
let mut hdr = Header {
1838+
kind: KIND_REQUEST,
1839+
code: protocol::METHOD_INCREMENT,
1840+
item_count: 1,
1841+
message_id: 43,
1842+
..Header::default()
1843+
};
1844+
session.send(&mut hdr, &[0xAA]).expect("send request");
1845+
1846+
let mut rbuf = [0u8; 256];
1847+
let err = session
1848+
.receive(&mut rbuf)
1849+
.expect_err("trailing bytes should be rejected");
1850+
assert!(matches!(err, NpError::Protocol(ref msg)
1851+
if msg.contains("exceeds declared payload_len")));
1852+
1853+
session.close();
1854+
server.join().expect("server join");
1855+
}
1856+
17971857
#[cfg(windows)]
17981858
#[test]
17991859
fn test_chunking_and_received_payload() {

src/go/pkg/netipc/transport/internal/framing/handshake.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ func NegotiateHello(hello protocol.Hello, config ServerHelloConfig) (protocol.He
121121
if hello.AuthToken != config.AuthToken {
122122
return protocol.HelloAck{}, protocol.StatusAuthFailed, false
123123
}
124+
// Level 1 keeps request payload limits client-proposed: the server rejects
125+
// values over the wire cap and echoes accepted values unchanged.
124126
if hello.MaxRequestPayloadBytes > protocol.MaxPayloadCap {
125127
return protocol.HelloAck{}, protocol.StatusLimitExceeded, false
126128
}

src/go/pkg/netipc/transport/internal/framing/receive.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ func (r Receiver) Receive(buf []byte) (protocol.Header, []byte, error) {
6060
return protocol.Header{}, nil, err
6161
}
6262
payloadLen := int(hdr.PayloadLen)
63-
if n >= totalMsg {
63+
if n > totalMsg {
64+
return protocol.Header{}, nil, r.ErrProtocol("packet exceeds declared payload_len")
65+
}
66+
if n == totalMsg {
6467
payload := buf[protocol.HeaderSize : protocol.HeaderSize+payloadLen]
6568
if err := r.validateBatchPayload(hdr, payload); err != nil {
6669
return protocol.Header{}, nil, err
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package framing
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
8+
"github.com/netdata/plugin-ipc/go/pkg/netipc/protocol"
9+
)
10+
11+
func TestReceiverRejectsPacketLongerThanDeclaredFrame(t *testing.T) {
12+
packet := encodeReceiveTestPacket(1, []byte("ab"))
13+
receiver := receiveTestReceiver(packet)
14+
15+
_, _, err := receiver.Receive(make([]byte, 64))
16+
if err == nil {
17+
t.Fatal("expected trailing packet bytes to be rejected")
18+
}
19+
if !strings.Contains(err.Error(), "exceeds declared payload_len") {
20+
t.Fatalf("unexpected error: %v", err)
21+
}
22+
}
23+
24+
func TestReceiverAcceptsExactDeclaredFrame(t *testing.T) {
25+
packet := encodeReceiveTestPacket(2, []byte("ab"))
26+
receiver := receiveTestReceiver(packet)
27+
28+
hdr, payload, err := receiver.Receive(make([]byte, 64))
29+
if err != nil {
30+
t.Fatalf("Receive failed: %v", err)
31+
}
32+
if hdr.PayloadLen != 2 {
33+
t.Fatalf("payload_len = %d, want 2", hdr.PayloadLen)
34+
}
35+
if string(payload) != "ab" {
36+
t.Fatalf("payload = %q, want ab", payload)
37+
}
38+
}
39+
40+
func receiveTestReceiver(packet []byte) Receiver {
41+
return Receiver{
42+
PacketSize: 64,
43+
MaxPayload: 64,
44+
MaxBatchItems: 1,
45+
Recv: func(buf []byte) (int, error) {
46+
return copy(buf, packet), nil
47+
},
48+
ErrLimitExceeded: func(msg string) error { return errors.New(msg) },
49+
ErrProtocol: func(msg string) error { return errors.New(msg) },
50+
ErrChunk: func(msg string) error { return errors.New(msg) },
51+
ErrUnknownMsgID: func(msg string) error { return errors.New(msg) },
52+
ErrRecv: func(msg string) error { return errors.New(msg) },
53+
}
54+
}
55+
56+
func encodeReceiveTestPacket(declaredPayloadLen uint32, payload []byte) []byte {
57+
hdr := protocol.Header{
58+
Magic: protocol.MagicMsg,
59+
Version: protocol.Version,
60+
HeaderLen: protocol.HeaderLen,
61+
Kind: protocol.KindRequest,
62+
TransportStatus: protocol.StatusOK,
63+
PayloadLen: declaredPayloadLen,
64+
ItemCount: 1,
65+
MessageID: 1,
66+
}
67+
packet := make([]byte, protocol.HeaderSize+len(payload))
68+
if hdr.Encode(packet) != protocol.HeaderSize {
69+
panic("header encode failed")
70+
}
71+
copy(packet[protocol.HeaderSize:], payload)
72+
return packet
73+
}

src/libnetdata/netipc/src/transport/posix/netipc_uds_receive.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ nipc_uds_error_t nipc_uds_receive(nipc_uds_session_t *session,
244244
if (!nipc_uds_header_payload_len(hdr_out->payload_len, &total_msg))
245245
return NIPC_UDS_ERR_LIMIT_EXCEEDED;
246246

247-
if ((size_t)n >= total_msg) {
247+
if ((size_t)n > total_msg)
248+
return NIPC_UDS_ERR_PROTOCOL;
249+
250+
if ((size_t)n == total_msg) {
248251
return return_complete_packet(buf, hdr_out, payload_out,
249252
payload_len_out);
250253
}

src/libnetdata/netipc/src/transport/windows/netipc_named_pipe.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1137,8 +1137,11 @@ nipc_np_error_t nipc_np_receive(nipc_np_session_t *session,
11371137
if (!header_payload_len(hdr_out->payload_len, &total_msg))
11381138
return NIPC_NP_ERR_LIMIT_EXCEEDED;
11391139

1140+
if (n > total_msg)
1141+
return NIPC_NP_ERR_PROTOCOL;
1142+
11401143
/* Non-chunked: entire message in one read */
1141-
if (n >= total_msg) {
1144+
if (n == total_msg) {
11421145
*payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
11431146
*payload_len_out = hdr_out->payload_len;
11441147

tests/fixtures/c/test_named_pipe.c

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ typedef enum {
9595
FAKE_ACK_GOOD_THEN_SHORT_RESPONSE,
9696
FAKE_ACK_GOOD_THEN_BAD_RESPONSE_HEADER,
9797
FAKE_ACK_GOOD_THEN_ZERO_MESSAGE,
98+
FAKE_ACK_GOOD_THEN_TRAILING_RESPONSE,
9899
FAKE_ACK_CLOSE_BEFORE_ACK,
99100
} fake_ack_mode_t;
100101

@@ -422,7 +423,8 @@ static DWORD WINAPI fake_ack_server_thread(LPVOID arg)
422423
ctx->mode == FAKE_ACK_GOOD_THEN_BAD_BATCH_DIR ||
423424
ctx->mode == FAKE_ACK_GOOD_THEN_BAD_CHUNKED_BATCH_DIR ||
424425
ctx->mode == FAKE_ACK_GOOD_THEN_SHORT_RESPONSE ||
425-
ctx->mode == FAKE_ACK_GOOD_THEN_BAD_RESPONSE_HEADER) {
426+
ctx->mode == FAKE_ACK_GOOD_THEN_BAD_RESPONSE_HEADER ||
427+
ctx->mode == FAKE_ACK_GOOD_THEN_TRAILING_RESPONSE) {
426428
if (!raw_pipe_read(pipe, buf, sizeof(buf), &bytes_read)) {
427429
CloseHandle(pipe);
428430
return 1;
@@ -768,6 +770,26 @@ static DWORD WINAPI fake_ack_server_thread(LPVOID arg)
768770
CloseHandle(pipe);
769771
return 1;
770772
}
773+
} else if (ctx->mode == FAKE_ACK_GOOD_THEN_TRAILING_RESPONSE) {
774+
uint8_t bad_pkt[NIPC_HEADER_LEN + 2] = { 0 };
775+
nipc_header_t resp_hdr = {
776+
.magic = NIPC_MAGIC_MSG,
777+
.version = NIPC_VERSION,
778+
.header_len = NIPC_HEADER_LEN,
779+
.kind = NIPC_KIND_RESPONSE,
780+
.code = req_hdr.code,
781+
.item_count = 1,
782+
.message_id = req_hdr.message_id,
783+
.payload_len = 1,
784+
.transport_status = NIPC_STATUS_OK,
785+
};
786+
nipc_header_encode(&resp_hdr, bad_pkt, NIPC_HEADER_LEN);
787+
bad_pkt[NIPC_HEADER_LEN] = 0xBE;
788+
bad_pkt[NIPC_HEADER_LEN + 1] = 0xEF;
789+
if (!raw_pipe_write(pipe, bad_pkt, (DWORD)sizeof(bad_pkt))) {
790+
CloseHandle(pipe);
791+
return 1;
792+
}
771793
}
772794
}
773795

@@ -2248,6 +2270,7 @@ static void test_response_protocol_validation(void)
22482270
} cases[] = {
22492271
{ "short response header rejected", FAKE_ACK_GOOD_THEN_SHORT_RESPONSE },
22502272
{ "bad decoded response header rejected", FAKE_ACK_GOOD_THEN_BAD_RESPONSE_HEADER },
2273+
{ "trailing response packet rejected", FAKE_ACK_GOOD_THEN_TRAILING_RESPONSE },
22512274
};
22522275

22532276
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {

0 commit comments

Comments
 (0)