Skip to content

Commit c5bb10e

Browse files
committed
Fix netipc send bounds and benchmark validation
1 parent 987482a commit c5bb10e

18 files changed

Lines changed: 494 additions & 142 deletions

File tree

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

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -463,64 +463,165 @@ Open decisions:
463463
- `codacy-analysis analyze --files ... --output-format json`: exit status 0, 0 issues, 1 known Revive adapter error:
464464
- Revive error: `Failed to run revive: findings is not iterable`.
465465

466+
### 2026-06-07 - C UDS Send AI Follow-up
467+
468+
- GitHub AI findings on `src/libnetdata/netipc/src/transport/posix/netipc_uds_send.c` reported:
469+
- ambiguous `min_size` helper name.
470+
- casts from `size_t` to `uint32_t` for payload length, chunk payload length, and total message length whose invariants were not explicit.
471+
- ceil division using `(remaining + chunk_payload_budget - 1)`, which could overflow in the abstract.
472+
- Local verification found one issue stronger than the GitHub wording:
473+
- `src/libnetdata/netipc/src/transport/posix/netipc_uds.c:nipc_uds_header_payload_len()` only checked `SIZE_MAX` overflow on 32-bit builds and did not reject framed total message lengths above `UINT32_MAX` on 64-bit builds.
474+
- `src/libnetdata/netipc/src/transport/windows/netipc_named_pipe.c:header_payload_len()` had the same helper shape, so the same contract issue existed in the Windows C transport.
475+
- Implemented follow-up:
476+
- renamed POSIX helper `min_size` to `min_of_size_t`.
477+
- added assertions at the narrowing cast sites.
478+
- changed POSIX and Windows C total-message helpers to reject `NIPC_HEADER_LEN + payload_len > UINT32_MAX`.
479+
- changed POSIX and Windows C chunk-count calculation to `1 + ((remaining - 1) / chunk_payload_budget)` to avoid forming a potentially overflowing addition.
480+
- changed chunk-budget checks from equality to `session->packet_size <= NIPC_HEADER_LEN` to avoid unsigned underflow if a malformed session reaches send.
481+
- added POSIX C regression coverage for underflow chunk budget and oversized framed total-message length.
482+
- Benchmark runner follow-up:
483+
- Full Windows benchmark startup failed before running benchmarks because `tests/run-windows-bench.sh` passed `/mingw64/bin/gcc` and `/mingw64/bin/g++` to CMake.
484+
- CMake 4.3.0 in the current MSYS environment translated those to `C:/msys64/mingw64/bin/gcc` and rejected them as non-existing full paths.
485+
- Updated the runner to pass Windows-style `.exe` paths from `cygpath -m`, for example `C:/msys64/mingw64/bin/gcc.exe`.
486+
- POSIX benchmark failure root cause:
487+
- Full POSIX benchmark initially failed deterministically on `uds-batch-ping-pong rust->c @ max`.
488+
- Manual focused repro failed before processing item zero.
489+
- Root cause: the Rust POSIX batch benchmark client/server configs raised batch item counts and response payload size, but did not raise request payload size. Large 2..1000 item batch requests could exceed the negotiated request payload limit before reaching the C server.
490+
- Fixed `bench/drivers/rust/src/main.rs` so POSIX Rust batch client and server configs set all four batch/request/response limits consistently.
491+
- Go benchmark determinism follow-up:
492+
- Local `go doc math/rand.Seed` for the installed Go toolchain says package-level random state is seeded randomly at startup when `Seed` is not called, and since Go 1.24 package-level `Seed` is a no-op.
493+
- C and Rust benchmark batch-size streams were deterministic xorshift32 with seed `12345`; Go was not.
494+
- Replaced Go package-global `math/rand` batch sizing in POSIX and Windows benchmark drivers with a local xorshift32 sequence matching the C/Rust seed.
495+
- Windows benchmark stability-policy follow-up:
496+
- Batch benchmark rows report item throughput, while `target_rps` controls batch request rate.
497+
- The Windows runner previously compared `target_rps` directly to prior max item throughput when deciding whether a fixed-rate row was oversubscribed.
498+
- Added fixed-rate target throughput calculation for batch rows using the 2..1000 batch-size midpoint, so oversubscription classification compares item-throughput to item-throughput.
499+
- Moved oversubscription handling before stable-ratio failure handling so an intentionally overloaded fixed-rate row is classified consistently.
500+
- Windows benchmark timeout evidence:
501+
- First fresh full Windows run reached `shm-ping-pong c->go @ max` and recorded one hard failure: the C client was killed by the runner outer `timeout` with exit 124 while the Go server had printed `READY`.
502+
- The C client produced no stderr because the outer timeout was shorter than the benchmark client's own 30s SHM receive timeout.
503+
- Fixed `tests/run-windows-bench.sh` by adding `NIPC_BENCH_CLIENT_TIMEOUT_GRACE_SEC`, default `35`, and using `duration + CLIENT_TIMEOUT_GRACE_SEC` for client process timeouts.
504+
- Stopped the invalid full Windows run and cleaned up only the exact remote benchmark PIDs tied to `/tmp/plugin-ipc-bench-full-results-fixed4` and `/tmp/netipc-bench-88189`.
505+
- Focused Win11 diagnostic for `shm-ping-pong c->go @ max` with diagnostics enabled passed: throughput `2714891/s`, p50 `3.700us`, p95 `8.900us`, p99 `16.200us`, stable ratio `1.017852`, no warnings.
506+
- A fresh full Windows benchmark rerun with the improved timeout (`fixed5`) was stopped after the first row failed the stability policy, not correctness: `np-ping-pong c->c @ max` samples were `75452`, `74311`, `52814`, `66071`, and `51296`, with no client/server errors.
507+
- Focused Win11 diagnostic for `np-ping-pong c->c @ max` with the same default durations passed: throughput `77680/s`, p50 `13.200us`, p95 `41.800us`, p99 `82.600us`, stable ratio `1.093959`, no warnings.
508+
- The final full Win11 benchmark rerun with diagnostics enabled passed with exit status 0:
509+
- output CSV: `/tmp/plugin-ipc-bench-full-results-final2/benchmarks-windows.csv`
510+
- row count: 202 lines.
511+
- hard failure scan: no `client failed`, `server failed`, `did not stop`, `error`, `One or more Windows benchmark scenarios failed`, or `Diagnostic rerun failed` matches.
512+
- one stability-only row was recovered by a successful diagnostic rerun: `np-batch-ping-pong go->go @ 10000/s`, first attempt stable ratio `1.849425`, diagnostic stable ratio `1.000165`, recovered median throughput `5040434/s`.
513+
- remaining warnings were raw repeated-throughput outliers or oversubscribed fixed-rate rows kept by the benchmark policy, with stable trimmed cores or intentional overload evidence.
514+
- Vendor script follow-up:
515+
- `vendor-to-netdata.sh` still had a hard-coded C file list from before the C source splits.
516+
- Updated it to rsync the full vendored C include tree and C source tree, preserving Netdata-specific root wrapper files outside those synced subtrees.
517+
- This is required before updating the NetIPC vendored copy in `~/src/netdata-ktsaou.git`; otherwise split C files would be silently missed.
518+
- Validation completed so far for this follow-up:
519+
- `bash -n tests/run-windows-bench.sh vendor-to-netdata.sh`: passed.
520+
- `git diff --check`: passed.
521+
- `cmake --build build`: passed.
522+
- `/usr/bin/ctest --test-dir build --output-on-failure`: 46/46 tests passed in 449.56 seconds.
523+
- `cargo test --manifest-path src/crates/netipc/Cargo.toml`: 332 tests passed; benchmark and interop test targets built and had 0 tests.
524+
- `cd src/go && go test ./...`: passed.
525+
- `cd bench/drivers/go && go test ./...`: passed.
526+
- `cd bench/drivers/go && GOOS=windows GOARCH=amd64 go test -c -o /tmp/plugin-ipc-go-bench-windows.test.exe .`: passed.
527+
- `bash -n tests/run-windows-bench.sh tests/test_windows_bench_stability_policy.sh vendor-to-netdata.sh`: passed.
528+
- Local targeted Codacy Analysis CLI scan with JSON output on the changed files: 0 issues, 1 known Revive adapter invocation error (`findings is not iterable`); latest output `/tmp/plugin-ipc-codacy-final6.json`.
529+
- Full POSIX benchmark rerun: 298 CSV lines, no warnings or failures, output at `/tmp/plugin-ipc-bench-full-posix-final/benchmarks-posix.csv`.
530+
- Full Win11 benchmark rerun: 202 CSV lines, no hard failures, output at `/tmp/plugin-ipc-bench-full-results-final2/benchmarks-windows.csv`.
531+
532+
### 2026-06-07 - Netdata Re-vendor Plan
533+
534+
The next repository step is to update the vendored NetIPC copy in `~/src/netdata-ktsaou.git` after this SDK commit is pushed.
535+
536+
Plan:
537+
538+
1. Read `~/src/netdata-ktsaou.git/AGENTS.md` and any active Netdata SOW instructions before editing that repository.
539+
2. Create or switch to a dedicated Netdata branch for the vendor update.
540+
3. Run `./vendor-to-netdata.sh ~/src/netdata-ktsaou.git` from this repository.
541+
4. Run `./diff-netdata-vendor.sh ~/src/netdata-ktsaou.git --unified` and inspect the result so only expected vendored NetIPC files changed.
542+
5. If the script reports required Go import rewrites, run the exact rewrite command in the Netdata repository and re-check the diff.
543+
6. Update Netdata build integration if the split C source files require changed source lists.
544+
7. Validate Netdata build/tests for the vendored NetIPC surface, including Windows compile/unit validation if the Netdata workflow exposes it locally.
545+
8. Commit only explicit Netdata vendor/update files, push the branch, and open a PR against Netdata.
546+
466547
## Validation
467548

468549
Acceptance criteria evidence:
469550

470-
- Pending.
551+
- Codacy global scope correction was implemented earlier in this SOW and pushed.
552+
- The C UDS send AI finding was addressed in `src/libnetdata/netipc/src/transport/posix/netipc_uds_send.c`:
553+
- helper renamed to `min_of_size_t`.
554+
- narrowing casts now have explicit assertions and safety-contract comments.
555+
- send chunk count no longer forms `(remaining + chunk_payload_budget - 1)`.
556+
- Same-pattern C transport issue was addressed in `src/libnetdata/netipc/src/transport/windows/netipc_named_pipe.c`.
557+
- Vendor script now copies the full vendored C include/source subtrees so split C files are not missed.
471558

472559
Tests or equivalent validation:
473560

474-
- Pending.
561+
- `cmake --build build`: passed.
562+
- `/usr/bin/ctest --test-dir build --output-on-failure`: 46/46 passed.
563+
- `cargo test --manifest-path src/crates/netipc/Cargo.toml`: 332 passed.
564+
- `cd src/go && go test ./...`: passed.
565+
- `cd bench/drivers/go && go test ./...`: passed.
566+
- `git diff --check`: passed.
567+
- `bash -n tests/run-windows-bench.sh tests/test_windows_bench_stability_policy.sh vendor-to-netdata.sh`: passed.
568+
- `codacy-analysis analyze --files ... --output-format json`: 0 issues; known local Revive adapter invocation error remains.
475569

476570
Real-use evidence:
477571

478-
- Pending.
572+
- Full POSIX benchmark: 298 CSV lines, no warnings or failures, artifact `/tmp/plugin-ipc-bench-full-posix-final/benchmarks-posix.csv`.
573+
- Full Win11 benchmark: 202 CSV lines, exit status 0, no hard failure scan matches, artifact `/tmp/plugin-ipc-bench-full-results-final2/benchmarks-windows.csv`.
574+
- Win11 diagnostic rerun recovered one stability-only row and published the diagnostic row.
479575

480576
Reviewer findings:
481577

482-
- No external reviewer used yet.
578+
- GitHub AI findings for `netipc_uds_send.c` were manually verified and addressed.
579+
- No external AI reviewer was used for this increment.
483580

484581
Same-failure scan:
485582

486-
- Pending.
583+
- Same C total-message-width helper pattern was found and fixed in the Windows named-pipe transport.
584+
- Same chunk-count overflow-prone ceil pattern was found and fixed in POSIX C, Windows C, POSIX Rust, Windows Rust, POSIX Go, and Windows Go transport paths.
585+
- Same benchmark batch sizing nondeterminism pattern was found and fixed in POSIX and Windows Go benchmark drivers.
487586

488587
Sensitive data gate:
489588

490-
- Pending final scan.
589+
- `.env` was not read or committed.
590+
- SOW evidence records local artifact paths, command names, and sanitized tool output only; no secrets or customer data were written.
491591

492592
Artifact maintenance gate:
493593

494-
- AGENTS.md: pending.
495-
- Runtime project skills: pending.
496-
- Specs: pending.
497-
- End-user/operator docs: pending.
498-
- End-user/operator skills: pending.
499-
- SOW lifecycle: pending.
594+
- AGENTS.md: no workflow or guardrail change needed.
595+
- Runtime project skills: no reusable project workflow changed.
596+
- Specs: no protocol/API spec update needed; wire fields were already `u32`, and the implementation now enforces that existing wire contract consistently.
597+
- End-user/operator docs: no public usage behavior changed.
598+
- End-user/operator skills: no exported/operator workflow changed.
599+
- SOW lifecycle: SOW remains `in-progress`; this increment records validation and the Netdata re-vendor plan.
500600

501601
Specs update:
502602

503-
- Pending.
603+
- No spec update needed for this increment; the implementation now matches the existing `u32` wire-field constraints.
504604

505605
Project skills update:
506606

507-
- Pending.
607+
- No project skill update needed; no reusable repo workflow changed beyond the current SOW evidence.
508608

509609
End-user/operator docs update:
510610

511-
- Pending.
611+
- No end-user/operator docs update needed; public API and documented usage did not change.
512612

513613
End-user/operator skills update:
514614

515-
- Pending.
615+
- No end-user/operator skill update needed.
516616

517617
Lessons:
518618

519-
- Pending.
619+
- Benchmark runners must distinguish correctness failures from stability-only noise and must preserve first-attempt evidence before any diagnostic recovery.
620+
- Vendor scripts should copy source subtrees after source-file organization splits; hard-coded file lists go stale quickly.
520621

521622
Follow-up mapping:
522623

523-
- Pending.
624+
- Netdata re-vendor work is planned in this SOW and will be performed after the SDK commit is pushed.
524625

525626
## Outcome
526627

‎bench/drivers/go/main.go‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ package main
1212
import (
1313
"encoding/binary"
1414
"fmt"
15-
"math/rand"
1615
"os"
1716
"os/signal"
1817
"runtime"
@@ -114,8 +113,23 @@ func estimateSamples(defaultSamples int, targetRPS uint64, durationSec int) int
114113
return int(samples) // #nosec G115 -- samples is bounded by maxLatencySamples above.
115114
}
116115

117-
func randomBatchSize() uint32 {
118-
return uint32(rand.Intn(maxBatchItems-1) + 2) // #nosec G115 -- maxBatchItems bounds the value to 2..1000.
116+
type xorshift32 struct {
117+
state uint32
118+
}
119+
120+
func newXorShift32() xorshift32 {
121+
return xorshift32{state: 12345}
122+
}
123+
124+
func (rng *xorshift32) next() uint32 {
125+
rng.state ^= rng.state << 13
126+
rng.state ^= rng.state >> 17
127+
rng.state ^= rng.state << 5
128+
return rng.state
129+
}
130+
131+
func randomBatchSize(rng *xorshift32) uint32 {
132+
return (rng.next() % (maxBatchItems - 1)) + 2
119133
}
120134

121135
// ---------------------------------------------------------------------------
@@ -451,6 +465,7 @@ func runBatchPingPongClient(runDir, service string, profiles uint32, durationSec
451465
estSamples := estimateSamples(2_000_000, targetRPS, durationSec)
452466
lr := newLatencyRecorder(estSamples)
453467
rl := newRateLimiter(targetRPS)
468+
rng := newXorShift32()
454469

455470
reqBuf := make([]byte, batchBufSize)
456471
respBuf := make([]byte, batchBufSize+protocol.HeaderSize)
@@ -468,7 +483,7 @@ func runBatchPingPongClient(runDir, service string, profiles uint32, durationSec
468483
rl.wait()
469484

470485
// Random batch size 2-1000 (server treats itemCount==1 as non-batch)
471-
batchSize := randomBatchSize()
486+
batchSize := randomBatchSize(&rng)
472487

473488
// Reuse a stack builder to avoid a heap object per request.
474489
var bb protocol.BatchBuilder
@@ -813,6 +828,7 @@ func runPipelineBatchClient(runDir, service string, durationSec int, targetRPS u
813828

814829
lr := newLatencyRecorder(2_000_000)
815830
rl := newRateLimiter(targetRPS)
831+
rng := newXorShift32()
816832

817833
// Pre-allocate per-depth buffers
818834
reqBufs := make([][]byte, depth)
@@ -837,7 +853,7 @@ func runPipelineBatchClient(runDir, service string, durationSec int, targetRPS u
837853
// Build and send `depth` batch requests
838854
sendOK := true
839855
for d := 0; d < depth; d++ {
840-
bs := randomBatchSize()
856+
bs := randomBatchSize(&rng)
841857
batchSizes[d] = bs
842858

843859
var bb protocol.BatchBuilder

‎bench/drivers/go/main_windows.go‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"encoding/binary"
1515
"fmt"
1616
"io"
17-
"math/rand"
1817
"os"
1918
"os/exec"
2019
"runtime"
@@ -67,6 +66,25 @@ func cacheHashNameWin(name string) uint32 {
6766
return h
6867
}
6968

69+
type xorshift32Win struct {
70+
state uint32
71+
}
72+
73+
func newXorShift32Win() xorshift32Win {
74+
return xorshift32Win{state: 12345}
75+
}
76+
77+
func (rng *xorshift32Win) next() uint32 {
78+
rng.state ^= rng.state << 13
79+
rng.state ^= rng.state >> 17
80+
rng.state ^= rng.state << 5
81+
return rng.state
82+
}
83+
84+
func randomBatchSizeWin(rng *xorshift32Win) uint32 {
85+
return (rng.next() % (maxBatchItemsWin - 1)) + 2
86+
}
87+
7088
// ---------------------------------------------------------------------------
7189
// Timing helpers
7290
// ---------------------------------------------------------------------------
@@ -818,6 +836,7 @@ func runBatchPingPongClientWin(runDir, service string, profiles uint32, duration
818836
}
819837

820838
runtime.GC()
839+
rng := newXorShift32Win()
821840

822841
cpuStart := cpuNSWin()
823842
wallStart := nowNS()
@@ -827,7 +846,7 @@ func runBatchPingPongClientWin(runDir, service string, profiles uint32, duration
827846
rl.wait()
828847

829848
// Random batch size 2-1000 (server treats itemCount==1 as non-batch)
830-
batchSize := uint32(rand.Intn(maxBatchItemsWin-1) + 2)
849+
batchSize := randomBatchSizeWin(&rng)
831850

832851
// Reuse a stack builder to avoid a heap object per request.
833852
var bb protocol.BatchBuilder
@@ -1348,6 +1367,7 @@ func runPipelineBatchClientWin(runDir, service string, durationSec int, targetRP
13481367
cpuStart := cpuNSWin()
13491368
wallStart := nowNS()
13501369
tickDeadline := tickMSWin() + uint64(durationSec)*1000
1370+
rng := newXorShift32Win()
13511371

13521372
for tickMSWin() < tickDeadline {
13531373
rl.wait()
@@ -1366,7 +1386,7 @@ func runPipelineBatchClientWin(runDir, service string, durationSec int, targetRP
13661386
for received < depth {
13671387
if sent < depth {
13681388
slot := sent
1369-
bs := uint32(rand.Intn(maxBatchItemsWin-1) + 2)
1389+
bs := randomBatchSizeWin(&rng)
13701390

13711391
var bb protocol.BatchBuilder
13721392
bb.Reset(reqBufs[slot], bs)

‎bench/drivers/rust/src/main.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,10 @@ mod posix_only {
173173
ServerConfig {
174174
supported_profiles: profiles,
175175
preferred_profiles: profiles,
176+
max_request_payload_bytes: BENCH_BATCH_BUF_SIZE as u32,
176177
max_request_batch_items: BENCH_MAX_BATCH_ITEMS,
177178
max_response_payload_bytes: BENCH_BATCH_BUF_SIZE as u32,
179+
max_response_batch_items: BENCH_MAX_BATCH_ITEMS,
178180
auth_token: AUTH_TOKEN,
179181
backlog: 4,
180182
..ServerConfig::default()
@@ -185,8 +187,10 @@ mod posix_only {
185187
ClientConfig {
186188
supported_profiles: profiles,
187189
preferred_profiles: profiles,
190+
max_request_payload_bytes: BENCH_BATCH_BUF_SIZE as u32,
188191
max_request_batch_items: BENCH_MAX_BATCH_ITEMS,
189192
max_response_payload_bytes: BENCH_BATCH_BUF_SIZE as u32,
193+
max_response_batch_items: BENCH_MAX_BATCH_ITEMS,
190194
auth_token: AUTH_TOKEN,
191195
..ClientConfig::default()
192196
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ impl UdsSession {
293293
self.max_response_batch_items,
294294
)
295295
};
296-
if payload.len() > max_payload as usize || payload.len() > u32::MAX as usize {
296+
if payload.len() > max_payload as usize
297+
|| payload.len() > (u32::MAX as usize).saturating_sub(HEADER_SIZE)
298+
{
297299
return Err(UdsError::LimitExceeded);
298300
}
299301
if hdr.item_count > max_items {
@@ -353,7 +355,7 @@ impl UdsSession {
353355
let remaining_after_first = payload.len() - first_chunk_payload;
354356

355357
let continuation_chunks = if remaining_after_first > 0 {
356-
(remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
358+
1 + ((remaining_after_first - 1) / chunk_payload_budget)
357359
} else {
358360
0
359361
};
@@ -488,7 +490,7 @@ impl UdsSession {
488490
// Expected chunk count
489491
let remaining_after_first = hdr.payload_len as usize - first_payload_bytes;
490492
let expected_continuations = if remaining_after_first > 0 && chunk_payload_budget > 0 {
491-
(remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
493+
1 + ((remaining_after_first - 1) / chunk_payload_budget)
492494
} else {
493495
0
494496
};

0 commit comments

Comments
 (0)