@cryptotaxi247 / netdata / commits / 36cf14dab

Update vendored NetIPC library (#22649)

Costa Tsaousis committed Jun 8, 2026 at 09:25 UTC 36cf14dabb3348b8522d9390ce65edea419c29a1
174 files changed +24753 -14029
CMakeLists.txt
+38 -2
@@ -2358,12 +2358,42 @@ set_source_files_properties(JudyLTables.c PROPERTIES COMPILE_OPTIONS "-I${CMAKE_
2358 # build netipc (standalone IPC library, no Netdata deps)
2359 #
2360
2361 +set(NETIPC_PROTOCOL_FILES
2362 + src/libnetdata/netipc/src/protocol/netipc_protocol.c
2363 + src/libnetdata/netipc/src/protocol/netipc_protocol_increment.c
2364 + src/libnetdata/netipc/src/protocol/netipc_protocol_string_reverse.c
2365 + src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_snapshot.c
2366 + src/libnetdata/netipc/src/protocol/netipc_protocol_lookup_common.c
2367 + src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_lookup.c
2368 + src/libnetdata/netipc/src/protocol/netipc_protocol_apps_lookup.c
2369 +)
2370 +
2371 +set(NETIPC_SERVICE_COMMON_FILES
2372 + src/libnetdata/netipc/src/service/netipc_service_common.c
2373 + src/libnetdata/netipc/src/service/netipc_service_cgroups_cache_common.c
2374 + src/libnetdata/netipc/src/service/netipc_service_cgroups_cache.c
2375 + src/libnetdata/netipc/src/service/netipc_service_cgroups_snapshot.c
2376 + src/libnetdata/netipc/src/service/netipc_service_cgroups_lookup.c
2377 + src/libnetdata/netipc/src/service/netipc_service_apps_lookup.c
2378 +)
2379 +
2380 if(OS_LINUX)
2381 set(NETIPC_FILES
2363 - src/libnetdata/netipc/src/protocol/netipc_protocol.c
2382 + ${NETIPC_PROTOCOL_FILES}
2383 src/libnetdata/netipc/src/transport/posix/netipc_uds.c
2384 + src/libnetdata/netipc/src/transport/posix/netipc_uds_handshake.c
2385 + src/libnetdata/netipc/src/transport/posix/netipc_uds_inflight.c
2386 + src/libnetdata/netipc/src/transport/posix/netipc_uds_lifecycle.c
2387 + src/libnetdata/netipc/src/transport/posix/netipc_uds_receive.c
2388 + src/libnetdata/netipc/src/transport/posix/netipc_uds_send.c
2389 src/libnetdata/netipc/src/transport/posix/netipc_shm.c
2390 + ${NETIPC_SERVICE_COMMON_FILES}
2391 src/libnetdata/netipc/src/service/netipc_service.c
2392 + src/libnetdata/netipc/src/service/netipc_service_posix_client.c
2393 + src/libnetdata/netipc/src/service/netipc_service_posix_client_connect.c
2394 + src/libnetdata/netipc/src/service/netipc_service_posix_client_call.c
2395 + src/libnetdata/netipc/src/service/netipc_service_posix_server.c
2396 + src/libnetdata/netipc/src/service/netipc_service_posix_server_session.c
2397 )
2398
2399 add_library(netipc STATIC ${NETIPC_FILES})
@@ -2372,10 +2402,16 @@ if(OS_LINUX)
2402 set_target_properties(netipc PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
2403 elseif(OS_WINDOWS)
2404 set(NETIPC_FILES
2375 - src/libnetdata/netipc/src/protocol/netipc_protocol.c
2405 + ${NETIPC_PROTOCOL_FILES}
2406 src/libnetdata/netipc/src/transport/windows/netipc_named_pipe.c
2407 src/libnetdata/netipc/src/transport/windows/netipc_win_shm.c
2408 + ${NETIPC_SERVICE_COMMON_FILES}
2409 src/libnetdata/netipc/src/service/netipc_service_win.c
2410 + src/libnetdata/netipc/src/service/netipc_service_win_client.c
2411 + src/libnetdata/netipc/src/service/netipc_service_win_client_connect.c
2412 + src/libnetdata/netipc/src/service/netipc_service_win_client_call.c
2413 + src/libnetdata/netipc/src/service/netipc_service_win_server.c
2414 + src/libnetdata/netipc/src/service/netipc_service_win_server_session.c
2415 )
2416
2417 add_library(netipc STATIC ${NETIPC_FILES})
src/crates/netipc/src/bin/interop_codec.rs
+309
@@ -167,6 +167,214 @@ fn do_encode(dir: &str) {
167 let total = b.finish();
168 write_file(dir, "cgroups_resp_empty.bin", &buf[..total]);
169 }
170 +
171 + // 8. CGROUPS_LOOKUP request variants
172 + {
173 + let mut buf = [0u8; 8192];
174 + let total = encode_cgroups_lookup_request(
175 + &[b"/sys/fs/cgroup/a", b"/system.slice/docker-abc.scope"],
176 + &mut buf,
177 + )
178 + .unwrap();
179 + write_file(dir, "cgroups_lookup_req.bin", &buf[..total]);
180 +
181 + let total = encode_cgroups_lookup_request(&[], &mut buf).unwrap();
182 + write_file(dir, "cgroups_lookup_req_empty.bin", &buf[..total]);
183 + }
184 +
185 + // 9. CGROUPS_LOOKUP response variants
186 + {
187 + let mut buf = [0u8; 8192];
188 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 100);
189 + b.add(
190 + CGROUP_LOOKUP_KNOWN,
191 + ORCHESTRATOR_K8S,
192 + b"/kubepods.slice/pod-a",
193 + b"pod-a",
194 + &[
195 + (b"namespace".as_slice(), b"default".as_slice()),
196 + (b"pod".as_slice(), b"web".as_slice()),
197 + ],
198 + )
199 + .unwrap();
200 + let total = b.finish().unwrap();
201 + write_file(
202 + dir,
203 + "cgroups_lookup_resp_known_with_labels.bin",
204 + &buf[..total],
205 + );
206 + }
207 + {
208 + let mut buf = [0u8; 8192];
209 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 101);
210 + b.add(
211 + CGROUP_LOOKUP_KNOWN,
212 + ORCHESTRATOR_DOCKER,
213 + b"/docker/abc",
214 + b"",
215 + &[],
216 + )
217 + .unwrap();
218 + let total = b.finish().unwrap();
219 + write_file(
220 + dir,
221 + "cgroups_lookup_resp_known_no_labels.bin",
222 + &buf[..total],
223 + );
224 + }
225 + {
226 + let mut buf = [0u8; 8192];
227 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 102);
228 + b.add(
229 + CGROUP_LOOKUP_UNKNOWN_RETRY_LATER,
230 + 0,
231 + b"/missing/retry",
232 + b"",
233 + &[],
234 + )
235 + .unwrap();
236 + let total = b.finish().unwrap();
237 + write_file(dir, "cgroups_lookup_resp_unknown_retry.bin", &buf[..total]);
238 + }
239 + {
240 + let mut buf = [0u8; 8192];
241 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 103);
242 + b.add(CGROUP_LOOKUP_UNKNOWN_PERMANENT, 0, b"/gone", b"", &[])
243 + .unwrap();
244 + let total = b.finish().unwrap();
245 + write_file(
246 + dir,
247 + "cgroups_lookup_resp_unknown_permanent.bin",
248 + &buf[..total],
249 + );
250 + }
251 + {
252 + let mut buf = [0u8; 8192];
253 + let b = CgroupsLookupBuilder::new(&mut buf, 0, 104);
254 + let total = b.finish().unwrap();
255 + write_file(dir, "cgroups_lookup_resp_empty.bin", &buf[..total]);
256 + }
257 +
258 + // 10. APPS_LOOKUP request variants
259 + {
260 + let mut buf = [0u8; 8192];
261 + let total = encode_apps_lookup_request(&[0, 1234, 4321], &mut buf).unwrap();
262 + write_file(dir, "apps_lookup_req.bin", &buf[..total]);
263 +
264 + let total = encode_apps_lookup_request(&[], &mut buf).unwrap();
265 + write_file(dir, "apps_lookup_req_empty.bin", &buf[..total]);
266 + }
267 +
268 + // 11. APPS_LOOKUP response variants
269 + {
270 + let mut buf = [0u8; 8192];
271 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 200);
272 + b.add(
273 + PID_LOOKUP_KNOWN,
274 + APPS_CGROUP_KNOWN,
275 + ORCHESTRATOR_DOCKER,
276 + 1234,
277 + 1,
278 + 1000,
279 + 123456,
280 + b"123456789012345",
281 + b"/docker/abc",
282 + b"container-a",
283 + &[
284 + (b"image".as_slice(), b"nginx:latest".as_slice()),
285 + (b"service".as_slice(), b"web".as_slice()),
286 + ],
287 + )
288 + .unwrap();
289 + let total = b.finish().unwrap();
290 + write_file(dir, "apps_lookup_resp_known_full.bin", &buf[..total]);
291 + }
292 + {
293 + let mut buf = [0u8; 8192];
294 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 201);
295 + b.add(
296 + PID_LOOKUP_KNOWN,
297 + APPS_CGROUP_UNKNOWN_RETRY_LATER,
298 + 0,
299 + 1235,
300 + 1,
301 + 1000,
302 + 123457,
303 + b"app",
304 + b"/pending",
305 + b"",
306 + &[],
307 + )
308 + .unwrap();
309 + let total = b.finish().unwrap();
310 + write_file(dir, "apps_lookup_resp_known_retry.bin", &buf[..total]);
311 + }
312 + {
313 + let mut buf = [0u8; 8192];
314 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 202);
315 + b.add(
316 + PID_LOOKUP_KNOWN,
317 + APPS_CGROUP_UNKNOWN_PERMANENT,
318 + 0,
319 + 1236,
320 + 1,
321 + 1000,
322 + 123458,
323 + b"app2",
324 + b"/permanent",
325 + b"",
326 + &[],
327 + )
328 + .unwrap();
329 + let total = b.finish().unwrap();
330 + write_file(dir, "apps_lookup_resp_known_permanent.bin", &buf[..total]);
331 + }
332 + {
333 + let mut buf = [0u8; 8192];
334 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 203);
335 + b.add(
336 + PID_LOOKUP_KNOWN,
337 + APPS_CGROUP_HOST_ROOT,
338 + 0,
339 + 1237,
340 + 1,
341 + 0,
342 + 123459,
343 + b"sshd",
344 + b"",
345 + b"",
346 + &[],
347 + )
348 + .unwrap();
349 + let total = b.finish().unwrap();
350 + write_file(dir, "apps_lookup_resp_known_host_root.bin", &buf[..total]);
351 + }
352 + {
353 + let mut buf = [0u8; 8192];
354 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 204);
355 + b.add(
356 + PID_LOOKUP_UNKNOWN,
357 + APPS_CGROUP_KNOWN,
358 + 0,
359 + 0,
360 + 0,
361 + NIPC_UID_UNSET,
362 + 0,
363 + b"",
364 + b"",
365 + b"",
366 + &[],
367 + )
368 + .unwrap();
369 + let total = b.finish().unwrap();
370 + write_file(dir, "apps_lookup_resp_unknown_pid.bin", &buf[..total]);
371 + }
372 + {
373 + let mut buf = [0u8; 8192];
374 + let b = AppsLookupBuilder::new(&mut buf, 0, 205);
375 + let total = b.finish().unwrap();
376 + write_file(dir, "apps_lookup_resp_empty.bin", &buf[..total]);
377 + }
378 }
379
380 fn do_decode(dir: &str) -> bool {
@@ -326,6 +534,107 @@ fn do_decode(dir: &str) -> bool {
534 }
535 }
536
537 + // 8. CGROUPS_LOOKUP request variants
538 + {
539 + let data = read_file(dir, "cgroups_lookup_req.bin");
540 + let view = CgroupsLookupRequestView::decode(&data);
541 + c.check(view.is_ok(), "decode cgroups_lookup_req");
542 + if let Ok(v) = view {
543 + c.check(v.item_count == 2, "cgroups_lookup_req item_count");
544 + c.check(
545 + v.item(0).unwrap().as_bytes() == b"/sys/fs/cgroup/a",
546 + "cgroups_lookup_req item0",
547 + );
548 + }
549 + }
550 + {
551 + let data = read_file(dir, "cgroups_lookup_req_empty.bin");
552 + let view = CgroupsLookupRequestView::decode(&data);
553 + c.check(view.is_ok(), "decode cgroups_lookup_req_empty");
554 + if let Ok(v) = view {
555 + c.check(v.item_count == 0, "cgroups_lookup_req_empty count");
556 + }
557 + }
558 +
559 + // 9. CGROUPS_LOOKUP response variants
560 + {
561 + let data = read_file(dir, "cgroups_lookup_resp_known_with_labels.bin");
562 + let view = CgroupsLookupResponseView::decode(&data);
563 + c.check(view.is_ok(), "decode cgroups_lookup known labels");
564 + if let Ok(v) = view {
565 + c.check(v.generation == 100, "cgroups_lookup generation");
566 + let item = v.item(0).unwrap();
567 + c.check(item.status == CGROUP_LOOKUP_KNOWN, "cgroups_lookup status");
568 + c.check(
569 + item.orchestrator == ORCHESTRATOR_K8S,
570 + "cgroups_lookup orchestrator",
571 + );
572 + c.check(item.label_count == 2, "cgroups_lookup label_count");
573 + c.check(
574 + item.label(0).unwrap().key.as_bytes() == b"namespace",
575 + "cgroups_lookup label",
576 + );
577 + }
578 + }
579 + for file in [
580 + "cgroups_lookup_resp_known_no_labels.bin",
581 + "cgroups_lookup_resp_unknown_retry.bin",
582 + "cgroups_lookup_resp_unknown_permanent.bin",
583 + "cgroups_lookup_resp_empty.bin",
584 + ] {
585 + let data = read_file(dir, file);
586 + c.check(CgroupsLookupResponseView::decode(&data).is_ok(), file);
587 + }
588 +
589 + // 10. APPS_LOOKUP request variants
590 + {
591 + let data = read_file(dir, "apps_lookup_req.bin");
592 + let view = AppsLookupRequestView::decode(&data);
593 + c.check(view.is_ok(), "decode apps_lookup_req");
594 + if let Ok(v) = view {
595 + c.check(v.item_count == 3, "apps_lookup_req item_count");
596 + c.check(v.item(0).unwrap() == 0, "apps_lookup_req pid0");
597 + }
598 + }
599 + {
600 + let data = read_file(dir, "apps_lookup_req_empty.bin");
601 + let view = AppsLookupRequestView::decode(&data);
602 + c.check(view.is_ok(), "decode apps_lookup_req_empty");
603 + if let Ok(v) = view {
604 + c.check(v.item_count == 0, "apps_lookup_req_empty count");
605 + }
606 + }
607 +
608 + // 11. APPS_LOOKUP response variants
609 + {
610 + let data = read_file(dir, "apps_lookup_resp_known_full.bin");
611 + let view = AppsLookupResponseView::decode(&data);
612 + c.check(view.is_ok(), "decode apps_lookup known full");
613 + if let Ok(v) = view {
614 + let item = v.item(0).unwrap();
615 + c.check(item.pid == 1234, "apps_lookup pid");
616 + c.check(item.comm.len == 15, "apps_lookup comm boundary");
617 + c.check(
618 + item.cgroup_status == APPS_CGROUP_KNOWN,
619 + "apps_lookup cgroup status",
620 + );
621 + c.check(
622 + item.label(0).unwrap().value.as_bytes() == b"nginx:latest",
623 + "apps_lookup label",
624 + );
625 + }
626 + }
627 + for file in [
628 + "apps_lookup_resp_known_retry.bin",
629 + "apps_lookup_resp_known_permanent.bin",
630 + "apps_lookup_resp_known_host_root.bin",
631 + "apps_lookup_resp_unknown_pid.bin",
632 + "apps_lookup_resp_empty.bin",
633 + ] {
634 + let data = read_file(dir, file);
635 + c.check(AppsLookupResponseView::decode(&data).is_ok(), file);
636 + }
637 +
638 c.report("Rust decode")
639 }
640
src/crates/netipc/src/protocol/cgroups_snapshot.rs renamed
src/crates/netipc/src/protocol/lookup.rs new
+13
@@ -0,0 +1,13 @@
1 +//! Lookup service-kind codecs.
2 +
3 +mod apps_lookup;
4 +mod cgroups_lookup;
5 +mod common;
6 +
7 +pub use apps_lookup::*;
8 +pub use cgroups_lookup::*;
9 +pub use common::{
10 + LookupLabelView, LOOKUP_DIR_ENTRY_SIZE, LOOKUP_LABEL_ENTRY_SIZE, ORCHESTRATOR_DOCKER,
11 + ORCHESTRATOR_K8S, ORCHESTRATOR_KVM, ORCHESTRATOR_LXC, ORCHESTRATOR_NSPAWN, ORCHESTRATOR_PODMAN,
12 + ORCHESTRATOR_SYSTEMD, ORCHESTRATOR_UNKNOWN,
13 +};
src/crates/netipc/src/protocol/lookup/apps_lookup.rs new
+892
@@ -0,0 +1,892 @@
1 +//! APPS_LOOKUP codec.
2 +
3 +use super::common::*;
4 +use crate::protocol::{align8, NipcError, StrView};
5 +
6 +pub const NIPC_UID_UNSET: u32 = u32::MAX;
7 +
8 +pub const PID_LOOKUP_KNOWN: u16 = 0;
9 +pub const PID_LOOKUP_UNKNOWN: u16 = 1;
10 +
11 +pub const APPS_CGROUP_KNOWN: u16 = 0;
12 +pub const APPS_CGROUP_UNKNOWN_RETRY_LATER: u16 = 1;
13 +pub const APPS_CGROUP_UNKNOWN_PERMANENT: u16 = 2;
14 +pub const APPS_CGROUP_HOST_ROOT: u16 = 3;
15 +
16 +pub const APPS_LOOKUP_REQ_HDR_SIZE: usize = 16;
17 +pub const APPS_LOOKUP_RESP_HDR_SIZE: usize = 16;
18 +pub const APPS_LOOKUP_ITEM_HDR_SIZE: usize = 60;
19 +pub const APPS_LOOKUP_KEY_SIZE: usize = 8;
20 +
21 +#[derive(Debug)]
22 +pub struct AppsLookupRequestView<'a> {
23 + pub item_count: u32,
24 + payload: &'a [u8],
25 +}
26 +
27 +#[derive(Debug)]
28 +pub struct AppsLookupResponseView<'a> {
29 + pub layout_version: u16,
30 + pub flags: u16,
31 + pub item_count: u32,
32 + pub generation: u64,
33 + payload: &'a [u8],
34 +}
35 +
36 +#[derive(Debug, Clone, Copy, PartialEq)]
37 +pub struct AppsLookupItemView<'a> {
38 + pub status: u16,
39 + pub orchestrator: u16,
40 + pub cgroup_status: u16,
41 + pub pid: u32,
42 + pub ppid: u32,
43 + pub uid: u32,
44 + pub starttime: u64,
45 + pub comm: StrView<'a>,
46 + pub cgroup_path: StrView<'a>,
47 + pub cgroup_name: StrView<'a>,
48 + pub label_count: u16,
49 + item: &'a [u8],
50 + label_table_offset: usize,
51 +}
52 +
53 +fn validate_apps_lookup_semantics(
54 + status: u16,
55 + cgroup_status: u16,
56 + orchestrator: u16,
57 + ppid: u32,
58 + uid: u32,
59 + starttime: u64,
60 + comm_len: u64,
61 + path_len: u64,
62 + name_len: u64,
63 + label_count: u64,
64 +) -> Result<(), NipcError> {
65 + validate_apps_lookup_domains(status, cgroup_status, comm_len)?;
66 + if status == PID_LOOKUP_UNKNOWN {
67 + return validate_apps_lookup_unknown(
68 + orchestrator,
69 + cgroup_status,
70 + ppid,
71 + uid,
72 + starttime,
73 + comm_len,
74 + path_len,
75 + name_len,
76 + label_count,
77 + );
78 + }
79 + validate_apps_lookup_known(
80 + cgroup_status,
81 + orchestrator,
82 + comm_len,
83 + path_len,
84 + name_len,
85 + label_count,
86 + )
87 +}
88 +
89 +fn validate_apps_lookup_domains(
90 + status: u16,
91 + cgroup_status: u16,
92 + comm_len: u64,
93 +) -> Result<(), NipcError> {
94 + if status != PID_LOOKUP_KNOWN && status != PID_LOOKUP_UNKNOWN {
95 + return Err(NipcError::BadLayout);
96 + }
97 + if cgroup_status != APPS_CGROUP_KNOWN
98 + && cgroup_status != APPS_CGROUP_UNKNOWN_RETRY_LATER
99 + && cgroup_status != APPS_CGROUP_UNKNOWN_PERMANENT
100 + && cgroup_status != APPS_CGROUP_HOST_ROOT
101 + {
102 + return Err(NipcError::BadLayout);
103 + }
104 + if comm_len > 15 {
105 + return Err(NipcError::BadLayout);
106 + }
107 + Ok(())
108 +}
109 +
110 +fn validate_apps_lookup_unknown(
111 + orchestrator: u16,
112 + cgroup_status: u16,
113 + ppid: u32,
114 + uid: u32,
115 + starttime: u64,
116 + comm_len: u64,
117 + path_len: u64,
118 + name_len: u64,
119 + label_count: u64,
120 +) -> Result<(), NipcError> {
121 + if orchestrator != 0
122 + || cgroup_status != 0
123 + || ppid != 0
124 + || uid != NIPC_UID_UNSET
125 + || starttime != 0
126 + || comm_len != 0
127 + || path_len != 0
128 + || name_len != 0
129 + || label_count != 0
130 + {
131 + return Err(NipcError::BadLayout);
132 + }
133 + Ok(())
134 +}
135 +
136 +fn validate_apps_lookup_known(
137 + cgroup_status: u16,
138 + orchestrator: u16,
139 + comm_len: u64,
140 + path_len: u64,
141 + name_len: u64,
142 + label_count: u64,
143 +) -> Result<(), NipcError> {
144 + if comm_len == 0 {
145 + return Err(NipcError::BadLayout);
146 + }
147 + match cgroup_status {
148 + APPS_CGROUP_KNOWN => {
149 + if path_len == 0 {
150 + return Err(NipcError::BadLayout);
151 + }
152 + }
153 + APPS_CGROUP_UNKNOWN_RETRY_LATER => {
154 + if orchestrator != 0 || name_len != 0 || label_count != 0 {
155 + return Err(NipcError::BadLayout);
156 + }
157 + }
158 + APPS_CGROUP_UNKNOWN_PERMANENT => {
159 + if path_len == 0 || orchestrator != 0 || name_len != 0 || label_count != 0 {
160 + return Err(NipcError::BadLayout);
161 + }
162 + }
163 + APPS_CGROUP_HOST_ROOT => {
164 + if orchestrator != 0 || path_len != 0 || name_len != 0 || label_count != 0 {
165 + return Err(NipcError::BadLayout);
166 + }
167 + }
168 + _ => return Err(NipcError::BadLayout),
169 + }
170 + Ok(())
171 +}
172 +
173 +pub fn encode_apps_lookup_request(pids: &[u32], buf: &mut [u8]) -> Result<usize, NipcError> {
174 + let count = pids.len();
175 + let dir_size = count
176 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
177 + .ok_or(NipcError::Overflow)?;
178 + let key_size = count
179 + .checked_mul(APPS_LOOKUP_KEY_SIZE)
180 + .ok_or(NipcError::Overflow)?;
181 + let packed_start = APPS_LOOKUP_REQ_HDR_SIZE
182 + .checked_add(dir_size)
183 + .ok_or(NipcError::Overflow)?;
184 + let total = packed_start
185 + .checked_add(key_size)
186 + .ok_or(NipcError::Overflow)?;
187 + if total > buf.len() {
188 + return Err(NipcError::Overflow);
189 + }
190 +
191 + for (i, pid) in pids.iter().enumerate() {
192 + let dir = APPS_LOOKUP_REQ_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
193 + let key_offset = i
194 + .checked_mul(APPS_LOOKUP_KEY_SIZE)
195 + .ok_or(NipcError::Overflow)?;
196 + put_u32(buf, dir, checked_u32(key_offset)?);
197 + put_u32(buf, dir + 4, APPS_LOOKUP_KEY_SIZE as u32);
198 + let key = packed_start + i * APPS_LOOKUP_KEY_SIZE;
199 + put_u32(buf, key, *pid);
200 + put_u32(buf, key + 4, 0);
201 + }
202 +
203 + put_u16(buf, 0, 1);
204 + put_u16(buf, 2, 0);
205 + put_u32(buf, 4, checked_u32(count)?);
206 + put_u32(buf, 8, 0);
207 + put_u32(buf, 12, 0);
208 + Ok(total)
209 +}
210 +
211 +impl<'a> AppsLookupRequestView<'a> {
212 + pub fn decode(buf: &'a [u8]) -> Result<Self, NipcError> {
213 + if buf.len() < APPS_LOOKUP_REQ_HDR_SIZE {
214 + return Err(NipcError::Truncated);
215 + }
216 + if u16_at(buf, 0) != 1 || u16_at(buf, 2) != 0 || u32_at(buf, 8) != 0 || u32_at(buf, 12) != 0
217 + {
218 + return Err(NipcError::BadLayout);
219 + }
220 + let item_count = u32_at(buf, 4);
221 + let dir_size = (item_count as usize)
222 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
223 + .ok_or(NipcError::BadItemCount)?;
224 + let dir_end = APPS_LOOKUP_REQ_HDR_SIZE
225 + .checked_add(dir_size)
226 + .ok_or(NipcError::BadItemCount)?;
227 + if dir_end > buf.len() {
228 + return Err(NipcError::Truncated);
229 + }
230 + validate_lookup_dir(
231 + buf,
232 + APPS_LOOKUP_REQ_HDR_SIZE,
233 + item_count,
234 + buf.len() - dir_end,
235 + 0,
236 + Some(APPS_LOOKUP_KEY_SIZE),
237 + )?;
238 + for i in 0..item_count as usize {
239 + let base = APPS_LOOKUP_REQ_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
240 + let off = u32_at(buf, base) as usize;
241 + let key = checked_subslice(buf, dir_end, off, APPS_LOOKUP_KEY_SIZE)?;
242 + if u32_at(key, 4) != 0 {
243 + return Err(NipcError::BadLayout);
244 + }
245 + }
246 + Ok(Self {
247 + item_count,
248 + payload: buf,
249 + })
250 + }
251 +
252 + pub fn item(&self, index: u32) -> Result<u32, NipcError> {
253 + if index >= self.item_count {
254 + return Err(NipcError::OutOfBounds);
255 + }
256 + let dir_size = (self.item_count as usize)
257 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
258 + .ok_or(NipcError::BadItemCount)?;
259 + let packed_start = APPS_LOOKUP_REQ_HDR_SIZE
260 + .checked_add(dir_size)
261 + .ok_or(NipcError::BadItemCount)?;
262 + let base = APPS_LOOKUP_REQ_HDR_SIZE
263 + .checked_add(
264 + (index as usize)
265 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
266 + .ok_or(NipcError::BadItemCount)?,
267 + )
268 + .ok_or(NipcError::BadItemCount)?;
269 + let off = u32_at(self.payload, base) as usize;
270 + Ok(u32_at(
271 + checked_subslice(self.payload, packed_start, off, APPS_LOOKUP_KEY_SIZE)?,
272 + 0,
273 + ))
274 + }
275 +}
276 +
277 +impl<'a> AppsLookupResponseView<'a> {
278 + pub fn decode(buf: &'a [u8]) -> Result<Self, NipcError> {
279 + if buf.len() < APPS_LOOKUP_RESP_HDR_SIZE {
280 + return Err(NipcError::Truncated);
281 + }
282 + if u16_at(buf, 0) != 1 || u16_at(buf, 2) != 0 {
283 + return Err(NipcError::BadLayout);
284 + }
285 + let item_count = u32_at(buf, 4);
286 + let dir_size = (item_count as usize)
287 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
288 + .ok_or(NipcError::BadItemCount)?;
289 + let dir_end = APPS_LOOKUP_RESP_HDR_SIZE
290 + .checked_add(dir_size)
291 + .ok_or(NipcError::BadItemCount)?;
292 + if dir_end > buf.len() {
293 + return Err(NipcError::Truncated);
294 + }
295 + validate_lookup_dir(
296 + buf,
297 + APPS_LOOKUP_RESP_HDR_SIZE,
298 + item_count,
299 + buf.len() - dir_end,
300 + APPS_LOOKUP_ITEM_HDR_SIZE,
301 + None,
302 + )?;
303 + for i in 0..item_count as usize {
304 + let base = APPS_LOOKUP_RESP_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
305 + let off = u32_at(buf, base) as usize;
306 + let len = u32_at(buf, base + 4) as usize;
307 + decode_apps_item(checked_subslice(buf, dir_end, off, len)?)?;
308 + }
309 + Ok(Self {
310 + layout_version: 1,
311 + flags: 0,
312 + item_count,
313 + generation: u64_at(buf, 8),
314 + payload: buf,
315 + })
316 + }
317 +
318 + pub fn item(&self, index: u32) -> Result<AppsLookupItemView<'a>, NipcError> {
319 + if index >= self.item_count {
320 + return Err(NipcError::OutOfBounds);
321 + }
322 + let packed_start = lookup_data_offset(APPS_LOOKUP_RESP_HDR_SIZE, self.item_count)?;
323 + let base = lookup_dir_entry_offset(APPS_LOOKUP_RESP_HDR_SIZE, index)?;
324 + let off = u32_at(self.payload, base) as usize;
325 + let len = u32_at(self.payload, base + 4) as usize;
326 + decode_apps_item(checked_subslice(self.payload, packed_start, off, len)?)
327 + }
328 +}
329 +
330 +fn decode_apps_item(item: &[u8]) -> Result<AppsLookupItemView<'_>, NipcError> {
331 + if item.len() < APPS_LOOKUP_ITEM_HDR_SIZE {
332 + return Err(NipcError::Truncated);
333 + }
334 + let status = u16_at(item, 2);
335 + let orchestrator = u16_at(item, 4);
336 + let cgroup_status = u16_at(item, 6);
337 + let pid = u32_at(item, 8);
338 + let ppid = u32_at(item, 12);
339 + let uid = u32_at(item, 16);
340 + let starttime = u64_at(item, 24);
341 + let comm_off = u32_at(item, 32) as usize;
342 + let comm_len = u32_at(item, 36) as usize;
343 + let path_off = u32_at(item, 40) as usize;
344 + let path_len = u32_at(item, 44) as usize;
345 + let name_off = u32_at(item, 48) as usize;
346 + let name_len = u32_at(item, 52) as usize;
347 + let label_count = u16_at(item, 56);
348 +
349 + if u16_at(item, 0) != 1 || u32_at(item, 20) != 0 || u16_at(item, 58) != 0 {
350 + return Err(NipcError::BadLayout);
351 + }
352 + validate_apps_lookup_semantics(
353 + status,
354 + cgroup_status,
355 + orchestrator,
356 + ppid,
357 + uid,
358 + starttime,
359 + comm_len as u64,
360 + path_len as u64,
361 + name_len as u64,
362 + label_count as u64,
363 + )?;
364 +
365 + let (comm, comm_end) = lookup_string(item, APPS_LOOKUP_ITEM_HDR_SIZE, comm_off, comm_len)?;
366 + let (cgroup_path, path_end) =
367 + lookup_string(item, APPS_LOOKUP_ITEM_HDR_SIZE, path_off, path_len)?;
368 + let (cgroup_name, name_end) =
369 + lookup_string(item, APPS_LOOKUP_ITEM_HDR_SIZE, name_off, name_len)?;
370 + if overlap(comm_off, comm_end, path_off, path_end)
371 + || overlap(comm_off, comm_end, name_off, name_end)
372 + || overlap(path_off, path_end, name_off, name_end)
373 + {
374 + return Err(NipcError::BadLayout);
375 + }
376 + let label_table_offset = validate_labels(
377 + item,
378 + APPS_LOOKUP_ITEM_HDR_SIZE,
379 + label_count,
380 + comm_end.max(path_end).max(name_end),
381 + )?;
382 + Ok(AppsLookupItemView {
383 + status,
384 + orchestrator,
385 + cgroup_status,
386 + pid,
387 + ppid,
388 + uid,
389 + starttime,
390 + comm,
391 + cgroup_path,
392 + cgroup_name,
393 + label_count,
394 + item,
395 + label_table_offset,
396 + })
397 +}
398 +
399 +impl<'a> AppsLookupItemView<'a> {
400 + pub fn label(&self, index: u32) -> Result<LookupLabelView<'a>, NipcError> {
401 + label_at(
402 + self.item,
403 + APPS_LOOKUP_ITEM_HDR_SIZE,
404 + self.label_count,
405 + self.label_table_offset,
406 + index,
407 + )
408 + }
409 +}
410 +
411 +pub struct AppsLookupBuilder<'a> {
412 + buf: &'a mut [u8],
413 + generation: u64,
414 + item_count: u32,
415 + max_items: u32,
416 + data_offset: usize,
417 + error: Option<NipcError>,
418 +}
419 +
420 +impl<'a> AppsLookupBuilder<'a> {
421 + pub fn new(buf: &'a mut [u8], max_items: u32, generation: u64) -> Self {
422 + let data_offset = (max_items as usize)
423 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
424 + .and_then(|v| APPS_LOOKUP_RESP_HDR_SIZE.checked_add(v))
425 + .expect("AppsLookupBuilder buffer too small");
426 + assert!(
427 + buf.len() >= data_offset,
428 + "AppsLookupBuilder buffer too small"
429 + );
430 + Self {
431 + buf,
432 + generation,
433 + item_count: 0,
434 + max_items,
435 + data_offset,
436 + error: None,
437 + }
438 + }
439 +
440 + pub fn set_generation(&mut self, generation: u64) {
441 + self.generation = generation;
442 + }
443 +
444 + #[allow(clippy::too_many_arguments)]
445 + pub fn add(
446 + &mut self,
447 + status: u16,
448 + cgroup_status: u16,
449 + orchestrator: u16,
450 + pid: u32,
451 + ppid: u32,
452 + uid: u32,
453 + starttime: u64,
454 + comm: &[u8],
455 + cgroup_path: &[u8],
456 + cgroup_name: &[u8],
457 + labels: &[(&[u8], &[u8])],
458 + ) -> Result<(), NipcError> {
459 + if self.item_count >= self.max_items {
460 + self.error = Some(NipcError::Overflow);
461 + return Err(NipcError::Overflow);
462 + }
463 + if let Err(err) = validate_apps_lookup_semantics(
464 + status,
465 + cgroup_status,
466 + orchestrator,
467 + ppid,
468 + uid,
469 + starttime,
470 + comm.len() as u64,
471 + cgroup_path.len() as u64,
472 + cgroup_name.len() as u64,
473 + labels.len() as u64,
474 + ) {
475 + self.error = Some(err);
476 + return Err(err);
477 + }
478 + if source_string_invalid(comm, status == PID_LOOKUP_KNOWN)
479 + || source_string_invalid(cgroup_path, false)
480 + || source_string_invalid(cgroup_name, false)
481 + {
482 + self.error = Some(NipcError::BadLayout);
483 + return Err(NipcError::BadLayout);
484 + }
485 + let label_count = match checked_u16(labels.len()) {
486 + Ok(v) => v,
487 + Err(err) => {
488 + self.error = Some(err);
489 + return Err(err);
490 + }
491 + };
492 +
493 + let item_start = align8(self.data_offset);
494 + let comm_offset = APPS_LOOKUP_ITEM_HDR_SIZE;
495 + let Some(path_offset) = comm_offset
496 + .checked_add(comm.len())
497 + .and_then(|v| v.checked_add(1))
498 + else {
499 + self.error = Some(NipcError::Overflow);
500 + return Err(NipcError::Overflow);
501 + };
502 + let Some(name_offset) = path_offset
503 + .checked_add(cgroup_path.len())
504 + .and_then(|v| v.checked_add(1))
505 + else {
506 + self.error = Some(NipcError::Overflow);
507 + return Err(NipcError::Overflow);
508 + };
509 + let Some(fixed_end) = name_offset
510 + .checked_add(cgroup_name.len())
511 + .and_then(|v| v.checked_add(1))
512 + else {
513 + self.error = Some(NipcError::Overflow);
514 + return Err(NipcError::Overflow);
515 + };
516 + let (table_start, table_bytes, mut item_size) = label_layout(fixed_end, labels)?;
517 + let item_end = item_start
518 + .checked_add(item_size)
519 + .ok_or(NipcError::Overflow)?;
520 + if item_end > self.buf.len() {
521 + self.error = Some(NipcError::Overflow);
522 + return Err(NipcError::Overflow);
523 + }
524 + if item_start > self.data_offset {
525 + self.buf[self.data_offset..item_start].fill(0);
526 + }
527 + let item = &mut self.buf[item_start..item_end];
528 + if let Err(err) = write_apps_item_header(
529 + item,
530 + status,
531 + orchestrator,
532 + cgroup_status,
533 + pid,
534 + ppid,
535 + uid,
536 + starttime,
537 + comm_offset,
538 + comm.len(),
539 + path_offset,
540 + cgroup_path.len(),
541 + name_offset,
542 + cgroup_name.len(),
543 + label_count,
544 + ) {
545 + self.error = Some(err);
546 + return Err(err);
547 + }
548 + item[comm_offset..comm_offset + comm.len()].copy_from_slice(comm);
549 + item[comm_offset + comm.len()] = 0;
550 + item[path_offset..path_offset + cgroup_path.len()].copy_from_slice(cgroup_path);
551 + item[path_offset + cgroup_path.len()] = 0;
552 + item[name_offset..name_offset + cgroup_name.len()].copy_from_slice(cgroup_name);
553 + item[name_offset + cgroup_name.len()] = 0;
554 + if !labels.is_empty() {
555 + item[fixed_end..table_start].fill(0);
556 + item_size = write_lookup_labels(item, table_start, table_bytes, labels)?;
557 + }
558 + let dir_offset = (self.item_count as usize)
559 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
560 + .ok_or(NipcError::Overflow)?;
561 + let dir = APPS_LOOKUP_RESP_HDR_SIZE
562 + .checked_add(dir_offset)
563 + .ok_or(NipcError::Overflow)?;
564 + put_u32(self.buf, dir, checked_u32(item_start)?);
565 + put_u32(self.buf, dir + 4, checked_u32(item_size)?);
566 + self.data_offset = item_end;
567 + self.item_count += 1;
568 + Ok(())
569 + }
570 +
571 + pub fn finish(self) -> Result<usize, NipcError> {
572 + finish_lookup_response(
573 + self.buf,
574 + APPS_LOOKUP_RESP_HDR_SIZE,
575 + self.item_count,
576 + self.data_offset,
577 + self.generation,
578 + )
579 + }
580 +
581 + pub fn error(&self) -> Option<NipcError> {
582 + self.error
583 + }
584 +
585 + pub fn item_count(&self) -> u32 {
586 + self.item_count
587 + }
588 +}
589 +
590 +#[allow(clippy::too_many_arguments)]
591 +fn write_apps_item_header(
592 + item: &mut [u8],
593 + status: u16,
594 + orchestrator: u16,
595 + cgroup_status: u16,
596 + pid: u32,
597 + ppid: u32,
598 + uid: u32,
599 + starttime: u64,
600 + comm_off: usize,
601 + comm_len: usize,
602 + path_off: usize,
603 + path_len: usize,
604 + name_off: usize,
605 + name_len: usize,
606 + label_count: u16,
607 +) -> Result<(), NipcError> {
608 + put_u16(item, 0, 1);
609 + put_u16(item, 2, status);
610 + put_u16(item, 4, orchestrator);
611 + put_u16(item, 6, cgroup_status);
612 + put_u32(item, 8, pid);
613 + put_u32(item, 12, ppid);
614 + put_u32(item, 16, uid);
615 + put_u32(item, 20, 0);
616 + put_u64(item, 24, starttime);
617 + put_u32(item, 32, checked_u32(comm_off)?);
618 + put_u32(item, 36, checked_u32(comm_len)?);
619 + put_u32(item, 40, checked_u32(path_off)?);
620 + put_u32(item, 44, checked_u32(path_len)?);
621 + put_u32(item, 48, checked_u32(name_off)?);
622 + put_u32(item, 52, checked_u32(name_len)?);
623 + put_u16(item, 56, label_count);
624 + put_u16(item, 58, 0);
625 + Ok(())
626 +}
627 +
628 +pub fn dispatch_apps_lookup<F>(req: &[u8], resp: &mut [u8], handler: F) -> Result<usize, NipcError>
629 +where
630 + F: FnOnce(&AppsLookupRequestView, &mut AppsLookupBuilder) -> bool,
631 +{
632 + let request = AppsLookupRequestView::decode(req)?;
633 + let min_required = lookup_data_offset(APPS_LOOKUP_RESP_HDR_SIZE, request.item_count)
634 + .map_err(|_| NipcError::Overflow)?;
635 + if resp.len() < min_required {
636 + return Err(NipcError::Overflow);
637 + }
638 + let mut builder = AppsLookupBuilder::new(resp, request.item_count, 0);
639 + if !handler(&request, &mut builder) {
640 + return Err(builder.error().unwrap_or(NipcError::BadLayout));
641 + }
642 + if let Some(err) = builder.error() {
643 + return Err(err);
644 + }
645 + if builder.item_count != request.item_count {
646 + return Err(NipcError::BadItemCount);
647 + }
648 + builder.finish()
649 +}
650 +
651 +#[cfg(test)]
652 +mod tests {
653 + use super::super::common::{put_u16, put_u32, response_item_bounds, LOOKUP_DIR_ENTRY_SIZE};
654 + use super::*;
655 + use crate::protocol::ORCHESTRATOR_DOCKER;
656 +
657 + #[test]
658 + fn apps_lookup_response_variants_roundtrip() {
659 + let mut buf = [0u8; 1024];
660 + let mut b = AppsLookupBuilder::new(&mut buf, 4, 7);
661 + b.add(
662 + PID_LOOKUP_KNOWN,
663 + APPS_CGROUP_KNOWN,
664 + ORCHESTRATOR_DOCKER,
665 + 123,
666 + 1,
667 + 1000,
668 + 42,
669 + b"nginx",
670 + b"/docker/abc",
671 + b"container-a",
672 + &[(b"image".as_slice(), b"nginx:latest".as_slice())],
673 + )
674 + .unwrap();
675 + b.add(
676 + PID_LOOKUP_KNOWN,
677 + APPS_CGROUP_UNKNOWN_RETRY_LATER,
678 + 0,
679 + 125,
680 + 1,
681 + 0,
682 + 44,
683 + b"worker",
684 + b"",
685 + b"",
686 + &[],
687 + )
688 + .unwrap();
689 + b.add(
690 + PID_LOOKUP_KNOWN,
691 + APPS_CGROUP_HOST_ROOT,
692 + 0,
693 + 124,
694 + 1,
695 + 0,
696 + 43,
697 + b"sshd",
698 + b"",
699 + b"",
700 + &[],
701 + )
702 + .unwrap();
703 + b.add(
704 + PID_LOOKUP_UNKNOWN,
705 + APPS_CGROUP_KNOWN,
706 + 0,
707 + 0,
708 + 0,
709 + NIPC_UID_UNSET,
710 + 0,
711 + b"",
712 + b"",
713 + b"",
714 + &[],
715 + )
716 + .unwrap();
717 + let n = b.finish().unwrap();
718 + let view = AppsLookupResponseView::decode(&buf[..n]).unwrap();
719 + assert_eq!(view.item_count, 4);
720 + assert_eq!(view.item(0).unwrap().comm.as_bytes(), b"nginx");
721 + assert_eq!(
722 + view.item(1).unwrap().cgroup_status,
723 + APPS_CGROUP_UNKNOWN_RETRY_LATER
724 + );
725 + assert_eq!(view.item(1).unwrap().cgroup_path.as_bytes(), b"");
726 + assert_eq!(view.item(2).unwrap().cgroup_status, APPS_CGROUP_HOST_ROOT);
727 + assert_eq!(view.item(3).unwrap().status, PID_LOOKUP_UNKNOWN);
728 + }
729 +
730 + #[test]
731 + fn apps_lookup_comm_boundary() {
732 + let mut buf = [0u8; 256];
733 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 0);
734 + assert!(b
735 + .add(
736 + PID_LOOKUP_KNOWN,
737 + APPS_CGROUP_HOST_ROOT,
738 + 0,
739 + 1,
740 + 0,
741 + 0,
742 + 1,
743 + b"123456789012345",
744 + b"",
745 + b"",
746 + &[],
747 + )
748 + .is_ok());
749 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 0);
750 + assert_eq!(
751 + b.add(
752 + PID_LOOKUP_KNOWN,
753 + APPS_CGROUP_HOST_ROOT,
754 + 0,
755 + 1,
756 + 0,
757 + 0,
758 + 1,
759 + b"1234567890123456",
760 + b"",
761 + b"",
762 + &[],
763 + )
764 + .unwrap_err(),
765 + NipcError::BadLayout
766 + );
767 + }
768 +
769 + fn apps_lookup_host_root_response() -> Vec<u8> {
770 + let mut buf = vec![0u8; 256];
771 + let mut b = AppsLookupBuilder::new(&mut buf, 1, 1);
772 + b.add(
773 + PID_LOOKUP_KNOWN,
774 + APPS_CGROUP_HOST_ROOT,
775 + 0,
776 + 123,
777 + 1,
778 + 1000,
779 + 42,
780 + b"a",
781 + b"",
782 + b"",
783 + &[],
784 + )
785 + .unwrap();
786 + let n = b.finish().unwrap();
787 + buf.truncate(n);
788 + buf
789 + }
790 +
791 + #[test]
792 + fn apps_lookup_empty_request_response() {
793 + let mut buf = [0u8; 64];
794 + let n = encode_apps_lookup_request(&[], &mut buf).unwrap();
795 + let view = AppsLookupRequestView::decode(&buf[..n]).unwrap();
796 + assert_eq!(view.item_count, 0);
797 +
798 + let mut abuf = [0u8; 64];
799 + let b = AppsLookupBuilder::new(&mut abuf, 0, 10);
800 + let n = b.finish().unwrap();
801 + let view = AppsLookupResponseView::decode(&abuf[..n]).unwrap();
802 + assert_eq!(view.item_count, 0);
803 + assert_eq!(view.generation, 10);
804 + }
805 +
806 + #[test]
807 + fn apps_lookup_dispatch_rejects_short_response_buffer() {
808 + let mut req = [0u8; 64];
809 + let n = encode_apps_lookup_request(&[1234], &mut req).unwrap();
810 + let mut resp = vec![0u8; APPS_LOOKUP_RESP_HDR_SIZE + LOOKUP_DIR_ENTRY_SIZE - 1];
811 + assert_eq!(
812 + dispatch_apps_lookup(&req[..n], &mut resp, |_, _| {
813 + panic!("handler must not run with undersized response buffer")
814 + })
815 + .unwrap_err(),
816 + NipcError::Overflow
817 + );
818 + }
819 +
820 + #[test]
821 + fn apps_lookup_request_rejects_bad_layouts() {
822 + let mut buf = [0u8; 128];
823 + let n = encode_apps_lookup_request(&[1234], &mut buf).unwrap();
824 + let mut bad = buf[..n].to_vec();
825 + put_u32(&mut bad, 8, 1);
826 + assert_eq!(
827 + AppsLookupRequestView::decode(&bad).unwrap_err(),
828 + NipcError::BadLayout
829 + );
830 +
831 + bad.copy_from_slice(&buf[..n]);
832 + put_u32(&mut bad, APPS_LOOKUP_REQ_HDR_SIZE, 1);
833 + assert_eq!(
834 + AppsLookupRequestView::decode(&bad).unwrap_err(),
835 + NipcError::BadAlignment
836 + );
837 +
838 + bad.copy_from_slice(&buf[..n]);
839 + put_u32(&mut bad, APPS_LOOKUP_REQ_HDR_SIZE + 4, 7);
840 + assert_eq!(
841 + AppsLookupRequestView::decode(&bad).unwrap_err(),
842 + NipcError::BadLayout
843 + );
844 +
845 + bad.copy_from_slice(&buf[..n]);
846 + put_u32(&mut bad, APPS_LOOKUP_REQ_HDR_SIZE, 8);
847 + assert_eq!(
848 + AppsLookupRequestView::decode(&bad).unwrap_err(),
849 + NipcError::OutOfBounds
850 + );
851 +
852 + bad.copy_from_slice(&buf[..n]);
853 + put_u32(
854 + &mut bad,
855 + APPS_LOOKUP_REQ_HDR_SIZE + LOOKUP_DIR_ENTRY_SIZE + 4,
856 + 1,
857 + );
858 + assert_eq!(
859 + AppsLookupRequestView::decode(&bad).unwrap_err(),
860 + NipcError::BadLayout
861 + );
862 + }
863 +
864 + #[test]
865 + fn apps_lookup_response_rejects_bad_layouts() {
866 + let buf = apps_lookup_host_root_response();
867 +
868 + let mut bad = buf.clone();
869 + let (item_start, _) = response_item_bounds(&bad, APPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
870 + put_u16(&mut bad, item_start + 2, 99);
871 + assert_eq!(
872 + AppsLookupResponseView::decode(&bad).unwrap_err(),
873 + NipcError::BadLayout
874 + );
875 +
876 + bad = buf.clone();
877 + let (item_start, _) = response_item_bounds(&bad, APPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
878 + put_u16(&mut bad, item_start + 6, 99);
879 + assert_eq!(
880 + AppsLookupResponseView::decode(&bad).unwrap_err(),
881 + NipcError::BadLayout
882 + );
883 +
884 + bad = buf.clone();
885 + let (item_start, _) = response_item_bounds(&bad, APPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
886 + put_u32(&mut bad, item_start + 36, 0);
887 + assert_eq!(
888 + AppsLookupResponseView::decode(&bad).unwrap_err(),
889 + NipcError::BadLayout
890 + );
891 + }
892 +}
src/crates/netipc/src/protocol/lookup/cgroups_lookup.rs new
+755
@@ -0,0 +1,755 @@
1 +//! CGROUPS_LOOKUP codec.
2 +
3 +use super::common::*;
4 +use crate::protocol::{align8, NipcError, StrView};
5 +
6 +pub const CGROUP_LOOKUP_KNOWN: u16 = 0;
7 +pub const CGROUP_LOOKUP_UNKNOWN_RETRY_LATER: u16 = 1;
8 +pub const CGROUP_LOOKUP_UNKNOWN_PERMANENT: u16 = 2;
9 +
10 +pub const CGROUPS_LOOKUP_REQ_HDR_SIZE: usize = 16;
11 +pub const CGROUPS_LOOKUP_RESP_HDR_SIZE: usize = 16;
12 +pub const CGROUPS_LOOKUP_ITEM_HDR_SIZE: usize = 28;
13 +
14 +#[derive(Debug)]
15 +pub struct CgroupsLookupRequestView<'a> {
16 + pub item_count: u32,
17 + payload: &'a [u8],
18 +}
19 +
20 +#[derive(Debug)]
21 +pub struct CgroupsLookupResponseView<'a> {
22 + pub layout_version: u16,
23 + pub flags: u16,
24 + pub item_count: u32,
25 + pub generation: u64,
26 + payload: &'a [u8],
27 +}
28 +
29 +#[derive(Debug, Clone, Copy, PartialEq)]
30 +pub struct CgroupsLookupItemView<'a> {
31 + pub status: u16,
32 + pub orchestrator: u16,
33 + pub path: StrView<'a>,
34 + pub name: StrView<'a>,
35 + pub label_count: u16,
36 + item: &'a [u8],
37 + label_table_offset: usize,
38 +}
39 +
40 +fn validate_cgroups_lookup_semantics(
41 + status: u16,
42 + orchestrator: u16,
43 + path_len: u64,
44 + name_len: u64,
45 + label_count: u64,
46 +) -> Result<(), NipcError> {
47 + if status != CGROUP_LOOKUP_KNOWN
48 + && status != CGROUP_LOOKUP_UNKNOWN_RETRY_LATER
49 + && status != CGROUP_LOOKUP_UNKNOWN_PERMANENT
50 + {
51 + return Err(NipcError::BadLayout);
52 + }
53 + if path_len == 0 {
54 + return Err(NipcError::BadLayout);
55 + }
56 + if status != CGROUP_LOOKUP_KNOWN && (orchestrator != 0 || name_len != 0 || label_count != 0) {
57 + return Err(NipcError::BadLayout);
58 + }
59 + Ok(())
60 +}
61 +
62 +pub fn encode_cgroups_lookup_request(paths: &[&[u8]], buf: &mut [u8]) -> Result<usize, NipcError> {
63 + let count = paths.len();
64 + let dir_size = count
65 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
66 + .ok_or(NipcError::Overflow)?;
67 + let packed_start = CGROUPS_LOOKUP_REQ_HDR_SIZE
68 + .checked_add(dir_size)
69 + .ok_or(NipcError::Overflow)?;
70 + if buf.len() < packed_start {
71 + return Err(NipcError::Overflow);
72 + }
73 +
74 + let mut data = packed_start;
75 + for (i, path) in paths.iter().enumerate() {
76 + if source_string_invalid(path, true) {
77 + return Err(NipcError::BadLayout);
78 + }
79 + let aligned = align8(data);
80 + let key_len = path.len().checked_add(1).ok_or(NipcError::Overflow)?;
81 + let end = aligned.checked_add(key_len).ok_or(NipcError::Overflow)?;
82 + if end > buf.len() {
83 + return Err(NipcError::Overflow);
84 + }
85 + if aligned > data {
86 + buf[data..aligned].fill(0);
87 + }
88 +
89 + let dir = CGROUPS_LOOKUP_REQ_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
90 + put_u32(buf, dir, checked_u32(aligned - packed_start)?);
91 + put_u32(buf, dir + 4, checked_u32(key_len)?);
92 + buf[aligned..aligned + path.len()].copy_from_slice(path);
93 + buf[aligned + path.len()] = 0;
94 + data = end;
95 + }
96 +
97 + put_u16(buf, 0, 1);
98 + put_u16(buf, 2, 0);
99 + put_u32(buf, 4, checked_u32(count)?);
100 + put_u32(buf, 8, 0);
101 + put_u32(buf, 12, 0);
102 + Ok(data)
103 +}
104 +
105 +impl<'a> CgroupsLookupRequestView<'a> {
106 + pub fn decode(buf: &'a [u8]) -> Result<Self, NipcError> {
107 + if buf.len() < CGROUPS_LOOKUP_REQ_HDR_SIZE {
108 + return Err(NipcError::Truncated);
109 + }
110 + if u16_at(buf, 0) != 1 || u16_at(buf, 2) != 0 || u32_at(buf, 8) != 0 || u32_at(buf, 12) != 0
111 + {
112 + return Err(NipcError::BadLayout);
113 + }
114 + let item_count = u32_at(buf, 4);
115 + let dir_size = (item_count as usize)
116 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
117 + .ok_or(NipcError::BadItemCount)?;
118 + let dir_end = CGROUPS_LOOKUP_REQ_HDR_SIZE
119 + .checked_add(dir_size)
120 + .ok_or(NipcError::BadItemCount)?;
121 + if dir_end > buf.len() {
122 + return Err(NipcError::Truncated);
123 + }
124 + let packed_len = buf.len() - dir_end;
125 + validate_lookup_dir(
126 + buf,
127 + CGROUPS_LOOKUP_REQ_HDR_SIZE,
128 + item_count,
129 + packed_len,
130 + 2,
131 + None,
132 + )?;
133 + for i in 0..item_count as usize {
134 + let base = CGROUPS_LOOKUP_REQ_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
135 + let off = u32_at(buf, base) as usize;
136 + let len = u32_at(buf, base + 4) as usize;
137 + let key = checked_subslice(buf, dir_end, off, len)?;
138 + if key[len - 1] != 0 {
139 + return Err(NipcError::MissingNul);
140 + }
141 + if key[..len - 1].contains(&0) {
142 + return Err(NipcError::BadLayout);
143 + }
144 + }
145 + Ok(Self {
146 + item_count,
147 + payload: buf,
148 + })
149 + }
150 +
151 + pub fn item(&self, index: u32) -> Result<StrView<'a>, NipcError> {
152 + if index >= self.item_count {
153 + return Err(NipcError::OutOfBounds);
154 + }
155 + let dir_size = (self.item_count as usize)
156 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
157 + .ok_or(NipcError::BadItemCount)?;
158 + let packed_start = CGROUPS_LOOKUP_REQ_HDR_SIZE
159 + .checked_add(dir_size)
160 + .ok_or(NipcError::BadItemCount)?;
161 + let base = CGROUPS_LOOKUP_REQ_HDR_SIZE
162 + .checked_add(
163 + (index as usize)
164 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
165 + .ok_or(NipcError::BadItemCount)?,
166 + )
167 + .ok_or(NipcError::BadItemCount)?;
168 + let off = u32_at(self.payload, base) as usize;
169 + let len = u32_at(self.payload, base + 4) as usize;
170 + Ok(StrView {
171 + bytes: checked_subslice(self.payload, packed_start, off, len)?,
172 + len: (len - 1) as u32,
173 + })
174 + }
175 +}
176 +
177 +impl<'a> CgroupsLookupResponseView<'a> {
178 + pub fn decode(buf: &'a [u8]) -> Result<Self, NipcError> {
179 + if buf.len() < CGROUPS_LOOKUP_RESP_HDR_SIZE {
180 + return Err(NipcError::Truncated);
181 + }
182 + if u16_at(buf, 0) != 1 || u16_at(buf, 2) != 0 {
183 + return Err(NipcError::BadLayout);
184 + }
185 + let item_count = u32_at(buf, 4);
186 + let dir_size = (item_count as usize)
187 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
188 + .ok_or(NipcError::BadItemCount)?;
189 + let dir_end = CGROUPS_LOOKUP_RESP_HDR_SIZE
190 + .checked_add(dir_size)
191 + .ok_or(NipcError::BadItemCount)?;
192 + if dir_end > buf.len() {
193 + return Err(NipcError::Truncated);
194 + }
195 + validate_lookup_dir(
196 + buf,
197 + CGROUPS_LOOKUP_RESP_HDR_SIZE,
198 + item_count,
199 + buf.len() - dir_end,
200 + CGROUPS_LOOKUP_ITEM_HDR_SIZE,
201 + None,
202 + )?;
203 + for i in 0..item_count as usize {
204 + let base = CGROUPS_LOOKUP_RESP_HDR_SIZE + i * LOOKUP_DIR_ENTRY_SIZE;
205 + let off = u32_at(buf, base) as usize;
206 + let len = u32_at(buf, base + 4) as usize;
207 + decode_cgroups_item(checked_subslice(buf, dir_end, off, len)?)?;
208 + }
209 + Ok(Self {
210 + layout_version: 1,
211 + flags: 0,
212 + item_count,
213 + generation: u64_at(buf, 8),
214 + payload: buf,
215 + })
216 + }
217 +
218 + pub fn item(&self, index: u32) -> Result<CgroupsLookupItemView<'a>, NipcError> {
219 + if index >= self.item_count {
220 + return Err(NipcError::OutOfBounds);
221 + }
222 + let packed_start = lookup_data_offset(CGROUPS_LOOKUP_RESP_HDR_SIZE, self.item_count)?;
223 + let base = lookup_dir_entry_offset(CGROUPS_LOOKUP_RESP_HDR_SIZE, index)?;
224 + let off = u32_at(self.payload, base) as usize;
225 + let len = u32_at(self.payload, base + 4) as usize;
226 + decode_cgroups_item(checked_subslice(self.payload, packed_start, off, len)?)
227 + }
228 +}
229 +
230 +fn decode_cgroups_item(item: &[u8]) -> Result<CgroupsLookupItemView<'_>, NipcError> {
231 + if item.len() < CGROUPS_LOOKUP_ITEM_HDR_SIZE {
232 + return Err(NipcError::Truncated);
233 + }
234 + let status = u16_at(item, 2);
235 + let orchestrator = u16_at(item, 4);
236 + let path_off = u32_at(item, 8) as usize;
237 + let path_len = u32_at(item, 12) as usize;
238 + let name_off = u32_at(item, 16) as usize;
239 + let name_len = u32_at(item, 20) as usize;
240 + let label_count = u16_at(item, 24);
241 +
242 + if u16_at(item, 0) != 1 || u16_at(item, 6) != 0 || u16_at(item, 26) != 0 {
243 + return Err(NipcError::BadLayout);
244 + }
245 + validate_cgroups_lookup_semantics(
246 + status,
247 + orchestrator,
248 + path_len as u64,
249 + name_len as u64,
250 + label_count as u64,
251 + )?;
252 +
253 + let (path, path_end) = lookup_string(item, CGROUPS_LOOKUP_ITEM_HDR_SIZE, path_off, path_len)?;
254 + let (name, name_end) = lookup_string(item, CGROUPS_LOOKUP_ITEM_HDR_SIZE, name_off, name_len)?;
255 + if overlap(path_off, path_end, name_off, name_end) {
256 + return Err(NipcError::BadLayout);
257 + }
258 + let label_table_offset = validate_labels(
259 + item,
260 + CGROUPS_LOOKUP_ITEM_HDR_SIZE,
261 + label_count,
262 + path_end.max(name_end),
263 + )?;
264 + Ok(CgroupsLookupItemView {
265 + status,
266 + orchestrator,
267 + path,
268 + name,
269 + label_count,
270 + item,
271 + label_table_offset,
272 + })
273 +}
274 +
275 +impl<'a> CgroupsLookupItemView<'a> {
276 + pub fn label(&self, index: u32) -> Result<LookupLabelView<'a>, NipcError> {
277 + label_at(
278 + self.item,
279 + CGROUPS_LOOKUP_ITEM_HDR_SIZE,
280 + self.label_count,
281 + self.label_table_offset,
282 + index,
283 + )
284 + }
285 +}
286 +
287 +pub struct CgroupsLookupBuilder<'a> {
288 + buf: &'a mut [u8],
289 + generation: u64,
290 + item_count: u32,
291 + max_items: u32,
292 + data_offset: usize,
293 + error: Option<NipcError>,
294 +}
295 +
296 +impl<'a> CgroupsLookupBuilder<'a> {
297 + pub fn new(buf: &'a mut [u8], max_items: u32, generation: u64) -> Self {
298 + let data_offset = (max_items as usize)
299 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
300 + .and_then(|v| CGROUPS_LOOKUP_RESP_HDR_SIZE.checked_add(v))
301 + .expect("CgroupsLookupBuilder buffer too small");
302 + assert!(
303 + buf.len() >= data_offset,
304 + "CgroupsLookupBuilder buffer too small"
305 + );
306 + Self {
307 + buf,
308 + generation,
309 + item_count: 0,
310 + max_items,
311 + data_offset,
312 + error: None,
313 + }
314 + }
315 +
316 + pub fn set_generation(&mut self, generation: u64) {
317 + self.generation = generation;
318 + }
319 +
320 + pub fn add(
321 + &mut self,
322 + status: u16,
323 + orchestrator: u16,
324 + path: &[u8],
325 + name: &[u8],
326 + labels: &[(&[u8], &[u8])],
327 + ) -> Result<(), NipcError> {
328 + if self.item_count >= self.max_items {
329 + self.error = Some(NipcError::Overflow);
330 + return Err(NipcError::Overflow);
331 + }
332 + if let Err(err) = validate_cgroups_lookup_semantics(
333 + status,
334 + orchestrator,
335 + path.len() as u64,
336 + name.len() as u64,
337 + labels.len() as u64,
338 + ) {
339 + self.error = Some(err);
340 + return Err(err);
341 + }
342 + if source_string_invalid(path, true) || source_string_invalid(name, false) {
343 + self.error = Some(NipcError::BadLayout);
344 + return Err(NipcError::BadLayout);
345 + }
346 + let label_count = match checked_u16(labels.len()) {
347 + Ok(v) => v,
348 + Err(err) => {
349 + self.error = Some(err);
350 + return Err(err);
351 + }
352 + };
353 +
354 + let item_start = align8(self.data_offset);
355 + let path_offset = CGROUPS_LOOKUP_ITEM_HDR_SIZE;
356 + let Some(name_offset) = path_offset
357 + .checked_add(path.len())
358 + .and_then(|v| v.checked_add(1))
359 + else {
360 + self.error = Some(NipcError::Overflow);
361 + return Err(NipcError::Overflow);
362 + };
363 + let Some(fixed_end) = name_offset
364 + .checked_add(name.len())
365 + .and_then(|v| v.checked_add(1))
366 + else {
367 + self.error = Some(NipcError::Overflow);
368 + return Err(NipcError::Overflow);
369 + };
370 + let (table_start, table_bytes, mut item_size) = label_layout(fixed_end, labels)?;
371 + let item_end = item_start
372 + .checked_add(item_size)
373 + .ok_or(NipcError::Overflow)?;
374 + if item_end > self.buf.len() {
375 + self.error = Some(NipcError::Overflow);
376 + return Err(NipcError::Overflow);
377 + }
378 + if item_start > self.data_offset {
379 + self.buf[self.data_offset..item_start].fill(0);
380 + }
381 + let item = &mut self.buf[item_start..item_end];
382 + if let Err(err) = write_cgroups_item_header(
383 + item,
384 + status,
385 + orchestrator,
386 + path_offset,
387 + path.len(),
388 + name_offset,
389 + name.len(),
390 + label_count,
391 + ) {
392 + self.error = Some(err);
393 + return Err(err);
394 + }
395 + item[path_offset..path_offset + path.len()].copy_from_slice(path);
396 + item[path_offset + path.len()] = 0;
397 + item[name_offset..name_offset + name.len()].copy_from_slice(name);
398 + item[name_offset + name.len()] = 0;
399 + if !labels.is_empty() {
400 + item[fixed_end..table_start].fill(0);
401 + item_size = write_lookup_labels(item, table_start, table_bytes, labels)?;
402 + }
403 + let dir_offset = (self.item_count as usize)
404 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
405 + .ok_or(NipcError::Overflow)?;
406 + let dir = CGROUPS_LOOKUP_RESP_HDR_SIZE
407 + .checked_add(dir_offset)
408 + .ok_or(NipcError::Overflow)?;
409 + put_u32(self.buf, dir, checked_u32(item_start)?);
410 + put_u32(self.buf, dir + 4, checked_u32(item_size)?);
411 + self.data_offset = item_end;
412 + self.item_count += 1;
413 + Ok(())
414 + }
415 +
416 + pub fn finish(self) -> Result<usize, NipcError> {
417 + finish_lookup_response(
418 + self.buf,
419 + CGROUPS_LOOKUP_RESP_HDR_SIZE,
420 + self.item_count,
421 + self.data_offset,
422 + self.generation,
423 + )
424 + }
425 +
426 + pub fn error(&self) -> Option<NipcError> {
427 + self.error
428 + }
429 +
430 + pub fn item_count(&self) -> u32 {
431 + self.item_count
432 + }
433 +}
434 +
435 +fn write_cgroups_item_header(
436 + item: &mut [u8],
437 + status: u16,
438 + orchestrator: u16,
439 + path_off: usize,
440 + path_len: usize,
441 + name_off: usize,
442 + name_len: usize,
443 + label_count: u16,
444 +) -> Result<(), NipcError> {
445 + put_u16(item, 0, 1);
446 + put_u16(item, 2, status);
447 + put_u16(item, 4, orchestrator);
448 + put_u16(item, 6, 0);
449 + put_u32(item, 8, checked_u32(path_off)?);
450 + put_u32(item, 12, checked_u32(path_len)?);
451 + put_u32(item, 16, checked_u32(name_off)?);
452 + put_u32(item, 20, checked_u32(name_len)?);
453 + put_u16(item, 24, label_count);
454 + put_u16(item, 26, 0);
455 + Ok(())
456 +}
457 +
458 +pub fn dispatch_cgroups_lookup<F>(
459 + req: &[u8],
460 + resp: &mut [u8],
461 + handler: F,
462 +) -> Result<usize, NipcError>
463 +where
464 + F: FnOnce(&CgroupsLookupRequestView, &mut CgroupsLookupBuilder) -> bool,
465 +{
466 + let request = CgroupsLookupRequestView::decode(req)?;
467 + let min_required = lookup_data_offset(CGROUPS_LOOKUP_RESP_HDR_SIZE, request.item_count)
468 + .map_err(|_| NipcError::Overflow)?;
469 + if resp.len() < min_required {
470 + return Err(NipcError::Overflow);
471 + }
472 + let mut builder = CgroupsLookupBuilder::new(resp, request.item_count, 0);
473 + if !handler(&request, &mut builder) {
474 + return Err(builder.error().unwrap_or(NipcError::BadLayout));
475 + }
476 + if let Some(err) = builder.error() {
477 + return Err(err);
478 + }
479 + if builder.item_count != request.item_count {
480 + return Err(NipcError::BadItemCount);
481 + }
482 + builder.finish()
483 +}
484 +
485 +#[cfg(test)]
486 +mod tests {
487 + use super::super::common::{
488 + put_u16, put_u32, response_item_bounds, u32_at, LOOKUP_DIR_ENTRY_SIZE,
489 + };
490 + use super::*;
491 + use crate::protocol::{align8, ORCHESTRATOR_K8S};
492 +
493 + #[test]
494 + fn cgroups_lookup_request_roundtrip() {
495 + let mut buf = [0u8; 128];
496 + let n = encode_cgroups_lookup_request(&[b"/a/b", b"/c"], &mut buf).unwrap();
497 + let view = CgroupsLookupRequestView::decode(&buf[..n]).unwrap();
498 + assert_eq!(view.item_count, 2);
499 + assert_eq!(view.item(0).unwrap().as_bytes(), b"/a/b");
500 + assert_eq!(view.item(1).unwrap().as_bytes(), b"/c");
501 + }
502 +
503 + #[test]
504 + fn cgroups_lookup_response_labels_roundtrip() {
505 + let mut buf = [0u8; 512];
506 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 99);
507 + b.add(
508 + CGROUP_LOOKUP_KNOWN,
509 + ORCHESTRATOR_K8S,
510 + b"/kubepod",
511 + b"pod-a",
512 + &[(b"namespace".as_slice(), b"default".as_slice())],
513 + )
514 + .unwrap();
515 + let n = b.finish().unwrap();
516 + let view = CgroupsLookupResponseView::decode(&buf[..n]).unwrap();
517 + assert_eq!(view.generation, 99);
518 + let item = view.item(0).unwrap();
519 + assert_eq!(item.path.as_bytes(), b"/kubepod");
520 + assert_eq!(item.name.as_bytes(), b"pod-a");
521 + let label = item.label(0).unwrap();
522 + assert_eq!(label.key.as_bytes(), b"namespace");
523 + assert_eq!(label.value.as_bytes(), b"default");
524 + }
525 +
526 + fn cgroups_lookup_labeled_response() -> Vec<u8> {
527 + let mut buf = vec![0u8; 512];
528 + let mut b = CgroupsLookupBuilder::new(&mut buf, 1, 1);
529 + b.add(
530 + CGROUP_LOOKUP_KNOWN,
531 + ORCHESTRATOR_K8S,
532 + b"/x",
533 + b"n",
534 + &[(b"k".as_slice(), b"v".as_slice())],
535 + )
536 + .unwrap();
537 + let n = b.finish().unwrap();
538 + buf.truncate(n);
539 + buf
540 + }
541 +
542 + #[test]
543 + fn cgroups_lookup_empty_request_response() {
544 + let mut buf = [0u8; 64];
545 + let n = encode_cgroups_lookup_request(&[], &mut buf).unwrap();
546 + let view = CgroupsLookupRequestView::decode(&buf[..n]).unwrap();
547 + assert_eq!(view.item_count, 0);
548 +
549 + let mut cbuf = [0u8; 64];
550 + let b = CgroupsLookupBuilder::new(&mut cbuf, 0, 9);
551 + let n = b.finish().unwrap();
552 + let view = CgroupsLookupResponseView::decode(&cbuf[..n]).unwrap();
553 + assert_eq!(view.item_count, 0);
554 + assert_eq!(view.generation, 9);
555 + }
556 +
557 + #[test]
558 + fn cgroups_lookup_dispatch_rejects_short_response_buffer() {
559 + let mut req = [0u8; 64];
560 + let n = encode_cgroups_lookup_request(&[b"/x"], &mut req).unwrap();
561 + let mut resp = vec![0u8; CGROUPS_LOOKUP_RESP_HDR_SIZE + LOOKUP_DIR_ENTRY_SIZE - 1];
562 + assert_eq!(
563 + dispatch_cgroups_lookup(&req[..n], &mut resp, |_, _| {
564 + panic!("handler must not run with undersized response buffer")
565 + })
566 + .unwrap_err(),
567 + NipcError::Overflow
568 + );
569 + }
570 +
571 + #[test]
572 + fn cgroups_lookup_request_rejects_bad_layouts() {
573 + let mut buf = [0u8; 128];
574 + let n = encode_cgroups_lookup_request(&[b"/x"], &mut buf).unwrap();
575 + assert_eq!(
576 + CgroupsLookupRequestView::decode(&buf[..CGROUPS_LOOKUP_REQ_HDR_SIZE - 1]).unwrap_err(),
577 + NipcError::Truncated
578 + );
579 +
580 + let mut bad = buf[..n].to_vec();
581 + put_u16(&mut bad, 0, 99);
582 + assert_eq!(
583 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
584 + NipcError::BadLayout
585 + );
586 +
587 + bad.copy_from_slice(&buf[..n]);
588 + put_u32(&mut bad, 8, 1);
589 + assert_eq!(
590 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
591 + NipcError::BadLayout
592 + );
593 +
594 + bad.copy_from_slice(&buf[..n]);
595 + put_u32(&mut bad, CGROUPS_LOOKUP_REQ_HDR_SIZE, 1);
596 + assert_eq!(
597 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
598 + NipcError::BadAlignment
599 + );
600 +
601 + bad.copy_from_slice(&buf[..n]);
602 + put_u32(&mut bad, CGROUPS_LOOKUP_REQ_HDR_SIZE + 4, 4096);
603 + assert_eq!(
604 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
605 + NipcError::OutOfBounds
606 + );
607 +
608 + bad.copy_from_slice(&buf[..n]);
609 + let last = bad.len() - 1;
610 + bad[last] = b'x';
611 + assert_eq!(
612 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
613 + NipcError::MissingNul
614 + );
615 +
616 + bad.copy_from_slice(&buf[..n]);
617 + bad[CGROUPS_LOOKUP_REQ_HDR_SIZE + LOOKUP_DIR_ENTRY_SIZE] = 0;
618 + assert_eq!(
619 + CgroupsLookupRequestView::decode(&bad).unwrap_err(),
620 + NipcError::BadLayout
621 + );
622 + }
623 +
624 + #[test]
625 + fn cgroups_lookup_response_rejects_bad_layouts() {
626 + let buf = cgroups_lookup_labeled_response();
627 + assert_eq!(
628 + CgroupsLookupResponseView::decode(&buf[..CGROUPS_LOOKUP_RESP_HDR_SIZE - 1])
629 + .unwrap_err(),
630 + NipcError::Truncated
631 + );
632 +
633 + let mut bad = buf.clone();
634 + put_u16(&mut bad, 0, 99);
635 + assert_eq!(
636 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
637 + NipcError::BadLayout
638 + );
639 +
640 + bad = buf.clone();
641 + put_u16(&mut bad, 2, 1);
642 + assert_eq!(
643 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
644 + NipcError::BadLayout
645 + );
646 +
647 + bad = buf.clone();
648 + put_u32(&mut bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1);
649 + assert_eq!(
650 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
651 + NipcError::BadAlignment
652 + );
653 +
654 + bad = buf.clone();
655 + put_u32(&mut bad, CGROUPS_LOOKUP_RESP_HDR_SIZE + 4, 4096);
656 + assert_eq!(
657 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
658 + NipcError::OutOfBounds
659 + );
660 +
661 + bad = buf.clone();
662 + put_u32(
663 + &mut bad,
664 + CGROUPS_LOOKUP_RESP_HDR_SIZE + 4,
665 + (CGROUPS_LOOKUP_ITEM_HDR_SIZE - 1) as u32,
666 + );
667 + assert_eq!(
668 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
669 + NipcError::BadLayout
670 + );
671 +
672 + bad = buf.clone();
673 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
674 + put_u16(&mut bad, item_start, 99);
675 + assert_eq!(
676 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
677 + NipcError::BadLayout
678 + );
679 +
680 + bad = buf.clone();
681 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
682 + let path_off = u32_at(&bad, item_start + 8) as usize;
683 + let path_len = u32_at(&bad, item_start + 12) as usize;
684 + bad[item_start + path_off + path_len] = b'x';
685 + assert_eq!(
686 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
687 + NipcError::MissingNul
688 + );
689 +
690 + bad = buf.clone();
691 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
692 + put_u32(&mut bad, item_start + 8, 4);
693 + assert_eq!(
694 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
695 + NipcError::OutOfBounds
696 + );
697 +
698 + bad = buf.clone();
699 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
700 + let path_off = u32_at(&bad, item_start + 8);
701 + let path_len = u32_at(&bad, item_start + 12);
702 + put_u32(&mut bad, item_start + 16, path_off);
703 + put_u32(&mut bad, item_start + 20, path_len);
704 + assert_eq!(
705 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
706 + NipcError::BadLayout
707 + );
708 +
709 + bad = buf.clone();
710 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
711 + let path_off = u32_at(&bad, item_start + 8) as usize;
712 + let path_len = u32_at(&bad, item_start + 12) as usize;
713 + let name_off = u32_at(&bad, item_start + 16) as usize;
714 + let name_len = u32_at(&bad, item_start + 20) as usize;
715 + let fixed_end = (path_off + path_len + 1).max(name_off + name_len + 1);
716 + bad[item_start + fixed_end] = 1;
717 + assert_eq!(
718 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
719 + NipcError::BadLayout
720 + );
721 +
722 + bad = buf.clone();
723 + let (item_start, _) = response_item_bounds(&bad, CGROUPS_LOOKUP_RESP_HDR_SIZE, 1, 0);
724 + let path_off = u32_at(&bad, item_start + 8) as usize;
725 + let path_len = u32_at(&bad, item_start + 12) as usize;
726 + let name_off = u32_at(&bad, item_start + 16) as usize;
727 + let name_len = u32_at(&bad, item_start + 20) as usize;
728 + let fixed_end = (path_off + path_len + 1).max(name_off + name_len + 1);
729 + let table_start = align8(fixed_end);
730 + put_u32(&mut bad, item_start + table_start + 4, 0);
731 + assert_eq!(
732 + CgroupsLookupResponseView::decode(&bad).unwrap_err(),
733 + NipcError::BadLayout
734 + );
735 +
736 + let mut two = vec![0u8; 512];
737 + let mut b = CgroupsLookupBuilder::new(&mut two, 2, 1);
738 + b.add(CGROUP_LOOKUP_UNKNOWN_PERMANENT, 0, b"/a", b"", &[])
739 + .unwrap();
740 + b.add(CGROUP_LOOKUP_UNKNOWN_PERMANENT, 0, b"/b", b"", &[])
741 + .unwrap();
742 + let n = b.finish().unwrap();
743 + two.truncate(n);
744 + let first_off = u32_at(&two, CGROUPS_LOOKUP_RESP_HDR_SIZE);
745 + put_u32(
746 + &mut two,
747 + CGROUPS_LOOKUP_RESP_HDR_SIZE + LOOKUP_DIR_ENTRY_SIZE,
748 + first_off,
749 + );
750 + assert_eq!(
751 + CgroupsLookupResponseView::decode(&two).unwrap_err(),
752 + NipcError::BadLayout
753 + );
754 + }
755 +}
src/crates/netipc/src/protocol/lookup/common.rs new
+390
@@ -0,0 +1,390 @@
1 +//! Shared helpers for lookup codecs.
2 +
3 +use crate::protocol::{align8, NipcError, StrView, ALIGNMENT};
4 +
5 +pub const LOOKUP_DIR_ENTRY_SIZE: usize = 8;
6 +pub const LOOKUP_LABEL_ENTRY_SIZE: usize = 16;
7 +
8 +pub const ORCHESTRATOR_UNKNOWN: u16 = 0;
9 +pub const ORCHESTRATOR_SYSTEMD: u16 = 1;
10 +pub const ORCHESTRATOR_DOCKER: u16 = 2;
11 +pub const ORCHESTRATOR_K8S: u16 = 3;
12 +pub const ORCHESTRATOR_KVM: u16 = 4;
13 +pub const ORCHESTRATOR_LXC: u16 = 5;
14 +pub const ORCHESTRATOR_PODMAN: u16 = 6;
15 +pub const ORCHESTRATOR_NSPAWN: u16 = 7;
16 +
17 +#[derive(Debug, Clone, Copy, PartialEq)]
18 +pub struct LookupLabelView<'a> {
19 + pub key: StrView<'a>,
20 + pub value: StrView<'a>,
21 +}
22 +
23 +#[inline]
24 +pub(super) fn u16_at(buf: &[u8], off: usize) -> u16 {
25 + u16::from_ne_bytes(buf[off..off + 2].try_into().unwrap())
26 +}
27 +
28 +#[inline]
29 +pub(super) fn u32_at(buf: &[u8], off: usize) -> u32 {
30 + u32::from_ne_bytes(buf[off..off + 4].try_into().unwrap())
31 +}
32 +
33 +#[inline]
34 +pub(super) fn u64_at(buf: &[u8], off: usize) -> u64 {
35 + u64::from_ne_bytes(buf[off..off + 8].try_into().unwrap())
36 +}
37 +
38 +#[inline]
39 +pub(super) fn put_u16(buf: &mut [u8], off: usize, value: u16) {
40 + buf[off..off + 2].copy_from_slice(&value.to_ne_bytes());
41 +}
42 +
43 +#[inline]
44 +pub(super) fn put_u32(buf: &mut [u8], off: usize, value: u32) {
45 + buf[off..off + 4].copy_from_slice(&value.to_ne_bytes());
46 +}
47 +
48 +#[inline]
49 +pub(super) fn put_u64(buf: &mut [u8], off: usize, value: u64) {
50 + buf[off..off + 8].copy_from_slice(&value.to_ne_bytes());
51 +}
52 +
53 +pub(super) fn checked_u32(value: usize) -> Result<u32, NipcError> {
54 + u32::try_from(value).map_err(|_| NipcError::Overflow)
55 +}
56 +
57 +pub(super) fn checked_u16(value: usize) -> Result<u16, NipcError> {
58 + u16::try_from(value).map_err(|_| NipcError::Overflow)
59 +}
60 +
61 +pub(super) fn source_string_invalid(bytes: &[u8], require_non_empty: bool) -> bool {
62 + (require_non_empty && bytes.is_empty()) || bytes.contains(&0)
63 +}
64 +
65 +pub(super) fn validate_lookup_dir(
66 + buf: &[u8],
67 + dir_start: usize,
68 + item_count: u32,
69 + packed_area_len: usize,
70 + min_len: usize,
71 + exact_len: Option<usize>,
72 +) -> Result<(), NipcError> {
73 + let dir_size = (item_count as usize)
74 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
75 + .ok_or(NipcError::BadItemCount)?;
76 + if dir_start
77 + .checked_add(dir_size)
78 + .ok_or(NipcError::BadItemCount)?
79 + > buf.len()
80 + {
81 + return Err(NipcError::Truncated);
82 + }
83 +
84 + let mut prev_end = 0usize;
85 + for i in 0..item_count as usize {
86 + let base = dir_start + i * LOOKUP_DIR_ENTRY_SIZE;
87 + let off = u32_at(buf, base) as usize;
88 + let len = u32_at(buf, base + 4) as usize;
89 + if off % ALIGNMENT != 0 {
90 + return Err(NipcError::BadAlignment);
91 + }
92 + if let Some(exact) = exact_len {
93 + if len != exact {
94 + return Err(NipcError::BadLayout);
95 + }
96 + } else if len < min_len {
97 + return Err(NipcError::BadLayout);
98 + }
99 + let end = off.checked_add(len).ok_or(NipcError::OutOfBounds)?;
100 + if end > packed_area_len {
101 + return Err(NipcError::OutOfBounds);
102 + }
103 + if i > 0 && off < prev_end {
104 + return Err(NipcError::BadLayout);
105 + }
106 + prev_end = end;
107 + }
108 + Ok(())
109 +}
110 +
111 +pub(super) fn lookup_string<'a>(
112 + item: &'a [u8],
113 + hdr_size: usize,
114 + offset: usize,
115 + length: usize,
116 +) -> Result<(StrView<'a>, usize), NipcError> {
117 + if offset < hdr_size {
118 + return Err(NipcError::OutOfBounds);
119 + }
120 + let nul = offset.checked_add(length).ok_or(NipcError::OutOfBounds)?;
121 + if nul >= item.len() {
122 + return Err(NipcError::OutOfBounds);
123 + }
124 + if item[nul] != 0 {
125 + return Err(NipcError::MissingNul);
126 + }
127 + if item[offset..nul].contains(&0) {
128 + return Err(NipcError::BadLayout);
129 + }
130 + Ok((
131 + StrView {
132 + bytes: &item[offset..nul + 1],
133 + len: length as u32,
134 + },
135 + nul + 1,
136 + ))
137 +}
138 +
139 +#[inline]
140 +pub(super) fn overlap(a_start: usize, a_end: usize, b_start: usize, b_end: usize) -> bool {
141 + a_start < b_end && b_start < a_end
142 +}
143 +
144 +pub(super) fn checked_subslice<'a>(
145 + buf: &'a [u8],
146 + base: usize,
147 + offset: usize,
148 + len: usize,
149 +) -> Result<&'a [u8], NipcError> {
150 + let start = base.checked_add(offset).ok_or(NipcError::OutOfBounds)?;
151 + let end = start.checked_add(len).ok_or(NipcError::OutOfBounds)?;
152 + buf.get(start..end).ok_or(NipcError::OutOfBounds)
153 +}
154 +
155 +pub(super) fn lookup_data_offset(hdr_size: usize, item_count: u32) -> Result<usize, NipcError> {
156 + (item_count as usize)
157 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
158 + .and_then(|v| hdr_size.checked_add(v))
159 + .ok_or(NipcError::BadItemCount)
160 +}
161 +
162 +pub(super) fn lookup_dir_entry_offset(hdr_size: usize, index: u32) -> Result<usize, NipcError> {
163 + (index as usize)
164 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
165 + .and_then(|v| hdr_size.checked_add(v))
166 + .ok_or(NipcError::BadItemCount)
167 +}
168 +
169 +pub(super) fn validate_labels(
170 + item: &[u8],
171 + hdr_size: usize,
172 + label_count: u16,
173 + fixed_end: usize,
174 +) -> Result<usize, NipcError> {
175 + if label_count == 0 {
176 + if fixed_end != item.len() {
177 + return Err(NipcError::BadLayout);
178 + }
179 + return Ok(fixed_end);
180 + }
181 +
182 + let table_start = align8(fixed_end);
183 + if table_start > item.len() {
184 + return Err(NipcError::OutOfBounds);
185 + }
186 + if item[fixed_end..table_start].iter().any(|&b| b != 0) {
187 + return Err(NipcError::BadLayout);
188 + }
189 +
190 + let table_bytes = (label_count as usize)
191 + .checked_mul(LOOKUP_LABEL_ENTRY_SIZE)
192 + .ok_or(NipcError::OutOfBounds)?;
193 + let mut expected = table_start
194 + .checked_add(table_bytes)
195 + .ok_or(NipcError::OutOfBounds)?;
196 + if expected > item.len() {
197 + return Err(NipcError::OutOfBounds);
198 + }
199 +
200 + for i in 0..label_count as usize {
201 + let entry_rel = i
202 + .checked_mul(LOOKUP_LABEL_ENTRY_SIZE)
203 + .ok_or(NipcError::OutOfBounds)?;
204 + let base = table_start
205 + .checked_add(entry_rel)
206 + .ok_or(NipcError::OutOfBounds)?;
207 + let key_off = u32_at(item, base) as usize;
208 + let key_len = u32_at(item, base + 4) as usize;
209 + let value_off = u32_at(item, base + 8) as usize;
210 + let value_len = u32_at(item, base + 12) as usize;
211 + if key_len == 0 || key_off != expected {
212 + return Err(NipcError::BadLayout);
213 + }
214 + let (_, key_end) = lookup_string(item, hdr_size, key_off, key_len)?;
215 + expected = key_end;
216 + if value_off != expected {
217 + return Err(NipcError::BadLayout);
218 + }
219 + let (_, value_end) = lookup_string(item, hdr_size, value_off, value_len)?;
220 + expected = value_end;
221 + }
222 +
223 + if expected != item.len() {
224 + return Err(NipcError::BadLayout);
225 + }
226 + Ok(table_start)
227 +}
228 +
229 +pub(super) fn label_at<'a>(
230 + item: &'a [u8],
231 + hdr_size: usize,
232 + label_count: u16,
233 + label_table_offset: usize,
234 + index: u32,
235 +) -> Result<LookupLabelView<'a>, NipcError> {
236 + if index >= label_count as u32 {
237 + return Err(NipcError::OutOfBounds);
238 + }
239 + let entry_offset = (index as usize)
240 + .checked_mul(LOOKUP_LABEL_ENTRY_SIZE)
241 + .ok_or(NipcError::OutOfBounds)?;
242 + let base = label_table_offset
243 + .checked_add(entry_offset)
244 + .ok_or(NipcError::OutOfBounds)?;
245 + let entry_end = base
246 + .checked_add(LOOKUP_LABEL_ENTRY_SIZE)
247 + .ok_or(NipcError::OutOfBounds)?;
248 + if entry_end > item.len() {
249 + return Err(NipcError::OutOfBounds);
250 + }
251 + let key_off = u32_at(item, base) as usize;
252 + let key_len = u32_at(item, base + 4) as usize;
253 + let value_off = u32_at(item, base + 8) as usize;
254 + let value_len = u32_at(item, base + 12) as usize;
255 + let (key, _) = lookup_string(item, hdr_size, key_off, key_len)?;
256 + let (value, _) = lookup_string(item, hdr_size, value_off, value_len)?;
257 + Ok(LookupLabelView { key, value })
258 +}
259 +
260 +pub(super) fn write_lookup_labels(
261 + item: &mut [u8],
262 + table_start: usize,
263 + table_bytes: usize,
264 + labels: &[(&[u8], &[u8])],
265 +) -> Result<usize, NipcError> {
266 + let mut next = table_start
267 + .checked_add(table_bytes)
268 + .ok_or(NipcError::Overflow)?;
269 + for (i, (key, value)) in labels.iter().enumerate() {
270 + let entry_offset = i
271 + .checked_mul(LOOKUP_LABEL_ENTRY_SIZE)
272 + .ok_or(NipcError::Overflow)?;
273 + let entry = table_start
274 + .checked_add(entry_offset)
275 + .ok_or(NipcError::Overflow)?;
276 + let value_offset = next
277 + .checked_add(key.len())
278 + .and_then(|v| v.checked_add(1))
279 + .ok_or(NipcError::Overflow)?;
280 + put_u32(item, entry, checked_u32(next)?);
281 + put_u32(item, entry + 4, checked_u32(key.len())?);
282 + put_u32(item, entry + 8, checked_u32(value_offset)?);
283 + put_u32(item, entry + 12, checked_u32(value.len())?);
284 + item[next..next + key.len()].copy_from_slice(key);
285 + item[next + key.len()] = 0;
286 + next = value_offset;
287 + item[next..next + value.len()].copy_from_slice(value);
288 + item[next + value.len()] = 0;
289 + next = next
290 + .checked_add(value.len())
291 + .and_then(|v| v.checked_add(1))
292 + .ok_or(NipcError::Overflow)?;
293 + }
294 + Ok(next)
295 +}
296 +
297 +pub(super) fn label_layout(
298 + fixed_end: usize,
299 + labels: &[(&[u8], &[u8])],
300 +) -> Result<(usize, usize, usize), NipcError> {
301 + if labels.is_empty() {
302 + return Ok((fixed_end, 0, fixed_end));
303 + }
304 + let table_start = align8(fixed_end);
305 + let table_bytes = labels
306 + .len()
307 + .checked_mul(LOOKUP_LABEL_ENTRY_SIZE)
308 + .ok_or(NipcError::Overflow)?;
309 + let mut item_size = table_start
310 + .checked_add(table_bytes)
311 + .ok_or(NipcError::Overflow)?;
312 + for (key, value) in labels {
313 + if source_string_invalid(key, true) || source_string_invalid(value, false) {
314 + return Err(NipcError::BadLayout);
315 + }
316 + let key_size = key.len().checked_add(1).ok_or(NipcError::Overflow)?;
317 + let value_size = value.len().checked_add(1).ok_or(NipcError::Overflow)?;
318 + item_size = item_size
319 + .checked_add(key_size)
320 + .and_then(|v| v.checked_add(value_size))
321 + .ok_or(NipcError::Overflow)?;
322 + }
323 + Ok((table_start, table_bytes, item_size))
324 +}
325 +
326 +pub(super) fn finish_lookup_response(
327 + buf: &mut [u8],
328 + hdr_size: usize,
329 + item_count: u32,
330 + data_offset: usize,
331 + generation: u64,
332 +) -> Result<usize, NipcError> {
333 + put_u16(buf, 0, 1);
334 + put_u16(buf, 2, 0);
335 + put_u32(buf, 4, item_count);
336 + put_u64(buf, 8, generation);
337 + if item_count == 0 {
338 + return Ok(hdr_size);
339 + }
340 + let final_packed_start = (item_count as usize)
341 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
342 + .and_then(|v| hdr_size.checked_add(v))
343 + .ok_or(NipcError::Overflow)?;
344 + let first_item_abs = u32_at(buf, hdr_size) as usize;
345 + let packed_data_len = data_offset
346 + .checked_sub(first_item_abs)
347 + .ok_or(NipcError::Overflow)?;
348 + if final_packed_start < first_item_abs {
349 + let copy_end = first_item_abs
350 + .checked_add(packed_data_len)
351 + .ok_or(NipcError::Overflow)?;
352 + if copy_end > buf.len() {
353 + return Err(NipcError::Overflow);
354 + }
355 + let dest_end = final_packed_start
356 + .checked_add(packed_data_len)
357 + .ok_or(NipcError::Overflow)?;
358 + if dest_end > buf.len() {
359 + return Err(NipcError::Overflow);
360 + }
361 + buf.copy_within(first_item_abs..copy_end, final_packed_start);
362 + }
363 + for i in 0..item_count as usize {
364 + let entry_offset = i
365 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
366 + .ok_or(NipcError::Overflow)?;
367 + let entry = hdr_size
368 + .checked_add(entry_offset)
369 + .ok_or(NipcError::Overflow)?;
370 + let abs = u32_at(buf, entry) as usize;
371 + let rel = abs.checked_sub(first_item_abs).ok_or(NipcError::Overflow)?;
372 + put_u32(buf, entry, checked_u32(rel)?);
373 + }
374 + final_packed_start
375 + .checked_add(packed_data_len)
376 + .ok_or(NipcError::Overflow)
377 +}
378 +
379 +#[cfg(test)]
380 +pub(super) fn response_item_bounds(
381 + buf: &[u8],
382 + hdr_size: usize,
383 + item_count: usize,
384 + index: usize,
385 +) -> (usize, usize) {
386 + let dir = hdr_size + index * LOOKUP_DIR_ENTRY_SIZE;
387 + let off = u32_at(buf, dir) as usize;
388 + let len = u32_at(buf, dir + 4) as usize;
389 + (hdr_size + item_count * LOOKUP_DIR_ENTRY_SIZE + off, len)
390 +}
src/crates/netipc/src/protocol/mod.rs
+7 -1778
@@ -6,13 +6,15 @@
6 //! Decoded `View` types borrow the underlying buffer and are valid only while
7 //! that buffer lives. Copy immediately if the data is needed later.
8
9 -mod cgroups;
9 +mod cgroups_snapshot;
10 mod increment;
11 +mod lookup;
12 mod string_reverse;
13
14 // Re-export all public symbols from submodules.
14 -pub use cgroups::*;
15 +pub use cgroups_snapshot::*;
16 pub use increment::*;
17 +pub use lookup::*;
18 pub use string_reverse::*;
19
20 // ---------------------------------------------------------------------------
@@ -50,6 +52,8 @@ pub const CODE_HELLO_ACK: u16 = 2;
52 pub const METHOD_INCREMENT: u16 = 1;
53 pub const METHOD_CGROUPS_SNAPSHOT: u16 = 2;
54 pub const METHOD_STRING_REVERSE: u16 = 3;
55 +pub const METHOD_CGROUPS_LOOKUP: u16 = 4;
56 +pub const METHOD_APPS_LOOKUP: u16 = 5;
57
58 // Profile bits
59 pub const PROFILE_BASELINE: u32 = 0x01;
@@ -616,1780 +620,5 @@ impl HelloAck {
620 }
621 }
622
619 -// ===========================================================================
620 -// Tests
621 -// ===========================================================================
622 -
623 #[cfg(test)]
624 -mod tests {
625 - use super::*;
626 -
627 - // -----------------------------------------------------------------------
628 - // Outer message header tests
629 - // -----------------------------------------------------------------------
630 -
631 - #[test]
632 - fn header_roundtrip() {
633 - let h = Header {
634 - magic: MAGIC_MSG,
635 - version: VERSION,
636 - header_len: HEADER_LEN,
637 - kind: KIND_REQUEST,
638 - flags: FLAG_BATCH,
639 - code: METHOD_CGROUPS_SNAPSHOT,
640 - transport_status: STATUS_OK,
641 - payload_len: 12345,
642 - item_count: 42,
643 - message_id: 0xDEAD_BEEF_CAFE_BABE,
644 - };
645 -
646 - let mut buf = [0u8; 64];
647 - let n = h.encode(&mut buf);
648 - assert_eq!(n, 32);
649 -
650 - let out = Header::decode(&buf[..n]).unwrap();
651 - assert_eq!(out, h);
652 - }
653 -
654 - #[test]
655 - fn header_encode_too_small() {
656 - let h = Header::default();
657 - let mut buf = [0u8; 16];
658 - assert_eq!(h.encode(&mut buf), 0);
659 - }
660 -
661 - #[test]
662 - fn header_decode_truncated() {
663 - let buf = [0u8; 31];
664 - assert_eq!(Header::decode(&buf), Err(NipcError::Truncated));
665 - }
666 -
667 - #[test]
668 - fn header_decode_bad_magic() {
669 - let h = Header {
670 - magic: 0x12345678,
671 - version: VERSION,
672 - header_len: HEADER_LEN,
673 - kind: KIND_REQUEST,
674 - ..Default::default()
675 - };
676 - let mut buf = [0u8; 32];
677 - h.encode(&mut buf);
678 - assert_eq!(Header::decode(&buf), Err(NipcError::BadMagic));
679 - }
680 -
681 - #[test]
682 - fn header_decode_bad_version() {
683 - let h = Header {
684 - magic: MAGIC_MSG,
685 - version: 99,
686 - header_len: HEADER_LEN,
687 - kind: KIND_REQUEST,
688 - ..Default::default()
689 - };
690 - let mut buf = [0u8; 32];
691 - h.encode(&mut buf);
692 - assert_eq!(Header::decode(&buf), Err(NipcError::BadVersion));
693 - }
694 -
695 - #[test]
696 - fn header_decode_bad_header_len() {
697 - let h = Header {
698 - magic: MAGIC_MSG,
699 - version: VERSION,
700 - header_len: 64,
701 - kind: KIND_REQUEST,
702 - ..Default::default()
703 - };
704 - let mut buf = [0u8; 32];
705 - h.encode(&mut buf);
706 - assert_eq!(Header::decode(&buf), Err(NipcError::BadHeaderLen));
707 - }
708 -
709 - #[test]
710 - fn header_decode_bad_kind() {
711 - // kind = 0
712 - let h = Header {
713 - magic: MAGIC_MSG,
714 - version: VERSION,
715 - header_len: HEADER_LEN,
716 - kind: 0,
717 - ..Default::default()
718 - };
719 - let mut buf = [0u8; 32];
720 - h.encode(&mut buf);
721 - assert_eq!(Header::decode(&buf), Err(NipcError::BadKind));
722 -
723 - // kind = 4
724 - let h2 = Header { kind: 4, ..h };
725 - h2.encode(&mut buf);
726 - assert_eq!(Header::decode(&buf), Err(NipcError::BadKind));
727 - }
728 -
729 - #[test]
730 - fn header_all_kinds() {
731 - for k in KIND_REQUEST..=KIND_CONTROL {
732 - let h = Header {
733 - magic: MAGIC_MSG,
734 - version: VERSION,
735 - header_len: HEADER_LEN,
736 - kind: k,
737 - ..Default::default()
738 - };
739 - let mut buf = [0u8; 32];
740 - h.encode(&mut buf);
741 - let out = Header::decode(&buf).unwrap();
742 - assert_eq!(out.kind, k);
743 - }
744 - }
745 -
746 - #[test]
747 - fn header_wire_bytes() {
748 - let h = Header {
749 - magic: MAGIC_MSG,
750 - version: VERSION,
751 - header_len: HEADER_LEN,
752 - kind: KIND_REQUEST,
753 - flags: 0,
754 - code: METHOD_CGROUPS_SNAPSHOT,
755 - transport_status: STATUS_OK,
756 - payload_len: 4,
757 - item_count: 1,
758 - message_id: 1,
759 - };
760 -
761 - let mut buf = [0u8; 32];
762 - h.encode(&mut buf);
763 -
764 - // magic = 0x4e495043 LE: 43 50 49 4e
765 - assert_eq!(&buf[0..4], &[0x43, 0x50, 0x49, 0x4e]);
766 - // version = 1 LE: 01 00
767 - assert_eq!(&buf[4..6], &[0x01, 0x00]);
768 - // header_len = 32 LE: 20 00
769 - assert_eq!(&buf[6..8], &[0x20, 0x00]);
770 - // kind = 1 LE: 01 00
771 - assert_eq!(&buf[8..10], &[0x01, 0x00]);
772 - // code = 2 LE: 02 00
773 - assert_eq!(&buf[12..14], &[0x02, 0x00]);
774 - }
775 -
776 - // -----------------------------------------------------------------------
777 - // Chunk continuation header tests
778 - // -----------------------------------------------------------------------
779 -
780 - #[test]
781 - fn chunk_header_roundtrip() {
782 - let c = ChunkHeader {
783 - magic: MAGIC_CHUNK,
784 - version: VERSION,
785 - flags: 0,
786 - message_id: 0x1234_5678_90AB_CDEF,
787 - total_message_len: 100000,
788 - chunk_index: 3,
789 - chunk_count: 10,
790 - chunk_payload_len: 8192,
791 - };
792 -
793 - let mut buf = [0u8; 64];
794 - let n = c.encode(&mut buf);
795 - assert_eq!(n, 32);
796 -
797 - let out = ChunkHeader::decode(&buf[..n]).unwrap();
798 - assert_eq!(out, c);
799 - }
800 -
801 - #[test]
802 - fn chunk_decode_truncated() {
803 - let buf = [0u8; 31];
804 - assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::Truncated));
805 - }
806 -
807 - #[test]
808 - fn chunk_decode_bad_magic() {
809 - let c = ChunkHeader {
810 - magic: MAGIC_MSG, // wrong magic for chunk
811 - version: VERSION,
812 - ..Default::default()
813 - };
814 - let mut buf = [0u8; 32];
815 - c.encode(&mut buf);
816 - assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadMagic));
817 - }
818 -
819 - #[test]
820 - fn chunk_decode_bad_version() {
821 - let c = ChunkHeader {
822 - magic: MAGIC_CHUNK,
823 - version: 2,
824 - ..Default::default()
825 - };
826 - let mut buf = [0u8; 32];
827 - c.encode(&mut buf);
828 - assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadVersion));
829 - }
830 -
831 - #[test]
832 - fn chunk_encode_too_small() {
833 - let c = ChunkHeader::default();
834 - let mut buf = [0u8; 16];
835 - assert_eq!(c.encode(&mut buf), 0);
836 - }
837 -
838 - #[test]
839 - fn chunk_wire_bytes() {
840 - let c = ChunkHeader {
841 - magic: MAGIC_CHUNK,
842 - version: VERSION,
843 - flags: 0,
844 - message_id: 1,
845 - total_message_len: 256,
846 - chunk_index: 1,
847 - chunk_count: 3,
848 - chunk_payload_len: 100,
849 - };
850 -
851 - let mut buf = [0u8; 32];
852 - c.encode(&mut buf);
853 -
854 - // magic = 0x4e43484b LE: 4b 48 43 4e
855 - assert_eq!(&buf[0..4], &[0x4b, 0x48, 0x43, 0x4e]);
856 - }
857 -
858 - // -----------------------------------------------------------------------
859 - // Batch item directory tests
860 - // -----------------------------------------------------------------------
861 -
862 - #[test]
863 - fn batch_dir_roundtrip() {
864 - let entries = [
865 - BatchEntry {
866 - offset: 0,
867 - length: 100,
868 - },
869 - BatchEntry {
870 - offset: 104,
871 - length: 200,
872 - },
873 - BatchEntry {
874 - offset: 304,
875 - length: 50,
876 - },
877 - ];
878 -
879 - let mut buf = [0u8; 64];
880 - let n = batch_dir_encode(&entries, &mut buf);
881 - assert_eq!(n, 24);
882 -
883 - let out = batch_dir_decode(&buf[..n], 3, 400).unwrap();
884 - assert_eq!(out[0], entries[0]);
885 - assert_eq!(out[1], entries[1]);
886 - assert_eq!(out[2], entries[2]);
887 - }
888 -
889 - #[test]
890 - fn batch_dir_decode_truncated() {
891 - let buf = [0u8; 12];
892 - assert_eq!(batch_dir_decode(&buf, 2, 1000), Err(NipcError::Truncated));
893 - }
894 -
895 - #[test]
896 - fn batch_dir_decode_oob() {
897 - let e = BatchEntry {
898 - offset: 0,
899 - length: 200,
900 - };
901 - let mut buf = [0u8; 8];
902 - batch_dir_encode(&[e], &mut buf);
903 - assert_eq!(batch_dir_decode(&buf, 1, 100), Err(NipcError::OutOfBounds));
904 - }
905 -
906 - #[test]
907 - fn batch_dir_decode_bad_alignment() {
908 - let mut buf = [0u8; 8];
909 - // Manually write unaligned offset
910 - buf[0..4].copy_from_slice(&3u32.to_ne_bytes());
911 - buf[4..8].copy_from_slice(&10u32.to_ne_bytes());
912 - assert_eq!(batch_dir_decode(&buf, 1, 100), Err(NipcError::BadAlignment));
913 - }
914 -
915 - // -----------------------------------------------------------------------
916 - // Batch builder + extraction tests
917 - // -----------------------------------------------------------------------
918 -
919 - #[test]
920 - fn batch_builder_roundtrip() {
921 - let mut buf = [0u8; 1024];
922 - let mut b = BatchBuilder::new(&mut buf, 4);
923 -
924 - let item1 = [1u8, 2, 3, 4, 5];
925 - let item2 = [10u8, 20, 30];
926 - let item3 = [0xAAu8, 0xBB];
927 -
928 - b.add(&item1).unwrap();
929 - b.add(&item2).unwrap();
930 - b.add(&item3).unwrap();
931 -
932 - let (total, count) = b.finish();
933 - assert_eq!(count, 3);
934 - assert!(total > 0);
935 -
936 - // Extract items
937 - let (data, len) = batch_item_get(&buf[..total], 3, 0).unwrap();
938 - assert_eq!(len as usize, item1.len());
939 - assert_eq!(data, &item1);
940 -
941 - let (data, len) = batch_item_get(&buf[..total], 3, 1).unwrap();
942 - assert_eq!(len as usize, item2.len());
943 - assert_eq!(data, &item2);
944 -
945 - let (data, len) = batch_item_get(&buf[..total], 3, 2).unwrap();
946 - assert_eq!(len as usize, item3.len());
947 - assert_eq!(data, &item3);
948 - }
949 -
950 - #[test]
951 - fn batch_builder_overflow() {
952 - let mut buf = [0u8; 32];
953 - let mut b = BatchBuilder::new(&mut buf, 1);
954 - let item = [1u8];
955 - b.add(&item).unwrap();
956 - assert_eq!(b.add(&item), Err(NipcError::Overflow));
957 - }
958 -
959 - #[test]
960 - fn batch_builder_buf_overflow() {
961 - let mut buf = [0u8; 24];
962 - let mut b = BatchBuilder::new(&mut buf, 1);
963 - let big = [0u8; 100];
964 - assert_eq!(b.add(&big), Err(NipcError::Overflow));
965 - }
966 -
967 - #[test]
968 - fn batch_item_get_oob_index() {
969 - let mut buf = [0u8; 64];
970 - let mut b = BatchBuilder::new(&mut buf, 2);
971 - b.add(&[1u8]).unwrap();
972 - let (total, count) = b.finish();
973 - assert_eq!(
974 - batch_item_get(&buf[..total], count, 5),
975 - Err(NipcError::OutOfBounds)
976 - );
977 - }
978 -
979 - #[test]
980 - fn batch_empty() {
981 - let mut buf = [0u8; 64];
982 - let b = BatchBuilder::new(&mut buf, 4);
983 - let (total, count) = b.finish();
984 - assert_eq!(count, 0);
985 - assert_eq!(total, 0);
986 - }
987 -
988 - // -----------------------------------------------------------------------
989 - // Hello payload tests
990 - // -----------------------------------------------------------------------
991 -
992 - #[test]
993 - fn hello_roundtrip() {
994 - let h = Hello {
995 - layout_version: 1,
996 - flags: 0,
997 - supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
998 - preferred_profiles: PROFILE_SHM_FUTEX,
999 - max_request_payload_bytes: 4096,
1000 - max_request_batch_items: 100,
1001 - max_response_payload_bytes: 1048576,
1002 - max_response_batch_items: 1,
1003 - auth_token: 0xAABB_CCDD_EEFF_0011,
1004 - packet_size: 65536,
1005 - };
1006 -
1007 - let mut buf = [0u8; 64];
1008 - let n = h.encode(&mut buf);
1009 - assert_eq!(n, 44);
1010 -
1011 - let out = Hello::decode(&buf[..n]).unwrap();
1012 - assert_eq!(out, h);
1013 - }
1014 -
1015 - #[test]
1016 - fn hello_decode_truncated() {
1017 - let buf = [0u8; 43];
1018 - assert_eq!(Hello::decode(&buf), Err(NipcError::Truncated));
1019 - }
1020 -
1021 - #[test]
1022 - fn hello_decode_bad_layout() {
1023 - let h = Hello {
1024 - layout_version: 99,
1025 - ..Default::default()
1026 - };
1027 - let mut buf = [0u8; 44];
1028 - h.encode(&mut buf);
1029 - assert_eq!(Hello::decode(&buf), Err(NipcError::BadLayout));
1030 - }
1031 -
1032 - #[test]
1033 - fn hello_encode_too_small() {
1034 - let h = Hello::default();
1035 - let mut buf = [0u8; 10];
1036 - assert_eq!(h.encode(&mut buf), 0);
1037 - }
1038 -
1039 - // -----------------------------------------------------------------------
1040 - // Hello-ack payload tests
1041 - // -----------------------------------------------------------------------
1042 -
1043 - #[test]
1044 - fn hello_ack_roundtrip() {
1045 - let h = HelloAck {
1046 - layout_version: 1,
1047 - flags: 0,
1048 - server_supported_profiles: 0x07,
1049 - intersection_profiles: 0x05,
1050 - selected_profile: PROFILE_SHM_FUTEX,
1051 - agreed_max_request_payload_bytes: 2048,
1052 - agreed_max_request_batch_items: 50,
1053 - agreed_max_response_payload_bytes: 65536,
1054 - agreed_max_response_batch_items: 1,
1055 - agreed_packet_size: 32768,
1056 - session_id: 42,
1057 - };
1058 -
1059 - let mut buf = [0u8; 64];
1060 - let n = h.encode(&mut buf);
1061 - assert_eq!(n, 48);
1062 -
1063 - let out = HelloAck::decode(&buf[..n]).unwrap();
1064 - assert_eq!(out, h);
1065 - }
1066 -
1067 - #[test]
1068 - fn hello_ack_decode_truncated() {
1069 - let buf = [0u8; 47];
1070 - assert_eq!(HelloAck::decode(&buf), Err(NipcError::Truncated));
1071 - }
1072 -
1073 - #[test]
1074 - fn hello_ack_decode_bad_layout() {
1075 - let h = HelloAck {
1076 - layout_version: 0,
1077 - ..Default::default()
1078 - };
1079 - let mut buf = [0u8; 48];
1080 - h.encode(&mut buf);
1081 - assert_eq!(HelloAck::decode(&buf), Err(NipcError::BadLayout));
1082 - }
1083 -
1084 - #[test]
1085 - fn hello_ack_encode_too_small() {
1086 - let h = HelloAck::default();
1087 - let mut buf = [0u8; 10];
1088 - assert_eq!(h.encode(&mut buf), 0);
1089 - }
1090 -
1091 - // -----------------------------------------------------------------------
1092 - // Cgroups snapshot request tests
1093 - // -----------------------------------------------------------------------
1094 -
1095 - #[test]
1096 - fn cgroups_req_roundtrip() {
1097 - let r = CgroupsRequest {
1098 - layout_version: 1,
1099 - flags: 0,
1100 - };
1101 -
1102 - let mut buf = [0u8; 16];
1103 - let n = r.encode(&mut buf);
1104 - assert_eq!(n, 4);
1105 -
1106 - let out = CgroupsRequest::decode(&buf[..n]).unwrap();
1107 - assert_eq!(out, r);
1108 - }
1109 -
1110 - #[test]
1111 - fn cgroups_req_decode_truncated() {
1112 - let buf = [0u8; 3];
1113 - assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::Truncated));
1114 - }
1115 -
1116 - #[test]
1117 - fn cgroups_req_decode_bad_layout() {
1118 - let r = CgroupsRequest {
1119 - layout_version: 5,
1120 - flags: 0,
1121 - };
1122 - let mut buf = [0u8; 4];
1123 - r.encode(&mut buf);
1124 - assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::BadLayout));
1125 - }
1126 -
1127 - #[test]
1128 - fn cgroups_req_encode_too_small() {
1129 - let r = CgroupsRequest::default();
1130 - let mut buf = [0u8; 2];
1131 - assert_eq!(r.encode(&mut buf), 0);
1132 - }
1133 -
1134 - // -----------------------------------------------------------------------
1135 - // Cgroups snapshot response tests
1136 - // -----------------------------------------------------------------------
1137 -
1138 - // Private constants needed by tests -- mirror the values from cgroups.rs
1139 - const CGROUPS_RESP_HDR_SIZE: usize = 24;
1140 - const CGROUPS_DIR_ENTRY_SIZE: usize = 8;
1141 -
1142 - #[test]
1143 - fn cgroups_resp_empty() {
1144 - let mut buf = [0u8; 4096];
1145 - let b = CgroupsBuilder::new(&mut buf, 0, 1, 42);
1146 - let total = b.finish();
1147 - assert_eq!(total, 24);
1148 -
1149 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1150 - assert_eq!(view.item_count, 0);
1151 - assert_eq!(view.systemd_enabled, 1);
1152 - assert_eq!(view.generation, 42);
1153 - }
1154 -
1155 - #[test]
1156 - fn cgroups_resp_single_item() {
1157 - let mut buf = [0u8; 4096];
1158 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 100);
1159 -
1160 - let name = b"docker-abc123";
1161 - let path = b"/sys/fs/cgroup/docker/abc123";
1162 - b.add(12345, 0x01, 1, name, path).unwrap();
1163 -
1164 - let total = b.finish();
1165 - assert!(total > 24);
1166 -
1167 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1168 - assert_eq!(view.item_count, 1);
1169 - assert_eq!(view.systemd_enabled, 0);
1170 - assert_eq!(view.generation, 100);
1171 -
1172 - let item = view.item(0).unwrap();
1173 - assert_eq!(item.hash, 12345);
1174 - assert_eq!(item.options, 0x01);
1175 - assert_eq!(item.enabled, 1);
1176 - assert_eq!(item.name.len as usize, name.len());
1177 - assert_eq!(item.name.as_bytes(), name);
1178 - assert_eq!(item.name.bytes[name.len()], 0); // NUL
1179 - assert_eq!(item.path.len as usize, path.len());
1180 - assert_eq!(item.path.as_bytes(), path);
1181 - assert_eq!(item.path.bytes[path.len()], 0); // NUL
1182 - }
1183 -
1184 - #[test]
1185 - fn cgroups_resp_multiple_items() {
1186 - let mut buf = [0u8; 8192];
1187 - let mut b = CgroupsBuilder::new(&mut buf, 5, 1, 999);
1188 -
1189 - // Item 0
1190 - let n0 = b"init.scope";
1191 - let p0 = b"/sys/fs/cgroup/init.scope";
1192 - b.add(100, 0, 1, n0, p0).unwrap();
1193 -
1194 - // Item 1
1195 - let n1 = b"system.slice/docker-abc.scope";
1196 - let p1 = b"/sys/fs/cgroup/system.slice/docker-abc.scope";
1197 - b.add(200, 0x02, 0, n1, p1).unwrap();
1198 -
1199 - // Item 2 - empty strings
1200 - b.add(300, 0, 1, b"", b"").unwrap();
1201 -
1202 - let total = b.finish();
1203 -
1204 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1205 - assert_eq!(view.item_count, 3);
1206 - assert_eq!(view.systemd_enabled, 1);
1207 - assert_eq!(view.generation, 999);
1208 -
1209 - // Verify item 0
1210 - let item = view.item(0).unwrap();
1211 - assert_eq!(item.hash, 100);
1212 - assert_eq!(item.name.len as usize, n0.len());
1213 - assert_eq!(item.name.as_bytes(), n0);
1214 - assert_eq!(item.path.len as usize, p0.len());
1215 - assert_eq!(item.path.as_bytes(), p0);
1216 -
1217 - // Verify item 1
1218 - let item = view.item(1).unwrap();
1219 - assert_eq!(item.hash, 200);
1220 - assert_eq!(item.options, 0x02);
1221 - assert_eq!(item.enabled, 0);
1222 - assert_eq!(item.name.len as usize, n1.len());
1223 - assert_eq!(item.name.as_bytes(), n1);
1224 -
1225 - // Verify item 2 (empty strings)
1226 - let item = view.item(2).unwrap();
1227 - assert_eq!(item.hash, 300);
1228 - assert_eq!(item.name.len, 0);
1229 - assert_eq!(item.name.bytes[0], 0); // NUL
1230 - assert_eq!(item.path.len, 0);
1231 - assert_eq!(item.path.bytes[0], 0); // NUL
1232 -
1233 - // Out-of-bounds index
1234 - assert_eq!(view.item(3), Err(NipcError::OutOfBounds));
1235 - }
1236 -
1237 - #[test]
1238 - fn cgroups_resp_decode_truncated_header() {
1239 - let buf = [0u8; 23];
1240 - assert_eq!(
1241 - CgroupsResponseView::decode(&buf).unwrap_err(),
1242 - NipcError::Truncated
1243 - );
1244 - }
1245 -
1246 - #[test]
1247 - fn cgroups_resp_decode_bad_layout() {
1248 - let mut buf = [0u8; 24];
1249 - buf[0..2].copy_from_slice(&99u16.to_ne_bytes());
1250 - assert_eq!(
1251 - CgroupsResponseView::decode(&buf).unwrap_err(),
1252 - NipcError::BadLayout
1253 - );
1254 - }
1255 -
1256 - #[test]
1257 - fn cgroups_resp_decode_truncated_dir() {
1258 - // Header says item_count=2 but payload is only 24 bytes
1259 - let mut buf = [0u8; 24];
1260 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
1261 - buf[4..8].copy_from_slice(&2u32.to_ne_bytes());
1262 - assert_eq!(
1263 - CgroupsResponseView::decode(&buf).unwrap_err(),
1264 - NipcError::Truncated
1265 - );
1266 - }
1267 -
1268 - #[test]
1269 - fn cgroups_resp_decode_oob_dir() {
1270 - // Header + 1 dir entry pointing beyond payload
1271 - let mut buf = [0u8; 64];
1272 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
1273 - buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
1274 - // Dir entry at offset 24: offset=0, length=9999
1275 - buf[24..28].copy_from_slice(&0u32.to_ne_bytes());
1276 - buf[28..32].copy_from_slice(&9999u32.to_ne_bytes());
1277 - assert_eq!(
1278 - CgroupsResponseView::decode(&buf).unwrap_err(),
1279 - NipcError::OutOfBounds
1280 - );
1281 - }
1282 -
1283 - #[test]
1284 - fn cgroups_resp_decode_item_too_small() {
1285 - // Dir entry with length < 32
1286 - let mut buf = [0u8; 64];
1287 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
1288 - buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
1289 - buf[24..28].copy_from_slice(&0u32.to_ne_bytes());
1290 - buf[28..32].copy_from_slice(&16u32.to_ne_bytes());
1291 - assert_eq!(
1292 - CgroupsResponseView::decode(&buf).unwrap_err(),
1293 - NipcError::Truncated
1294 - );
1295 - }
1296 -
1297 - #[test]
1298 - fn cgroups_resp_item_missing_nul() {
1299 - // Build valid snapshot then corrupt the NUL terminator
1300 - let mut buf = [0u8; 4096];
1301 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1302 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1303 - let total = b.finish();
1304 -
1305 - // Find item data and corrupt the name's NUL terminator
1306 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1307 - let item_off = u32::from_ne_bytes(
1308 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1309 - .try_into()
1310 - .unwrap(),
1311 - ) as usize;
1312 - let item_start = dir_end + item_off;
1313 -
1314 - let noff =
1315 - u32::from_ne_bytes(buf[item_start + 16..item_start + 20].try_into().unwrap()) as usize;
1316 - let nlen =
1317 - u32::from_ne_bytes(buf[item_start + 20..item_start + 24].try_into().unwrap()) as usize;
1318 -
1319 - buf[item_start + noff + nlen] = b'X'; // corrupt NUL
1320 -
1321 - // Re-decode after corruption -- header/dir still valid
1322 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1323 - assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
1324 - }
1325 -
1326 - #[test]
1327 - fn cgroups_resp_item_string_oob() {
1328 - // Build valid snapshot then corrupt string length to be huge
1329 - let mut buf = [0u8; 4096];
1330 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1331 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1332 - let total = b.finish();
1333 -
1334 - // Corrupt name_length to huge value
1335 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1336 - let item_off = u32::from_ne_bytes(
1337 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1338 - .try_into()
1339 - .unwrap(),
1340 - ) as usize;
1341 - let item_start = dir_end + item_off;
1342 -
1343 - buf[item_start + 20..item_start + 24].copy_from_slice(&99999u32.to_ne_bytes());
1344 -
1345 - // Re-decode after corruption
1346 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1347 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1348 - }
1349 -
1350 - #[test]
1351 - fn cgroups_builder_overflow() {
1352 - let mut buf = [0u8; 64]; // too small for any real item
1353 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 0);
1354 - let long_name = [b'A'; 200];
1355 - assert_eq!(b.add(1, 0, 1, &long_name, b""), Err(NipcError::Overflow));
1356 - }
1357 -
1358 - #[test]
1359 - fn cgroups_builder_max_items_exceeded() {
1360 - let mut buf = [0u8; 4096];
1361 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 0);
1362 - b.add(1, 0, 1, b"a", b"b").unwrap();
1363 - assert_eq!(b.add(2, 0, 1, b"c", b"d"), Err(NipcError::Overflow));
1364 - }
1365 -
1366 - #[test]
1367 - fn cgroups_builder_compaction() {
1368 - let mut buf = [0u8; 4096];
1369 - // Reserve 10 directory slots but only add 2 items
1370 - let mut b = CgroupsBuilder::new(&mut buf, 10, 1, 77);
1371 -
1372 - b.add(10, 0, 1, b"slice-a", b"/cgroup/slice-a").unwrap();
1373 - b.add(20, 0, 0, b"slice-b", b"/cgroup/slice-b").unwrap();
1374 -
1375 - let total = b.finish();
1376 -
1377 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1378 - assert_eq!(view.item_count, 2);
1379 - assert_eq!(view.generation, 77);
1380 -
1381 - let item = view.item(0).unwrap();
1382 - assert_eq!(item.hash, 10);
1383 - assert_eq!(item.name.as_bytes(), b"slice-a");
1384 -
1385 - let item = view.item(1).unwrap();
1386 - assert_eq!(item.hash, 20);
1387 - assert_eq!(item.name.as_bytes(), b"slice-b");
1388 - }
1389 -
1390 - // -----------------------------------------------------------------------
1391 - // Alignment utility test
1392 - // -----------------------------------------------------------------------
1393 -
1394 - #[test]
1395 - fn test_align8() {
1396 - assert_eq!(align8(0), 0);
1397 - assert_eq!(align8(1), 8);
1398 - assert_eq!(align8(7), 8);
1399 - assert_eq!(align8(8), 8);
1400 - assert_eq!(align8(9), 16);
1401 - assert_eq!(align8(16), 16);
1402 - assert_eq!(align8(17), 24);
1403 - }
1404 -
1405 - // -----------------------------------------------------------------------
1406 - // Cross-language wire compatibility: C-Rust byte identity
1407 - //
1408 - // These tests encode in Rust and verify the exact bytes match what the
1409 - // C implementation produces for the same inputs. This ensures identical
1410 - // wire output across languages.
1411 - // -----------------------------------------------------------------------
1412 -
1413 - #[test]
1414 - fn c_rust_header_bytes_identical() {
1415 - // Encode in Rust
1416 - let h = Header {
1417 - magic: MAGIC_MSG,
1418 - version: VERSION,
1419 - header_len: HEADER_LEN,
1420 - kind: KIND_REQUEST,
1421 - flags: FLAG_BATCH,
1422 - code: METHOD_CGROUPS_SNAPSHOT,
1423 - transport_status: STATUS_OK,
1424 - payload_len: 12345,
1425 - item_count: 42,
1426 - message_id: 0xDEAD_BEEF_CAFE_BABE,
1427 - };
1428 - let mut rust_buf = [0u8; 32];
1429 - h.encode(&mut rust_buf);
1430 -
1431 - // Known LE bytes for this header
1432 - let expected: [u8; 32] = [
1433 - 0x43, 0x50, 0x49, 0x4e, // magic
1434 - 0x01, 0x00, // version
1435 - 0x20, 0x00, // header_len
1436 - 0x01, 0x00, // kind
1437 - 0x01, 0x00, // flags
1438 - 0x02, 0x00, // code
1439 - 0x00, 0x00, // transport_status
1440 - 0x39, 0x30, 0x00, 0x00, // payload_len = 12345
1441 - 0x2a, 0x00, 0x00, 0x00, // item_count = 42
1442 - 0xbe, 0xba, 0xfe, 0xca, 0xef, 0xbe, 0xad, 0xde, // message_id
1443 - ];
1444 - assert_eq!(rust_buf, expected);
1445 - }
1446 -
1447 - #[test]
1448 - fn c_rust_chunk_bytes_identical() {
1449 - let c = ChunkHeader {
1450 - magic: MAGIC_CHUNK,
1451 - version: VERSION,
1452 - flags: 0,
1453 - message_id: 1,
1454 - total_message_len: 256,
1455 - chunk_index: 1,
1456 - chunk_count: 3,
1457 - chunk_payload_len: 100,
1458 - };
1459 - let mut rust_buf = [0u8; 32];
1460 - c.encode(&mut rust_buf);
1461 -
1462 - let expected: [u8; 32] = [
1463 - 0x4b, 0x48, 0x43, 0x4e, // magic
1464 - 0x01, 0x00, // version
1465 - 0x00, 0x00, // flags
1466 - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // message_id
1467 - 0x00, 0x01, 0x00, 0x00, // total_message_len = 256
1468 - 0x01, 0x00, 0x00, 0x00, // chunk_index
1469 - 0x03, 0x00, 0x00, 0x00, // chunk_count
1470 - 0x64, 0x00, 0x00, 0x00, // chunk_payload_len = 100
1471 - ];
1472 - assert_eq!(rust_buf, expected);
1473 - }
1474 -
1475 - #[test]
1476 - fn c_rust_hello_bytes_identical() {
1477 - let h = Hello {
1478 - layout_version: 1,
1479 - flags: 0,
1480 - supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
1481 - preferred_profiles: PROFILE_SHM_FUTEX,
1482 - max_request_payload_bytes: 4096,
1483 - max_request_batch_items: 100,
1484 - max_response_payload_bytes: 1048576,
1485 - max_response_batch_items: 1,
1486 - auth_token: 0xAABB_CCDD_EEFF_0011,
1487 - packet_size: 65536,
1488 - };
1489 -
1490 - let mut rust_buf = [0u8; 44];
1491 - h.encode(&mut rust_buf);
1492 -
1493 - // Verify key byte positions
1494 - assert_eq!(&rust_buf[0..2], &[0x01, 0x00]); // layout_version
1495 - assert_eq!(&rust_buf[2..4], &[0x00, 0x00]); // flags
1496 - assert_eq!(&rust_buf[4..8], &[0x05, 0x00, 0x00, 0x00]); // supported = 0x05
1497 - assert_eq!(&rust_buf[8..12], &[0x04, 0x00, 0x00, 0x00]); // preferred = 0x04
1498 - assert_eq!(&rust_buf[28..32], &[0x00, 0x00, 0x00, 0x00]); // padding = 0
1499 - assert_eq!(
1500 - &rust_buf[32..40],
1501 - &[0x11, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA]
1502 - ); // auth_token
1503 -
1504 - // Round-trip
1505 - let out = Hello::decode(&rust_buf).unwrap();
1506 - assert_eq!(out, h);
1507 - }
1508 -
1509 - #[test]
1510 - fn c_rust_hello_ack_bytes_identical() {
1511 - let h = HelloAck {
1512 - layout_version: 1,
1513 - flags: 0,
1514 - server_supported_profiles: 0x07,
1515 - intersection_profiles: 0x05,
1516 - selected_profile: PROFILE_SHM_FUTEX,
1517 - agreed_max_request_payload_bytes: 2048,
1518 - agreed_max_request_batch_items: 50,
1519 - agreed_max_response_payload_bytes: 65536,
1520 - agreed_max_response_batch_items: 1,
1521 - agreed_packet_size: 32768,
1522 - session_id: 0x0000_0001_0000_0007,
1523 - };
1524 - let mut rust_buf = [0u8; 48];
1525 - h.encode(&mut rust_buf);
1526 -
1527 - assert_eq!(&rust_buf[0..2], &[0x01, 0x00]);
1528 - assert_eq!(&rust_buf[4..8], &[0x07, 0x00, 0x00, 0x00]); // server_supported
1529 - assert_eq!(&rust_buf[12..16], &[0x04, 0x00, 0x00, 0x00]); // selected = SHM_FUTEX
1530 - assert_eq!(&rust_buf[36..40], &[0x00, 0x00, 0x00, 0x00]); // padding
1531 - assert_eq!(
1532 - &rust_buf[40..48],
1533 - &[0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]
1534 - ); // session_id LE
1535 -
1536 - let out = HelloAck::decode(&rust_buf).unwrap();
1537 - assert_eq!(out, h);
1538 - }
1539 -
1540 - #[test]
1541 - fn c_rust_cgroups_req_bytes_identical() {
1542 - let r = CgroupsRequest {
1543 - layout_version: 1,
1544 - flags: 0,
1545 - };
1546 - let mut rust_buf = [0u8; 4];
1547 - r.encode(&mut rust_buf);
1548 -
1549 - assert_eq!(rust_buf, [0x01, 0x00, 0x00, 0x00]);
1550 -
1551 - let out = CgroupsRequest::decode(&rust_buf).unwrap();
1552 - assert_eq!(out, r);
1553 - }
1554 -
1555 - #[test]
1556 - fn c_rust_cgroups_snapshot_bytes_identical() {
1557 - // Build a snapshot with the exact same inputs as the C test
1558 - let mut buf = [0u8; 4096];
1559 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 100);
1560 - b.add(
1561 - 12345,
1562 - 0x01,
1563 - 1,
1564 - b"docker-abc123",
1565 - b"/sys/fs/cgroup/docker/abc123",
1566 - )
1567 - .unwrap();
1568 - let total = b.finish();
1569 -
1570 - // Verify the snapshot header bytes
1571 - assert_eq!(&buf[0..2], &[0x01, 0x00]); // layout_version
1572 - assert_eq!(&buf[2..4], &[0x00, 0x00]); // flags
1573 - assert_eq!(&buf[4..8], &[0x01, 0x00, 0x00, 0x00]); // item_count
1574 - assert_eq!(&buf[8..12], &[0x00, 0x00, 0x00, 0x00]); // systemd_enabled
1575 - assert_eq!(&buf[12..16], &[0x00, 0x00, 0x00, 0x00]); // reserved
1576 - assert_eq!(
1577 - &buf[16..24],
1578 - &[0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
1579 - ); // generation
1580 -
1581 - // Verify it decodes correctly
1582 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1583 - assert_eq!(view.item_count, 1);
1584 - assert_eq!(view.generation, 100);
1585 -
1586 - let item = view.item(0).unwrap();
1587 - assert_eq!(item.hash, 12345);
1588 - assert_eq!(item.name.as_bytes(), b"docker-abc123");
1589 - assert_eq!(item.path.as_bytes(), b"/sys/fs/cgroup/docker/abc123");
1590 - }
1591 -
1592 - #[test]
1593 - fn cgroups_resp_dir_bad_alignment() {
1594 - // Dir entry with unaligned offset
1595 - let mut buf = [0u8; 128];
1596 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
1597 - buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
1598 - // offset=3 (not 8-byte aligned), length=32
1599 - buf[24..28].copy_from_slice(&3u32.to_ne_bytes());
1600 - buf[28..32].copy_from_slice(&32u32.to_ne_bytes());
1601 - assert_eq!(
1602 - CgroupsResponseView::decode(&buf).unwrap_err(),
1603 - NipcError::BadAlignment
1604 - );
1605 - }
1606 -
1607 - #[test]
1608 - fn cgroups_resp_item_bad_layout_version() {
1609 - // Build valid snapshot, then corrupt the item's layout_version
1610 - let mut buf = [0u8; 4096];
1611 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1612 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1613 - let total = b.finish();
1614 -
1615 - // Corrupt item layout_version
1616 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1617 - let item_off = u32::from_ne_bytes(
1618 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1619 - .try_into()
1620 - .unwrap(),
1621 - ) as usize;
1622 - let item_start = dir_end + item_off;
1623 - buf[item_start..item_start + 2].copy_from_slice(&99u16.to_ne_bytes());
1624 -
1625 - // Re-decode after corruption
1626 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1627 - assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1628 - }
1629 -
1630 - #[test]
1631 - fn cgroups_resp_item_name_off_below_header() {
1632 - // Build valid snapshot, then set name_offset < 32
1633 - let mut buf = [0u8; 4096];
1634 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1635 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1636 - let total = b.finish();
1637 -
1638 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1639 - let item_off = u32::from_ne_bytes(
1640 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1641 - .try_into()
1642 - .unwrap(),
1643 - ) as usize;
1644 - let item_start = dir_end + item_off;
1645 - // Set name_offset to 0 (below header)
1646 - buf[item_start + 16..item_start + 20].copy_from_slice(&0u32.to_ne_bytes());
1647 -
1648 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1649 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1650 - }
1651 -
1652 - #[test]
1653 - fn cgroups_resp_item_path_off_below_header() {
1654 - let mut buf = [0u8; 4096];
1655 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1656 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1657 - let total = b.finish();
1658 -
1659 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1660 - let item_off = u32::from_ne_bytes(
1661 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1662 - .try_into()
1663 - .unwrap(),
1664 - ) as usize;
1665 - let item_start = dir_end + item_off;
1666 - // Set path_offset to 16 (below header)
1667 - buf[item_start + 24..item_start + 28].copy_from_slice(&16u32.to_ne_bytes());
1668 -
1669 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1670 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1671 - }
1672 -
1673 - #[test]
1674 - fn cgroups_resp_item_path_missing_nul() {
1675 - let mut buf = [0u8; 4096];
1676 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1677 - b.add(1, 0, 1, b"test", b"/test").unwrap();
1678 - let total = b.finish();
1679 -
1680 - // Corrupt path NUL
1681 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1682 - let item_off = u32::from_ne_bytes(
1683 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1684 - .try_into()
1685 - .unwrap(),
1686 - ) as usize;
1687 - let item_start = dir_end + item_off;
1688 - let poff =
1689 - u32::from_ne_bytes(buf[item_start + 24..item_start + 28].try_into().unwrap()) as usize;
1690 - let plen =
1691 - u32::from_ne_bytes(buf[item_start + 28..item_start + 32].try_into().unwrap()) as usize;
1692 - buf[item_start + poff + plen] = b'X';
1693 -
1694 - // Re-decode after corruption
1695 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1696 - assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
1697 - }
1698 -
1699 - #[test]
1700 - fn cgroups_resp_item_overlap_rejected() {
1701 - // Build a valid item, then manually set path_offset to overlap with name
1702 - let mut buf = [0u8; 4096];
1703 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1704 - b.add(1, 0, 1, b"hello", b"/path").unwrap();
1705 - let total = b.finish();
1706 -
1707 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1708 - let item_off = u32::from_ne_bytes(
1709 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1710 - .try_into()
1711 - .unwrap(),
1712 - ) as usize;
1713 - let item_start = dir_end + item_off;
1714 -
1715 - // name_off=32, name_len=5, so name region is [32..38)
1716 - // Set path_off=34 (inside name region), path_len=1
1717 - buf[item_start + 24..item_start + 28].copy_from_slice(&34u32.to_ne_bytes());
1718 - buf[item_start + 28..item_start + 32].copy_from_slice(&1u32.to_ne_bytes());
1719 - // Ensure NUL at item[34+1]=item[35]
1720 - buf[item_start + 35] = 0;
1721 -
1722 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1723 - assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1724 - }
1725 -
1726 - // -------------------------------------------------------------------
1727 - // Proptest: fuzz / property-based tests for all decode paths
1728 - // -------------------------------------------------------------------
1729 -
1730 - mod proptests {
1731 - use super::*;
1732 - use proptest::prelude::*;
1733 -
1734 - // Arbitrary bytes -- no decode path may panic on any input.
1735 -
1736 - proptest! {
1737 - #[test]
1738 - fn decode_header_never_panics(data: Vec<u8>) {
1739 - let _ = Header::decode(&data);
1740 - }
1741 -
1742 - #[test]
1743 - fn decode_chunk_header_never_panics(data: Vec<u8>) {
1744 - let _ = ChunkHeader::decode(&data);
1745 - }
1746 -
1747 - #[test]
1748 - fn decode_hello_never_panics(data: Vec<u8>) {
1749 - let _ = Hello::decode(&data);
1750 - }
1751 -
1752 - #[test]
1753 - fn decode_hello_ack_never_panics(data: Vec<u8>) {
1754 - let _ = HelloAck::decode(&data);
1755 - }
1756 -
1757 - #[test]
1758 - fn decode_cgroups_request_never_panics(data: Vec<u8>) {
1759 - let _ = CgroupsRequest::decode(&data);
1760 - }
1761 -
1762 - #[test]
1763 - fn decode_cgroups_response_never_panics(data: Vec<u8>) {
1764 - let result = CgroupsResponseView::decode(&data);
1765 - if let Ok(view) = result {
1766 - // Exercise item access on valid decodes.
1767 - let limit = view.item_count.min(64);
1768 - for i in 0..limit {
1769 - let _ = view.item(i);
1770 - }
1771 - // Out-of-bounds must not panic.
1772 - let _ = view.item(view.item_count);
1773 - }
1774 - }
1775 -
1776 - #[test]
1777 - fn batch_dir_decode_never_panics(
1778 - data: Vec<u8>,
1779 - item_count in 0u32..128,
1780 - packed_area_len in 0u32..65536,
1781 - ) {
1782 - let _ = batch_dir_decode(&data, item_count, packed_area_len);
1783 - }
1784 -
1785 - #[test]
1786 - fn batch_item_get_never_panics(
1787 - data: Vec<u8>,
1788 - item_count in 0u32..128,
1789 - index in 0u32..128,
1790 - ) {
1791 - let _ = batch_item_get(&data, item_count, index);
1792 - }
1793 - }
1794 -
1795 - // Roundtrip tests: encode random valid values, decode, verify match.
1796 -
1797 - proptest! {
1798 - #[test]
1799 - fn encode_decode_header_roundtrip(
1800 - kind in 1u16..=3,
1801 - flags in any::<u16>(),
1802 - code in any::<u16>(),
1803 - transport_status in any::<u16>(),
1804 - payload_len in any::<u32>(),
1805 - item_count in any::<u32>(),
1806 - message_id in any::<u64>(),
1807 - ) {
1808 - let h = Header {
1809 - magic: MAGIC_MSG,
1810 - version: VERSION,
1811 - header_len: HEADER_LEN,
1812 - kind,
1813 - flags,
1814 - code,
1815 - transport_status,
1816 - payload_len,
1817 - item_count,
1818 - message_id,
1819 - };
1820 - let mut buf = [0u8; 64];
1821 - let n = h.encode(&mut buf);
1822 - prop_assert_eq!(n, HEADER_SIZE);
1823 - let decoded = Header::decode(&buf[..n]).unwrap();
1824 - prop_assert_eq!(decoded, h);
1825 - }
1826 -
1827 - #[test]
1828 - fn encode_decode_hello_roundtrip(
1829 - supported in any::<u32>(),
1830 - preferred in any::<u32>(),
1831 - max_req_payload in any::<u32>(),
1832 - max_req_batch in any::<u32>(),
1833 - max_resp_payload in any::<u32>(),
1834 - max_resp_batch in any::<u32>(),
1835 - auth_token in any::<u64>(),
1836 - packet_size in any::<u32>(),
1837 - ) {
1838 - let h = Hello {
1839 - layout_version: 1,
1840 - flags: 0,
1841 - supported_profiles: supported,
1842 - preferred_profiles: preferred,
1843 - max_request_payload_bytes: max_req_payload,
1844 - max_request_batch_items: max_req_batch,
1845 - max_response_payload_bytes: max_resp_payload,
1846 - max_response_batch_items: max_resp_batch,
1847 - auth_token,
1848 - packet_size,
1849 - };
1850 - let mut buf = [0u8; 64];
1851 - let n = h.encode(&mut buf);
1852 - prop_assert_eq!(n, HELLO_SIZE);
1853 - let decoded = Hello::decode(&buf[..n]).unwrap();
1854 - prop_assert_eq!(decoded, h);
1855 - }
1856 - }
1857 - }
1858 -
1859 - // -----------------------------------------------------------------------
1860 - // NipcError Display coverage
1861 - // -----------------------------------------------------------------------
1862 -
1863 - #[test]
1864 - fn nipc_error_display_all_variants() {
1865 - // Exercise the Display impl for every NipcError variant (lines 101-113)
1866 - let cases: Vec<(NipcError, &str)> = vec![
1867 - (NipcError::Truncated, "buffer too short"),
1868 - (NipcError::BadMagic, "magic value mismatch"),
1869 - (NipcError::BadVersion, "unsupported version"),
1870 - (NipcError::BadHeaderLen, "header_len != 32"),
1871 - (NipcError::BadKind, "unknown message kind"),
1872 - (NipcError::BadLayout, "unknown layout_version"),
1873 - (NipcError::OutOfBounds, "offset+length exceeds data"),
1874 - (NipcError::MissingNul, "string not NUL-terminated"),
1875 - (NipcError::BadAlignment, "item not 8-byte aligned"),
1876 - (NipcError::BadItemCount, "item count inconsistent"),
1877 - (NipcError::Overflow, "builder out of space"),
1878 - ];
1879 - for (err, expected) in cases {
1880 - let msg = format!("{}", err);
1881 - assert_eq!(msg, expected, "Display for {:?}", err);
1882 - }
1883 - // Also verify std::error::Error is implemented
1884 - let err: &dyn std::error::Error = &NipcError::Truncated;
1885 - let _ = format!("{err}");
1886 - }
1887 -
1888 - // -----------------------------------------------------------------------
1889 - // ChunkHeader decode: flags != 0 and chunk_payload_len == 0
1890 - // -----------------------------------------------------------------------
1891 -
1892 - #[test]
1893 - fn chunk_decode_bad_flags() {
1894 - // Line 257: flags != 0 -> BadLayout
1895 - let c = ChunkHeader {
1896 - magic: MAGIC_CHUNK,
1897 - version: VERSION,
1898 - flags: 0x01, // non-zero flags
1899 - message_id: 1,
1900 - total_message_len: 100,
1901 - chunk_index: 0,
1902 - chunk_count: 1,
1903 - chunk_payload_len: 50,
1904 - };
1905 - let mut buf = [0u8; 32];
1906 - c.encode(&mut buf);
1907 - assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadLayout));
1908 - }
1909 -
1910 - #[test]
1911 - fn chunk_decode_zero_payload_len() {
1912 - // Line 260: chunk_payload_len == 0 -> BadLayout
1913 - let c = ChunkHeader {
1914 - magic: MAGIC_CHUNK,
1915 - version: VERSION,
1916 - flags: 0,
1917 - message_id: 1,
1918 - total_message_len: 100,
1919 - chunk_index: 0,
1920 - chunk_count: 1,
1921 - chunk_payload_len: 0,
1922 - };
1923 - let mut buf = [0u8; 32];
1924 - c.encode(&mut buf);
1925 - assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadLayout));
1926 - }
1927 -
1928 - // -----------------------------------------------------------------------
1929 - // batch_dir_encode: buffer too small (line 282)
1930 - // -----------------------------------------------------------------------
1931 -
1932 - #[test]
1933 - fn batch_dir_encode_too_small() {
1934 - let entries = [
1935 - BatchEntry {
1936 - offset: 0,
1937 - length: 8,
1938 - },
1939 - BatchEntry {
1940 - offset: 8,
1941 - length: 8,
1942 - },
1943 - ];
1944 - let mut buf = [0u8; 12]; // needs 16, only 12
1945 - assert_eq!(batch_dir_encode(&entries, &mut buf), 0);
1946 - }
1947 -
1948 - // -----------------------------------------------------------------------
1949 - // batch_dir_validate error paths (lines 329, 336, 339)
1950 - // -----------------------------------------------------------------------
1951 -
1952 - #[test]
1953 - fn batch_dir_validate_truncated() {
1954 - let buf = [0u8; 4]; // too short for 1 entry (needs 8)
1955 - assert_eq!(batch_dir_validate(&buf, 1, 100), Err(NipcError::Truncated));
1956 - }
1957 -
1958 - #[test]
1959 - fn batch_dir_validate_bad_alignment() {
1960 - let mut buf = [0u8; 8];
1961 - buf[0..4].copy_from_slice(&3u32.to_ne_bytes()); // unaligned offset
1962 - buf[4..8].copy_from_slice(&8u32.to_ne_bytes());
1963 - assert_eq!(
1964 - batch_dir_validate(&buf, 1, 100),
1965 - Err(NipcError::BadAlignment)
1966 - );
1967 - }
1968 -
1969 - #[test]
1970 - fn batch_dir_validate_out_of_bounds() {
1971 - let mut buf = [0u8; 8];
1972 - buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
1973 - buf[4..8].copy_from_slice(&200u32.to_ne_bytes()); // exceeds packed_area_len
1974 - assert_eq!(
1975 - batch_dir_validate(&buf, 1, 100),
1976 - Err(NipcError::OutOfBounds)
1977 - );
1978 - }
1979 -
1980 - #[test]
1981 - fn batch_dir_validate_ok() {
1982 - let mut buf = [0u8; 16];
1983 - buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
1984 - buf[4..8].copy_from_slice(&8u32.to_ne_bytes());
1985 - buf[8..12].copy_from_slice(&8u32.to_ne_bytes());
1986 - buf[12..16].copy_from_slice(&8u32.to_ne_bytes());
1987 - assert!(batch_dir_validate(&buf, 2, 100).is_ok());
1988 - }
1989 -
1990 - // -----------------------------------------------------------------------
1991 - // batch_item_get: alignment check (line 375)
1992 - // -----------------------------------------------------------------------
1993 -
1994 - #[test]
1995 - fn batch_item_get_bad_alignment() {
1996 - // Manually craft a batch payload with unaligned offset
1997 - let mut buf = [0u8; 64];
1998 - // Directory: 1 entry at offset 0 of buf
1999 - buf[0..4].copy_from_slice(&3u32.to_ne_bytes()); // unaligned offset
2000 - buf[4..8].copy_from_slice(&4u32.to_ne_bytes());
2001 - assert_eq!(batch_item_get(&buf, 1, 0), Err(NipcError::BadAlignment));
2002 - }
2003 -
2004 - #[test]
2005 - fn batch_item_get_truncated_dir() {
2006 - // Payload too small to hold the directory
2007 - let buf = [0u8; 4]; // needs at least 8 for 1 item directory
2008 - assert_eq!(batch_item_get(&buf, 1, 0), Err(NipcError::Truncated));
2009 - }
2010 -
2011 - // -----------------------------------------------------------------------
2012 - // BatchBuilder::finish compaction (lines 451-456)
2013 - // -----------------------------------------------------------------------
2014 -
2015 - #[test]
2016 - fn batch_builder_compaction() {
2017 - // Reserve space for 8 items but add only 2 -- triggers compaction
2018 - let mut buf = [0u8; 1024];
2019 - let mut b = BatchBuilder::new(&mut buf, 8);
2020 -
2021 - let item1 = [1u8, 2, 3, 4, 5, 6, 7, 8];
2022 - let item2 = [10u8, 20, 30, 40];
2023 -
2024 - b.add(&item1).unwrap();
2025 - b.add(&item2).unwrap();
2026 -
2027 - // dir_end for 8 items = align8(8*8) = 64
2028 - // final_dir_aligned for 2 items = align8(2*8) = 16
2029 - // This triggers the copy_within compaction branch (line 453-456)
2030 - let (total, count) = b.finish();
2031 - assert_eq!(count, 2);
2032 - assert!(total > 0);
2033 -
2034 - // Verify the items can still be extracted correctly
2035 - let (data, len) = batch_item_get(&buf[..total], 2, 0).unwrap();
2036 - assert_eq!(len as usize, item1.len());
2037 - assert_eq!(data, &item1);
2038 -
2039 - let (data, len) = batch_item_get(&buf[..total], 2, 1).unwrap();
2040 - assert_eq!(len as usize, item2.len());
2041 - assert_eq!(data, &item2);
2042 - }
2043 -
2044 - // -----------------------------------------------------------------------
2045 - // HelloAck decode: flags != 0 (line 606)
2046 - // -----------------------------------------------------------------------
2047 -
2048 - #[test]
2049 - fn hello_ack_decode_bad_flags() {
2050 - let h = HelloAck {
2051 - layout_version: 1,
2052 - flags: 1, // non-zero flags
2053 - ..Default::default()
2054 - };
2055 - let mut buf = [0u8; 48];
2056 - h.encode(&mut buf);
2057 - assert_eq!(HelloAck::decode(&buf), Err(NipcError::BadLayout));
2058 - }
2059 -
2060 - // -----------------------------------------------------------------------
2061 - // Hello decode: non-zero padding (line 527)
2062 - // -----------------------------------------------------------------------
2063 -
2064 - #[test]
2065 - fn hello_decode_bad_padding() {
2066 - let h = Hello {
2067 - layout_version: 1,
2068 - flags: 0,
2069 - ..Default::default()
2070 - };
2071 - let mut buf = [0u8; 44];
2072 - h.encode(&mut buf);
2073 - // Corrupt the padding bytes at 28..32
2074 - buf[28..32].copy_from_slice(&1u32.to_ne_bytes());
2075 - assert_eq!(Hello::decode(&buf), Err(NipcError::BadLayout));
2076 - }
2077 -
2078 - // -----------------------------------------------------------------------
2079 - // Cgroups request: non-zero flags (line 45)
2080 - // -----------------------------------------------------------------------
2081 -
2082 - #[test]
2083 - fn cgroups_req_decode_bad_flags() {
2084 - let r = CgroupsRequest {
2085 - layout_version: 1,
2086 - flags: 1, // non-zero flags -> BadLayout
2087 - };
2088 - let mut buf = [0u8; 4];
2089 - r.encode(&mut buf);
2090 - assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::BadLayout));
2091 - }
2092 -
2093 - // -----------------------------------------------------------------------
2094 - // CgroupsResponseView: non-zero flags and reserved (lines 125, 130)
2095 - // -----------------------------------------------------------------------
2096 -
2097 - #[test]
2098 - fn cgroups_resp_decode_bad_flags() {
2099 - // Line 125: flags != 0 -> BadLayout
2100 - let mut buf = [0u8; 24];
2101 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version = 1
2102 - buf[2..4].copy_from_slice(&1u16.to_ne_bytes()); // flags = 1 (non-zero)
2103 - assert_eq!(
2104 - CgroupsResponseView::decode(&buf).unwrap_err(),
2105 - NipcError::BadLayout
2106 - );
2107 - }
2108 -
2109 - #[test]
2110 - fn cgroups_resp_decode_bad_reserved() {
2111 - // Line 130: reserved != 0 -> BadLayout
2112 - let mut buf = [0u8; 24];
2113 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version = 1
2114 - buf[2..4].copy_from_slice(&0u16.to_ne_bytes()); // flags = 0
2115 - buf[12..16].copy_from_slice(&1u32.to_ne_bytes()); // reserved = 1
2116 - assert_eq!(
2117 - CgroupsResponseView::decode(&buf).unwrap_err(),
2118 - NipcError::BadLayout
2119 - );
2120 - }
2121 -
2122 - // -----------------------------------------------------------------------
2123 - // CgroupsResponseView: bad alignment in directory (line 149)
2124 - // -----------------------------------------------------------------------
2125 -
2126 - #[test]
2127 - fn cgroups_resp_decode_bad_dir_alignment() {
2128 - // dir entry with offset not aligned to 8
2129 - let mut buf = [0u8; 128];
2130 - buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version
2131 - buf[4..8].copy_from_slice(&1u32.to_ne_bytes()); // item_count = 1
2132 - // Dir entry at offset 24: offset=3 (unaligned), length=32
2133 - buf[24..28].copy_from_slice(&3u32.to_ne_bytes());
2134 - buf[28..32].copy_from_slice(&32u32.to_ne_bytes());
2135 - assert_eq!(
2136 - CgroupsResponseView::decode(&buf).unwrap_err(),
2137 - NipcError::BadAlignment
2138 - );
2139 - }
2140 -
2141 - // -----------------------------------------------------------------------
2142 - // CgroupsItemView: bad layout_version, bad flags (lines 200-206)
2143 - // -----------------------------------------------------------------------
2144 -
2145 - #[test]
2146 - fn cgroups_item_bad_layout_version() {
2147 - // Build valid snapshot then corrupt item layout_version
2148 - let mut buf = [0u8; 4096];
2149 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2150 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2151 - let total = b.finish();
2152 -
2153 - // Find item start
2154 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2155 - let item_off = u32::from_ne_bytes(
2156 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2157 - .try_into()
2158 - .unwrap(),
2159 - ) as usize;
2160 - let item_start = dir_end + item_off;
2161 -
2162 - // Corrupt layout_version to 99
2163 - buf[item_start..item_start + 2].copy_from_slice(&99u16.to_ne_bytes());
2164 -
2165 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2166 - assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
2167 - }
2168 -
2169 - #[test]
2170 - fn cgroups_item_bad_flags() {
2171 - // Build valid snapshot then corrupt item flags
2172 - let mut buf = [0u8; 4096];
2173 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2174 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2175 - let total = b.finish();
2176 -
2177 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2178 - let item_off = u32::from_ne_bytes(
2179 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2180 - .try_into()
2181 - .unwrap(),
2182 - ) as usize;
2183 - let item_start = dir_end + item_off;
2184 -
2185 - // Corrupt flags to non-zero
2186 - buf[item_start + 2..item_start + 4].copy_from_slice(&1u16.to_ne_bytes());
2187 -
2188 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2189 - assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
2190 - }
2191 -
2192 - // -----------------------------------------------------------------------
2193 - // CgroupsItemView: name_off < ITEM_HDR_SIZE (line 211)
2194 - // -----------------------------------------------------------------------
2195 -
2196 - #[test]
2197 - fn cgroups_item_name_off_too_small() {
2198 - let mut buf = [0u8; 4096];
2199 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2200 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2201 - let total = b.finish();
2202 -
2203 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2204 - let item_off = u32::from_ne_bytes(
2205 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2206 - .try_into()
2207 - .unwrap(),
2208 - ) as usize;
2209 - let item_start = dir_end + item_off;
2210 -
2211 - // Set name_offset to 0 (< 32 = CGROUPS_ITEM_HDR_SIZE)
2212 - buf[item_start + 16..item_start + 20].copy_from_slice(&0u32.to_ne_bytes());
2213 -
2214 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2215 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
2216 - }
2217 -
2218 - // -----------------------------------------------------------------------
2219 - // CgroupsItemView: path NUL missing (line 228)
2220 - // -----------------------------------------------------------------------
2221 -
2222 - #[test]
2223 - fn cgroups_item_path_missing_nul() {
2224 - let mut buf = [0u8; 4096];
2225 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2226 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2227 - let total = b.finish();
2228 -
2229 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2230 - let item_off = u32::from_ne_bytes(
2231 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2232 - .try_into()
2233 - .unwrap(),
2234 - ) as usize;
2235 - let item_start = dir_end + item_off;
2236 -
2237 - // Find the path NUL terminator and corrupt it
2238 - let path_off =
2239 - u32::from_ne_bytes(buf[item_start + 24..item_start + 28].try_into().unwrap()) as usize;
2240 - let path_len =
2241 - u32::from_ne_bytes(buf[item_start + 28..item_start + 32].try_into().unwrap()) as usize;
2242 - buf[item_start + path_off + path_len] = b'X'; // corrupt path NUL
2243 -
2244 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2245 - assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
2246 - }
2247 -
2248 - // -----------------------------------------------------------------------
2249 - // CgroupsItemView: path_off < ITEM_HDR_SIZE (line 222)
2250 - // -----------------------------------------------------------------------
2251 -
2252 - #[test]
2253 - fn cgroups_item_path_off_too_small() {
2254 - let mut buf = [0u8; 4096];
2255 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2256 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2257 - let total = b.finish();
2258 -
2259 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2260 - let item_off = u32::from_ne_bytes(
2261 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2262 - .try_into()
2263 - .unwrap(),
2264 - ) as usize;
2265 - let item_start = dir_end + item_off;
2266 -
2267 - // Set path_offset to 0 (< 32 = CGROUPS_ITEM_HDR_SIZE)
2268 - buf[item_start + 24..item_start + 28].copy_from_slice(&0u32.to_ne_bytes());
2269 -
2270 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2271 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
2272 - }
2273 -
2274 - // -----------------------------------------------------------------------
2275 - // CgroupsItemView: path string OOB (line 225)
2276 - // -----------------------------------------------------------------------
2277 -
2278 - #[test]
2279 - fn cgroups_item_path_string_oob() {
2280 - let mut buf = [0u8; 4096];
2281 - let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
2282 - b.add(1, 0, 1, b"test", b"/test").unwrap();
2283 - let total = b.finish();
2284 -
2285 - let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
2286 - let item_off = u32::from_ne_bytes(
2287 - buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
2288 - .try_into()
2289 - .unwrap(),
2290 - ) as usize;
2291 - let item_start = dir_end + item_off;
2292 -
2293 - // Corrupt path_length to huge value
2294 - buf[item_start + 28..item_start + 32].copy_from_slice(&99999u32.to_ne_bytes());
2295 -
2296 - let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
2297 - assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
2298 - }
2299 -
2300 - // -----------------------------------------------------------------------
2301 - // Cgroups dispatch (lines 438-447)
2302 - // -----------------------------------------------------------------------
2303 -
2304 - #[test]
2305 - fn dispatch_cgroups_snapshot_bad_request() {
2306 - // Bad request (too short) -> dispatch returns None (line 438)
2307 - let mut resp = [0u8; 4096];
2308 - let result =
2309 - crate::protocol::dispatch_cgroups_snapshot(&[], &mut resp, 1, |_req, _builder| true);
2310 - assert!(result.is_none());
2311 - }
2312 -
2313 - #[test]
2314 - fn dispatch_cgroups_snapshot_handler_returns_false() {
2315 - // Handler returns false -> dispatch returns None (lines 440-441)
2316 - let req = CgroupsRequest {
2317 - layout_version: 1,
2318 - flags: 0,
2319 - };
2320 - let mut req_buf = [0u8; 4];
2321 - req.encode(&mut req_buf);
2322 - let mut resp = [0u8; 4096];
2323 - let result =
2324 - crate::protocol::dispatch_cgroups_snapshot(&req_buf, &mut resp, 1, |_req, _builder| {
2325 - false
2326 - });
2327 - assert!(result.is_none());
2328 - }
2329 -
2330 - #[test]
2331 - fn dispatch_cgroups_snapshot_success() {
2332 - let req = CgroupsRequest {
2333 - layout_version: 1,
2334 - flags: 0,
2335 - };
2336 - let mut req_buf = [0u8; 4];
2337 - req.encode(&mut req_buf);
2338 - let mut resp = [0u8; 4096];
2339 - let result =
2340 - crate::protocol::dispatch_cgroups_snapshot(&req_buf, &mut resp, 2, |_req, builder| {
2341 - builder.add(1, 0, 1, b"cg1", b"/test").unwrap();
2342 - true
2343 - });
2344 - assert!(result.is_some());
2345 - let n = result.unwrap();
2346 - let view = CgroupsResponseView::decode(&resp[..n]).unwrap();
2347 - assert_eq!(view.item_count, 1);
2348 - }
2349 -
2350 - // -----------------------------------------------------------------------
2351 - // Dispatch increment / string_reverse buf overflow (lines 27, 58)
2352 - // -----------------------------------------------------------------------
2353 -
2354 - #[test]
2355 - fn dispatch_increment_resp_too_small() {
2356 - // Response buffer too small -> encode returns 0 -> dispatch returns None
2357 - let req_val = 42u64;
2358 - let mut req_buf = [0u8; 8];
2359 - crate::protocol::increment_encode(req_val, &mut req_buf);
2360 - let mut resp = [0u8; 4]; // too small for 8-byte response
2361 - let result = crate::protocol::dispatch_increment(&req_buf, &mut resp, |v| Some(v + 1));
2362 - assert!(result.is_none());
2363 - }
2364 -
2365 - #[test]
2366 - fn dispatch_increment_handler_none() {
2367 - let mut req_buf = [0u8; 8];
2368 - crate::protocol::increment_encode(42, &mut req_buf);
2369 - let mut resp = [0u8; 8];
2370 - let result = crate::protocol::dispatch_increment(&req_buf, &mut resp, |_| None);
2371 - assert!(result.is_none());
2372 - }
2373 -
2374 - #[test]
2375 - fn dispatch_string_reverse_resp_too_small() {
2376 - let s = b"hello";
2377 - let mut req_buf = [0u8; 64];
2378 - crate::protocol::string_reverse_encode(s, &mut req_buf);
2379 - let mut resp = [0u8; 4]; // too small
2380 - let result = crate::protocol::dispatch_string_reverse(&req_buf, &mut resp, |data| {
2381 - Some(data.iter().rev().copied().collect())
2382 - });
2383 - assert!(result.is_none());
2384 - }
2385 -
2386 - #[test]
2387 - fn dispatch_string_reverse_handler_none() {
2388 - let s = b"hello";
2389 - let mut req_buf = [0u8; 64];
2390 - crate::protocol::string_reverse_encode(s, &mut req_buf);
2391 - let mut resp = [0u8; 64];
2392 - let result = crate::protocol::dispatch_string_reverse(&req_buf, &mut resp, |_| None);
2393 - assert!(result.is_none());
2394 - }
2395 -}
624 +mod tests;
src/crates/netipc/src/protocol/tests.rs new
+1828
@@ -0,0 +1,1828 @@
1 +use super::*;
2 +
3 +// -----------------------------------------------------------------------
4 +// Outer message header tests
5 +// -----------------------------------------------------------------------
6 +
7 +#[test]
8 +fn header_roundtrip() {
9 + let h = Header {
10 + magic: MAGIC_MSG,
11 + version: VERSION,
12 + header_len: HEADER_LEN,
13 + kind: KIND_REQUEST,
14 + flags: FLAG_BATCH,
15 + code: METHOD_CGROUPS_SNAPSHOT,
16 + transport_status: STATUS_OK,
17 + payload_len: 12345,
18 + item_count: 42,
19 + message_id: 0xDEAD_BEEF_CAFE_BABE,
20 + };
21 +
22 + let mut buf = [0u8; 64];
23 + let n = h.encode(&mut buf);
24 + assert_eq!(n, 32);
25 +
26 + let out = Header::decode(&buf[..n]).unwrap();
27 + assert_eq!(out, h);
28 +}
29 +
30 +#[test]
31 +fn header_encode_too_small() {
32 + let h = Header::default();
33 + let mut buf = [0u8; 16];
34 + assert_eq!(h.encode(&mut buf), 0);
35 +}
36 +
37 +#[test]
38 +fn header_decode_truncated() {
39 + let buf = [0u8; 31];
40 + assert_eq!(Header::decode(&buf), Err(NipcError::Truncated));
41 +}
42 +
43 +#[test]
44 +fn header_decode_bad_magic() {
45 + let h = Header {
46 + magic: 0x12345678,
47 + version: VERSION,
48 + header_len: HEADER_LEN,
49 + kind: KIND_REQUEST,
50 + ..Default::default()
51 + };
52 + let mut buf = [0u8; 32];
53 + h.encode(&mut buf);
54 + assert_eq!(Header::decode(&buf), Err(NipcError::BadMagic));
55 +}
56 +
57 +#[test]
58 +fn header_decode_bad_version() {
59 + let h = Header {
60 + magic: MAGIC_MSG,
61 + version: 99,
62 + header_len: HEADER_LEN,
63 + kind: KIND_REQUEST,
64 + ..Default::default()
65 + };
66 + let mut buf = [0u8; 32];
67 + h.encode(&mut buf);
68 + assert_eq!(Header::decode(&buf), Err(NipcError::BadVersion));
69 +}
70 +
71 +#[test]
72 +fn header_decode_bad_header_len() {
73 + let h = Header {
74 + magic: MAGIC_MSG,
75 + version: VERSION,
76 + header_len: 64,
77 + kind: KIND_REQUEST,
78 + ..Default::default()
79 + };
80 + let mut buf = [0u8; 32];
81 + h.encode(&mut buf);
82 + assert_eq!(Header::decode(&buf), Err(NipcError::BadHeaderLen));
83 +}
84 +
85 +#[test]
86 +fn header_decode_bad_kind() {
87 + // kind = 0
88 + let h = Header {
89 + magic: MAGIC_MSG,
90 + version: VERSION,
91 + header_len: HEADER_LEN,
92 + kind: 0,
93 + ..Default::default()
94 + };
95 + let mut buf = [0u8; 32];
96 + h.encode(&mut buf);
97 + assert_eq!(Header::decode(&buf), Err(NipcError::BadKind));
98 +
99 + // kind = 4
100 + let h2 = Header { kind: 4, ..h };
101 + h2.encode(&mut buf);
102 + assert_eq!(Header::decode(&buf), Err(NipcError::BadKind));
103 +}
104 +
105 +#[test]
106 +fn header_all_kinds() {
107 + for k in KIND_REQUEST..=KIND_CONTROL {
108 + let h = Header {
109 + magic: MAGIC_MSG,
110 + version: VERSION,
111 + header_len: HEADER_LEN,
112 + kind: k,
113 + ..Default::default()
114 + };
115 + let mut buf = [0u8; 32];
116 + h.encode(&mut buf);
117 + let out = Header::decode(&buf).unwrap();
118 + assert_eq!(out.kind, k);
119 + }
120 +}
121 +
122 +#[test]
123 +fn header_wire_bytes() {
124 + let h = Header {
125 + magic: MAGIC_MSG,
126 + version: VERSION,
127 + header_len: HEADER_LEN,
128 + kind: KIND_REQUEST,
129 + flags: 0,
130 + code: METHOD_CGROUPS_SNAPSHOT,
131 + transport_status: STATUS_OK,
132 + payload_len: 4,
133 + item_count: 1,
134 + message_id: 1,
135 + };
136 +
137 + let mut buf = [0u8; 32];
138 + h.encode(&mut buf);
139 +
140 + // magic = 0x4e495043 LE: 43 50 49 4e
141 + assert_eq!(&buf[0..4], &[0x43, 0x50, 0x49, 0x4e]);
142 + // version = 1 LE: 01 00
143 + assert_eq!(&buf[4..6], &[0x01, 0x00]);
144 + // header_len = 32 LE: 20 00
145 + assert_eq!(&buf[6..8], &[0x20, 0x00]);
146 + // kind = 1 LE: 01 00
147 + assert_eq!(&buf[8..10], &[0x01, 0x00]);
148 + // code = 2 LE: 02 00
149 + assert_eq!(&buf[12..14], &[0x02, 0x00]);
150 +}
151 +
152 +// -----------------------------------------------------------------------
153 +// Chunk continuation header tests
154 +// -----------------------------------------------------------------------
155 +
156 +#[test]
157 +fn chunk_header_roundtrip() {
158 + let c = ChunkHeader {
159 + magic: MAGIC_CHUNK,
160 + version: VERSION,
161 + flags: 0,
162 + message_id: 0x1234_5678_90AB_CDEF,
163 + total_message_len: 100000,
164 + chunk_index: 3,
165 + chunk_count: 10,
166 + chunk_payload_len: 8192,
167 + };
168 +
169 + let mut buf = [0u8; 64];
170 + let n = c.encode(&mut buf);
171 + assert_eq!(n, 32);
172 +
173 + let out = ChunkHeader::decode(&buf[..n]).unwrap();
174 + assert_eq!(out, c);
175 +}
176 +
177 +#[test]
178 +fn chunk_decode_truncated() {
179 + let buf = [0u8; 31];
180 + assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::Truncated));
181 +}
182 +
183 +#[test]
184 +fn chunk_decode_bad_magic() {
185 + let c = ChunkHeader {
186 + magic: MAGIC_MSG, // wrong magic for chunk
187 + version: VERSION,
188 + ..Default::default()
189 + };
190 + let mut buf = [0u8; 32];
191 + c.encode(&mut buf);
192 + assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadMagic));
193 +}
194 +
195 +#[test]
196 +fn chunk_decode_bad_version() {
197 + let c = ChunkHeader {
198 + magic: MAGIC_CHUNK,
199 + version: 2,
200 + ..Default::default()
201 + };
202 + let mut buf = [0u8; 32];
203 + c.encode(&mut buf);
204 + assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadVersion));
205 +}
206 +
207 +#[test]
208 +fn chunk_encode_too_small() {
209 + let c = ChunkHeader::default();
210 + let mut buf = [0u8; 16];
211 + assert_eq!(c.encode(&mut buf), 0);
212 +}
213 +
214 +#[test]
215 +fn chunk_wire_bytes() {
216 + let c = ChunkHeader {
217 + magic: MAGIC_CHUNK,
218 + version: VERSION,
219 + flags: 0,
220 + message_id: 1,
221 + total_message_len: 256,
222 + chunk_index: 1,
223 + chunk_count: 3,
224 + chunk_payload_len: 100,
225 + };
226 +
227 + let mut buf = [0u8; 32];
228 + c.encode(&mut buf);
229 +
230 + // magic = 0x4e43484b LE: 4b 48 43 4e
231 + assert_eq!(&buf[0..4], &[0x4b, 0x48, 0x43, 0x4e]);
232 +}
233 +
234 +// -----------------------------------------------------------------------
235 +// Batch item directory tests
236 +// -----------------------------------------------------------------------
237 +
238 +#[test]
239 +fn batch_dir_roundtrip() {
240 + let entries = [
241 + BatchEntry {
242 + offset: 0,
243 + length: 100,
244 + },
245 + BatchEntry {
246 + offset: 104,
247 + length: 200,
248 + },
249 + BatchEntry {
250 + offset: 304,
251 + length: 50,
252 + },
253 + ];
254 +
255 + let mut buf = [0u8; 64];
256 + let n = batch_dir_encode(&entries, &mut buf);
257 + assert_eq!(n, 24);
258 +
259 + let out = batch_dir_decode(&buf[..n], 3, 400).unwrap();
260 + assert_eq!(out[0], entries[0]);
261 + assert_eq!(out[1], entries[1]);
262 + assert_eq!(out[2], entries[2]);
263 +}
264 +
265 +#[test]
266 +fn batch_dir_decode_truncated() {
267 + let buf = [0u8; 12];
268 + assert_eq!(batch_dir_decode(&buf, 2, 1000), Err(NipcError::Truncated));
269 +}
270 +
271 +#[test]
272 +fn batch_dir_decode_oob() {
273 + let e = BatchEntry {
274 + offset: 0,
275 + length: 200,
276 + };
277 + let mut buf = [0u8; 8];
278 + batch_dir_encode(&[e], &mut buf);
279 + assert_eq!(batch_dir_decode(&buf, 1, 100), Err(NipcError::OutOfBounds));
280 +}
281 +
282 +#[test]
283 +fn batch_dir_decode_bad_alignment() {
284 + let mut buf = [0u8; 8];
285 + // Manually write unaligned offset
286 + buf[0..4].copy_from_slice(&3u32.to_ne_bytes());
287 + buf[4..8].copy_from_slice(&10u32.to_ne_bytes());
288 + assert_eq!(batch_dir_decode(&buf, 1, 100), Err(NipcError::BadAlignment));
289 +}
290 +
291 +// -----------------------------------------------------------------------
292 +// Batch builder + extraction tests
293 +// -----------------------------------------------------------------------
294 +
295 +#[test]
296 +fn batch_builder_roundtrip() {
297 + let mut buf = [0u8; 1024];
298 + let mut b = BatchBuilder::new(&mut buf, 4);
299 +
300 + let item1 = [1u8, 2, 3, 4, 5];
301 + let item2 = [10u8, 20, 30];
302 + let item3 = [0xAAu8, 0xBB];
303 +
304 + b.add(&item1).unwrap();
305 + b.add(&item2).unwrap();
306 + b.add(&item3).unwrap();
307 +
308 + let (total, count) = b.finish();
309 + assert_eq!(count, 3);
310 + assert!(total > 0);
311 +
312 + // Extract items
313 + let (data, len) = batch_item_get(&buf[..total], 3, 0).unwrap();
314 + assert_eq!(len as usize, item1.len());
315 + assert_eq!(data, &item1);
316 +
317 + let (data, len) = batch_item_get(&buf[..total], 3, 1).unwrap();
318 + assert_eq!(len as usize, item2.len());
319 + assert_eq!(data, &item2);
320 +
321 + let (data, len) = batch_item_get(&buf[..total], 3, 2).unwrap();
322 + assert_eq!(len as usize, item3.len());
323 + assert_eq!(data, &item3);
324 +}
325 +
326 +#[test]
327 +fn batch_builder_overflow() {
328 + let mut buf = [0u8; 32];
329 + let mut b = BatchBuilder::new(&mut buf, 1);
330 + let item = [1u8];
331 + b.add(&item).unwrap();
332 + assert_eq!(b.add(&item), Err(NipcError::Overflow));
333 +}
334 +
335 +#[test]
336 +fn batch_builder_buf_overflow() {
337 + let mut buf = [0u8; 24];
338 + let mut b = BatchBuilder::new(&mut buf, 1);
339 + let big = [0u8; 100];
340 + assert_eq!(b.add(&big), Err(NipcError::Overflow));
341 +}
342 +
343 +#[test]
344 +fn batch_item_get_oob_index() {
345 + let mut buf = [0u8; 64];
346 + let mut b = BatchBuilder::new(&mut buf, 2);
347 + b.add(&[1u8]).unwrap();
348 + let (total, count) = b.finish();
349 + assert_eq!(
350 + batch_item_get(&buf[..total], count, 5),
351 + Err(NipcError::OutOfBounds)
352 + );
353 +}
354 +
355 +#[test]
356 +fn batch_empty() {
357 + let mut buf = [0u8; 64];
358 + let b = BatchBuilder::new(&mut buf, 4);
359 + let (total, count) = b.finish();
360 + assert_eq!(count, 0);
361 + assert_eq!(total, 0);
362 +}
363 +
364 +// -----------------------------------------------------------------------
365 +// Hello payload tests
366 +// -----------------------------------------------------------------------
367 +
368 +#[test]
369 +fn hello_roundtrip() {
370 + let h = Hello {
371 + layout_version: 1,
372 + flags: 0,
373 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
374 + preferred_profiles: PROFILE_SHM_FUTEX,
375 + max_request_payload_bytes: 4096,
376 + max_request_batch_items: 100,
377 + max_response_payload_bytes: 1048576,
378 + max_response_batch_items: 1,
379 + auth_token: 0xAABB_CCDD_EEFF_0011,
380 + packet_size: 65536,
381 + };
382 +
383 + let mut buf = [0u8; 64];
384 + let n = h.encode(&mut buf);
385 + assert_eq!(n, 44);
386 +
387 + let out = Hello::decode(&buf[..n]).unwrap();
388 + assert_eq!(out, h);
389 +}
390 +
391 +#[test]
392 +fn hello_decode_truncated() {
393 + let buf = [0u8; 43];
394 + assert_eq!(Hello::decode(&buf), Err(NipcError::Truncated));
395 +}
396 +
397 +#[test]
398 +fn hello_decode_bad_layout() {
399 + let h = Hello {
400 + layout_version: 99,
401 + ..Default::default()
402 + };
403 + let mut buf = [0u8; 44];
404 + h.encode(&mut buf);
405 + assert_eq!(Hello::decode(&buf), Err(NipcError::BadLayout));
406 +}
407 +
408 +#[test]
409 +fn hello_encode_too_small() {
410 + let h = Hello::default();
411 + let mut buf = [0u8; 10];
412 + assert_eq!(h.encode(&mut buf), 0);
413 +}
414 +
415 +// -----------------------------------------------------------------------
416 +// Hello-ack payload tests
417 +// -----------------------------------------------------------------------
418 +
419 +#[test]
420 +fn hello_ack_roundtrip() {
421 + let h = HelloAck {
422 + layout_version: 1,
423 + flags: 0,
424 + server_supported_profiles: 0x07,
425 + intersection_profiles: 0x05,
426 + selected_profile: PROFILE_SHM_FUTEX,
427 + agreed_max_request_payload_bytes: 2048,
428 + agreed_max_request_batch_items: 50,
429 + agreed_max_response_payload_bytes: 65536,
430 + agreed_max_response_batch_items: 1,
431 + agreed_packet_size: 32768,
432 + session_id: 42,
433 + };
434 +
435 + let mut buf = [0u8; 64];
436 + let n = h.encode(&mut buf);
437 + assert_eq!(n, 48);
438 +
439 + let out = HelloAck::decode(&buf[..n]).unwrap();
440 + assert_eq!(out, h);
441 +}
442 +
443 +#[test]
444 +fn hello_ack_decode_truncated() {
445 + let buf = [0u8; 47];
446 + assert_eq!(HelloAck::decode(&buf), Err(NipcError::Truncated));
447 +}
448 +
449 +#[test]
450 +fn hello_ack_decode_bad_layout() {
451 + let h = HelloAck {
452 + layout_version: 0,
453 + ..Default::default()
454 + };
455 + let mut buf = [0u8; 48];
456 + h.encode(&mut buf);
457 + assert_eq!(HelloAck::decode(&buf), Err(NipcError::BadLayout));
458 +}
459 +
460 +#[test]
461 +fn hello_ack_encode_too_small() {
462 + let h = HelloAck::default();
463 + let mut buf = [0u8; 10];
464 + assert_eq!(h.encode(&mut buf), 0);
465 +}
466 +
467 +// -----------------------------------------------------------------------
468 +// Cgroups snapshot request tests
469 +// -----------------------------------------------------------------------
470 +
471 +#[test]
472 +fn cgroups_req_roundtrip() {
473 + let r = CgroupsRequest {
474 + layout_version: 1,
475 + flags: 0,
476 + };
477 +
478 + let mut buf = [0u8; 16];
479 + let n = r.encode(&mut buf);
480 + assert_eq!(n, 4);
481 +
482 + let out = CgroupsRequest::decode(&buf[..n]).unwrap();
483 + assert_eq!(out, r);
484 +}
485 +
486 +#[test]
487 +fn cgroups_req_decode_truncated() {
488 + let buf = [0u8; 3];
489 + assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::Truncated));
490 +}
491 +
492 +#[test]
493 +fn cgroups_req_decode_bad_layout() {
494 + let r = CgroupsRequest {
495 + layout_version: 5,
496 + flags: 0,
497 + };
498 + let mut buf = [0u8; 4];
499 + r.encode(&mut buf);
500 + assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::BadLayout));
501 +}
502 +
503 +#[test]
504 +fn cgroups_req_encode_too_small() {
505 + let r = CgroupsRequest::default();
506 + let mut buf = [0u8; 2];
507 + assert_eq!(r.encode(&mut buf), 0);
508 +}
509 +
510 +// -----------------------------------------------------------------------
511 +// Cgroups snapshot response tests
512 +// -----------------------------------------------------------------------
513 +
514 +// Private constants needed by tests -- mirror cgroups_snapshot.rs values.
515 +const CGROUPS_RESP_HDR_SIZE: usize = 24;
516 +const CGROUPS_DIR_ENTRY_SIZE: usize = 8;
517 +
518 +#[test]
519 +fn cgroups_resp_empty() {
520 + let mut buf = [0u8; 4096];
521 + let b = CgroupsBuilder::new(&mut buf, 0, 1, 42);
522 + let total = b.finish();
523 + assert_eq!(total, 24);
524 +
525 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
526 + assert_eq!(view.item_count, 0);
527 + assert_eq!(view.systemd_enabled, 1);
528 + assert_eq!(view.generation, 42);
529 +}
530 +
531 +#[test]
532 +fn cgroups_resp_single_item() {
533 + let mut buf = [0u8; 4096];
534 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 100);
535 +
536 + let name = b"docker-abc123";
537 + let path = b"/sys/fs/cgroup/docker/abc123";
538 + b.add(12345, 0x01, 1, name, path).unwrap();
539 +
540 + let total = b.finish();
541 + assert!(total > 24);
542 +
543 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
544 + assert_eq!(view.item_count, 1);
545 + assert_eq!(view.systemd_enabled, 0);
546 + assert_eq!(view.generation, 100);
547 +
548 + let item = view.item(0).unwrap();
549 + assert_eq!(item.hash, 12345);
550 + assert_eq!(item.options, 0x01);
551 + assert_eq!(item.enabled, 1);
552 + assert_eq!(item.name.len as usize, name.len());
553 + assert_eq!(item.name.as_bytes(), name);
554 + assert_eq!(item.name.bytes[name.len()], 0); // NUL
555 + assert_eq!(item.path.len as usize, path.len());
556 + assert_eq!(item.path.as_bytes(), path);
557 + assert_eq!(item.path.bytes[path.len()], 0); // NUL
558 +}
559 +
560 +#[test]
561 +fn cgroups_resp_multiple_items() {
562 + let mut buf = [0u8; 8192];
563 + let mut b = CgroupsBuilder::new(&mut buf, 5, 1, 999);
564 +
565 + // Item 0
566 + let n0 = b"init.scope";
567 + let p0 = b"/sys/fs/cgroup/init.scope";
568 + b.add(100, 0, 1, n0, p0).unwrap();
569 +
570 + // Item 1
571 + let n1 = b"system.slice/docker-abc.scope";
572 + let p1 = b"/sys/fs/cgroup/system.slice/docker-abc.scope";
573 + b.add(200, 0x02, 0, n1, p1).unwrap();
574 +
575 + // Item 2 - empty strings
576 + b.add(300, 0, 1, b"", b"").unwrap();
577 +
578 + let total = b.finish();
579 +
580 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
581 + assert_eq!(view.item_count, 3);
582 + assert_eq!(view.systemd_enabled, 1);
583 + assert_eq!(view.generation, 999);
584 +
585 + // Verify item 0
586 + let item = view.item(0).unwrap();
587 + assert_eq!(item.hash, 100);
588 + assert_eq!(item.name.len as usize, n0.len());
589 + assert_eq!(item.name.as_bytes(), n0);
590 + assert_eq!(item.path.len as usize, p0.len());
591 + assert_eq!(item.path.as_bytes(), p0);
592 +
593 + // Verify item 1
594 + let item = view.item(1).unwrap();
595 + assert_eq!(item.hash, 200);
596 + assert_eq!(item.options, 0x02);
597 + assert_eq!(item.enabled, 0);
598 + assert_eq!(item.name.len as usize, n1.len());
599 + assert_eq!(item.name.as_bytes(), n1);
600 +
601 + // Verify item 2 (empty strings)
602 + let item = view.item(2).unwrap();
603 + assert_eq!(item.hash, 300);
604 + assert_eq!(item.name.len, 0);
605 + assert_eq!(item.name.bytes[0], 0); // NUL
606 + assert_eq!(item.path.len, 0);
607 + assert_eq!(item.path.bytes[0], 0); // NUL
608 +
609 + // Out-of-bounds index
610 + assert_eq!(view.item(3), Err(NipcError::OutOfBounds));
611 +}
612 +
613 +#[test]
614 +fn cgroups_resp_decode_truncated_header() {
615 + let buf = [0u8; 23];
616 + assert_eq!(
617 + CgroupsResponseView::decode(&buf).unwrap_err(),
618 + NipcError::Truncated
619 + );
620 +}
621 +
622 +#[test]
623 +fn cgroups_resp_decode_bad_layout() {
624 + let mut buf = [0u8; 24];
625 + buf[0..2].copy_from_slice(&99u16.to_ne_bytes());
626 + assert_eq!(
627 + CgroupsResponseView::decode(&buf).unwrap_err(),
628 + NipcError::BadLayout
629 + );
630 +}
631 +
632 +#[test]
633 +fn cgroups_resp_decode_truncated_dir() {
634 + // Header says item_count=2 but payload is only 24 bytes
635 + let mut buf = [0u8; 24];
636 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
637 + buf[4..8].copy_from_slice(&2u32.to_ne_bytes());
638 + assert_eq!(
639 + CgroupsResponseView::decode(&buf).unwrap_err(),
640 + NipcError::Truncated
641 + );
642 +}
643 +
644 +#[test]
645 +fn cgroups_resp_decode_oob_dir() {
646 + // Header + 1 dir entry pointing beyond payload
647 + let mut buf = [0u8; 64];
648 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
649 + buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
650 + // Dir entry at offset 24: offset=0, length=9999
651 + buf[24..28].copy_from_slice(&0u32.to_ne_bytes());
652 + buf[28..32].copy_from_slice(&9999u32.to_ne_bytes());
653 + assert_eq!(
654 + CgroupsResponseView::decode(&buf).unwrap_err(),
655 + NipcError::OutOfBounds
656 + );
657 +}
658 +
659 +#[test]
660 +fn cgroups_resp_decode_item_too_small() {
661 + // Dir entry with length < 32
662 + let mut buf = [0u8; 64];
663 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
664 + buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
665 + buf[24..28].copy_from_slice(&0u32.to_ne_bytes());
666 + buf[28..32].copy_from_slice(&16u32.to_ne_bytes());
667 + assert_eq!(
668 + CgroupsResponseView::decode(&buf).unwrap_err(),
669 + NipcError::Truncated
670 + );
671 +}
672 +
673 +#[test]
674 +fn cgroups_resp_item_missing_nul() {
675 + // Build valid snapshot then corrupt the NUL terminator
676 + let mut buf = [0u8; 4096];
677 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
678 + b.add(1, 0, 1, b"test", b"/test").unwrap();
679 + let total = b.finish();
680 +
681 + // Find item data and corrupt the name's NUL terminator
682 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
683 + let item_off = u32::from_ne_bytes(
684 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
685 + .try_into()
686 + .unwrap(),
687 + ) as usize;
688 + let item_start = dir_end + item_off;
689 +
690 + let noff =
691 + u32::from_ne_bytes(buf[item_start + 16..item_start + 20].try_into().unwrap()) as usize;
692 + let nlen =
693 + u32::from_ne_bytes(buf[item_start + 20..item_start + 24].try_into().unwrap()) as usize;
694 +
695 + buf[item_start + noff + nlen] = b'X'; // corrupt NUL
696 +
697 + // Re-decode after corruption -- header/dir still valid
698 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
699 + assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
700 +}
701 +
702 +#[test]
703 +fn cgroups_resp_item_string_oob() {
704 + // Build valid snapshot then corrupt string length to be huge
705 + let mut buf = [0u8; 4096];
706 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
707 + b.add(1, 0, 1, b"test", b"/test").unwrap();
708 + let total = b.finish();
709 +
710 + // Corrupt name_length to huge value
711 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
712 + let item_off = u32::from_ne_bytes(
713 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
714 + .try_into()
715 + .unwrap(),
716 + ) as usize;
717 + let item_start = dir_end + item_off;
718 +
719 + buf[item_start + 20..item_start + 24].copy_from_slice(&99999u32.to_ne_bytes());
720 +
721 + // Re-decode after corruption
722 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
723 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
724 +}
725 +
726 +#[test]
727 +fn cgroups_builder_overflow() {
728 + let mut buf = [0u8; 64]; // too small for any real item
729 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 0);
730 + let long_name = [b'A'; 200];
731 + assert_eq!(b.add(1, 0, 1, &long_name, b""), Err(NipcError::Overflow));
732 +}
733 +
734 +#[test]
735 +fn cgroups_builder_max_items_exceeded() {
736 + let mut buf = [0u8; 4096];
737 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 0);
738 + b.add(1, 0, 1, b"a", b"b").unwrap();
739 + assert_eq!(b.add(2, 0, 1, b"c", b"d"), Err(NipcError::Overflow));
740 +}
741 +
742 +#[test]
743 +fn cgroups_builder_compaction() {
744 + let mut buf = [0u8; 4096];
745 + // Reserve 10 directory slots but only add 2 items
746 + let mut b = CgroupsBuilder::new(&mut buf, 10, 1, 77);
747 +
748 + b.add(10, 0, 1, b"slice-a", b"/cgroup/slice-a").unwrap();
749 + b.add(20, 0, 0, b"slice-b", b"/cgroup/slice-b").unwrap();
750 +
751 + let total = b.finish();
752 +
753 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
754 + assert_eq!(view.item_count, 2);
755 + assert_eq!(view.generation, 77);
756 +
757 + let item = view.item(0).unwrap();
758 + assert_eq!(item.hash, 10);
759 + assert_eq!(item.name.as_bytes(), b"slice-a");
760 +
761 + let item = view.item(1).unwrap();
762 + assert_eq!(item.hash, 20);
763 + assert_eq!(item.name.as_bytes(), b"slice-b");
764 +}
765 +
766 +// -----------------------------------------------------------------------
767 +// Alignment utility test
768 +// -----------------------------------------------------------------------
769 +
770 +#[test]
771 +fn test_align8() {
772 + assert_eq!(align8(0), 0);
773 + assert_eq!(align8(1), 8);
774 + assert_eq!(align8(7), 8);
775 + assert_eq!(align8(8), 8);
776 + assert_eq!(align8(9), 16);
777 + assert_eq!(align8(16), 16);
778 + assert_eq!(align8(17), 24);
779 +}
780 +
781 +// -----------------------------------------------------------------------
782 +// Cross-language wire compatibility: C-Rust byte identity
783 +//
784 +// These tests encode in Rust and verify the exact bytes match what the
785 +// C implementation produces for the same inputs. This ensures identical
786 +// wire output across languages.
787 +// -----------------------------------------------------------------------
788 +
789 +#[test]
790 +fn c_rust_header_bytes_identical() {
791 + // Encode in Rust
792 + let h = Header {
793 + magic: MAGIC_MSG,
794 + version: VERSION,
795 + header_len: HEADER_LEN,
796 + kind: KIND_REQUEST,
797 + flags: FLAG_BATCH,
798 + code: METHOD_CGROUPS_SNAPSHOT,
799 + transport_status: STATUS_OK,
800 + payload_len: 12345,
801 + item_count: 42,
802 + message_id: 0xDEAD_BEEF_CAFE_BABE,
803 + };
804 + let mut rust_buf = [0u8; 32];
805 + h.encode(&mut rust_buf);
806 +
807 + // Known LE bytes for this header
808 + let expected: [u8; 32] = [
809 + 0x43, 0x50, 0x49, 0x4e, // magic
810 + 0x01, 0x00, // version
811 + 0x20, 0x00, // header_len
812 + 0x01, 0x00, // kind
813 + 0x01, 0x00, // flags
814 + 0x02, 0x00, // code
815 + 0x00, 0x00, // transport_status
816 + 0x39, 0x30, 0x00, 0x00, // payload_len = 12345
817 + 0x2a, 0x00, 0x00, 0x00, // item_count = 42
818 + 0xbe, 0xba, 0xfe, 0xca, 0xef, 0xbe, 0xad, 0xde, // message_id
819 + ];
820 + assert_eq!(rust_buf, expected);
821 +}
822 +
823 +#[test]
824 +fn c_rust_chunk_bytes_identical() {
825 + let c = ChunkHeader {
826 + magic: MAGIC_CHUNK,
827 + version: VERSION,
828 + flags: 0,
829 + message_id: 1,
830 + total_message_len: 256,
831 + chunk_index: 1,
832 + chunk_count: 3,
833 + chunk_payload_len: 100,
834 + };
835 + let mut rust_buf = [0u8; 32];
836 + c.encode(&mut rust_buf);
837 +
838 + let expected: [u8; 32] = [
839 + 0x4b, 0x48, 0x43, 0x4e, // magic
840 + 0x01, 0x00, // version
841 + 0x00, 0x00, // flags
842 + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // message_id
843 + 0x00, 0x01, 0x00, 0x00, // total_message_len = 256
844 + 0x01, 0x00, 0x00, 0x00, // chunk_index
845 + 0x03, 0x00, 0x00, 0x00, // chunk_count
846 + 0x64, 0x00, 0x00, 0x00, // chunk_payload_len = 100
847 + ];
848 + assert_eq!(rust_buf, expected);
849 +}
850 +
851 +#[test]
852 +fn c_rust_hello_bytes_identical() {
853 + let h = Hello {
854 + layout_version: 1,
855 + flags: 0,
856 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
857 + preferred_profiles: PROFILE_SHM_FUTEX,
858 + max_request_payload_bytes: 4096,
859 + max_request_batch_items: 100,
860 + max_response_payload_bytes: 1048576,
861 + max_response_batch_items: 1,
862 + auth_token: 0xAABB_CCDD_EEFF_0011,
863 + packet_size: 65536,
864 + };
865 +
866 + let mut rust_buf = [0u8; 44];
867 + h.encode(&mut rust_buf);
868 +
869 + // Verify key byte positions
870 + assert_eq!(&rust_buf[0..2], &[0x01, 0x00]); // layout_version
871 + assert_eq!(&rust_buf[2..4], &[0x00, 0x00]); // flags
872 + assert_eq!(&rust_buf[4..8], &[0x05, 0x00, 0x00, 0x00]); // supported = 0x05
873 + assert_eq!(&rust_buf[8..12], &[0x04, 0x00, 0x00, 0x00]); // preferred = 0x04
874 + assert_eq!(&rust_buf[28..32], &[0x00, 0x00, 0x00, 0x00]); // padding = 0
875 + assert_eq!(
876 + &rust_buf[32..40],
877 + &[0x11, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA]
878 + ); // auth_token
879 +
880 + // Round-trip
881 + let out = Hello::decode(&rust_buf).unwrap();
882 + assert_eq!(out, h);
883 +}
884 +
885 +#[test]
886 +fn c_rust_hello_ack_bytes_identical() {
887 + let h = HelloAck {
888 + layout_version: 1,
889 + flags: 0,
890 + server_supported_profiles: 0x07,
891 + intersection_profiles: 0x05,
892 + selected_profile: PROFILE_SHM_FUTEX,
893 + agreed_max_request_payload_bytes: 2048,
894 + agreed_max_request_batch_items: 50,
895 + agreed_max_response_payload_bytes: 65536,
896 + agreed_max_response_batch_items: 1,
897 + agreed_packet_size: 32768,
898 + session_id: 0x0000_0001_0000_0007,
899 + };
900 + let mut rust_buf = [0u8; 48];
901 + h.encode(&mut rust_buf);
902 +
903 + assert_eq!(&rust_buf[0..2], &[0x01, 0x00]);
904 + assert_eq!(&rust_buf[4..8], &[0x07, 0x00, 0x00, 0x00]); // server_supported
905 + assert_eq!(&rust_buf[12..16], &[0x04, 0x00, 0x00, 0x00]); // selected = SHM_FUTEX
906 + assert_eq!(&rust_buf[36..40], &[0x00, 0x00, 0x00, 0x00]); // padding
907 + assert_eq!(
908 + &rust_buf[40..48],
909 + &[0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]
910 + ); // session_id LE
911 +
912 + let out = HelloAck::decode(&rust_buf).unwrap();
913 + assert_eq!(out, h);
914 +}
915 +
916 +#[test]
917 +fn c_rust_cgroups_req_bytes_identical() {
918 + let r = CgroupsRequest {
919 + layout_version: 1,
920 + flags: 0,
921 + };
922 + let mut rust_buf = [0u8; 4];
923 + r.encode(&mut rust_buf);
924 +
925 + assert_eq!(rust_buf, [0x01, 0x00, 0x00, 0x00]);
926 +
927 + let out = CgroupsRequest::decode(&rust_buf).unwrap();
928 + assert_eq!(out, r);
929 +}
930 +
931 +#[test]
932 +fn c_rust_cgroups_snapshot_bytes_identical() {
933 + // Build a snapshot with the exact same inputs as the C test
934 + let mut buf = [0u8; 4096];
935 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 100);
936 + b.add(
937 + 12345,
938 + 0x01,
939 + 1,
940 + b"docker-abc123",
941 + b"/sys/fs/cgroup/docker/abc123",
942 + )
943 + .unwrap();
944 + let total = b.finish();
945 +
946 + // Verify the snapshot header bytes
947 + assert_eq!(&buf[0..2], &[0x01, 0x00]); // layout_version
948 + assert_eq!(&buf[2..4], &[0x00, 0x00]); // flags
949 + assert_eq!(&buf[4..8], &[0x01, 0x00, 0x00, 0x00]); // item_count
950 + assert_eq!(&buf[8..12], &[0x00, 0x00, 0x00, 0x00]); // systemd_enabled
951 + assert_eq!(&buf[12..16], &[0x00, 0x00, 0x00, 0x00]); // reserved
952 + assert_eq!(
953 + &buf[16..24],
954 + &[0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
955 + ); // generation
956 +
957 + // Verify it decodes correctly
958 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
959 + assert_eq!(view.item_count, 1);
960 + assert_eq!(view.generation, 100);
961 +
962 + let item = view.item(0).unwrap();
963 + assert_eq!(item.hash, 12345);
964 + assert_eq!(item.name.as_bytes(), b"docker-abc123");
965 + assert_eq!(item.path.as_bytes(), b"/sys/fs/cgroup/docker/abc123");
966 +}
967 +
968 +#[test]
969 +fn cgroups_resp_dir_bad_alignment() {
970 + // Dir entry with unaligned offset
971 + let mut buf = [0u8; 128];
972 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes());
973 + buf[4..8].copy_from_slice(&1u32.to_ne_bytes());
974 + // offset=3 (not 8-byte aligned), length=32
975 + buf[24..28].copy_from_slice(&3u32.to_ne_bytes());
976 + buf[28..32].copy_from_slice(&32u32.to_ne_bytes());
977 + assert_eq!(
978 + CgroupsResponseView::decode(&buf).unwrap_err(),
979 + NipcError::BadAlignment
980 + );
981 +}
982 +
983 +#[test]
984 +fn cgroups_resp_item_bad_layout_version() {
985 + // Build valid snapshot, then corrupt the item's layout_version
986 + let mut buf = [0u8; 4096];
987 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
988 + b.add(1, 0, 1, b"test", b"/test").unwrap();
989 + let total = b.finish();
990 +
991 + // Corrupt item layout_version
992 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
993 + let item_off = u32::from_ne_bytes(
994 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
995 + .try_into()
996 + .unwrap(),
997 + ) as usize;
998 + let item_start = dir_end + item_off;
999 + buf[item_start..item_start + 2].copy_from_slice(&99u16.to_ne_bytes());
1000 +
1001 + // Re-decode after corruption
1002 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1003 + assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1004 +}
1005 +
1006 +#[test]
1007 +fn cgroups_resp_item_name_off_below_header() {
1008 + // Build valid snapshot, then set name_offset < 32
1009 + let mut buf = [0u8; 4096];
1010 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1011 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1012 + let total = b.finish();
1013 +
1014 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1015 + let item_off = u32::from_ne_bytes(
1016 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1017 + .try_into()
1018 + .unwrap(),
1019 + ) as usize;
1020 + let item_start = dir_end + item_off;
1021 + // Set name_offset to 0 (below header)
1022 + buf[item_start + 16..item_start + 20].copy_from_slice(&0u32.to_ne_bytes());
1023 +
1024 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1025 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1026 +}
1027 +
1028 +#[test]
1029 +fn cgroups_resp_item_path_off_below_header() {
1030 + let mut buf = [0u8; 4096];
1031 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1032 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1033 + let total = b.finish();
1034 +
1035 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1036 + let item_off = u32::from_ne_bytes(
1037 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1038 + .try_into()
1039 + .unwrap(),
1040 + ) as usize;
1041 + let item_start = dir_end + item_off;
1042 + // Set path_offset to 16 (below header)
1043 + buf[item_start + 24..item_start + 28].copy_from_slice(&16u32.to_ne_bytes());
1044 +
1045 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1046 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1047 +}
1048 +
1049 +#[test]
1050 +fn cgroups_resp_item_path_missing_nul() {
1051 + let mut buf = [0u8; 4096];
1052 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1053 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1054 + let total = b.finish();
1055 +
1056 + // Corrupt path NUL
1057 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1058 + let item_off = u32::from_ne_bytes(
1059 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1060 + .try_into()
1061 + .unwrap(),
1062 + ) as usize;
1063 + let item_start = dir_end + item_off;
1064 + let poff =
1065 + u32::from_ne_bytes(buf[item_start + 24..item_start + 28].try_into().unwrap()) as usize;
1066 + let plen =
1067 + u32::from_ne_bytes(buf[item_start + 28..item_start + 32].try_into().unwrap()) as usize;
1068 + buf[item_start + poff + plen] = b'X';
1069 +
1070 + // Re-decode after corruption
1071 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1072 + assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
1073 +}
1074 +
1075 +#[test]
1076 +fn cgroups_resp_item_overlap_rejected() {
1077 + // Build a valid item, then manually set path_offset to overlap with name
1078 + let mut buf = [0u8; 4096];
1079 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1080 + b.add(1, 0, 1, b"hello", b"/path").unwrap();
1081 + let total = b.finish();
1082 +
1083 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1084 + let item_off = u32::from_ne_bytes(
1085 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1086 + .try_into()
1087 + .unwrap(),
1088 + ) as usize;
1089 + let item_start = dir_end + item_off;
1090 +
1091 + // name_off=32, name_len=5, so name region is [32..38)
1092 + // Set path_off=34 (inside name region), path_len=1
1093 + buf[item_start + 24..item_start + 28].copy_from_slice(&34u32.to_ne_bytes());
1094 + buf[item_start + 28..item_start + 32].copy_from_slice(&1u32.to_ne_bytes());
1095 + // Ensure NUL at item[34+1]=item[35]
1096 + buf[item_start + 35] = 0;
1097 +
1098 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1099 + assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1100 +}
1101 +
1102 +// -------------------------------------------------------------------
1103 +// Proptest: fuzz / property-based tests for all decode paths
1104 +// -------------------------------------------------------------------
1105 +
1106 +mod proptests {
1107 + use super::*;
1108 + use proptest::prelude::*;
1109 +
1110 + // Arbitrary bytes -- no decode path may panic on any input.
1111 +
1112 + proptest! {
1113 + #[test]
1114 + fn decode_header_never_panics(data: Vec<u8>) {
1115 + let _ = Header::decode(&data);
1116 + }
1117 +
1118 + #[test]
1119 + fn decode_chunk_header_never_panics(data: Vec<u8>) {
1120 + let _ = ChunkHeader::decode(&data);
1121 + }
1122 +
1123 + #[test]
1124 + fn decode_hello_never_panics(data: Vec<u8>) {
1125 + let _ = Hello::decode(&data);
1126 + }
1127 +
1128 + #[test]
1129 + fn decode_hello_ack_never_panics(data: Vec<u8>) {
1130 + let _ = HelloAck::decode(&data);
1131 + }
1132 +
1133 + #[test]
1134 + fn decode_cgroups_request_never_panics(data: Vec<u8>) {
1135 + let _ = CgroupsRequest::decode(&data);
1136 + }
1137 +
1138 + #[test]
1139 + fn decode_cgroups_response_never_panics(data: Vec<u8>) {
1140 + let result = CgroupsResponseView::decode(&data);
1141 + if let Ok(view) = result {
1142 + // Exercise item access on valid decodes.
1143 + let limit = view.item_count.min(64);
1144 + for i in 0..limit {
1145 + let _ = view.item(i);
1146 + }
1147 + // Out-of-bounds must not panic.
1148 + let _ = view.item(view.item_count);
1149 + }
1150 + }
1151 +
1152 + #[test]
1153 + fn decode_cgroups_lookup_request_never_panics(data: Vec<u8>) {
1154 + let result = CgroupsLookupRequestView::decode(&data);
1155 + if let Ok(view) = result {
1156 + let limit = view.item_count.min(64);
1157 + for i in 0..limit {
1158 + let _ = view.item(i);
1159 + }
1160 + let _ = view.item(view.item_count);
1161 + }
1162 + }
1163 +
1164 + #[test]
1165 + fn decode_cgroups_lookup_response_never_panics(data: Vec<u8>) {
1166 + let result = CgroupsLookupResponseView::decode(&data);
1167 + if let Ok(view) = result {
1168 + let limit = view.item_count.min(64);
1169 + for i in 0..limit {
1170 + if let Ok(item) = view.item(i) {
1171 + let label_limit = item.label_count.min(64);
1172 + for j in 0..label_limit {
1173 + let _ = item.label(j.into());
1174 + }
1175 + let _ = item.label(item.label_count.into());
1176 + }
1177 + }
1178 + let _ = view.item(view.item_count);
1179 + }
1180 + }
1181 +
1182 + #[test]
1183 + fn decode_apps_lookup_request_never_panics(data: Vec<u8>) {
1184 + let result = AppsLookupRequestView::decode(&data);
1185 + if let Ok(view) = result {
1186 + let limit = view.item_count.min(64);
1187 + for i in 0..limit {
1188 + let _ = view.item(i);
1189 + }
1190 + let _ = view.item(view.item_count);
1191 + }
1192 + }
1193 +
1194 + #[test]
1195 + fn decode_apps_lookup_response_never_panics(data: Vec<u8>) {
1196 + let result = AppsLookupResponseView::decode(&data);
1197 + if let Ok(view) = result {
1198 + let limit = view.item_count.min(64);
1199 + for i in 0..limit {
1200 + if let Ok(item) = view.item(i) {
1201 + let label_limit = item.label_count.min(64);
1202 + for j in 0..label_limit {
1203 + let _ = item.label(j.into());
1204 + }
1205 + let _ = item.label(item.label_count.into());
1206 + }
1207 + }
1208 + let _ = view.item(view.item_count);
1209 + }
1210 + }
1211 +
1212 + #[test]
1213 + fn batch_dir_decode_never_panics(
1214 + data: Vec<u8>,
1215 + item_count in 0u32..128,
1216 + packed_area_len in 0u32..65536,
1217 + ) {
1218 + let _ = batch_dir_decode(&data, item_count, packed_area_len);
1219 + }
1220 +
1221 + #[test]
1222 + fn batch_item_get_never_panics(
1223 + data: Vec<u8>,
1224 + item_count in 0u32..128,
1225 + index in 0u32..128,
1226 + ) {
1227 + let _ = batch_item_get(&data, item_count, index);
1228 + }
1229 + }
1230 +
1231 + // Roundtrip tests: encode random valid values, decode, verify match.
1232 +
1233 + proptest! {
1234 + #[test]
1235 + fn encode_decode_header_roundtrip(
1236 + kind in 1u16..=3,
1237 + flags in any::<u16>(),
1238 + code in any::<u16>(),
1239 + transport_status in any::<u16>(),
1240 + payload_len in any::<u32>(),
1241 + item_count in any::<u32>(),
1242 + message_id in any::<u64>(),
1243 + ) {
1244 + let h = Header {
1245 + magic: MAGIC_MSG,
1246 + version: VERSION,
1247 + header_len: HEADER_LEN,
1248 + kind,
1249 + flags,
1250 + code,
1251 + transport_status,
1252 + payload_len,
1253 + item_count,
1254 + message_id,
1255 + };
1256 + let mut buf = [0u8; 64];
1257 + let n = h.encode(&mut buf);
1258 + prop_assert_eq!(n, HEADER_SIZE);
1259 + let decoded = Header::decode(&buf[..n]).unwrap();
1260 + prop_assert_eq!(decoded, h);
1261 + }
1262 +
1263 + #[test]
1264 + fn encode_decode_hello_roundtrip(
1265 + supported in any::<u32>(),
1266 + preferred in any::<u32>(),
1267 + max_req_payload in any::<u32>(),
1268 + max_req_batch in any::<u32>(),
1269 + max_resp_payload in any::<u32>(),
1270 + max_resp_batch in any::<u32>(),
1271 + auth_token in any::<u64>(),
1272 + packet_size in any::<u32>(),
1273 + ) {
1274 + let h = Hello {
1275 + layout_version: 1,
1276 + flags: 0,
1277 + supported_profiles: supported,
1278 + preferred_profiles: preferred,
1279 + max_request_payload_bytes: max_req_payload,
1280 + max_request_batch_items: max_req_batch,
1281 + max_response_payload_bytes: max_resp_payload,
1282 + max_response_batch_items: max_resp_batch,
1283 + auth_token,
1284 + packet_size,
1285 + };
1286 + let mut buf = [0u8; 64];
1287 + let n = h.encode(&mut buf);
1288 + prop_assert_eq!(n, HELLO_SIZE);
1289 + let decoded = Hello::decode(&buf[..n]).unwrap();
1290 + prop_assert_eq!(decoded, h);
1291 + }
1292 + }
1293 +}
1294 +
1295 +// -----------------------------------------------------------------------
1296 +// NipcError Display coverage
1297 +// -----------------------------------------------------------------------
1298 +
1299 +#[test]
1300 +fn nipc_error_display_all_variants() {
1301 + // Exercise the Display impl for every NipcError variant (lines 101-113)
1302 + let cases: Vec<(NipcError, &str)> = vec![
1303 + (NipcError::Truncated, "buffer too short"),
1304 + (NipcError::BadMagic, "magic value mismatch"),
1305 + (NipcError::BadVersion, "unsupported version"),
1306 + (NipcError::BadHeaderLen, "header_len != 32"),
1307 + (NipcError::BadKind, "unknown message kind"),
1308 + (NipcError::BadLayout, "unknown layout_version"),
1309 + (NipcError::OutOfBounds, "offset+length exceeds data"),
1310 + (NipcError::MissingNul, "string not NUL-terminated"),
1311 + (NipcError::BadAlignment, "item not 8-byte aligned"),
1312 + (NipcError::BadItemCount, "item count inconsistent"),
1313 + (NipcError::Overflow, "builder out of space"),
1314 + ];
1315 + for (err, expected) in cases {
1316 + let msg = format!("{}", err);
1317 + assert_eq!(msg, expected, "Display for {:?}", err);
1318 + }
1319 + // Also verify std::error::Error is implemented
1320 + let err: &dyn std::error::Error = &NipcError::Truncated;
1321 + let _ = format!("{err}");
1322 +}
1323 +
1324 +// -----------------------------------------------------------------------
1325 +// ChunkHeader decode: flags != 0 and chunk_payload_len == 0
1326 +// -----------------------------------------------------------------------
1327 +
1328 +#[test]
1329 +fn chunk_decode_bad_flags() {
1330 + // Line 257: flags != 0 -> BadLayout
1331 + let c = ChunkHeader {
1332 + magic: MAGIC_CHUNK,
1333 + version: VERSION,
1334 + flags: 0x01, // non-zero flags
1335 + message_id: 1,
1336 + total_message_len: 100,
1337 + chunk_index: 0,
1338 + chunk_count: 1,
1339 + chunk_payload_len: 50,
1340 + };
1341 + let mut buf = [0u8; 32];
1342 + c.encode(&mut buf);
1343 + assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadLayout));
1344 +}
1345 +
1346 +#[test]
1347 +fn chunk_decode_zero_payload_len() {
1348 + // Line 260: chunk_payload_len == 0 -> BadLayout
1349 + let c = ChunkHeader {
1350 + magic: MAGIC_CHUNK,
1351 + version: VERSION,
1352 + flags: 0,
1353 + message_id: 1,
1354 + total_message_len: 100,
1355 + chunk_index: 0,
1356 + chunk_count: 1,
1357 + chunk_payload_len: 0,
1358 + };
1359 + let mut buf = [0u8; 32];
1360 + c.encode(&mut buf);
1361 + assert_eq!(ChunkHeader::decode(&buf), Err(NipcError::BadLayout));
1362 +}
1363 +
1364 +// -----------------------------------------------------------------------
1365 +// batch_dir_encode: buffer too small (line 282)
1366 +// -----------------------------------------------------------------------
1367 +
1368 +#[test]
1369 +fn batch_dir_encode_too_small() {
1370 + let entries = [
1371 + BatchEntry {
1372 + offset: 0,
1373 + length: 8,
1374 + },
1375 + BatchEntry {
1376 + offset: 8,
1377 + length: 8,
1378 + },
1379 + ];
1380 + let mut buf = [0u8; 12]; // needs 16, only 12
1381 + assert_eq!(batch_dir_encode(&entries, &mut buf), 0);
1382 +}
1383 +
1384 +// -----------------------------------------------------------------------
1385 +// batch_dir_validate error paths (lines 329, 336, 339)
1386 +// -----------------------------------------------------------------------
1387 +
1388 +#[test]
1389 +fn batch_dir_validate_truncated() {
1390 + let buf = [0u8; 4]; // too short for 1 entry (needs 8)
1391 + assert_eq!(batch_dir_validate(&buf, 1, 100), Err(NipcError::Truncated));
1392 +}
1393 +
1394 +#[test]
1395 +fn batch_dir_validate_bad_alignment() {
1396 + let mut buf = [0u8; 8];
1397 + buf[0..4].copy_from_slice(&3u32.to_ne_bytes()); // unaligned offset
1398 + buf[4..8].copy_from_slice(&8u32.to_ne_bytes());
1399 + assert_eq!(
1400 + batch_dir_validate(&buf, 1, 100),
1401 + Err(NipcError::BadAlignment)
1402 + );
1403 +}
1404 +
1405 +#[test]
1406 +fn batch_dir_validate_out_of_bounds() {
1407 + let mut buf = [0u8; 8];
1408 + buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
1409 + buf[4..8].copy_from_slice(&200u32.to_ne_bytes()); // exceeds packed_area_len
1410 + assert_eq!(
1411 + batch_dir_validate(&buf, 1, 100),
1412 + Err(NipcError::OutOfBounds)
1413 + );
1414 +}
1415 +
1416 +#[test]
1417 +fn batch_dir_validate_ok() {
1418 + let mut buf = [0u8; 16];
1419 + buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
1420 + buf[4..8].copy_from_slice(&8u32.to_ne_bytes());
1421 + buf[8..12].copy_from_slice(&8u32.to_ne_bytes());
1422 + buf[12..16].copy_from_slice(&8u32.to_ne_bytes());
1423 + assert!(batch_dir_validate(&buf, 2, 100).is_ok());
1424 +}
1425 +
1426 +// -----------------------------------------------------------------------
1427 +// batch_item_get: alignment check (line 375)
1428 +// -----------------------------------------------------------------------
1429 +
1430 +#[test]
1431 +fn batch_item_get_bad_alignment() {
1432 + // Manually craft a batch payload with unaligned offset
1433 + let mut buf = [0u8; 64];
1434 + // Directory: 1 entry at offset 0 of buf
1435 + buf[0..4].copy_from_slice(&3u32.to_ne_bytes()); // unaligned offset
1436 + buf[4..8].copy_from_slice(&4u32.to_ne_bytes());
1437 + assert_eq!(batch_item_get(&buf, 1, 0), Err(NipcError::BadAlignment));
1438 +}
1439 +
1440 +#[test]
1441 +fn batch_item_get_truncated_dir() {
1442 + // Payload too small to hold the directory
1443 + let buf = [0u8; 4]; // needs at least 8 for 1 item directory
1444 + assert_eq!(batch_item_get(&buf, 1, 0), Err(NipcError::Truncated));
1445 +}
1446 +
1447 +// -----------------------------------------------------------------------
1448 +// BatchBuilder::finish compaction (lines 451-456)
1449 +// -----------------------------------------------------------------------
1450 +
1451 +#[test]
1452 +fn batch_builder_compaction() {
1453 + // Reserve space for 8 items but add only 2 -- triggers compaction
1454 + let mut buf = [0u8; 1024];
1455 + let mut b = BatchBuilder::new(&mut buf, 8);
1456 +
1457 + let item1 = [1u8, 2, 3, 4, 5, 6, 7, 8];
1458 + let item2 = [10u8, 20, 30, 40];
1459 +
1460 + b.add(&item1).unwrap();
1461 + b.add(&item2).unwrap();
1462 +
1463 + // dir_end for 8 items = align8(8*8) = 64
1464 + // final_dir_aligned for 2 items = align8(2*8) = 16
1465 + // This triggers the copy_within compaction branch (line 453-456)
1466 + let (total, count) = b.finish();
1467 + assert_eq!(count, 2);
1468 + assert!(total > 0);
1469 +
1470 + // Verify the items can still be extracted correctly
1471 + let (data, len) = batch_item_get(&buf[..total], 2, 0).unwrap();
1472 + assert_eq!(len as usize, item1.len());
1473 + assert_eq!(data, &item1);
1474 +
1475 + let (data, len) = batch_item_get(&buf[..total], 2, 1).unwrap();
1476 + assert_eq!(len as usize, item2.len());
1477 + assert_eq!(data, &item2);
1478 +}
1479 +
1480 +// -----------------------------------------------------------------------
1481 +// HelloAck decode: flags != 0 (line 606)
1482 +// -----------------------------------------------------------------------
1483 +
1484 +#[test]
1485 +fn hello_ack_decode_bad_flags() {
1486 + let h = HelloAck {
1487 + layout_version: 1,
1488 + flags: 1, // non-zero flags
1489 + ..Default::default()
1490 + };
1491 + let mut buf = [0u8; 48];
1492 + h.encode(&mut buf);
1493 + assert_eq!(HelloAck::decode(&buf), Err(NipcError::BadLayout));
1494 +}
1495 +
1496 +// -----------------------------------------------------------------------
1497 +// Hello decode: non-zero padding (line 527)
1498 +// -----------------------------------------------------------------------
1499 +
1500 +#[test]
1501 +fn hello_decode_bad_padding() {
1502 + let h = Hello {
1503 + layout_version: 1,
1504 + flags: 0,
1505 + ..Default::default()
1506 + };
1507 + let mut buf = [0u8; 44];
1508 + h.encode(&mut buf);
1509 + // Corrupt the padding bytes at 28..32
1510 + buf[28..32].copy_from_slice(&1u32.to_ne_bytes());
1511 + assert_eq!(Hello::decode(&buf), Err(NipcError::BadLayout));
1512 +}
1513 +
1514 +// -----------------------------------------------------------------------
1515 +// Cgroups request: non-zero flags (line 45)
1516 +// -----------------------------------------------------------------------
1517 +
1518 +#[test]
1519 +fn cgroups_req_decode_bad_flags() {
1520 + let r = CgroupsRequest {
1521 + layout_version: 1,
1522 + flags: 1, // non-zero flags -> BadLayout
1523 + };
1524 + let mut buf = [0u8; 4];
1525 + r.encode(&mut buf);
1526 + assert_eq!(CgroupsRequest::decode(&buf), Err(NipcError::BadLayout));
1527 +}
1528 +
1529 +// -----------------------------------------------------------------------
1530 +// CgroupsResponseView: non-zero flags and reserved (lines 125, 130)
1531 +// -----------------------------------------------------------------------
1532 +
1533 +#[test]
1534 +fn cgroups_resp_decode_bad_flags() {
1535 + // Line 125: flags != 0 -> BadLayout
1536 + let mut buf = [0u8; 24];
1537 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version = 1
1538 + buf[2..4].copy_from_slice(&1u16.to_ne_bytes()); // flags = 1 (non-zero)
1539 + assert_eq!(
1540 + CgroupsResponseView::decode(&buf).unwrap_err(),
1541 + NipcError::BadLayout
1542 + );
1543 +}
1544 +
1545 +#[test]
1546 +fn cgroups_resp_decode_bad_reserved() {
1547 + // Line 130: reserved != 0 -> BadLayout
1548 + let mut buf = [0u8; 24];
1549 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version = 1
1550 + buf[2..4].copy_from_slice(&0u16.to_ne_bytes()); // flags = 0
1551 + buf[12..16].copy_from_slice(&1u32.to_ne_bytes()); // reserved = 1
1552 + assert_eq!(
1553 + CgroupsResponseView::decode(&buf).unwrap_err(),
1554 + NipcError::BadLayout
1555 + );
1556 +}
1557 +
1558 +// -----------------------------------------------------------------------
1559 +// CgroupsResponseView: bad alignment in directory (line 149)
1560 +// -----------------------------------------------------------------------
1561 +
1562 +#[test]
1563 +fn cgroups_resp_decode_bad_dir_alignment() {
1564 + // dir entry with offset not aligned to 8
1565 + let mut buf = [0u8; 128];
1566 + buf[0..2].copy_from_slice(&1u16.to_ne_bytes()); // layout_version
1567 + buf[4..8].copy_from_slice(&1u32.to_ne_bytes()); // item_count = 1
1568 + // Dir entry at offset 24: offset=3 (unaligned), length=32
1569 + buf[24..28].copy_from_slice(&3u32.to_ne_bytes());
1570 + buf[28..32].copy_from_slice(&32u32.to_ne_bytes());
1571 + assert_eq!(
1572 + CgroupsResponseView::decode(&buf).unwrap_err(),
1573 + NipcError::BadAlignment
1574 + );
1575 +}
1576 +
1577 +// -----------------------------------------------------------------------
1578 +// CgroupsItemView: bad layout_version, bad flags (lines 200-206)
1579 +// -----------------------------------------------------------------------
1580 +
1581 +#[test]
1582 +fn cgroups_item_bad_layout_version() {
1583 + // Build valid snapshot then corrupt item layout_version
1584 + let mut buf = [0u8; 4096];
1585 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1586 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1587 + let total = b.finish();
1588 +
1589 + // Find item start
1590 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1591 + let item_off = u32::from_ne_bytes(
1592 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1593 + .try_into()
1594 + .unwrap(),
1595 + ) as usize;
1596 + let item_start = dir_end + item_off;
1597 +
1598 + // Corrupt layout_version to 99
1599 + buf[item_start..item_start + 2].copy_from_slice(&99u16.to_ne_bytes());
1600 +
1601 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1602 + assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1603 +}
1604 +
1605 +#[test]
1606 +fn cgroups_item_bad_flags() {
1607 + // Build valid snapshot then corrupt item flags
1608 + let mut buf = [0u8; 4096];
1609 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1610 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1611 + let total = b.finish();
1612 +
1613 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1614 + let item_off = u32::from_ne_bytes(
1615 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1616 + .try_into()
1617 + .unwrap(),
1618 + ) as usize;
1619 + let item_start = dir_end + item_off;
1620 +
1621 + // Corrupt flags to non-zero
1622 + buf[item_start + 2..item_start + 4].copy_from_slice(&1u16.to_ne_bytes());
1623 +
1624 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1625 + assert_eq!(view.item(0).unwrap_err(), NipcError::BadLayout);
1626 +}
1627 +
1628 +// -----------------------------------------------------------------------
1629 +// CgroupsItemView: name_off < ITEM_HDR_SIZE (line 211)
1630 +// -----------------------------------------------------------------------
1631 +
1632 +#[test]
1633 +fn cgroups_item_name_off_too_small() {
1634 + let mut buf = [0u8; 4096];
1635 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1636 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1637 + let total = b.finish();
1638 +
1639 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1640 + let item_off = u32::from_ne_bytes(
1641 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1642 + .try_into()
1643 + .unwrap(),
1644 + ) as usize;
1645 + let item_start = dir_end + item_off;
1646 +
1647 + // Set name_offset to 0 (< 32 = CGROUPS_ITEM_HDR_SIZE)
1648 + buf[item_start + 16..item_start + 20].copy_from_slice(&0u32.to_ne_bytes());
1649 +
1650 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1651 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1652 +}
1653 +
1654 +// -----------------------------------------------------------------------
1655 +// CgroupsItemView: path NUL missing (line 228)
1656 +// -----------------------------------------------------------------------
1657 +
1658 +#[test]
1659 +fn cgroups_item_path_missing_nul() {
1660 + let mut buf = [0u8; 4096];
1661 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1662 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1663 + let total = b.finish();
1664 +
1665 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1666 + let item_off = u32::from_ne_bytes(
1667 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1668 + .try_into()
1669 + .unwrap(),
1670 + ) as usize;
1671 + let item_start = dir_end + item_off;
1672 +
1673 + // Find the path NUL terminator and corrupt it
1674 + let path_off =
1675 + u32::from_ne_bytes(buf[item_start + 24..item_start + 28].try_into().unwrap()) as usize;
1676 + let path_len =
1677 + u32::from_ne_bytes(buf[item_start + 28..item_start + 32].try_into().unwrap()) as usize;
1678 + buf[item_start + path_off + path_len] = b'X'; // corrupt path NUL
1679 +
1680 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1681 + assert_eq!(view.item(0).unwrap_err(), NipcError::MissingNul);
1682 +}
1683 +
1684 +// -----------------------------------------------------------------------
1685 +// CgroupsItemView: path_off < ITEM_HDR_SIZE (line 222)
1686 +// -----------------------------------------------------------------------
1687 +
1688 +#[test]
1689 +fn cgroups_item_path_off_too_small() {
1690 + let mut buf = [0u8; 4096];
1691 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1692 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1693 + let total = b.finish();
1694 +
1695 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1696 + let item_off = u32::from_ne_bytes(
1697 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1698 + .try_into()
1699 + .unwrap(),
1700 + ) as usize;
1701 + let item_start = dir_end + item_off;
1702 +
1703 + // Set path_offset to 0 (< 32 = CGROUPS_ITEM_HDR_SIZE)
1704 + buf[item_start + 24..item_start + 28].copy_from_slice(&0u32.to_ne_bytes());
1705 +
1706 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1707 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1708 +}
1709 +
1710 +// -----------------------------------------------------------------------
1711 +// CgroupsItemView: path string OOB (line 225)
1712 +// -----------------------------------------------------------------------
1713 +
1714 +#[test]
1715 +fn cgroups_item_path_string_oob() {
1716 + let mut buf = [0u8; 4096];
1717 + let mut b = CgroupsBuilder::new(&mut buf, 1, 0, 1);
1718 + b.add(1, 0, 1, b"test", b"/test").unwrap();
1719 + let total = b.finish();
1720 +
1721 + let dir_end = CGROUPS_RESP_HDR_SIZE + 1 * CGROUPS_DIR_ENTRY_SIZE;
1722 + let item_off = u32::from_ne_bytes(
1723 + buf[CGROUPS_RESP_HDR_SIZE..CGROUPS_RESP_HDR_SIZE + 4]
1724 + .try_into()
1725 + .unwrap(),
1726 + ) as usize;
1727 + let item_start = dir_end + item_off;
1728 +
1729 + // Corrupt path_length to huge value
1730 + buf[item_start + 28..item_start + 32].copy_from_slice(&99999u32.to_ne_bytes());
1731 +
1732 + let view = CgroupsResponseView::decode(&buf[..total]).unwrap();
1733 + assert_eq!(view.item(0).unwrap_err(), NipcError::OutOfBounds);
1734 +}
1735 +
1736 +// -----------------------------------------------------------------------
1737 +// Cgroups dispatch (lines 438-447)
1738 +// -----------------------------------------------------------------------
1739 +
1740 +#[test]
1741 +fn dispatch_cgroups_snapshot_bad_request() {
1742 + // Bad request (too short) -> dispatch returns None (line 438)
1743 + let mut resp = [0u8; 4096];
1744 + let result =
1745 + crate::protocol::dispatch_cgroups_snapshot(&[], &mut resp, 1, |_req, _builder| true);
1746 + assert!(result.is_none());
1747 +}
1748 +
1749 +#[test]
1750 +fn dispatch_cgroups_snapshot_handler_returns_false() {
1751 + // Handler returns false -> dispatch returns None (lines 440-441)
1752 + let req = CgroupsRequest {
1753 + layout_version: 1,
1754 + flags: 0,
1755 + };
1756 + let mut req_buf = [0u8; 4];
1757 + req.encode(&mut req_buf);
1758 + let mut resp = [0u8; 4096];
1759 + let result =
1760 + crate::protocol::dispatch_cgroups_snapshot(&req_buf, &mut resp, 1, |_req, _builder| false);
1761 + assert!(result.is_none());
1762 +}
1763 +
1764 +#[test]
1765 +fn dispatch_cgroups_snapshot_success() {
1766 + let req = CgroupsRequest {
1767 + layout_version: 1,
1768 + flags: 0,
1769 + };
1770 + let mut req_buf = [0u8; 4];
1771 + req.encode(&mut req_buf);
1772 + let mut resp = [0u8; 4096];
1773 + let result =
1774 + crate::protocol::dispatch_cgroups_snapshot(&req_buf, &mut resp, 2, |_req, builder| {
1775 + builder.add(1, 0, 1, b"cg1", b"/test").unwrap();
1776 + true
1777 + });
1778 + assert!(result.is_some());
1779 + let n = result.unwrap();
1780 + let view = CgroupsResponseView::decode(&resp[..n]).unwrap();
1781 + assert_eq!(view.item_count, 1);
1782 +}
1783 +
1784 +// -----------------------------------------------------------------------
1785 +// Dispatch increment / string_reverse buf overflow (lines 27, 58)
1786 +// -----------------------------------------------------------------------
1787 +
1788 +#[test]
1789 +fn dispatch_increment_resp_too_small() {
1790 + // Response buffer too small -> encode returns 0 -> dispatch returns None
1791 + let req_val = 42u64;
1792 + let mut req_buf = [0u8; 8];
1793 + crate::protocol::increment_encode(req_val, &mut req_buf);
1794 + let mut resp = [0u8; 4]; // too small for 8-byte response
1795 + let result = crate::protocol::dispatch_increment(&req_buf, &mut resp, |v| Some(v + 1));
1796 + assert!(result.is_none());
1797 +}
1798 +
1799 +#[test]
1800 +fn dispatch_increment_handler_none() {
1801 + let mut req_buf = [0u8; 8];
1802 + crate::protocol::increment_encode(42, &mut req_buf);
1803 + let mut resp = [0u8; 8];
1804 + let result = crate::protocol::dispatch_increment(&req_buf, &mut resp, |_| None);
1805 + assert!(result.is_none());
1806 +}
1807 +
1808 +#[test]
1809 +fn dispatch_string_reverse_resp_too_small() {
1810 + let s = b"hello";
1811 + let mut req_buf = [0u8; 64];
1812 + crate::protocol::string_reverse_encode(s, &mut req_buf);
1813 + let mut resp = [0u8; 4]; // too small
1814 + let result = crate::protocol::dispatch_string_reverse(&req_buf, &mut resp, |data| {
1815 + Some(data.iter().rev().copied().collect())
1816 + });
1817 + assert!(result.is_none());
1818 +}
1819 +
1820 +#[test]
1821 +fn dispatch_string_reverse_handler_none() {
1822 + let s = b"hello";
1823 + let mut req_buf = [0u8; 64];
1824 + crate::protocol::string_reverse_encode(s, &mut req_buf);
1825 + let mut resp = [0u8; 64];
1826 + let result = crate::protocol::dispatch_string_reverse(&req_buf, &mut resp, |_| None);
1827 + assert!(result.is_none());
1828 +}
src/crates/netipc/src/service/apps_lookup.rs new
+277
@@ -0,0 +1,277 @@
1 +//! L2 apps-lookup service facade.
2 +
3 +use super::raw;
4 +use crate::protocol::{AppsLookupResponseView, NipcError, METHOD_APPS_LOOKUP, PROFILE_BASELINE};
5 +
6 +#[cfg(unix)]
7 +use crate::transport::posix::{
8 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
9 +};
10 +
11 +#[cfg(windows)]
12 +use crate::transport::windows::{
13 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
14 +};
15 +
16 +use std::sync::atomic::AtomicBool;
17 +use std::sync::Arc;
18 +
19 +pub use raw::{AppsLookupHandler, ClientState, ClientStatus};
20 +
21 +#[derive(Debug, Clone)]
22 +pub struct ClientConfig {
23 + pub supported_profiles: u32,
24 + pub preferred_profiles: u32,
25 + pub max_request_batch_items: u32,
26 + pub max_response_payload_bytes: u32,
27 + pub auth_token: u64,
28 +}
29 +
30 +impl Default for ClientConfig {
31 + fn default() -> Self {
32 + Self {
33 + supported_profiles: PROFILE_BASELINE,
34 + preferred_profiles: 0,
35 + max_request_batch_items: 0,
36 + max_response_payload_bytes: 0,
37 + auth_token: 0,
38 + }
39 + }
40 +}
41 +
42 +impl ClientConfig {
43 + fn into_transport(self) -> TransportClientConfig {
44 + let mut transport = TransportClientConfig::default();
45 + transport.supported_profiles = self.supported_profiles;
46 + transport.preferred_profiles = self.preferred_profiles;
47 + transport.max_request_batch_items = self.max_request_batch_items;
48 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
49 + transport.max_response_batch_items = self.max_request_batch_items;
50 + transport.auth_token = self.auth_token;
51 + transport
52 + }
53 +}
54 +
55 +#[derive(Debug, Clone)]
56 +pub struct ServerConfig {
57 + pub supported_profiles: u32,
58 + pub preferred_profiles: u32,
59 + pub max_request_batch_items: u32,
60 + pub max_response_payload_bytes: u32,
61 + pub auth_token: u64,
62 +}
63 +
64 +impl Default for ServerConfig {
65 + fn default() -> Self {
66 + Self {
67 + supported_profiles: PROFILE_BASELINE,
68 + preferred_profiles: 0,
69 + max_request_batch_items: 0,
70 + max_response_payload_bytes: 0,
71 + auth_token: 0,
72 + }
73 + }
74 +}
75 +
76 +impl ServerConfig {
77 + fn into_transport(self) -> TransportServerConfig {
78 + let mut transport = TransportServerConfig::default();
79 + transport.supported_profiles = self.supported_profiles;
80 + transport.preferred_profiles = self.preferred_profiles;
81 + transport.max_request_batch_items = self.max_request_batch_items;
82 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
83 + transport.max_response_batch_items = self.max_request_batch_items;
84 + transport.auth_token = self.auth_token;
85 + transport
86 + }
87 +}
88 +
89 +pub struct AppsLookupClient {
90 + inner: raw::RawClient,
91 +}
92 +
93 +impl AppsLookupClient {
94 + pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
95 + Self {
96 + inner: raw::RawClient::new_apps_lookup(run_dir, service_name, config.into_transport()),
97 + }
98 + }
99 +
100 + pub fn refresh(&mut self) -> bool {
101 + self.inner.refresh()
102 + }
103 +
104 + pub fn ready(&self) -> bool {
105 + self.inner.ready()
106 + }
107 +
108 + pub fn status(&self) -> ClientStatus {
109 + self.inner.status()
110 + }
111 +
112 + pub fn call(&mut self, pids: &[u32]) -> Result<AppsLookupResponseView<'_>, NipcError> {
113 + self.inner.call_apps_lookup(pids)
114 + }
115 +
116 + pub fn close(&mut self) {
117 + self.inner.close();
118 + }
119 +}
120 +
121 +impl Drop for AppsLookupClient {
122 + fn drop(&mut self) {
123 + self.close();
124 + }
125 +}
126 +
127 +#[derive(Clone, Default)]
128 +pub struct Handler {
129 + pub handle: Option<AppsLookupHandler>,
130 +}
131 +
132 +pub struct ManagedServer {
133 + inner: raw::ManagedServer,
134 +}
135 +
136 +impl ManagedServer {
137 + pub fn new(run_dir: &str, service_name: &str, config: ServerConfig, handler: Handler) -> Self {
138 + Self::with_workers(run_dir, service_name, config, handler, 8)
139 + }
140 +
141 + pub fn with_workers(
142 + run_dir: &str,
143 + service_name: &str,
144 + config: ServerConfig,
145 + handler: Handler,
146 + worker_count: usize,
147 + ) -> Self {
148 + let raw_handler = handler.handle.map(raw::apps_lookup_dispatch);
149 + Self {
150 + inner: raw::ManagedServer::with_workers(
151 + run_dir,
152 + service_name,
153 + config.into_transport(),
154 + METHOD_APPS_LOOKUP,
155 + raw_handler,
156 + worker_count,
157 + ),
158 + }
159 + }
160 +
161 + pub fn run(&mut self) -> Result<(), NipcError> {
162 + self.inner.run()
163 + }
164 +
165 + pub fn stop(&self) {
166 + self.inner.stop();
167 + }
168 +
169 + pub fn running_flag(&self) -> Arc<AtomicBool> {
170 + self.inner.running_flag()
171 + }
172 +}
173 +
174 +#[cfg(test)]
175 +mod tests {
176 + use super::*;
177 + use crate::protocol::PROFILE_SHM_FUTEX;
178 + use std::sync::atomic::Ordering;
179 + use std::sync::Arc;
180 +
181 + #[test]
182 + fn client_config_maps_to_transport() {
183 + let cfg = ClientConfig {
184 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
185 + preferred_profiles: PROFILE_SHM_FUTEX,
186 + max_request_batch_items: 17,
187 + max_response_payload_bytes: 8192,
188 + auth_token: 99,
189 + };
190 +
191 + let transport = cfg.into_transport();
192 + assert_eq!(
193 + transport.supported_profiles,
194 + PROFILE_BASELINE | PROFILE_SHM_FUTEX
195 + );
196 + assert_eq!(transport.preferred_profiles, PROFILE_SHM_FUTEX);
197 + assert_eq!(transport.max_request_batch_items, 17);
198 + assert_eq!(transport.max_response_batch_items, 17);
199 + assert_eq!(transport.max_response_payload_bytes, 8192);
200 + assert_eq!(transport.auth_token, 99);
201 + }
202 +
203 + #[test]
204 + fn server_config_maps_to_transport() {
205 + let cfg = ServerConfig {
206 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
207 + preferred_profiles: PROFILE_SHM_FUTEX,
208 + max_request_batch_items: 23,
209 + max_response_payload_bytes: 16384,
210 + auth_token: 123,
211 + };
212 +
213 + let transport = cfg.into_transport();
214 + assert_eq!(
215 + transport.supported_profiles,
216 + PROFILE_BASELINE | PROFILE_SHM_FUTEX
217 + );
218 + assert_eq!(transport.preferred_profiles, PROFILE_SHM_FUTEX);
219 + assert_eq!(transport.max_request_batch_items, 23);
220 + assert_eq!(transport.max_response_batch_items, 23);
221 + assert_eq!(transport.max_response_payload_bytes, 16384);
222 + assert_eq!(transport.auth_token, 123);
223 + }
224 +
225 + #[test]
226 + fn managed_server_initializes_stopped_with_handler() {
227 + let handler = Handler {
228 + handle: Some(Arc::new(|_, _| true)),
229 + };
230 + let server = ManagedServer::with_workers(
231 + "/tmp/netipc-apps-lookup-test",
232 + "apps-lookup-facade-test",
233 + ServerConfig::default(),
234 + handler,
235 + 0,
236 + );
237 + let running = server.running_flag();
238 +
239 + assert!(!running.load(Ordering::SeqCst));
240 + server.stop();
241 + assert!(!running.load(Ordering::SeqCst));
242 + }
243 +
244 + #[cfg(windows)]
245 + #[test]
246 + fn client_lifecycle_without_server_windows() {
247 + let mut client = AppsLookupClient::new(
248 + r"C:\Temp\nipc-apps-lookup-facade",
249 + "apps-lookup-facade-no-server",
250 + ClientConfig::default(),
251 + );
252 + assert!(!client.ready());
253 + assert!(client.refresh());
254 + assert_eq!(client.status().state, ClientState::NotFound);
255 +
256 + assert_eq!(client.call(&[123]).unwrap_err(), NipcError::BadLayout);
257 + assert_eq!(client.status().error_count, 1);
258 +
259 + client.close();
260 + assert_eq!(client.status().state, ClientState::Disconnected);
261 + }
262 +
263 + #[cfg(windows)]
264 + #[test]
265 + fn managed_server_new_initializes_stopped_windows() {
266 + let server = ManagedServer::new(
267 + r"C:\Temp\nipc-apps-lookup-facade",
268 + "apps-lookup-facade-new",
269 + ServerConfig::default(),
270 + Handler::default(),
271 + );
272 + let running = server.running_flag();
273 + assert!(!running.load(Ordering::SeqCst));
274 + server.stop();
275 + assert!(!running.load(Ordering::SeqCst));
276 + }
277 +}
src/crates/netipc/src/service/cgroups.rs
+3 -262
@@ -1,264 +1,5 @@
1 -//! L2 cgroups-snapshot service facade.
1 +//! Compatibility module for the historical cgroups-snapshot service path.
2 //!
3 -//! The public service surface is service-kind specific: one endpoint, one
4 -//! request kind. The request code remains in the outer envelope only for
5 -//! validation, not for public multi-method dispatch.
3 +//! New code should prefer [`crate::service::cgroups_snapshot`].
4
7 -use super::raw;
8 -use crate::protocol::{CgroupsResponseView, NipcError, METHOD_CGROUPS_SNAPSHOT, PROFILE_BASELINE};
9 -
10 -#[cfg(unix)]
11 -use crate::transport::posix::{
12 - ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
13 -};
14 -
15 -#[cfg(windows)]
16 -use crate::transport::windows::{
17 - ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
18 -};
19 -
20 -use std::sync::atomic::AtomicBool;
21 -use std::sync::Arc;
22 -
23 -pub use raw::{CgroupsCacheItem, CgroupsCacheStatus, ClientState, ClientStatus, SnapshotHandler};
24 -
25 -/// Public L2/L3 client configuration for the cgroups-snapshot service.
26 -///
27 -/// This service-level configuration is shared across supported operating
28 -/// systems. Transport-only tuning stays below the public typed API.
29 -#[derive(Debug, Clone)]
30 -pub struct ClientConfig {
31 - pub supported_profiles: u32,
32 - pub preferred_profiles: u32,
33 - pub max_request_batch_items: u32,
34 - pub max_response_payload_bytes: u32,
35 - pub auth_token: u64,
36 -}
37 -
38 -impl Default for ClientConfig {
39 - fn default() -> Self {
40 - Self {
41 - supported_profiles: PROFILE_BASELINE,
42 - preferred_profiles: 0,
43 - max_request_batch_items: 0,
44 - max_response_payload_bytes: 0,
45 - auth_token: 0,
46 - }
47 - }
48 -}
49 -
50 -impl ClientConfig {
51 - fn into_transport(self) -> TransportClientConfig {
52 - let mut transport = TransportClientConfig::default();
53 - transport.supported_profiles = self.supported_profiles;
54 - transport.preferred_profiles = self.preferred_profiles;
55 - transport.max_request_batch_items = self.max_request_batch_items;
56 - transport.max_response_payload_bytes = self.max_response_payload_bytes;
57 - transport.max_response_batch_items = self.max_request_batch_items;
58 - transport.auth_token = self.auth_token;
59 - transport
60 - }
61 -}
62 -
63 -/// Public typed-server configuration for the cgroups-snapshot service.
64 -///
65 -/// This configuration is intentionally transport-agnostic. Transport-only
66 -/// knobs such as socket backlog or packet sizing stay below the service layer.
67 -#[derive(Debug, Clone)]
68 -pub struct ServerConfig {
69 - pub supported_profiles: u32,
70 - pub preferred_profiles: u32,
71 - pub max_request_batch_items: u32,
72 - pub max_response_payload_bytes: u32,
73 - pub auth_token: u64,
74 -}
75 -
76 -impl Default for ServerConfig {
77 - fn default() -> Self {
78 - Self {
79 - supported_profiles: PROFILE_BASELINE,
80 - preferred_profiles: 0,
81 - max_request_batch_items: 0,
82 - max_response_payload_bytes: 0,
83 - auth_token: 0,
84 - }
85 - }
86 -}
87 -
88 -impl ServerConfig {
89 - fn into_transport(self) -> TransportServerConfig {
90 - let mut transport = TransportServerConfig::default();
91 - transport.supported_profiles = self.supported_profiles;
92 - transport.preferred_profiles = self.preferred_profiles;
93 - transport.max_request_batch_items = self.max_request_batch_items;
94 - transport.max_response_payload_bytes = self.max_response_payload_bytes;
95 - transport.max_response_batch_items = self.max_request_batch_items;
96 - transport.auth_token = self.auth_token;
97 - transport
98 - }
99 -}
100 -
101 -/// L2 client context for the cgroups-snapshot service.
102 -pub struct CgroupsClient {
103 - inner: raw::RawClient,
104 -}
105 -
106 -impl CgroupsClient {
107 - /// Create a new client context. Does NOT connect. Does NOT require the
108 - /// server to be running.
109 - pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
110 - Self {
111 - inner: raw::RawClient::new_snapshot(run_dir, service_name, config.into_transport()),
112 - }
113 - }
114 -
115 - /// Attempt connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
116 - /// Returns true if the state changed.
117 - pub fn refresh(&mut self) -> bool {
118 - self.inner.refresh()
119 - }
120 -
121 - /// Cheap cached boolean. No I/O, no syscalls.
122 - #[inline]
123 - pub fn ready(&self) -> bool {
124 - self.inner.ready()
125 - }
126 -
127 - /// Detailed status snapshot for diagnostics.
128 - pub fn status(&self) -> ClientStatus {
129 - self.inner.status()
130 - }
131 -
132 - /// Blocking typed call for the cgroups-snapshot service.
133 - pub fn call_snapshot(&mut self) -> Result<CgroupsResponseView<'_>, NipcError> {
134 - self.inner.call_snapshot()
135 - }
136 -
137 - /// Tear down connection and release resources.
138 - pub fn close(&mut self) {
139 - self.inner.close();
140 - }
141 -}
142 -
143 -impl Drop for CgroupsClient {
144 - fn drop(&mut self) {
145 - self.close();
146 - }
147 -}
148 -
149 -/// Typed server handler surface for the cgroups-snapshot service.
150 -#[derive(Clone, Default)]
151 -pub struct Handler {
152 - pub handle: Option<SnapshotHandler>,
153 - pub snapshot_max_items: u32,
154 -}
155 -
156 -/// Managed server for the cgroups-snapshot service kind.
157 -pub struct ManagedServer {
158 - inner: raw::ManagedServer,
159 -}
160 -
161 -impl ManagedServer {
162 - /// Create a new managed server. Does NOT start listening yet.
163 - pub fn new(run_dir: &str, service_name: &str, config: ServerConfig, handler: Handler) -> Self {
164 - Self::with_workers(run_dir, service_name, config, handler, 8)
165 - }
166 -
167 - /// Create a server with an explicit worker count limit.
168 - pub fn with_workers(
169 - run_dir: &str,
170 - service_name: &str,
171 - config: ServerConfig,
172 - handler: Handler,
173 - worker_count: usize,
174 - ) -> Self {
175 - let raw_handler = handler
176 - .handle
177 - .map(|handle| raw::snapshot_dispatch(handle, handler.snapshot_max_items));
178 -
179 - Self {
180 - inner: raw::ManagedServer::with_workers(
181 - run_dir,
182 - service_name,
183 - config.into_transport(),
184 - METHOD_CGROUPS_SNAPSHOT,
185 - raw_handler,
186 - worker_count,
187 - ),
188 - }
189 - }
190 -
191 - /// Run the acceptor loop. Blocking. Returns when `stop()` is called or on
192 - /// fatal error.
193 - pub fn run(&mut self) -> Result<(), NipcError> {
194 - self.inner.run()
195 - }
196 -
197 - /// Signal shutdown.
198 - pub fn stop(&self) {
199 - self.inner.stop();
200 - }
201 -
202 - /// Clone of the internal running flag for diagnostics and test helpers.
203 - ///
204 - /// For reliable shutdown, call `stop()`. On Windows, changing this flag
205 - /// alone does not wake a blocking listener accept.
206 - pub fn running_flag(&self) -> Arc<AtomicBool> {
207 - self.inner.running_flag()
208 - }
209 -}
210 -
211 -/// L3 client-side cgroups snapshot cache.
212 -pub struct CgroupsCache {
213 - inner: raw::CgroupsCache,
214 -}
215 -
216 -impl CgroupsCache {
217 - /// Create a new L3 cache. Creates the underlying L2 client context.
218 - /// Does NOT connect. Does NOT require the server to be running.
219 - pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
220 - Self {
221 - inner: raw::CgroupsCache::new(run_dir, service_name, config.into_transport()),
222 - }
223 - }
224 -
225 - /// Refresh the cache. Returns true if the cache was updated.
226 - pub fn refresh(&mut self) -> bool {
227 - self.inner.refresh()
228 - }
229 -
230 - /// Returns true if at least one successful refresh has occurred.
231 - #[inline]
232 - pub fn ready(&self) -> bool {
233 - self.inner.ready()
234 - }
235 -
236 - /// Look up a cached item by hash + name. O(1), no I/O.
237 - pub fn lookup(&self, hash: u32, name: &str) -> Option<&CgroupsCacheItem> {
238 - self.inner.lookup(hash, name)
239 - }
240 -
241 - /// Fill a status snapshot for diagnostics.
242 - pub fn status(&self) -> CgroupsCacheStatus {
243 - self.inner.status()
244 - }
245 -
246 - /// Close the cache and underlying L2 client.
247 - pub fn close(&mut self) {
248 - self.inner.close();
249 - }
250 -}
251 -
252 -impl Drop for CgroupsCache {
253 - fn drop(&mut self) {
254 - self.close();
255 - }
256 -}
257 -
258 -#[cfg(all(test, unix))]
259 -#[path = "cgroups_unix_tests.rs"]
260 -mod tests;
261 -
262 -#[cfg(all(test, windows))]
263 -#[path = "cgroups_windows_tests.rs"]
264 -mod windows_tests;
5 +pub use super::cgroups_snapshot::*;
src/crates/netipc/src/service/cgroups_lookup.rs new
+284
@@ -0,0 +1,284 @@
1 +//! L2 cgroups-lookup service facade.
2 +
3 +use super::raw;
4 +use crate::protocol::{
5 + CgroupsLookupResponseView, NipcError, METHOD_CGROUPS_LOOKUP, PROFILE_BASELINE,
6 +};
7 +
8 +#[cfg(unix)]
9 +use crate::transport::posix::{
10 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
11 +};
12 +
13 +#[cfg(windows)]
14 +use crate::transport::windows::{
15 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
16 +};
17 +
18 +use std::sync::atomic::AtomicBool;
19 +use std::sync::Arc;
20 +
21 +pub use raw::{CgroupsLookupHandler, ClientState, ClientStatus};
22 +
23 +#[derive(Debug, Clone)]
24 +pub struct ClientConfig {
25 + pub supported_profiles: u32,
26 + pub preferred_profiles: u32,
27 + pub max_request_batch_items: u32,
28 + pub max_response_payload_bytes: u32,
29 + pub auth_token: u64,
30 +}
31 +
32 +impl Default for ClientConfig {
33 + fn default() -> Self {
34 + Self {
35 + supported_profiles: PROFILE_BASELINE,
36 + preferred_profiles: 0,
37 + max_request_batch_items: 0,
38 + max_response_payload_bytes: 0,
39 + auth_token: 0,
40 + }
41 + }
42 +}
43 +
44 +impl ClientConfig {
45 + fn into_transport(self) -> TransportClientConfig {
46 + let mut transport = TransportClientConfig::default();
47 + transport.supported_profiles = self.supported_profiles;
48 + transport.preferred_profiles = self.preferred_profiles;
49 + transport.max_request_batch_items = self.max_request_batch_items;
50 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
51 + transport.max_response_batch_items = self.max_request_batch_items;
52 + transport.auth_token = self.auth_token;
53 + transport
54 + }
55 +}
56 +
57 +#[derive(Debug, Clone)]
58 +pub struct ServerConfig {
59 + pub supported_profiles: u32,
60 + pub preferred_profiles: u32,
61 + pub max_request_batch_items: u32,
62 + pub max_response_payload_bytes: u32,
63 + pub auth_token: u64,
64 +}
65 +
66 +impl Default for ServerConfig {
67 + fn default() -> Self {
68 + Self {
69 + supported_profiles: PROFILE_BASELINE,
70 + preferred_profiles: 0,
71 + max_request_batch_items: 0,
72 + max_response_payload_bytes: 0,
73 + auth_token: 0,
74 + }
75 + }
76 +}
77 +
78 +impl ServerConfig {
79 + fn into_transport(self) -> TransportServerConfig {
80 + let mut transport = TransportServerConfig::default();
81 + transport.supported_profiles = self.supported_profiles;
82 + transport.preferred_profiles = self.preferred_profiles;
83 + transport.max_request_batch_items = self.max_request_batch_items;
84 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
85 + transport.max_response_batch_items = self.max_request_batch_items;
86 + transport.auth_token = self.auth_token;
87 + transport
88 + }
89 +}
90 +
91 +pub struct CgroupsLookupClient {
92 + inner: raw::RawClient,
93 +}
94 +
95 +impl CgroupsLookupClient {
96 + pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
97 + Self {
98 + inner: raw::RawClient::new_cgroups_lookup(
99 + run_dir,
100 + service_name,
101 + config.into_transport(),
102 + ),
103 + }
104 + }
105 +
106 + pub fn refresh(&mut self) -> bool {
107 + self.inner.refresh()
108 + }
109 +
110 + pub fn ready(&self) -> bool {
111 + self.inner.ready()
112 + }
113 +
114 + pub fn status(&self) -> ClientStatus {
115 + self.inner.status()
116 + }
117 +
118 + pub fn call(&mut self, paths: &[&[u8]]) -> Result<CgroupsLookupResponseView<'_>, NipcError> {
119 + self.inner.call_cgroups_lookup(paths)
120 + }
121 +
122 + pub fn close(&mut self) {
123 + self.inner.close();
124 + }
125 +}
126 +
127 +impl Drop for CgroupsLookupClient {
128 + fn drop(&mut self) {
129 + self.close();
130 + }
131 +}
132 +
133 +#[derive(Clone, Default)]
134 +pub struct Handler {
135 + pub handle: Option<CgroupsLookupHandler>,
136 +}
137 +
138 +pub struct ManagedServer {
139 + inner: raw::ManagedServer,
140 +}
141 +
142 +impl ManagedServer {
143 + pub fn new(run_dir: &str, service_name: &str, config: ServerConfig, handler: Handler) -> Self {
144 + Self::with_workers(run_dir, service_name, config, handler, 8)
145 + }
146 +
147 + pub fn with_workers(
148 + run_dir: &str,
149 + service_name: &str,
150 + config: ServerConfig,
151 + handler: Handler,
152 + worker_count: usize,
153 + ) -> Self {
154 + let raw_handler = handler.handle.map(raw::cgroups_lookup_dispatch);
155 + Self {
156 + inner: raw::ManagedServer::with_workers(
157 + run_dir,
158 + service_name,
159 + config.into_transport(),
160 + METHOD_CGROUPS_LOOKUP,
161 + raw_handler,
162 + worker_count,
163 + ),
164 + }
165 + }
166 +
167 + pub fn run(&mut self) -> Result<(), NipcError> {
168 + self.inner.run()
169 + }
170 +
171 + pub fn stop(&self) {
172 + self.inner.stop();
173 + }
174 +
175 + pub fn running_flag(&self) -> Arc<AtomicBool> {
176 + self.inner.running_flag()
177 + }
178 +}
179 +
180 +#[cfg(test)]
181 +mod tests {
182 + use super::*;
183 + use crate::protocol::PROFILE_SHM_FUTEX;
184 + use std::sync::atomic::Ordering;
185 + use std::sync::Arc;
186 +
187 + #[test]
188 + fn client_config_maps_to_transport() {
189 + let cfg = ClientConfig {
190 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
191 + preferred_profiles: PROFILE_SHM_FUTEX,
192 + max_request_batch_items: 17,
193 + max_response_payload_bytes: 8192,
194 + auth_token: 99,
195 + };
196 +
197 + let transport = cfg.into_transport();
198 + assert_eq!(
199 + transport.supported_profiles,
200 + PROFILE_BASELINE | PROFILE_SHM_FUTEX
201 + );
202 + assert_eq!(transport.preferred_profiles, PROFILE_SHM_FUTEX);
203 + assert_eq!(transport.max_request_batch_items, 17);
204 + assert_eq!(transport.max_response_batch_items, 17);
205 + assert_eq!(transport.max_response_payload_bytes, 8192);
206 + assert_eq!(transport.auth_token, 99);
207 + }
208 +
209 + #[test]
210 + fn server_config_maps_to_transport() {
211 + let cfg = ServerConfig {
212 + supported_profiles: PROFILE_BASELINE | PROFILE_SHM_FUTEX,
213 + preferred_profiles: PROFILE_SHM_FUTEX,
214 + max_request_batch_items: 23,
215 + max_response_payload_bytes: 16384,
216 + auth_token: 123,
217 + };
218 +
219 + let transport = cfg.into_transport();
220 + assert_eq!(
221 + transport.supported_profiles,
222 + PROFILE_BASELINE | PROFILE_SHM_FUTEX
223 + );
224 + assert_eq!(transport.preferred_profiles, PROFILE_SHM_FUTEX);
225 + assert_eq!(transport.max_request_batch_items, 23);
226 + assert_eq!(transport.max_response_batch_items, 23);
227 + assert_eq!(transport.max_response_payload_bytes, 16384);
228 + assert_eq!(transport.auth_token, 123);
229 + }
230 +
231 + #[test]
232 + fn managed_server_initializes_stopped_with_handler() {
233 + let handler = Handler {
234 + handle: Some(Arc::new(|_, _| true)),
235 + };
236 + let server = ManagedServer::with_workers(
237 + "/tmp/netipc-cgroups-lookup-test",
238 + "cgroups-lookup-facade-test",
239 + ServerConfig::default(),
240 + handler,
241 + 0,
242 + );
243 + let running = server.running_flag();
244 +
245 + assert!(!running.load(Ordering::SeqCst));
246 + server.stop();
247 + assert!(!running.load(Ordering::SeqCst));
248 + }
249 +
250 + #[cfg(windows)]
251 + #[test]
252 + fn client_lifecycle_without_server_windows() {
253 + let mut client = CgroupsLookupClient::new(
254 + r"C:\Temp\nipc-cgroups-lookup-facade",
255 + "cgroups-lookup-facade-no-server",
256 + ClientConfig::default(),
257 + );
258 + assert!(!client.ready());
259 + assert!(client.refresh());
260 + assert_eq!(client.status().state, ClientState::NotFound);
261 +
262 + let paths: [&[u8]; 1] = [b"/x".as_slice()];
263 + assert_eq!(client.call(&paths).unwrap_err(), NipcError::BadLayout);
264 + assert_eq!(client.status().error_count, 1);
265 +
266 + client.close();
267 + assert_eq!(client.status().state, ClientState::Disconnected);
268 + }
269 +
270 + #[cfg(windows)]
271 + #[test]
272 + fn managed_server_new_initializes_stopped_windows() {
273 + let server = ManagedServer::new(
274 + r"C:\Temp\nipc-cgroups-lookup-facade",
275 + "cgroups-lookup-facade-new",
276 + ServerConfig::default(),
277 + Handler::default(),
278 + );
279 + let running = server.running_flag();
280 + assert!(!running.load(Ordering::SeqCst));
281 + server.stop();
282 + assert!(!running.load(Ordering::SeqCst));
283 + }
284 +}
src/crates/netipc/src/service/cgroups_snapshot.rs new
+264
@@ -0,0 +1,264 @@
1 +//! L2 cgroups-snapshot service facade.
2 +//!
3 +//! The public service surface is service-kind specific: one endpoint, one
4 +//! request kind. The request code remains in the outer envelope only for
5 +//! validation, not for public multi-method dispatch.
6 +
7 +use super::raw;
8 +use crate::protocol::{CgroupsResponseView, NipcError, METHOD_CGROUPS_SNAPSHOT, PROFILE_BASELINE};
9 +
10 +#[cfg(unix)]
11 +use crate::transport::posix::{
12 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
13 +};
14 +
15 +#[cfg(windows)]
16 +use crate::transport::windows::{
17 + ClientConfig as TransportClientConfig, ServerConfig as TransportServerConfig,
18 +};
19 +
20 +use std::sync::atomic::AtomicBool;
21 +use std::sync::Arc;
22 +
23 +pub use raw::{CgroupsCacheItem, CgroupsCacheStatus, ClientState, ClientStatus, SnapshotHandler};
24 +
25 +/// Public L2/L3 client configuration for the cgroups-snapshot service.
26 +///
27 +/// This service-level configuration is shared across supported operating
28 +/// systems. Transport-only tuning stays below the public typed API.
29 +#[derive(Debug, Clone)]
30 +pub struct ClientConfig {
31 + pub supported_profiles: u32,
32 + pub preferred_profiles: u32,
33 + pub max_request_batch_items: u32,
34 + pub max_response_payload_bytes: u32,
35 + pub auth_token: u64,
36 +}
37 +
38 +impl Default for ClientConfig {
39 + fn default() -> Self {
40 + Self {
41 + supported_profiles: PROFILE_BASELINE,
42 + preferred_profiles: 0,
43 + max_request_batch_items: 0,
44 + max_response_payload_bytes: 0,
45 + auth_token: 0,
46 + }
47 + }
48 +}
49 +
50 +impl ClientConfig {
51 + fn into_transport(self) -> TransportClientConfig {
52 + let mut transport = TransportClientConfig::default();
53 + transport.supported_profiles = self.supported_profiles;
54 + transport.preferred_profiles = self.preferred_profiles;
55 + transport.max_request_batch_items = self.max_request_batch_items;
56 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
57 + transport.max_response_batch_items = self.max_request_batch_items;
58 + transport.auth_token = self.auth_token;
59 + transport
60 + }
61 +}
62 +
63 +/// Public typed-server configuration for the cgroups-snapshot service.
64 +///
65 +/// This configuration is intentionally transport-agnostic. Transport-only
66 +/// knobs such as socket backlog or packet sizing stay below the service layer.
67 +#[derive(Debug, Clone)]
68 +pub struct ServerConfig {
69 + pub supported_profiles: u32,
70 + pub preferred_profiles: u32,
71 + pub max_request_batch_items: u32,
72 + pub max_response_payload_bytes: u32,
73 + pub auth_token: u64,
74 +}
75 +
76 +impl Default for ServerConfig {
77 + fn default() -> Self {
78 + Self {
79 + supported_profiles: PROFILE_BASELINE,
80 + preferred_profiles: 0,
81 + max_request_batch_items: 0,
82 + max_response_payload_bytes: 0,
83 + auth_token: 0,
84 + }
85 + }
86 +}
87 +
88 +impl ServerConfig {
89 + fn into_transport(self) -> TransportServerConfig {
90 + let mut transport = TransportServerConfig::default();
91 + transport.supported_profiles = self.supported_profiles;
92 + transport.preferred_profiles = self.preferred_profiles;
93 + transport.max_request_batch_items = self.max_request_batch_items;
94 + transport.max_response_payload_bytes = self.max_response_payload_bytes;
95 + transport.max_response_batch_items = self.max_request_batch_items;
96 + transport.auth_token = self.auth_token;
97 + transport
98 + }
99 +}
100 +
101 +/// L2 client context for the cgroups-snapshot service.
102 +pub struct CgroupsClient {
103 + inner: raw::RawClient,
104 +}
105 +
106 +impl CgroupsClient {
107 + /// Create a new client context. Does NOT connect. Does NOT require the
108 + /// server to be running.
109 + pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
110 + Self {
111 + inner: raw::RawClient::new_snapshot(run_dir, service_name, config.into_transport()),
112 + }
113 + }
114 +
115 + /// Attempt connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
116 + /// Returns true if the state changed.
117 + pub fn refresh(&mut self) -> bool {
118 + self.inner.refresh()
119 + }
120 +
121 + /// Cheap cached boolean. No I/O, no syscalls.
122 + #[inline]
123 + pub fn ready(&self) -> bool {
124 + self.inner.ready()
125 + }
126 +
127 + /// Detailed status snapshot for diagnostics.
128 + pub fn status(&self) -> ClientStatus {
129 + self.inner.status()
130 + }
131 +
132 + /// Blocking typed call for the cgroups-snapshot service.
133 + pub fn call_snapshot(&mut self) -> Result<CgroupsResponseView<'_>, NipcError> {
134 + self.inner.call_snapshot()
135 + }
136 +
137 + /// Tear down connection and release resources.
138 + pub fn close(&mut self) {
139 + self.inner.close();
140 + }
141 +}
142 +
143 +impl Drop for CgroupsClient {
144 + fn drop(&mut self) {
145 + self.close();
146 + }
147 +}
148 +
149 +/// Typed server handler surface for the cgroups-snapshot service.
150 +#[derive(Clone, Default)]
151 +pub struct Handler {
152 + pub handle: Option<SnapshotHandler>,
153 + pub snapshot_max_items: u32,
154 +}
155 +
156 +/// Managed server for the cgroups-snapshot service kind.
157 +pub struct ManagedServer {
158 + inner: raw::ManagedServer,
159 +}
160 +
161 +impl ManagedServer {
162 + /// Create a new managed server. Does NOT start listening yet.
163 + pub fn new(run_dir: &str, service_name: &str, config: ServerConfig, handler: Handler) -> Self {
164 + Self::with_workers(run_dir, service_name, config, handler, 8)
165 + }
166 +
167 + /// Create a server with an explicit worker count limit.
168 + pub fn with_workers(
169 + run_dir: &str,
170 + service_name: &str,
171 + config: ServerConfig,
172 + handler: Handler,
173 + worker_count: usize,
174 + ) -> Self {
175 + let raw_handler = handler
176 + .handle
177 + .map(|handle| raw::snapshot_dispatch(handle, handler.snapshot_max_items));
178 +
179 + Self {
180 + inner: raw::ManagedServer::with_workers(
181 + run_dir,
182 + service_name,
183 + config.into_transport(),
184 + METHOD_CGROUPS_SNAPSHOT,
185 + raw_handler,
186 + worker_count,
187 + ),
188 + }
189 + }
190 +
191 + /// Run the acceptor loop. Blocking. Returns when `stop()` is called or on
192 + /// fatal error.
193 + pub fn run(&mut self) -> Result<(), NipcError> {
194 + self.inner.run()
195 + }
196 +
197 + /// Signal shutdown.
198 + pub fn stop(&self) {
199 + self.inner.stop();
200 + }
201 +
202 + /// Clone of the internal running flag for diagnostics and test helpers.
203 + ///
204 + /// For reliable shutdown, call `stop()`. On Windows, changing this flag
205 + /// alone does not wake a blocking listener accept.
206 + pub fn running_flag(&self) -> Arc<AtomicBool> {
207 + self.inner.running_flag()
208 + }
209 +}
210 +
211 +/// L3 client-side cgroups snapshot cache.
212 +pub struct CgroupsCache {
213 + inner: raw::CgroupsCache,
214 +}
215 +
216 +impl CgroupsCache {
217 + /// Create a new L3 cache. Creates the underlying L2 client context.
218 + /// Does NOT connect. Does NOT require the server to be running.
219 + pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
220 + Self {
221 + inner: raw::CgroupsCache::new(run_dir, service_name, config.into_transport()),
222 + }
223 + }
224 +
225 + /// Refresh the cache. Returns true if the cache was updated.
226 + pub fn refresh(&mut self) -> bool {
227 + self.inner.refresh()
228 + }
229 +
230 + /// Returns true if at least one successful refresh has occurred.
231 + #[inline]
232 + pub fn ready(&self) -> bool {
233 + self.inner.ready()
234 + }
235 +
236 + /// Look up a cached item by hash + name. O(1), no I/O.
237 + pub fn lookup(&self, hash: u32, name: &str) -> Option<&CgroupsCacheItem> {
238 + self.inner.lookup(hash, name)
239 + }
240 +
241 + /// Fill a status snapshot for diagnostics.
242 + pub fn status(&self) -> CgroupsCacheStatus {
243 + self.inner.status()
244 + }
245 +
246 + /// Close the cache and underlying L2 client.
247 + pub fn close(&mut self) {
248 + self.inner.close();
249 + }
250 +}
251 +
252 +impl Drop for CgroupsCache {
253 + fn drop(&mut self) {
254 + self.close();
255 + }
256 +}
257 +
258 +#[cfg(all(test, unix))]
259 +#[path = "cgroups_unix_tests.rs"]
260 +mod tests;
261 +
262 +#[cfg(all(test, windows))]
263 +#[path = "cgroups_windows_tests.rs"]
264 +mod windows_tests;
src/crates/netipc/src/service/mod.rs
+3
@@ -4,6 +4,9 @@
4 //! remain generic for tests and benchmarks, but every running endpoint still
5 //! serves exactly one request kind.
6
7 +pub mod apps_lookup;
8 pub mod cgroups;
9 +pub mod cgroups_lookup;
10 +pub mod cgroups_snapshot;
11 #[doc(hidden)]
12 pub mod raw;
src/crates/netipc/src/service/raw.rs
+53 -2622
@@ -4,2635 +4,66 @@
4 //! internal benchmark/stress coverage while the public service modules expose
5 //! one service kind per endpoint.
6
7 -use crate::protocol::{
8 - self, batch_item_get, increment_decode, increment_encode, string_reverse_decode,
9 - string_reverse_encode, BatchBuilder, CgroupsRequest, CgroupsResponseView, Header, NipcError,
10 - FLAG_BATCH, HEADER_SIZE, INCREMENT_PAYLOAD_SIZE, KIND_REQUEST, KIND_RESPONSE, MAGIC_MSG,
11 - MAX_PAYLOAD_CAP, MAX_PAYLOAD_DEFAULT, METHOD_CGROUPS_SNAPSHOT, METHOD_INCREMENT,
12 - METHOD_STRING_REVERSE, STATUS_BAD_ENVELOPE, STATUS_INTERNAL_ERROR, STATUS_LIMIT_EXCEEDED,
13 - STATUS_OK, STRING_REVERSE_HDR_SIZE, VERSION,
14 -};
15 -
7 +mod apps_lookup;
8 +mod cgroups_cache;
9 +mod cgroups_lookup;
10 +mod cgroups_snapshot;
11 +mod client;
12 +mod client_call;
13 #[cfg(unix)]
17 -use crate::protocol::{PROFILE_SHM_FUTEX, PROFILE_SHM_HYBRID};
18 -
14 +mod client_unix;
15 +#[cfg(windows)]
16 +mod client_windows;
17 +mod common;
18 +mod dispatch;
19 +mod increment;
20 +mod server;
21 #[cfg(unix)]
20 -use crate::transport::posix::{ClientConfig, ServerConfig, UdsListener, UdsSession};
21 -
22 -#[cfg(target_os = "linux")]
23 -use crate::transport::shm::ShmContext;
24 -
22 +mod server_session_unix;
23 #[cfg(windows)]
26 -use crate::transport::windows::{ClientConfig, NpError, NpListener, NpSession, ServerConfig};
27 -
24 +mod server_session_windows;
25 +#[cfg(unix)]
26 +mod server_unix;
27 #[cfg(windows)]
29 -use crate::transport::win_shm::{
30 - WinShmContext, PROFILE_BUSYWAIT as WIN_SHM_PROFILE_BUSYWAIT,
31 - PROFILE_HYBRID as WIN_SHM_PROFILE_HYBRID,
32 -};
28 +mod server_windows;
29 +mod string_reverse;
30 +
31 +pub use apps_lookup::{apps_lookup_dispatch, AppsLookupHandler};
32 +pub use cgroups_cache::{CgroupsCache, CgroupsCacheItem, CgroupsCacheStatus};
33 +pub use cgroups_lookup::{cgroups_lookup_dispatch, CgroupsLookupHandler};
34 +pub use cgroups_snapshot::{snapshot_dispatch, snapshot_max_items, SnapshotHandler};
35 +pub use client::{ClientState, ClientStatus, RawClient};
36 +pub use dispatch::{DispatchError, DispatchHandler};
37 +pub use increment::{increment_dispatch, IncrementHandler};
38 +pub use server::ManagedServer;
39 +pub use string_reverse::{string_reverse_dispatch, StringReverseHandler};
40
34 -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
41 +#[cfg(all(test, unix))]
42 +use crate::protocol::{
43 + self, batch_item_get, increment_decode, string_reverse_decode, string_reverse_encode,
44 + CgroupsRequest, Header, NipcError, FLAG_BATCH, HEADER_SIZE, INCREMENT_PAYLOAD_SIZE,
45 + KIND_REQUEST, KIND_RESPONSE, MAGIC_MSG, METHOD_CGROUPS_SNAPSHOT, METHOD_INCREMENT,
46 + METHOD_STRING_REVERSE, STATUS_BAD_ENVELOPE, STATUS_INTERNAL_ERROR, STATUS_OK, VERSION,
47 +};
48 +#[cfg(all(test, unix))]
49 +use crate::transport::posix::{ClientConfig, ServerConfig, UdsListener, UdsSession};
50 +#[cfg(all(test, target_os = "linux"))]
51 +use crate::transport::shm::ShmContext;
52 +#[cfg(all(test, windows))]
53 +use crate::transport::windows::{ClientConfig, NpListener, NpSession, ServerConfig};
54 +#[cfg(all(test, unix))]
55 +use std::sync::atomic::{AtomicBool, Ordering};
56 +#[cfg(all(test, unix))]
57 use std::sync::Arc;
58
37 -/// Poll/receive timeout for server loops (ms). Controls shutdown detection latency.
38 -const SERVER_POLL_TIMEOUT_MS: u32 = 100;
39 -const CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS: u64 = 5;
40 -const CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS: u64 = 5_000;
41 -
42 -fn next_power_of_2_u32(n: u32) -> u32 {
43 - if n < 16 {
44 - return 16;
45 - }
46 -
47 - // Cap at 2^31 — the largest power of 2 that fits in u32
48 - if n > (1u32 << 31) {
49 - return 1u32 << 31;
50 - }
51 -
52 - let mut value = n - 1;
53 - value |= value >> 1;
54 - value |= value >> 2;
55 - value |= value >> 4;
56 - value |= value >> 8;
57 - value |= value >> 16;
58 - value + 1
59 -}
60 -
61 -// ---------------------------------------------------------------------------
62 -// Client state
63 -// ---------------------------------------------------------------------------
64 -
65 -/// Client connection state machine.
66 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67 -pub enum ClientState {
68 - Disconnected,
69 - Connecting,
70 - Ready,
71 - NotFound,
72 - AuthFailed,
73 - Incompatible,
74 - Broken,
75 -}
76 -
77 -/// Diagnostic counters snapshot.
78 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79 -pub struct ClientStatus {
80 - pub state: ClientState,
81 - pub connect_count: u32,
82 - pub reconnect_count: u32,
83 - pub call_count: u32,
84 - pub error_count: u32,
85 -}
86 -
87 -// ---------------------------------------------------------------------------
88 -// Client context
89 -// ---------------------------------------------------------------------------
90 -
91 -/// L2 client context bound to one service kind.
92 -///
93 -/// Manages connection lifecycle and provides typed blocking calls with
94 -/// at-least-once retry semantics. The outer request code remains only for
95 -/// validation; each client instance is bound to one expected request kind.
96 -pub struct RawClient {
97 - state: ClientState,
98 - run_dir: String,
99 - service_name: String,
100 - expected_method_code: u16,
101 - transport_config: ClientConfig,
102 -
103 - // Connection (managed internally)
104 - #[cfg(unix)]
105 - session: Option<UdsSession>,
106 - #[cfg(target_os = "linux")]
107 - shm: Option<ShmContext>,
108 -
109 - #[cfg(windows)]
110 - session: Option<NpSession>,
111 - #[cfg(windows)]
112 - shm: Option<WinShmContext>,
113 -
114 - // Reusable scratch buffers owned by the client for hot request paths.
115 - request_buf: Vec<u8>,
116 - send_buf: Vec<u8>,
117 - transport_buf: Vec<u8>,
118 -
119 - // Stats
120 - connect_count: u32,
121 - reconnect_count: u32,
122 - call_count: u32,
123 - error_count: u32,
124 -}
125 -
126 -impl RawClient {
127 - fn new_bound(
128 - run_dir: &str,
129 - service_name: &str,
130 - expected_method_code: u16,
131 - config: ClientConfig,
132 - ) -> Self {
133 - RawClient {
134 - state: ClientState::Disconnected,
135 - run_dir: run_dir.to_string(),
136 - service_name: service_name.to_string(),
137 - expected_method_code,
138 - transport_config: config,
139 - session: None,
140 - #[cfg(target_os = "linux")]
141 - shm: None,
142 - #[cfg(windows)]
143 - shm: None,
144 - request_buf: Vec::new(),
145 - send_buf: Vec::new(),
146 - transport_buf: Vec::new(),
147 - connect_count: 0,
148 - reconnect_count: 0,
149 - call_count: 0,
150 - error_count: 0,
151 - }
152 - }
153 -
154 - /// Create a new client context bound to the cgroups-snapshot service kind.
155 - /// Does NOT connect. Does NOT require the server to be running.
156 - pub fn new_snapshot(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
157 - Self::new_bound(run_dir, service_name, METHOD_CGROUPS_SNAPSHOT, config)
158 - }
159 -
160 - /// Create a new client context bound to the increment service kind.
161 - /// Does NOT connect. Does NOT require the server to be running.
162 - pub fn new_increment(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
163 - Self::new_bound(run_dir, service_name, METHOD_INCREMENT, config)
164 - }
165 -
166 - /// Create a new client context bound to the string-reverse service kind.
167 - /// Does NOT connect. Does NOT require the server to be running.
168 - pub fn new_string_reverse(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
169 - Self::new_bound(run_dir, service_name, METHOD_STRING_REVERSE, config)
170 - }
171 -
172 - /// Attempt connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
173 - /// Returns true if the state changed.
174 - pub fn refresh(&mut self) -> bool {
175 - let old_state = self.state;
176 -
177 - match self.state {
178 - ClientState::Disconnected | ClientState::NotFound => {
179 - self.state = ClientState::Connecting;
180 - self.state = self.try_connect();
181 - if self.state == ClientState::Ready {
182 - self.connect_count += 1;
183 - }
184 - }
185 - ClientState::Broken => {
186 - self.disconnect();
187 - self.state = ClientState::Connecting;
188 - self.state = self.try_connect();
189 - if self.state == ClientState::Ready {
190 - self.reconnect_count += 1;
191 - }
192 - }
193 - ClientState::Ready
194 - | ClientState::Connecting
195 - | ClientState::AuthFailed
196 - | ClientState::Incompatible => {}
197 - }
198 -
199 - self.state != old_state
200 - }
201 -
202 - /// Cheap cached boolean. No I/O, no syscalls.
203 - #[inline]
204 - pub fn ready(&self) -> bool {
205 - self.state == ClientState::Ready
206 - }
207 -
208 - /// Detailed status snapshot for diagnostics.
209 - pub fn status(&self) -> ClientStatus {
210 - ClientStatus {
211 - state: self.state,
212 - connect_count: self.connect_count,
213 - reconnect_count: self.reconnect_count,
214 - call_count: self.call_count,
215 - error_count: self.error_count,
216 - }
217 - }
218 -
219 - fn session_max_request_payload_bytes(&self) -> u32 {
220 - #[cfg(unix)]
221 - if let Some(ref session) = self.session {
222 - return session.max_request_payload_bytes;
223 - }
224 -
225 - #[cfg(windows)]
226 - if let Some(ref session) = self.session {
227 - return session.max_request_payload_bytes;
228 - }
229 -
230 - self.transport_config.max_request_payload_bytes
231 - }
232 -
233 - fn session_max_response_payload_bytes(&self) -> u32 {
234 - #[cfg(unix)]
235 - if let Some(ref session) = self.session {
236 - return session.max_response_payload_bytes;
237 - }
238 -
239 - #[cfg(windows)]
240 - if let Some(ref session) = self.session {
241 - return session.max_response_payload_bytes;
242 - }
243 -
244 - self.transport_config.max_response_payload_bytes
245 - }
246 -
247 - fn client_note_request_capacity(&mut self, payload_len: u32) {
248 - let grown = next_power_of_2_u32(payload_len).min(MAX_PAYLOAD_CAP);
249 - if grown > self.transport_config.max_request_payload_bytes {
250 - self.transport_config.max_request_payload_bytes = grown;
251 - }
252 - }
253 -
254 - fn client_note_response_capacity(&mut self, payload_len: u32) {
255 - let grown = next_power_of_2_u32(payload_len).min(MAX_PAYLOAD_CAP);
256 - if grown > self.transport_config.max_response_payload_bytes {
257 - self.transport_config.max_response_payload_bytes = grown;
258 - }
259 - }
260 -
261 - fn validate_method(&self, method_code: u16) -> Result<(), NipcError> {
262 - if self.expected_method_code == method_code {
263 - Ok(())
264 - } else {
265 - Err(NipcError::BadLayout)
266 - }
267 - }
268 -
269 - /// Blocking typed call: encode request, send, receive, check
270 - /// transport_status, decode response.
271 - ///
272 - /// The returned view is valid until the next typed call on this client.
273 - ///
274 - /// Retry policy (per spec): if the call fails and the context was
275 - /// previously READY, disconnect, reconnect (full handshake), and retry.
276 - /// Ordinary failures retry once. Overflow-driven resize recovery may
277 - /// reconnect more than once while negotiated capacities grow.
278 - pub fn call_snapshot(&mut self) -> Result<CgroupsResponseView<'_>, NipcError> {
279 - self.validate_method(METHOD_CGROUPS_SNAPSHOT)?;
280 - let req = CgroupsRequest {
281 - layout_version: 1,
282 - flags: 0,
283 - };
284 - let mut req_buf = [0u8; 4];
285 - let req_len = req.encode(&mut req_buf);
286 - if req_len == 0 {
287 - return Err(NipcError::Truncated);
288 - }
289 -
290 - let response = self.raw_call_with_retry(METHOD_CGROUPS_SNAPSHOT, &req_buf[..req_len])?;
291 - CgroupsResponseView::decode(self.response_payload(response)?)
292 - }
293 -
294 - /// Blocking typed call: INCREMENT method.
295 - /// Sends a u64 value, receives the incremented u64 back.
296 - pub fn call_increment(&mut self, value: u64) -> Result<u64, NipcError> {
297 - self.validate_method(METHOD_INCREMENT)?;
298 - let mut req_buf = [0u8; INCREMENT_PAYLOAD_SIZE];
299 - let req_len = increment_encode(value, &mut req_buf);
300 - if req_len == 0 {
301 - return Err(NipcError::Truncated);
302 - }
303 -
304 - let response = self.raw_call_with_retry(METHOD_INCREMENT, &req_buf[..req_len])?;
305 - increment_decode(self.response_payload(response)?)
306 - }
307 -
308 - /// Blocking typed call: STRING_REVERSE method.
309 - /// Sends a string, receives the reversed string back.
310 - ///
311 - /// The returned view is valid until the next typed call on this client.
312 - pub fn call_string_reverse(
313 - &mut self,
314 - s: &str,
315 - ) -> Result<protocol::StringReverseView<'_>, NipcError> {
316 - self.validate_method(METHOD_STRING_REVERSE)?;
317 - let req_size = STRING_REVERSE_HDR_SIZE + s.len() + 1;
318 - let req_buf = ensure_client_scratch(&mut self.request_buf, req_size);
319 - let req_len = string_reverse_encode(s.as_bytes(), req_buf);
320 - if req_len == 0 {
321 - return Err(NipcError::Truncated);
322 - }
323 -
324 - let response = self.raw_call_with_retry_request_buf(METHOD_STRING_REVERSE, req_len)?;
325 - string_reverse_decode(self.response_payload(response)?)
326 - }
327 -
328 - /// Blocking typed batch call: INCREMENT method.
329 - /// Sends multiple u64 values, receives the incremented u64s back.
330 - pub fn call_increment_batch(&mut self, values: &[u64]) -> Result<Vec<u64>, NipcError> {
331 - self.validate_method(METHOD_INCREMENT)?;
332 - if values.is_empty() {
333 - return Ok(Vec::new());
334 - }
335 -
336 - // Single value: use the non-batch path
337 - if values.len() == 1 {
338 - let r = self.call_increment(values[0])?;
339 - return Ok(vec![r]);
340 - }
341 -
342 - let count = values.len() as u32;
343 -
344 - let req_buf_size = protocol::align8(count as usize * 8)
345 - + count as usize * protocol::align8(INCREMENT_PAYLOAD_SIZE)
346 - + 64;
347 - let req_buf = ensure_client_scratch(&mut self.request_buf, req_buf_size);
348 - let req_len = {
349 - let mut bb = BatchBuilder::new(req_buf, count);
350 - for &v in values {
351 - let mut item_buf = [0u8; INCREMENT_PAYLOAD_SIZE];
352 - if increment_encode(v, &mut item_buf) == 0 {
353 - return Err(NipcError::Truncated);
354 - }
355 - bb.add(&item_buf).map_err(|_| NipcError::Overflow)?;
356 - }
357 - let (req_len, _out_count) = bb.finish();
358 - req_len
359 - };
360 -
361 - let response =
362 - self.raw_batch_call_with_retry_request_buf(METHOD_INCREMENT, req_len, count)?;
363 - let resp_payload = self.response_payload(response)?;
364 - let mut results = Vec::with_capacity(values.len());
365 - for i in 0..count {
366 - let (item_data, _item_len) = batch_item_get(resp_payload, count, i)?;
367 - let val = increment_decode(item_data)?;
368 - results.push(val);
369 - }
370 -
371 - Ok(results)
372 - }
373 -
374 - /// Tear down connection and release resources.
375 - pub fn close(&mut self) {
376 - self.disconnect();
377 - self.state = ClientState::Disconnected;
378 - }
379 -
380 - // ------------------------------------------------------------------
381 - // Internal helpers
382 - // ------------------------------------------------------------------
383 -
384 - /// Tear down the current connection.
385 - fn disconnect(&mut self) {
386 - #[cfg(target_os = "linux")]
387 - {
388 - if let Some(mut shm) = self.shm.take() {
389 - shm.close();
390 - }
391 - }
392 -
393 - #[cfg(windows)]
394 - {
395 - if let Some(mut shm) = self.shm.take() {
396 - shm.close();
397 - }
398 - }
399 -
400 - // Drop the session (closes handle/fd via Drop impl)
401 - self.session.take();
402 - }
403 -
404 - /// Attempt a full connection: transport connect + handshake, then SHM
405 - /// upgrade if negotiated.
406 - #[cfg(unix)]
407 - fn try_connect(&mut self) -> ClientState {
408 - match UdsSession::connect(&self.run_dir, &self.service_name, &self.transport_config) {
409 - Ok(session) => {
410 - #[cfg(target_os = "linux")]
411 - let selected_profile = session.selected_profile;
412 - #[cfg(target_os = "linux")]
413 - let session_id = session.session_id;
414 -
415 - // SHM upgrade if negotiated
416 - #[cfg(target_os = "linux")]
417 - {
418 - if selected_profile == PROFILE_SHM_HYBRID
419 - || selected_profile == PROFILE_SHM_FUTEX
420 - {
421 - // Retry attach: server creates the SHM region after
422 - // the UDS handshake, so it may not exist yet.
423 - let mut shm_ok = false;
424 - let deadline = std::time::Instant::now()
425 - + std::time::Duration::from_millis(CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS);
426 - loop {
427 - match ShmContext::client_attach(
428 - &self.run_dir,
429 - &self.service_name,
430 - session_id,
431 - ) {
432 - Ok(ctx) => {
433 - self.shm = Some(ctx);
434 - shm_ok = true;
435 - break;
436 - }
437 - Err(_) => {
438 - if std::time::Instant::now() >= deadline {
439 - break;
440 - }
441 - std::thread::sleep(std::time::Duration::from_millis(
442 - CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS,
443 - ));
444 - }
445 - }
446 - }
447 - if !shm_ok {
448 - // SHM attach failed after negotiation. Close that session,
449 - // blacklist SHM for this client context, and retry baseline.
450 - drop(session);
451 - self.transport_config.supported_profiles &=
452 - !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
453 - self.transport_config.preferred_profiles &=
454 - !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
455 - if self.transport_config.supported_profiles == 0 {
456 - return ClientState::Disconnected;
457 - }
458 - return self.try_connect();
459 - }
460 - }
461 - }
462 -
463 - self.session = Some(session);
464 - ClientState::Ready
465 - }
466 - Err(e) => {
467 - use crate::transport::posix::UdsError;
468 - match e {
469 - UdsError::Connect(_) => ClientState::NotFound,
470 - UdsError::AuthFailed => ClientState::AuthFailed,
471 - UdsError::NoProfile => ClientState::Incompatible,
472 - UdsError::Incompatible(_) => ClientState::Incompatible,
473 - _ => ClientState::Disconnected,
474 - }
475 - }
476 - }
477 - }
478 -
479 - /// Windows: attempt a full Named Pipe connection + Win SHM upgrade.
480 - #[cfg(windows)]
481 - fn try_connect(&mut self) -> ClientState {
482 - match NpSession::connect(&self.run_dir, &self.service_name, &self.transport_config) {
483 - Ok(session) => {
484 - let selected_profile = session.selected_profile;
485 -
486 - // Win SHM upgrade if negotiated
487 - if selected_profile == WIN_SHM_PROFILE_HYBRID
488 - || selected_profile == WIN_SHM_PROFILE_BUSYWAIT
489 - {
490 - let mut shm_ok = false;
491 - let deadline = std::time::Instant::now()
492 - + std::time::Duration::from_millis(CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS);
493 - loop {
494 - match WinShmContext::client_attach(
495 - &self.run_dir,
496 - &self.service_name,
497 - self.transport_config.auth_token,
498 - session.session_id,
499 - selected_profile,
500 - ) {
501 - Ok(ctx) => {
502 - self.shm = Some(ctx);
503 - shm_ok = true;
504 - break;
505 - }
506 - Err(_) => {
507 - if std::time::Instant::now() >= deadline {
508 - break;
509 - }
510 - std::thread::sleep(std::time::Duration::from_millis(
511 - CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS,
512 - ));
513 - }
514 - }
515 - }
516 - if !shm_ok {
517 - // WinSHM attach failed after negotiation. Close that
518 - // session, blacklist WinSHM for this client context,
519 - // and retry baseline.
520 - drop(session);
521 - self.transport_config.supported_profiles &=
522 - !(WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
523 - self.transport_config.preferred_profiles &=
524 - !(WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
525 - if self.transport_config.supported_profiles == 0 {
526 - return ClientState::Disconnected;
527 - }
528 - return self.try_connect();
529 - }
530 - }
531 -
532 - self.session = Some(session);
533 - ClientState::Ready
534 - }
535 - Err(e) => match e {
536 - NpError::Connect(_) => ClientState::NotFound,
537 - NpError::AuthFailed => ClientState::AuthFailed,
538 - NpError::NoProfile => ClientState::Incompatible,
539 - NpError::Incompatible(_) => ClientState::Incompatible,
540 - _ => ClientState::Disconnected,
541 - },
542 - }
543 - }
544 -
545 - /// Reconnect-driven recovery for a single-item raw call.
546 - /// Ordinary failures retry once. Overflow-driven resize recovery may
547 - /// reconnect more than once while negotiated capacities grow.
548 - fn raw_call_with_retry<'a>(
549 - &mut self,
550 - method_code: u16,
551 - request_payload: &[u8],
552 - ) -> Result<ClientResponseRef, NipcError> {
553 - if self.state != ClientState::Ready {
554 - self.error_count += 1;
555 - return Err(NipcError::BadLayout);
556 - }
557 -
558 - // Cap overflow-driven retries: payloads grow by powers of 2, so 8
559 - // retries allows ~256x growth from the initial negotiated size.
560 - let mut overflow_retries = 0u32;
561 - loop {
562 - let prev_req = self.session_max_request_payload_bytes();
563 - let prev_resp = self.session_max_response_payload_bytes();
564 - let prev_cfg_req = self.transport_config.max_request_payload_bytes;
565 - let prev_cfg_resp = self.transport_config.max_response_payload_bytes;
566 -
567 - match self.do_raw_call(method_code, request_payload) {
568 - Ok(payload) => {
569 - self.call_count += 1;
570 - return Ok(payload);
571 - }
572 - Err(first_err) => {
573 - if first_err != NipcError::Overflow {
574 - self.disconnect();
575 - self.state = ClientState::Broken;
576 - self.state = self.try_connect();
577 - if self.state != ClientState::Ready {
578 - self.error_count += 1;
579 - return Err(first_err);
580 - }
581 - self.reconnect_count += 1;
582 -
583 - match self.do_raw_call(method_code, request_payload) {
584 - Ok(payload) => {
585 - self.call_count += 1;
586 - return Ok(payload);
587 - }
588 - Err(retry_err) => {
589 - self.disconnect();
590 - self.state = ClientState::Broken;
591 - self.error_count += 1;
592 - return Err(retry_err);
593 - }
594 - }
595 - }
596 -
597 - self.disconnect();
598 - self.state = ClientState::Broken;
599 - self.state = self.try_connect();
600 - if self.state != ClientState::Ready {
601 - self.error_count += 1;
602 - return Err(first_err);
603 - }
604 - self.reconnect_count += 1;
605 -
606 - if self.session_max_request_payload_bytes() <= prev_req
607 - && self.session_max_response_payload_bytes() <= prev_resp
608 - && self.transport_config.max_request_payload_bytes <= prev_cfg_req
609 - && self.transport_config.max_response_payload_bytes <= prev_cfg_resp
610 - {
611 - self.disconnect();
612 - self.state = ClientState::Broken;
613 - self.error_count += 1;
614 - return Err(first_err);
615 - }
616 -
617 - overflow_retries += 1;
618 - if overflow_retries >= 8 {
619 - self.disconnect();
620 - self.state = ClientState::Broken;
621 - self.error_count += 1;
622 - return Err(first_err);
623 - }
624 - }
625 - }
626 - }
627 - }
628 -
629 - fn raw_call_with_retry_request_buf<'a>(
630 - &mut self,
631 - method_code: u16,
632 - req_len: usize,
633 - ) -> Result<ClientResponseRef, NipcError> {
634 - if self.state != ClientState::Ready {
635 - self.error_count += 1;
636 - return Err(NipcError::BadLayout);
637 - }
638 -
639 - let mut overflow_retries = 0u32;
640 - loop {
641 - let prev_req = self.session_max_request_payload_bytes();
642 - let prev_resp = self.session_max_response_payload_bytes();
643 - let prev_cfg_req = self.transport_config.max_request_payload_bytes;
644 - let prev_cfg_resp = self.transport_config.max_response_payload_bytes;
645 -
646 - match self.do_raw_call_from_request_buf(method_code, req_len) {
647 - Ok(payload) => {
648 - self.call_count += 1;
649 - return Ok(payload);
650 - }
651 - Err(first_err) => {
652 - if first_err != NipcError::Overflow {
653 - self.disconnect();
654 - self.state = ClientState::Broken;
655 - self.state = self.try_connect();
656 - if self.state != ClientState::Ready {
657 - self.error_count += 1;
658 - return Err(first_err);
659 - }
660 - self.reconnect_count += 1;
661 -
662 - match self.do_raw_call_from_request_buf(method_code, req_len) {
663 - Ok(payload) => {
664 - self.call_count += 1;
665 - return Ok(payload);
666 - }
667 - Err(retry_err) => {
668 - self.disconnect();
669 - self.state = ClientState::Broken;
670 - self.error_count += 1;
671 - return Err(retry_err);
672 - }
673 - }
674 - }
675 -
676 - self.disconnect();
677 - self.state = ClientState::Broken;
678 - self.state = self.try_connect();
679 - if self.state != ClientState::Ready {
680 - self.error_count += 1;
681 - return Err(first_err);
682 - }
683 - self.reconnect_count += 1;
684 -
685 - if self.session_max_request_payload_bytes() <= prev_req
686 - && self.session_max_response_payload_bytes() <= prev_resp
687 - && self.transport_config.max_request_payload_bytes <= prev_cfg_req
688 - && self.transport_config.max_response_payload_bytes <= prev_cfg_resp
689 - {
690 - self.disconnect();
691 - self.state = ClientState::Broken;
692 - self.error_count += 1;
693 - return Err(first_err);
694 - }
695 -
696 - overflow_retries += 1;
697 - if overflow_retries >= 8 {
698 - self.disconnect();
699 - self.state = ClientState::Broken;
700 - self.error_count += 1;
701 - return Err(first_err);
702 - }
703 - }
704 - }
705 - }
706 - }
707 -
708 - fn raw_batch_call_with_retry_request_buf<'a>(
709 - &mut self,
710 - method_code: u16,
711 - req_len: usize,
712 - item_count: u32,
713 - ) -> Result<ClientResponseRef, NipcError> {
714 - if self.state != ClientState::Ready {
715 - self.error_count += 1;
716 - return Err(NipcError::BadLayout);
717 - }
718 -
719 - let mut overflow_retries = 0u32;
720 - loop {
721 - let prev_req = self.session_max_request_payload_bytes();
722 - let prev_resp = self.session_max_response_payload_bytes();
723 - let prev_cfg_req = self.transport_config.max_request_payload_bytes;
724 - let prev_cfg_resp = self.transport_config.max_response_payload_bytes;
725 -
726 - match self.do_raw_batch_call_from_request_buf(method_code, req_len, item_count) {
727 - Ok(payload) => {
728 - self.call_count += 1;
729 - return Ok(payload);
730 - }
731 - Err(first_err) => {
732 - if first_err != NipcError::Overflow {
733 - self.disconnect();
734 - self.state = ClientState::Broken;
735 - self.state = self.try_connect();
736 - if self.state != ClientState::Ready {
737 - self.error_count += 1;
738 - return Err(first_err);
739 - }
740 - self.reconnect_count += 1;
741 -
742 - match self.do_raw_batch_call_from_request_buf(
743 - method_code,
744 - req_len,
745 - item_count,
746 - ) {
747 - Ok(payload) => {
748 - self.call_count += 1;
749 - return Ok(payload);
750 - }
751 - Err(retry_err) => {
752 - self.disconnect();
753 - self.state = ClientState::Broken;
754 - self.error_count += 1;
755 - return Err(retry_err);
756 - }
757 - }
758 - }
759 -
760 - self.disconnect();
761 - self.state = ClientState::Broken;
762 - self.state = self.try_connect();
763 - if self.state != ClientState::Ready {
764 - self.error_count += 1;
765 - return Err(first_err);
766 - }
767 - self.reconnect_count += 1;
768 -
769 - if self.session_max_request_payload_bytes() <= prev_req
770 - && self.session_max_response_payload_bytes() <= prev_resp
771 - && self.transport_config.max_request_payload_bytes <= prev_cfg_req
772 - && self.transport_config.max_response_payload_bytes <= prev_cfg_resp
773 - {
774 - self.disconnect();
775 - self.state = ClientState::Broken;
776 - self.error_count += 1;
777 - return Err(first_err);
778 - }
779 -
780 - overflow_retries += 1;
781 - if overflow_retries >= 8 {
782 - self.disconnect();
783 - self.state = ClientState::Broken;
784 - self.error_count += 1;
785 - return Err(first_err);
786 - }
787 - }
788 - }
789 - }
790 - }
791 -
792 - /// Single attempt at a raw call for any method.
793 - fn do_raw_call(
794 - &mut self,
795 - method_code: u16,
796 - request_payload: &[u8],
797 - ) -> Result<ClientResponseRef, NipcError> {
798 - // 1. Build outer header
799 - let mut hdr = Header {
800 - kind: KIND_REQUEST,
801 - code: method_code,
802 - flags: 0,
803 - item_count: 1,
804 - message_id: (self.call_count as u64) + 1,
805 - transport_status: STATUS_OK,
806 - ..Header::default()
807 - };
808 -
809 - // 2. Send via L1 (SHM or UDS)
810 - self.transport_send(&mut hdr, request_payload)?;
811 -
812 - // 3. Receive via L1
813 - let (resp_hdr, response) = self.transport_receive()?;
814 -
815 - // 4. Verify response envelope fields before decode
816 - if resp_hdr.kind != KIND_RESPONSE {
817 - return Err(NipcError::BadKind);
818 - }
819 - if resp_hdr.code != method_code {
820 - return Err(NipcError::BadLayout);
821 - }
822 - if resp_hdr.message_id != hdr.message_id {
823 - return Err(NipcError::BadLayout);
824 - }
825 -
826 - // 5. Check transport_status BEFORE decode (spec requirement)
827 - match resp_hdr.transport_status {
828 - STATUS_OK => {}
829 - STATUS_LIMIT_EXCEEDED => {
830 - let current = self.session_max_response_payload_bytes();
831 - if current > 0 {
832 - self.client_note_response_capacity(current.saturating_mul(2));
833 - }
834 - return Err(NipcError::Overflow);
835 - }
836 - _ => return Err(NipcError::BadLayout),
837 - }
838 - Ok(response)
839 - }
840 -
841 - fn do_raw_call_from_request_buf(
842 - &mut self,
843 - method_code: u16,
844 - req_len: usize,
845 - ) -> Result<ClientResponseRef, NipcError> {
846 - let mut hdr = Header {
847 - kind: KIND_REQUEST,
848 - code: method_code,
849 - flags: 0,
850 - item_count: 1,
851 - message_id: (self.call_count as u64) + 1,
852 - transport_status: STATUS_OK,
853 - ..Header::default()
854 - };
855 -
856 - self.transport_send_request_buf(&mut hdr, req_len)?;
857 - let (resp_hdr, response) = self.transport_receive()?;
858 -
859 - if resp_hdr.kind != KIND_RESPONSE {
860 - return Err(NipcError::BadKind);
861 - }
862 - if resp_hdr.code != method_code {
863 - return Err(NipcError::BadLayout);
864 - }
865 - if resp_hdr.message_id != hdr.message_id {
866 - return Err(NipcError::BadLayout);
867 - }
868 - match resp_hdr.transport_status {
869 - STATUS_OK => {}
870 - STATUS_LIMIT_EXCEEDED => {
871 - let current = self.session_max_response_payload_bytes();
872 - if current > 0 {
873 - self.client_note_response_capacity(current.saturating_mul(2));
874 - }
875 - return Err(NipcError::Overflow);
876 - }
877 - _ => return Err(NipcError::BadLayout),
878 - }
879 - Ok(response)
880 - }
881 -
882 - /// Single attempt at a raw batch call. Like `do_raw_call` but sets
883 - /// FLAG_BATCH and item_count, and validates the response matches.
884 - fn do_raw_batch_call_from_request_buf(
885 - &mut self,
886 - method_code: u16,
887 - req_len: usize,
888 - item_count: u32,
889 - ) -> Result<ClientResponseRef, NipcError> {
890 - let mut hdr = Header {
891 - kind: KIND_REQUEST,
892 - code: method_code,
893 - flags: FLAG_BATCH,
894 - item_count,
895 - message_id: (self.call_count as u64) + 1,
896 - transport_status: STATUS_OK,
897 - ..Header::default()
898 - };
899 -
900 - self.transport_send_request_buf(&mut hdr, req_len)?;
901 -
902 - let (resp_hdr, response) = self.transport_receive()?;
903 -
904 - if resp_hdr.kind != KIND_RESPONSE {
905 - return Err(NipcError::BadKind);
906 - }
907 - if resp_hdr.code != method_code {
908 - return Err(NipcError::BadLayout);
909 - }
910 - if resp_hdr.message_id != hdr.message_id {
911 - return Err(NipcError::BadLayout);
912 - }
913 - match resp_hdr.transport_status {
914 - STATUS_OK => {}
915 - STATUS_LIMIT_EXCEEDED => {
916 - let current = self.session_max_response_payload_bytes();
917 - if current > 0 {
918 - self.client_note_response_capacity(current.saturating_mul(2));
919 - }
920 - return Err(NipcError::Overflow);
921 - }
922 - _ => return Err(NipcError::BadLayout),
923 - }
924 - if resp_hdr.item_count != item_count {
925 - return Err(NipcError::BadItemCount);
926 - }
927 - Ok(response)
928 - }
929 -
930 - /// Send via the active transport (SHM if available, baseline otherwise).
931 - fn transport_send(&mut self, hdr: &mut Header, payload: &[u8]) -> Result<(), NipcError> {
932 - let max_request_payload_bytes = self.session_max_request_payload_bytes();
933 -
934 - // SHM path (POSIX or Windows)
935 - #[cfg(target_os = "linux")]
936 - {
937 - if let Some(ref mut shm) = self.shm {
938 - if payload.len() > max_request_payload_bytes as usize {
939 - self.client_note_request_capacity(payload.len() as u32);
940 - return Err(NipcError::Overflow);
941 - }
942 -
943 - let msg_len = HEADER_SIZE + payload.len();
944 - let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
945 -
946 - hdr.magic = MAGIC_MSG;
947 - hdr.version = VERSION;
948 - hdr.header_len = protocol::HEADER_LEN;
949 - hdr.payload_len = payload.len() as u32;
950 -
951 - hdr.encode(&mut msg[..HEADER_SIZE]);
952 - if !payload.is_empty() {
953 - msg[HEADER_SIZE..].copy_from_slice(payload);
954 - }
955 -
956 - let send_result = shm.send(&msg);
957 - return match send_result {
958 - Ok(()) => Ok(()),
959 - Err(crate::transport::shm::ShmError::MsgTooLarge) => {
960 - self.client_note_request_capacity(payload.len() as u32);
961 - Err(NipcError::Overflow)
962 - }
963 - Err(_) => Err(NipcError::Truncated),
964 - };
965 - }
966 - }
967 -
968 - #[cfg(windows)]
969 - {
970 - if let Some(ref mut shm) = self.shm {
971 - if payload.len() > max_request_payload_bytes as usize {
972 - self.client_note_request_capacity(payload.len() as u32);
973 - return Err(NipcError::Overflow);
974 - }
975 -
976 - let msg_len = HEADER_SIZE + payload.len();
977 - let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
978 -
979 - hdr.magic = MAGIC_MSG;
980 - hdr.version = VERSION;
981 - hdr.header_len = protocol::HEADER_LEN;
982 - hdr.payload_len = payload.len() as u32;
983 -
984 - hdr.encode(&mut msg[..HEADER_SIZE]);
985 - if !payload.is_empty() {
986 - msg[HEADER_SIZE..].copy_from_slice(payload);
987 - }
988 -
989 - let send_result = shm.send(&msg);
990 - return match send_result {
991 - Ok(()) => Ok(()),
992 - Err(crate::transport::win_shm::WinShmError::MsgTooLarge) => {
993 - self.client_note_request_capacity(payload.len() as u32);
994 - Err(NipcError::Overflow)
995 - }
996 - Err(_) => Err(NipcError::Truncated),
997 - };
998 - }
999 - }
1000 -
1001 - // Baseline transport path
1002 - let send_result = {
1003 - let session = self.session.as_mut().ok_or(NipcError::Truncated)?;
1004 - session.send(hdr, payload)
1005 - };
1006 - match send_result {
1007 - Ok(()) => Ok(()),
1008 - #[cfg(unix)]
1009 - Err(crate::transport::posix::UdsError::LimitExceeded) => {
1010 - self.client_note_request_capacity(payload.len() as u32);
1011 - Err(NipcError::Overflow)
1012 - }
1013 - #[cfg(windows)]
1014 - Err(crate::transport::windows::NpError::LimitExceeded) => {
1015 - self.client_note_request_capacity(payload.len() as u32);
1016 - Err(NipcError::Overflow)
1017 - }
1018 - Err(_) => Err(NipcError::Truncated),
1019 - }
1020 - }
1021 -
1022 - fn transport_send_request_buf(
1023 - &mut self,
1024 - hdr: &mut Header,
1025 - req_len: usize,
1026 - ) -> Result<(), NipcError> {
1027 - let max_request_payload_bytes = self.session_max_request_payload_bytes();
1028 -
1029 - #[cfg(target_os = "linux")]
1030 - {
1031 - if let Some(ref mut shm) = self.shm {
1032 - if req_len > max_request_payload_bytes as usize {
1033 - self.client_note_request_capacity(req_len as u32);
1034 - return Err(NipcError::Overflow);
1035 - }
1036 -
1037 - let msg_len = HEADER_SIZE + req_len;
1038 - let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
1039 -
1040 - hdr.magic = MAGIC_MSG;
1041 - hdr.version = VERSION;
1042 - hdr.header_len = protocol::HEADER_LEN;
1043 - hdr.payload_len = req_len as u32;
1044 -
1045 - hdr.encode(&mut msg[..HEADER_SIZE]);
1046 - if req_len > 0 {
1047 - msg[HEADER_SIZE..HEADER_SIZE + req_len]
1048 - .copy_from_slice(&self.request_buf[..req_len]);
1049 - }
1050 -
1051 - let send_result = shm.send(&msg[..msg_len]);
1052 - return match send_result {
1053 - Ok(()) => Ok(()),
1054 - Err(crate::transport::shm::ShmError::MsgTooLarge) => {
1055 - self.client_note_request_capacity(req_len as u32);
1056 - Err(NipcError::Overflow)
1057 - }
1058 - Err(_) => Err(NipcError::Truncated),
1059 - };
1060 - }
1061 - }
1062 -
1063 - #[cfg(windows)]
1064 - {
1065 - if let Some(ref mut shm) = self.shm {
1066 - if req_len > max_request_payload_bytes as usize {
1067 - self.client_note_request_capacity(req_len as u32);
1068 - return Err(NipcError::Overflow);
1069 - }
1070 -
1071 - let msg_len = HEADER_SIZE + req_len;
1072 - let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
1073 -
1074 - hdr.magic = MAGIC_MSG;
1075 - hdr.version = VERSION;
1076 - hdr.header_len = protocol::HEADER_LEN;
1077 - hdr.payload_len = req_len as u32;
1078 -
1079 - hdr.encode(&mut msg[..HEADER_SIZE]);
1080 - if req_len > 0 {
1081 - msg[HEADER_SIZE..HEADER_SIZE + req_len]
1082 - .copy_from_slice(&self.request_buf[..req_len]);
1083 - }
1084 -
1085 - let send_result = shm.send(&msg[..msg_len]);
1086 - return match send_result {
1087 - Ok(()) => Ok(()),
1088 - Err(crate::transport::win_shm::WinShmError::MsgTooLarge) => {
1089 - self.client_note_request_capacity(req_len as u32);
1090 - Err(NipcError::Overflow)
1091 - }
1092 - Err(_) => Err(NipcError::Truncated),
1093 - };
1094 - }
1095 - }
1096 -
1097 - let send_result = {
1098 - let session = self.session.as_mut().ok_or(NipcError::Truncated)?;
1099 - session.send(hdr, &self.request_buf[..req_len])
1100 - };
1101 - match send_result {
1102 - Ok(()) => Ok(()),
1103 - #[cfg(unix)]
1104 - Err(crate::transport::posix::UdsError::LimitExceeded) => {
1105 - self.client_note_request_capacity(req_len as u32);
1106 - Err(NipcError::Overflow)
1107 - }
1108 - #[cfg(windows)]
1109 - Err(crate::transport::windows::NpError::LimitExceeded) => {
1110 - self.client_note_request_capacity(req_len as u32);
1111 - Err(NipcError::Overflow)
1112 - }
1113 - Err(_) => Err(NipcError::Truncated),
1114 - }
1115 - }
1116 -
1117 - /// Receive via the active transport. Returns (header, payload_view).
1118 - fn transport_receive(&mut self) -> Result<(Header, ClientResponseRef), NipcError> {
1119 - let needed = self.max_receive_message_bytes();
1120 - let scratch = ensure_client_scratch(&mut self.transport_buf, needed);
1121 -
1122 - // SHM path (POSIX or Windows)
1123 - #[cfg(target_os = "linux")]
1124 - {
1125 - if let Some(ref mut shm) = self.shm {
1126 - let mlen = shm
1127 - .receive(scratch, 30000)
1128 - .map_err(|_| NipcError::Truncated)?;
1129 -
1130 - if mlen < HEADER_SIZE {
1131 - return Err(NipcError::Truncated);
1132 - }
1133 -
1134 - let hdr = Header::decode(&scratch[..mlen])?;
1135 - return Ok((
1136 - hdr,
1137 - ClientResponseRef {
1138 - source: ClientResponseSource::TransportBuf,
1139 - len: mlen - HEADER_SIZE,
1140 - },
1141 - ));
1142 - }
1143 - }
1144 -
1145 - #[cfg(windows)]
1146 - {
1147 - if let Some(ref mut shm) = self.shm {
1148 - let mlen = shm
1149 - .receive(scratch, 30000)
1150 - .map_err(|_| NipcError::Truncated)?;
1151 -
1152 - if mlen < HEADER_SIZE {
1153 - return Err(NipcError::Truncated);
1154 - }
1155 -
1156 - let hdr = Header::decode(&scratch[..mlen])?;
1157 - return Ok((
1158 - hdr,
1159 - ClientResponseRef {
1160 - source: ClientResponseSource::TransportBuf,
1161 - len: mlen - HEADER_SIZE,
1162 - },
1163 - ));
1164 - }
1165 - }
1166 -
1167 - // Baseline transport: UDS on POSIX, Named Pipe on Windows
1168 - let session = self.session.as_mut().ok_or(NipcError::Truncated)?;
1169 -
1170 - #[cfg(unix)]
1171 - {
1172 - let scratch_payload_ptr = unsafe { scratch.as_ptr().add(HEADER_SIZE) };
1173 - let (hdr, payload) = session.receive(scratch).map_err(|_| NipcError::Truncated)?;
1174 - let source = if payload.as_ptr() == scratch_payload_ptr {
1175 - ClientResponseSource::TransportBuf
1176 - } else {
1177 - ClientResponseSource::SessionBuf
1178 - };
1179 - Ok((
1180 - hdr,
1181 - ClientResponseRef {
1182 - source,
1183 - len: payload.len(),
1184 - },
1185 - ))
1186 - }
1187 -
1188 - #[cfg(windows)]
1189 - {
1190 - let scratch_payload_ptr = unsafe { scratch.as_ptr().add(HEADER_SIZE) };
1191 - let (hdr, payload) = session.receive(scratch).map_err(|_| NipcError::Truncated)?;
1192 - let source = if payload.as_ptr() == scratch_payload_ptr {
1193 - ClientResponseSource::TransportBuf
1194 - } else {
1195 - ClientResponseSource::SessionBuf
1196 - };
1197 - Ok((
1198 - hdr,
1199 - ClientResponseRef {
1200 - source,
1201 - len: payload.len(),
1202 - },
1203 - ))
1204 - }
1205 - }
1206 -
1207 - fn response_payload(&self, response: ClientResponseRef) -> Result<&[u8], NipcError> {
1208 - match response.source {
1209 - ClientResponseSource::TransportBuf => {
1210 - let start = HEADER_SIZE;
1211 - let end = HEADER_SIZE + response.len;
1212 - if end > self.transport_buf.len() {
1213 - return Err(NipcError::Truncated);
1214 - }
1215 - Ok(&self.transport_buf[start..end])
1216 - }
1217 - ClientResponseSource::SessionBuf => {
1218 - #[cfg(unix)]
1219 - {
1220 - let session = self.session.as_ref().ok_or(NipcError::Truncated)?;
1221 - return Ok(session.received_payload(response.len));
1222 - }
1223 - #[cfg(windows)]
1224 - {
1225 - let session = self.session.as_ref().ok_or(NipcError::Truncated)?;
1226 - return Ok(session.received_payload(response.len));
1227 - }
1228 - #[allow(unreachable_code)]
1229 - Err(NipcError::Truncated)
1230 - }
1231 - }
1232 - }
1233 -
1234 - fn max_receive_message_bytes(&self) -> usize {
1235 - let mut max_payload = self.transport_config.max_response_payload_bytes as usize;
1236 - #[cfg(unix)]
1237 - if let Some(ref session) = self.session {
1238 - if session.max_response_payload_bytes > 0 {
1239 - max_payload = session.max_response_payload_bytes as usize;
1240 - }
1241 - }
1242 - #[cfg(windows)]
1243 - if let Some(ref session) = self.session {
1244 - if session.max_response_payload_bytes > 0 {
1245 - max_payload = session.max_response_payload_bytes as usize;
1246 - }
1247 - }
1248 - if max_payload == 0 {
1249 - max_payload = CACHE_RESPONSE_BUF_SIZE;
1250 - }
1251 - HEADER_SIZE + max_payload
1252 - }
1253 -}
1254 -
1255 -impl Drop for RawClient {
1256 - fn drop(&mut self) {
1257 - self.close();
1258 - }
1259 -}
1260 -
1261 -fn ensure_client_scratch(buf: &mut Vec<u8>, needed: usize) -> &mut [u8] {
1262 - if buf.len() < needed {
1263 - buf.resize(needed, 0);
1264 - }
1265 - &mut buf[..needed]
1266 -}
1267 -
1268 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1269 -enum ClientResponseSource {
1270 - TransportBuf,
1271 - SessionBuf,
1272 -}
1273 -
1274 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1275 -struct ClientResponseRef {
1276 - source: ClientResponseSource,
1277 - len: usize,
1278 -}
1279 -
1280 -fn dispatch_single_internal(
1281 - expected_method_code: u16,
1282 - handler: Option<&DispatchHandler>,
1283 - method_code: u16,
1284 - request: &[u8],
1285 - response_buf: &mut [u8],
1286 -) -> Result<usize, DispatchError> {
1287 - if method_code != expected_method_code {
1288 - return Err(DispatchError::HandlerFailed);
1289 - }
1290 -
1291 - match handler {
1292 - Some(dispatch) => match dispatch(request, response_buf) {
1293 - Ok(n) if n <= response_buf.len() => Ok(n),
1294 - Ok(_) => Err(DispatchError::Overflow),
1295 - Err(err) => Err(err),
1296 - },
1297 - None => Err(DispatchError::HandlerFailed),
1298 - }
1299 -}
1300 -
1301 -#[cfg(test)]
1302 -#[allow(dead_code)]
1303 -fn dispatch_single(
1304 - expected_method_code: u16,
1305 - handler: Option<&DispatchHandler>,
1306 - method_code: u16,
1307 - request: &[u8],
1308 - response_buf: &mut [u8],
1309 -) -> Result<usize, DispatchError> {
1310 - dispatch_single_internal(
1311 - expected_method_code,
1312 - handler,
1313 - method_code,
1314 - request,
1315 - response_buf,
1316 - )
1317 -}
1318 -
1319 -fn method_supported_internal(
1320 - expected_method_code: u16,
1321 - handler: Option<&DispatchHandler>,
1322 - method_code: u16,
1323 -) -> bool {
1324 - handler.is_some() && method_code == expected_method_code
1325 -}
1326 -
1327 -fn server_note_payload_capacity(target: &AtomicU32, payload_len: u32) {
1328 - let grown = next_power_of_2_u32(payload_len);
1329 - let mut current = target.load(Ordering::Relaxed);
1330 - while grown > current {
1331 - match target.compare_exchange_weak(current, grown, Ordering::Release, Ordering::Relaxed) {
1332 - Ok(_) => break,
1333 - Err(observed) => current = observed,
1334 - }
1335 - }
1336 -}
1337 -
1338 -// ---------------------------------------------------------------------------
1339 -// Managed server
1340 -// ---------------------------------------------------------------------------
1341 -
1342 -pub type IncrementHandler = Arc<dyn Fn(u64) -> Option<u64> + Send + Sync>;
1343 -pub type StringReverseHandler = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
1344 -pub type SnapshotHandler =
1345 - Arc<dyn for<'a> Fn(&CgroupsRequest, &mut protocol::CgroupsBuilder<'a>) -> bool + Send + Sync>;
1346 -
1347 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1348 -pub enum DispatchError {
1349 - BadEnvelope,
1350 - Overflow,
1351 - HandlerFailed,
1352 -}
1353 -
1354 -pub type DispatchHandler =
1355 - Arc<dyn Fn(&[u8], &mut [u8]) -> Result<usize, DispatchError> + Send + Sync>;
1356 -
1357 -pub fn increment_dispatch(handler: IncrementHandler) -> DispatchHandler {
1358 - Arc::new(move |request, response_buf| {
1359 - let value = increment_decode(request).map_err(|_| DispatchError::BadEnvelope)?;
1360 - let result = handler(value).ok_or(DispatchError::HandlerFailed)?;
1361 - let n = increment_encode(result, response_buf);
1362 - if n == 0 {
1363 - return Err(DispatchError::Overflow);
1364 - }
1365 - Ok(n)
1366 - })
1367 -}
1368 -
1369 -pub fn string_reverse_dispatch(handler: StringReverseHandler) -> DispatchHandler {
1370 - Arc::new(move |request, response_buf| {
1371 - let view = string_reverse_decode(request).map_err(|_| DispatchError::BadEnvelope)?;
1372 - let result = handler(view.as_str()).ok_or(DispatchError::HandlerFailed)?;
1373 - let n = string_reverse_encode(result.as_bytes(), response_buf);
1374 - if n == 0 {
1375 - return Err(DispatchError::Overflow);
1376 - }
1377 - Ok(n)
1378 - })
1379 -}
1380 -
1381 -pub fn snapshot_max_items(response_buf_size: usize, override_max_items: u32) -> u32 {
1382 - if override_max_items != 0 {
1383 - return override_max_items;
1384 - }
1385 - protocol::estimate_cgroups_max_items(response_buf_size)
1386 -}
1387 -
1388 -pub fn snapshot_dispatch(handler: SnapshotHandler, max_items: u32) -> DispatchHandler {
1389 - Arc::new(move |request, response_buf| {
1390 - let request = CgroupsRequest::decode(request).map_err(|_| DispatchError::BadEnvelope)?;
1391 - let item_budget = snapshot_max_items(response_buf.len(), max_items);
1392 - if item_budget == 0 {
1393 - return Err(DispatchError::Overflow);
1394 - }
1395 - let mut builder = protocol::CgroupsBuilder::new(response_buf, item_budget, 0, 0);
1396 - if !handler(&request, &mut builder) {
1397 - return Err(DispatchError::HandlerFailed);
1398 - }
1399 - let n = builder.finish();
1400 - if n == 0 {
1401 - return Err(DispatchError::Overflow);
1402 - }
1403 - Ok(n)
1404 - })
1405 -}
1406 -
1407 -/// L2 managed server. Typed request/response dispatcher.
1408 -///
1409 -/// Handles accept, spawns a thread per session (up to worker_count),
1410 -/// reads requests, dispatches to handler, sends responses.
1411 -pub struct ManagedServer {
1412 - run_dir: String,
1413 - service_name: String,
1414 - server_config: ServerConfig,
1415 - expected_method_code: u16,
1416 - handler: Option<DispatchHandler>,
1417 - running: Arc<AtomicBool>,
1418 - learned_request_payload_bytes: Arc<AtomicU32>,
1419 - learned_response_payload_bytes: Arc<AtomicU32>,
1420 - next_session_id: u64,
1421 - worker_count: usize,
1422 - /// Windows: stored listener handle so stop() can close it to unblock Accept.
1423 - #[cfg(windows)]
1424 - listener_handle: Arc<std::sync::Mutex<Option<usize>>>,
1425 -}
1426 -
1427 -impl ManagedServer {
1428 - /// Create a new managed server for a single service kind.
1429 - pub fn new(
1430 - run_dir: &str,
1431 - service_name: &str,
1432 - config: ServerConfig,
1433 - expected_method_code: u16,
1434 - handler: Option<DispatchHandler>,
1435 - ) -> Self {
1436 - Self::with_workers(
1437 - run_dir,
1438 - service_name,
1439 - config,
1440 - expected_method_code,
1441 - handler,
1442 - 8,
1443 - )
1444 - }
1445 -
1446 - /// Create a managed server with an explicit worker count.
1447 - pub fn with_workers(
1448 - run_dir: &str,
1449 - service_name: &str,
1450 - config: ServerConfig,
1451 - expected_method_code: u16,
1452 - handler: Option<DispatchHandler>,
1453 - worker_count: usize,
1454 - ) -> Self {
1455 - let learned_request = if config.max_request_payload_bytes != 0 {
1456 - config.max_request_payload_bytes
1457 - } else {
1458 - MAX_PAYLOAD_DEFAULT
1459 - };
1460 - let learned_response = if config.max_response_payload_bytes != 0 {
1461 - config.max_response_payload_bytes
1462 - } else {
1463 - MAX_PAYLOAD_DEFAULT
1464 - };
1465 -
1466 - ManagedServer {
1467 - run_dir: run_dir.to_string(),
1468 - service_name: service_name.to_string(),
1469 - server_config: config,
1470 - expected_method_code,
1471 - handler,
1472 - running: Arc::new(AtomicBool::new(false)),
1473 - learned_request_payload_bytes: Arc::new(AtomicU32::new(learned_request)),
1474 - learned_response_payload_bytes: Arc::new(AtomicU32::new(learned_response)),
1475 - next_session_id: 1,
1476 - worker_count: if worker_count < 1 { 1 } else { worker_count },
1477 - #[cfg(windows)]
1478 - listener_handle: Arc::new(std::sync::Mutex::new(None)),
1479 - }
1480 - }
1481 -
1482 - /// Run the acceptor loop. Blocking. Accepts clients, spawns a
1483 - /// thread per session (up to worker_count concurrent sessions).
1484 - ///
1485 - /// Returns when `stop()` is called or on fatal error.
1486 - #[cfg(unix)]
1487 - pub fn run(&mut self) -> Result<(), NipcError> {
1488 - #[cfg(target_os = "linux")]
1489 - crate::transport::shm::cleanup_stale(&self.run_dir, &self.service_name);
1490 -
1491 - let listener = UdsListener::bind(
1492 - &self.run_dir,
1493 - &self.service_name,
1494 - self.server_config.clone(),
1495 - )
1496 - .map_err(|_| NipcError::BadLayout)?;
1497 -
1498 - self.running.store(true, Ordering::Release);
1499 -
1500 - let mut session_threads: Vec<std::thread::JoinHandle<()>> = Vec::new();
1501 -
1502 - while self.running.load(Ordering::Acquire) {
1503 - let ready = poll_fd(listener.fd(), SERVER_POLL_TIMEOUT_MS as i32);
1504 - if ready < 0 {
1505 - break;
1506 - }
1507 - if ready == 0 {
1508 - // Reap finished threads periodically
1509 - session_threads.retain(|t| !t.is_finished());
1510 - continue;
1511 - }
1512 -
1513 - let (session_id, accept_cfg, precreated_shm, ready) = self.prepare_unix_accept();
1514 - if !ready {
1515 - std::thread::sleep(std::time::Duration::from_millis(10));
1516 - continue;
1517 - }
1518 -
1519 - let session = match listener.accept_with_config(session_id, accept_cfg) {
1520 - Ok(s) => s,
1521 - Err(_) => {
1522 - #[cfg(target_os = "linux")]
1523 - if let Some(mut shm) = precreated_shm {
1524 - shm.destroy();
1525 - }
1526 - if !self.running.load(Ordering::Acquire) {
1527 - break;
1528 - }
1529 - std::thread::sleep(std::time::Duration::from_millis(10));
1530 - continue;
1531 - }
1532 - };
1533 -
1534 - // Check worker count limit (non-blocking)
1535 - // Reap finished threads first
1536 - session_threads.retain(|t| !t.is_finished());
1537 - if session_threads.len() >= self.worker_count {
1538 - // At capacity: reject client
1539 - #[cfg(target_os = "linux")]
1540 - if let Some(mut shm) = precreated_shm {
1541 - shm.destroy();
1542 - }
1543 - drop(session);
1544 - continue;
1545 - }
1546 -
1547 - #[cfg(target_os = "linux")]
1548 - let shm = match self.finalize_unix_shm(&session, precreated_shm) {
1549 - Some(shm) => Some(shm),
1550 - None if session.selected_profile == PROFILE_SHM_HYBRID
1551 - || session.selected_profile == PROFILE_SHM_FUTEX =>
1552 - {
1553 - drop(session);
1554 - continue;
1555 - }
1556 - None => None,
1557 - };
1558 - #[cfg(not(target_os = "linux"))]
1559 - let shm: Option<()> = None;
1560 -
1561 - // Spawn a handler thread for this session
1562 - let expected_method_code = self.expected_method_code;
1563 - let handler = self.handler.clone();
1564 - let running = self.running.clone();
1565 - let learned_request_payload_bytes = self.learned_request_payload_bytes.clone();
1566 - let learned_response_payload_bytes = self.learned_response_payload_bytes.clone();
1567 -
1568 - let t = std::thread::spawn(move || {
1569 - handle_session_threaded(
1570 - session,
1571 - #[cfg(target_os = "linux")]
1572 - shm,
1573 - #[cfg(not(target_os = "linux"))]
1574 - shm,
1575 - expected_method_code,
1576 - handler,
1577 - running,
1578 - learned_request_payload_bytes,
1579 - learned_response_payload_bytes,
1580 - );
1581 - });
1582 - session_threads.push(t);
1583 - }
1584 -
1585 - // Wait for all active session threads
1586 - for t in session_threads {
1587 - let _ = t.join();
1588 - }
1589 -
1590 - Ok(())
1591 - }
1592 -
1593 - /// Windows: run the acceptor loop over Named Pipes.
1594 - #[cfg(windows)]
1595 - pub fn run(&mut self) -> Result<(), NipcError> {
1596 - // Win SHM cleanup is a no-op: kernel objects auto-clean on handle close.
1597 -
1598 - let mut listener = NpListener::bind(
1599 - &self.run_dir,
1600 - &self.service_name,
1601 - self.server_config.clone(),
1602 - )
1603 - .map_err(|_| NipcError::BadLayout)?;
1604 -
1605 - // Store listener handle so stop() can close it to unblock Accept
1606 - *self.listener_handle.lock().unwrap() = Some(listener.handle() as usize);
1607 -
1608 - self.running.store(true, Ordering::Release);
1609 -
1610 - let mut session_threads: Vec<std::thread::JoinHandle<()>> = Vec::new();
1611 -
1612 - while self.running.load(Ordering::Acquire) {
1613 - let (session_id, accept_cfg, prepared_shm, ready) = self.prepare_windows_accept();
1614 - if !ready {
1615 - std::thread::sleep(std::time::Duration::from_millis(10));
1616 - continue;
1617 - }
1618 -
1619 - let session = match listener.accept_with_config(session_id, accept_cfg) {
1620 - Ok(s) => s,
1621 - Err(_) => {
1622 - if let Some(mut prepared) = prepared_shm {
1623 - prepared.destroy_all();
1624 - }
1625 - if !self.running.load(Ordering::Acquire) {
1626 - break;
1627 - }
1628 - std::thread::sleep(std::time::Duration::from_millis(10));
1629 - continue;
1630 - }
1631 - };
1632 -
1633 - // Reap finished threads
1634 - session_threads.retain(|t| !t.is_finished());
1635 - if session_threads.len() >= self.worker_count {
1636 - if let Some(mut prepared) = prepared_shm {
1637 - prepared.destroy_all();
1638 - }
1639 - drop(session);
1640 - continue;
1641 - }
1642 -
1643 - let shm = match self.finalize_windows_shm(&session, prepared_shm) {
1644 - Some(shm) => Some(shm),
1645 - None if session.selected_profile == WIN_SHM_PROFILE_HYBRID
1646 - || session.selected_profile == WIN_SHM_PROFILE_BUSYWAIT =>
1647 - {
1648 - drop(session);
1649 - continue;
1650 - }
1651 - None => None,
1652 - };
1653 -
1654 - if shm.is_none()
1655 - && (session.selected_profile == WIN_SHM_PROFILE_HYBRID
1656 - || session.selected_profile == WIN_SHM_PROFILE_BUSYWAIT)
1657 - {
1658 - drop(session);
1659 - continue;
1660 - }
1661 -
1662 - let expected_method_code = self.expected_method_code;
1663 - let handler = self.handler.clone();
1664 - let running = self.running.clone();
1665 - let learned_request_payload_bytes = self.learned_request_payload_bytes.clone();
1666 - let learned_response_payload_bytes = self.learned_response_payload_bytes.clone();
1667 - let t = std::thread::spawn(move || {
1668 - handle_session_win_threaded(
1669 - session,
1670 - shm,
1671 - expected_method_code,
1672 - handler,
1673 - running,
1674 - learned_request_payload_bytes,
1675 - learned_response_payload_bytes,
1676 - );
1677 - });
1678 - session_threads.push(t);
1679 - }
1680 -
1681 - for t in session_threads {
1682 - let _ = t.join();
1683 - }
1684 -
1685 - Ok(())
1686 - }
1687 -
1688 - /// Signal shutdown. On Windows, also closes the listener pipe to
1689 - /// unblock ConnectNamedPipe in the accept loop.
1690 - pub fn stop(&self) {
1691 - self.running.store(false, Ordering::Release);
1692 -
1693 - #[cfg(windows)]
1694 - {
1695 - let mut guard = self.listener_handle.lock().unwrap();
1696 - if let Some(h) = guard.take() {
1697 - // Close the listener pipe to unblock ConnectNamedPipe
1698 - extern "system" {
1699 - fn CloseHandle(h: isize) -> i32;
1700 - }
1701 - unsafe {
1702 - CloseHandle(h as isize);
1703 - }
1704 - }
1705 - }
1706 - }
1707 -
1708 - /// Returns the internal running flag for diagnostics and test helpers.
1709 - ///
1710 - /// For reliable shutdown, call `stop()`. On Windows, flipping this flag
1711 - /// alone does not wake a blocking listener accept.
1712 - pub fn running_flag(&self) -> Arc<AtomicBool> {
1713 - self.running.clone()
1714 - }
1715 -
1716 - // ------------------------------------------------------------------
1717 - // Internal helpers
1718 - // ------------------------------------------------------------------
1719 -
1720 - #[cfg(target_os = "linux")]
1721 - fn prepare_unix_accept(&mut self) -> (u64, ServerConfig, Option<ShmContext>, bool) {
1722 - let session_id = self.next_session_id;
1723 - self.next_session_id += 1;
1724 -
1725 - let mut cfg = self.server_config.clone();
1726 - cfg.max_request_payload_bytes = self.learned_request_payload_bytes.load(Ordering::Acquire);
1727 - cfg.max_response_payload_bytes =
1728 - self.learned_response_payload_bytes.load(Ordering::Acquire);
1729 -
1730 - let shm_profiles = cfg.supported_profiles & (PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
1731 - if shm_profiles == 0 {
1732 - return (session_id, cfg, None, true);
1733 - }
1734 -
1735 - match ShmContext::server_create(
1736 - &self.run_dir,
1737 - &self.service_name,
1738 - session_id,
1739 - cfg.max_request_payload_bytes + HEADER_SIZE as u32,
1740 - cfg.max_response_payload_bytes + HEADER_SIZE as u32,
1741 - ) {
1742 - Ok(ctx) => (session_id, cfg, Some(ctx), true),
1743 - Err(_) => {
1744 - cfg.supported_profiles &= !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
1745 - cfg.preferred_profiles &= !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
1746 - (session_id, cfg.clone(), None, cfg.supported_profiles != 0)
1747 - }
1748 - }
1749 - }
1750 -
1751 - #[cfg(target_os = "linux")]
1752 - fn finalize_unix_shm(
1753 - &self,
1754 - session: &UdsSession,
1755 - mut shm: Option<ShmContext>,
1756 - ) -> Option<ShmContext> {
1757 - let profile = session.selected_profile;
1758 - if profile != PROFILE_SHM_HYBRID && profile != PROFILE_SHM_FUTEX {
1759 - if let Some(ref mut ctx) = shm {
1760 - ctx.destroy();
1761 - }
1762 - return None;
1763 - }
1764 - shm
1765 - }
1766 -
1767 - #[cfg(windows)]
1768 - fn prepare_windows_accept(&mut self) -> (u64, ServerConfig, Option<PreparedWinShm>, bool) {
1769 - let session_id = self.next_session_id;
1770 - self.next_session_id += 1;
1771 -
1772 - let mut cfg = self.server_config.clone();
1773 - cfg.max_request_payload_bytes = self.learned_request_payload_bytes.load(Ordering::Acquire);
1774 - cfg.max_response_payload_bytes =
1775 - self.learned_response_payload_bytes.load(Ordering::Acquire);
1776 -
1777 - let shm_profiles =
1778 - cfg.supported_profiles & (WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
1779 - if shm_profiles == 0 {
1780 - return (session_id, cfg, None, true);
1781 - }
1782 -
1783 - let mut prepared = PreparedWinShm::default();
1784 - for profile in [WIN_SHM_PROFILE_HYBRID, WIN_SHM_PROFILE_BUSYWAIT] {
1785 - if cfg.supported_profiles & profile == 0 {
1786 - continue;
1787 - }
1788 -
1789 - match WinShmContext::server_create(
1790 - &self.run_dir,
1791 - &self.service_name,
1792 - self.server_config.auth_token,
1793 - session_id,
1794 - profile,
1795 - cfg.max_request_payload_bytes + HEADER_SIZE as u32,
1796 - cfg.max_response_payload_bytes + HEADER_SIZE as u32,
1797 - ) {
1798 - Ok(ctx) => prepared.insert(profile, ctx),
1799 - Err(_) => {
1800 - cfg.supported_profiles &= !profile;
1801 - cfg.preferred_profiles &= !profile;
1802 - }
1803 - }
1804 - }
1805 -
1806 - if cfg.supported_profiles == 0 {
1807 - prepared.destroy_all();
1808 - return (session_id, cfg, None, false);
1809 - }
1810 -
1811 - if prepared.is_empty() {
1812 - return (session_id, cfg, None, true);
1813 - }
1814 -
1815 - (session_id, cfg, Some(prepared), true)
1816 - }
1817 -
1818 - #[cfg(windows)]
1819 - fn finalize_windows_shm(
1820 - &self,
1821 - session: &NpSession,
1822 - mut prepared: Option<PreparedWinShm>,
1823 - ) -> Option<WinShmContext> {
1824 - let profile = session.selected_profile;
1825 - if profile != WIN_SHM_PROFILE_HYBRID && profile != WIN_SHM_PROFILE_BUSYWAIT {
1826 - if let Some(ref mut prepared) = prepared {
1827 - prepared.destroy_all();
1828 - }
1829 - return None;
1830 - }
1831 - let mut prepared = prepared?;
1832 - let selected = prepared.take(profile);
1833 - prepared.destroy_all();
1834 - selected
1835 - }
1836 -}
1837 -
1838 -#[cfg(windows)]
1839 -#[derive(Default)]
1840 -struct PreparedWinShm {
1841 - hybrid: Option<WinShmContext>,
1842 - busywait: Option<WinShmContext>,
1843 -}
1844 -
1845 -#[cfg(windows)]
1846 -impl PreparedWinShm {
1847 - fn insert(&mut self, profile: u32, ctx: WinShmContext) {
1848 - if profile == WIN_SHM_PROFILE_HYBRID {
1849 - self.hybrid = Some(ctx);
1850 - } else if profile == WIN_SHM_PROFILE_BUSYWAIT {
1851 - self.busywait = Some(ctx);
1852 - }
1853 - }
1854 -
1855 - fn take(&mut self, profile: u32) -> Option<WinShmContext> {
1856 - if profile == WIN_SHM_PROFILE_HYBRID {
1857 - self.hybrid.take()
1858 - } else if profile == WIN_SHM_PROFILE_BUSYWAIT {
1859 - self.busywait.take()
1860 - } else {
1861 - None
1862 - }
1863 - }
1864 -
1865 - fn destroy_all(&mut self) {
1866 - if let Some(mut ctx) = self.hybrid.take() {
1867 - ctx.destroy();
1868 - }
1869 - if let Some(mut ctx) = self.busywait.take() {
1870 - ctx.destroy();
1871 - }
1872 - }
1873 -
1874 - fn is_empty(&self) -> bool {
1875 - self.hybrid.is_none() && self.busywait.is_none()
1876 - }
1877 -}
1878 -
1879 -/// Windows: handle one client session over Named Pipe + optional Win SHM.
1880 -/// Standalone function for use in per-session threads.
1881 -#[cfg(windows)]
1882 -fn handle_session_win_threaded(
1883 - mut session: NpSession,
1884 - mut shm: Option<WinShmContext>,
1885 - expected_method_code: u16,
1886 - handler: Option<DispatchHandler>,
1887 - running: Arc<AtomicBool>,
1888 - learned_request_payload_bytes: Arc<AtomicU32>,
1889 - learned_response_payload_bytes: Arc<AtomicU32>,
1890 -) {
1891 - let mut recv_buf = vec![0u8; HEADER_SIZE + session.max_request_payload_bytes as usize];
1892 - let mut resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
1893 - let mut item_resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
1894 - let mut msg_buf = vec![0u8; HEADER_SIZE + session.max_response_payload_bytes as usize];
1895 -
1896 - while running.load(Ordering::Acquire) {
1897 - let (hdr, payload) = {
1898 - if let Some(ref mut shm_ctx) = shm {
1899 - match shm_ctx.receive(&mut recv_buf, SERVER_POLL_TIMEOUT_MS) {
1900 - Ok(mlen) => {
1901 - if mlen < HEADER_SIZE {
1902 - break;
1903 - }
1904 - let hdr = match Header::decode(&recv_buf[..mlen]) {
1905 - Ok(h) => h,
1906 - Err(_) => break,
1907 - };
1908 - let payload = &recv_buf[HEADER_SIZE..mlen];
1909 - (hdr, payload)
1910 - }
1911 - Err(crate::transport::win_shm::WinShmError::Timeout) => continue,
1912 - Err(_) => break,
1913 - }
1914 - } else {
1915 - // Named Pipe path
1916 - match session.wait_readable(SERVER_POLL_TIMEOUT_MS) {
1917 - Ok(true) => {}
1918 - Ok(false) => continue,
1919 - Err(_) => break,
1920 - }
1921 - match session.receive(&mut recv_buf) {
1922 - Ok((hdr, payload)) => (hdr, payload),
1923 - Err(_) => break,
1924 - }
1925 - }
1926 - };
1927 -
1928 - // Protocol violation: unexpected message kind terminates session
1929 - if hdr.kind != KIND_REQUEST {
1930 - break;
1931 - }
1932 -
1933 - if payload.len() <= u32::MAX as usize {
1934 - server_note_payload_capacity(&learned_request_payload_bytes, payload.len() as u32);
1935 - }
1936 -
1937 - if !method_supported_internal(expected_method_code, handler.as_ref(), hdr.code) {
1938 - let mut resp_hdr = Header {
1939 - kind: KIND_RESPONSE,
1940 - code: hdr.code,
1941 - message_id: hdr.message_id,
1942 - transport_status: protocol::STATUS_UNSUPPORTED,
1943 - item_count: 1,
1944 - ..Header::default()
1945 - };
1946 -
1947 - if let Some(ref mut shm_ctx) = shm {
1948 - let msg = ensure_client_scratch(&mut msg_buf, HEADER_SIZE);
1949 - resp_hdr.magic = MAGIC_MSG;
1950 - resp_hdr.version = VERSION;
1951 - resp_hdr.header_len = protocol::HEADER_LEN;
1952 - resp_hdr.payload_len = 0;
1953 - resp_hdr.encode(&mut msg[..HEADER_SIZE]);
1954 - if shm_ctx.send(&msg[..HEADER_SIZE]).is_err() {
1955 - break;
1956 - }
1957 - } else if session.send(&mut resp_hdr, &[]).is_err() {
1958 - break;
1959 - }
1960 - continue;
1961 - }
1962 -
1963 - // Dispatch: single-item or batch
1964 - let is_batch = (hdr.flags & FLAG_BATCH) != 0 && hdr.item_count >= 1;
1965 - let response_len;
1966 - let dispatch_result = if !is_batch {
1967 - dispatch_single_internal(
1968 - expected_method_code,
1969 - handler.as_ref(),
1970 - hdr.code,
1971 - payload,
1972 - &mut resp_buf,
1973 - )
1974 - } else {
1975 - let mut bb = BatchBuilder::new(&mut resp_buf, hdr.item_count);
1976 - let mut batch_result = Ok(0usize);
1977 -
1978 - for i in 0..hdr.item_count {
1979 - let (item_data, _item_len) = match batch_item_get(payload, hdr.item_count, i) {
1980 - Ok(v) => v,
1981 - Err(_) => {
1982 - batch_result = Err(DispatchError::BadEnvelope);
1983 - break;
1984 - }
1985 - };
1986 - let item_len = match dispatch_single_internal(
1987 - expected_method_code,
1988 - handler.as_ref(),
1989 - hdr.code,
1990 - item_data,
1991 - &mut item_resp_buf,
1992 - ) {
1993 - Ok(n) => n,
1994 - Err(err) => {
1995 - batch_result = Err(err);
1996 - break;
1997 - }
1998 - };
1999 - if bb.add(&item_resp_buf[..item_len]).is_err() {
2000 - batch_result = Err(DispatchError::Overflow);
2001 - break;
2002 - }
2003 - }
2004 - if batch_result.is_ok() {
2005 - let (n, _) = bb.finish();
2006 - batch_result = Ok(n);
2007 - }
2008 - batch_result
2009 - };
2010 -
2011 - let mut resp_hdr = Header {
2012 - kind: KIND_RESPONSE,
2013 - code: hdr.code,
2014 - message_id: hdr.message_id,
2015 - ..Header::default()
2016 - };
2017 -
2018 - match dispatch_result {
2019 - Ok(n) => {
2020 - response_len = n;
2021 - if response_len <= u32::MAX as usize {
2022 - server_note_payload_capacity(
2023 - &learned_response_payload_bytes,
2024 - response_len as u32,
2025 - );
2026 - }
2027 - resp_hdr.transport_status = STATUS_OK;
2028 - if is_batch {
2029 - resp_hdr.flags = FLAG_BATCH;
2030 - resp_hdr.item_count = hdr.item_count;
2031 - } else {
2032 - resp_hdr.flags = 0;
2033 - resp_hdr.item_count = 1;
2034 - }
2035 - }
2036 - Err(DispatchError::Overflow) => {
2037 - let current = session.max_response_payload_bytes;
2038 - if current >= u32::MAX / 2 {
2039 - server_note_payload_capacity(&learned_response_payload_bytes, u32::MAX);
2040 - } else {
2041 - server_note_payload_capacity(&learned_response_payload_bytes, current * 2);
2042 - }
2043 - resp_hdr.transport_status = STATUS_LIMIT_EXCEEDED;
2044 - resp_hdr.item_count = 1;
2045 - resp_hdr.flags = 0;
2046 - response_len = 0;
2047 - }
2048 - Err(DispatchError::BadEnvelope) => {
2049 - resp_hdr.transport_status = STATUS_BAD_ENVELOPE;
2050 - resp_hdr.item_count = 1;
2051 - resp_hdr.flags = 0;
2052 - response_len = 0;
2053 - }
2054 - Err(DispatchError::HandlerFailed) => {
2055 - resp_hdr.transport_status = STATUS_INTERNAL_ERROR;
2056 - resp_hdr.item_count = 1;
2057 - resp_hdr.flags = 0;
2058 - response_len = 0;
2059 - }
2060 - }
2061 -
2062 - if let Some(ref mut shm_ctx) = shm {
2063 - let msg_len = HEADER_SIZE + response_len;
2064 - let msg = ensure_client_scratch(&mut msg_buf, msg_len);
2065 -
2066 - resp_hdr.magic = MAGIC_MSG;
2067 - resp_hdr.version = VERSION;
2068 - resp_hdr.header_len = protocol::HEADER_LEN;
2069 - resp_hdr.payload_len = response_len as u32;
2070 -
2071 - resp_hdr.encode(&mut msg[..HEADER_SIZE]);
2072 - if response_len > 0 {
2073 - msg[HEADER_SIZE..].copy_from_slice(&resp_buf[..response_len]);
2074 - }
2075 -
2076 - if shm_ctx.send(msg).is_err() {
2077 - break;
2078 - }
2079 - if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
2080 - break;
2081 - }
2082 - continue;
2083 - }
2084 -
2085 - if session
2086 - .send(&mut resp_hdr, &resp_buf[..response_len])
2087 - .is_err()
2088 - {
2089 - break;
2090 - }
2091 - if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
2092 - break;
2093 - }
2094 - }
2095 -
2096 - if let Some(mut shm_ctx) = shm {
2097 - shm_ctx.destroy();
2098 - }
2099 - session.close();
2100 -}
2101 -
2102 -/// POSIX: Handle one client session in its own thread.
2103 -#[cfg(unix)]
2104 -fn handle_session_threaded(
2105 - mut session: UdsSession,
2106 - #[cfg(target_os = "linux")] mut shm: Option<ShmContext>,
2107 - #[cfg(not(target_os = "linux"))] _shm: Option<()>,
2108 - expected_method_code: u16,
2109 - handler: Option<DispatchHandler>,
2110 - running: Arc<AtomicBool>,
2111 - learned_request_payload_bytes: Arc<AtomicU32>,
2112 - learned_response_payload_bytes: Arc<AtomicU32>,
2113 -) {
2114 - let mut recv_buf = vec![0u8; HEADER_SIZE + session.max_request_payload_bytes as usize];
2115 - let mut resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
2116 - let mut item_resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
2117 - let mut msg_buf = vec![0u8; HEADER_SIZE + session.max_response_payload_bytes as usize];
2118 -
2119 - while running.load(Ordering::Acquire) {
2120 - // Receive request via the active transport
2121 - let (hdr, payload) = {
2122 - #[cfg(target_os = "linux")]
2123 - {
2124 - if let Some(ref mut shm_ctx) = shm {
2125 - match shm_ctx.receive(&mut recv_buf, SERVER_POLL_TIMEOUT_MS) {
2126 - Ok(mlen) => {
2127 - if mlen < HEADER_SIZE {
2128 - break;
2129 - }
2130 - let hdr = match Header::decode(&recv_buf[..mlen]) {
2131 - Ok(h) => h,
2132 - Err(_) => break,
2133 - };
2134 - let payload = &recv_buf[HEADER_SIZE..mlen];
2135 - (hdr, payload)
2136 - }
2137 - Err(crate::transport::shm::ShmError::Timeout) => continue,
2138 - Err(_) => break,
2139 - }
2140 - } else {
2141 - // UDS path with poll
2142 - let ready = poll_fd(session.fd(), SERVER_POLL_TIMEOUT_MS as i32);
2143 - if ready < 0 {
2144 - break;
2145 - }
2146 - if ready == 0 {
2147 - continue;
2148 - }
2149 -
2150 - match session.receive(&mut recv_buf) {
2151 - Ok((hdr, payload)) => (hdr, payload),
2152 - Err(_) => break,
2153 - }
2154 - }
2155 - }
2156 -
2157 - #[cfg(not(target_os = "linux"))]
2158 - {
2159 - let ready = poll_fd(session.fd(), SERVER_POLL_TIMEOUT_MS as i32);
2160 - if ready < 0 {
2161 - break;
2162 - }
2163 - if ready == 0 {
2164 - continue;
2165 - }
2166 -
2167 - match session.receive(&mut recv_buf) {
2168 - Ok((hdr, payload)) => (hdr, payload),
2169 - Err(_) => break,
2170 - }
2171 - }
2172 - };
2173 -
2174 - // Protocol violation: unexpected message kind terminates session
2175 - if hdr.kind != KIND_REQUEST {
2176 - break;
2177 - }
2178 -
2179 - if payload.len() <= u32::MAX as usize {
2180 - server_note_payload_capacity(&learned_request_payload_bytes, payload.len() as u32);
2181 - }
2182 -
2183 - if !method_supported_internal(expected_method_code, handler.as_ref(), hdr.code) {
2184 - let mut resp_hdr = Header {
2185 - kind: KIND_RESPONSE,
2186 - code: hdr.code,
2187 - message_id: hdr.message_id,
2188 - transport_status: protocol::STATUS_UNSUPPORTED,
2189 - item_count: 1,
2190 - ..Header::default()
2191 - };
2192 -
2193 - #[cfg(target_os = "linux")]
2194 - {
2195 - if let Some(ref mut shm_ctx) = shm {
2196 - let msg = ensure_client_scratch(&mut msg_buf, HEADER_SIZE);
2197 - resp_hdr.magic = MAGIC_MSG;
2198 - resp_hdr.version = VERSION;
2199 - resp_hdr.header_len = protocol::HEADER_LEN;
2200 - resp_hdr.payload_len = 0;
2201 - resp_hdr.encode(&mut msg[..HEADER_SIZE]);
2202 - if shm_ctx.send(&msg[..HEADER_SIZE]).is_err() {
2203 - break;
2204 - }
2205 - continue;
2206 - }
2207 - }
2208 -
2209 - if session.send(&mut resp_hdr, &[]).is_err() {
2210 - break;
2211 - }
2212 - continue;
2213 - }
2214 -
2215 - // Dispatch: single-item or batch
2216 - let is_batch = (hdr.flags & FLAG_BATCH) != 0 && hdr.item_count >= 1;
2217 - let response_len;
2218 - let dispatch_result = if !is_batch {
2219 - dispatch_single_internal(
2220 - expected_method_code,
2221 - handler.as_ref(),
2222 - hdr.code,
2223 - payload,
2224 - &mut resp_buf,
2225 - )
2226 - } else {
2227 - let mut bb = BatchBuilder::new(&mut resp_buf, hdr.item_count);
2228 - let mut batch_result = Ok(0usize);
2229 -
2230 - for i in 0..hdr.item_count {
2231 - let (item_data, _item_len) = match batch_item_get(payload, hdr.item_count, i) {
2232 - Ok(v) => v,
2233 - Err(_) => {
2234 - batch_result = Err(DispatchError::BadEnvelope);
2235 - break;
2236 - }
2237 - };
2238 - let item_len = match dispatch_single_internal(
2239 - expected_method_code,
2240 - handler.as_ref(),
2241 - hdr.code,
2242 - item_data,
2243 - &mut item_resp_buf,
2244 - ) {
2245 - Ok(n) => n,
2246 - Err(err) => {
2247 - batch_result = Err(err);
2248 - break;
2249 - }
2250 - };
2251 - if bb.add(&item_resp_buf[..item_len]).is_err() {
2252 - batch_result = Err(DispatchError::Overflow);
2253 - break;
2254 - }
2255 - }
2256 - if batch_result.is_ok() {
2257 - let (n, _) = bb.finish();
2258 - batch_result = Ok(n);
2259 - }
2260 - batch_result
2261 - };
2262 -
2263 - // Build response header
2264 - let mut resp_hdr = Header {
2265 - kind: KIND_RESPONSE,
2266 - code: hdr.code,
2267 - message_id: hdr.message_id,
2268 - ..Header::default()
2269 - };
2270 -
2271 - match dispatch_result {
2272 - Ok(n) => {
2273 - response_len = n;
2274 - if response_len <= u32::MAX as usize {
2275 - server_note_payload_capacity(
2276 - &learned_response_payload_bytes,
2277 - response_len as u32,
2278 - );
2279 - }
2280 - resp_hdr.transport_status = STATUS_OK;
2281 - if is_batch {
2282 - resp_hdr.flags = FLAG_BATCH;
2283 - resp_hdr.item_count = hdr.item_count;
2284 - } else {
2285 - resp_hdr.flags = 0;
2286 - resp_hdr.item_count = 1;
2287 - }
2288 - }
2289 - Err(DispatchError::Overflow) => {
2290 - let current = session.max_response_payload_bytes;
2291 - if current >= u32::MAX / 2 {
2292 - server_note_payload_capacity(&learned_response_payload_bytes, u32::MAX);
2293 - } else {
2294 - server_note_payload_capacity(&learned_response_payload_bytes, current * 2);
2295 - }
2296 - resp_hdr.transport_status = STATUS_LIMIT_EXCEEDED;
2297 - resp_hdr.item_count = 1;
2298 - resp_hdr.flags = 0;
2299 - response_len = 0;
2300 - }
2301 - Err(DispatchError::BadEnvelope) => {
2302 - resp_hdr.transport_status = STATUS_BAD_ENVELOPE;
2303 - resp_hdr.item_count = 1;
2304 - resp_hdr.flags = 0;
2305 - response_len = 0;
2306 - }
2307 - Err(DispatchError::HandlerFailed) => {
2308 - resp_hdr.transport_status = STATUS_INTERNAL_ERROR;
2309 - resp_hdr.item_count = 1;
2310 - resp_hdr.flags = 0;
2311 - response_len = 0;
2312 - }
2313 - }
2314 -
2315 - // Send response via the active transport
2316 - #[cfg(target_os = "linux")]
2317 - {
2318 - if let Some(ref mut shm_ctx) = shm {
2319 - let msg_len = HEADER_SIZE + response_len;
2320 - let msg = ensure_client_scratch(&mut msg_buf, msg_len);
2321 -
2322 - resp_hdr.magic = MAGIC_MSG;
2323 - resp_hdr.version = VERSION;
2324 - resp_hdr.header_len = protocol::HEADER_LEN;
2325 - resp_hdr.payload_len = response_len as u32;
2326 -
2327 - resp_hdr.encode(&mut msg[..HEADER_SIZE]);
2328 - if response_len > 0 {
2329 - msg[HEADER_SIZE..].copy_from_slice(&resp_buf[..response_len]);
2330 - }
2331 -
2332 - if shm_ctx.send(msg).is_err() {
2333 - break;
2334 - }
2335 - if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
2336 - break;
2337 - }
2338 - continue;
2339 - }
2340 - }
2341 -
2342 - // UDS path
2343 - if session
2344 - .send(&mut resp_hdr, &resp_buf[..response_len])
2345 - .is_err()
2346 - {
2347 - break;
2348 - }
2349 - if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
2350 - break;
2351 - }
2352 - }
2353 -
2354 - // Cleanup
2355 - #[cfg(target_os = "linux")]
2356 - {
2357 - if let Some(mut shm_ctx) = shm {
2358 - shm_ctx.destroy();
2359 - }
2360 - }
2361 - drop(session);
2362 -}
2363 -
2364 -// ---------------------------------------------------------------------------
2365 -// Internal: poll helper
2366 -// ---------------------------------------------------------------------------
2367 -
2368 -/// Poll a file descriptor for readability with a timeout in milliseconds.
2369 -/// Returns: 1 = data ready, 0 = timeout, -1 = error/hangup.
2370 -#[cfg(unix)]
2371 -fn poll_fd(fd: i32, timeout_ms: i32) -> i32 {
2372 - let mut pfd = libc::pollfd {
2373 - fd,
2374 - events: libc::POLLIN,
2375 - revents: 0,
2376 - };
2377 -
2378 - let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
2379 -
2380 - if ret < 0 {
2381 - let errno = unsafe { *libc::__errno_location() };
2382 - if errno == libc::EINTR {
2383 - return 0;
2384 - }
2385 - return -1;
2386 - }
2387 -
2388 - if ret == 0 {
2389 - return 0;
2390 - }
2391 -
2392 - if pfd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 {
2393 - return -1;
2394 - }
2395 -
2396 - if pfd.revents & libc::POLLIN != 0 {
2397 - return 1;
2398 - }
2399 -
2400 - 0
2401 -}
2402 -
2403 -// ---------------------------------------------------------------------------
2404 -// L3: Client-side cgroups snapshot cache
2405 -// ---------------------------------------------------------------------------
2406 -
2407 -/// Cached copy of a single cgroup item. Owns its strings.
2408 -/// Built from ephemeral L2 views during cache construction.
2409 -#[derive(Debug, Clone)]
2410 -pub struct CgroupsCacheItem {
2411 - pub hash: u32,
2412 - pub options: u32,
2413 - pub enabled: u32,
2414 - pub name: String,
2415 - pub path: String,
2416 -}
2417 -
2418 -/// L3 cache status snapshot (for diagnostics, not hot path).
2419 -#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2420 -pub struct CgroupsCacheStatus {
2421 - pub populated: bool,
2422 - pub item_count: u32,
2423 - pub systemd_enabled: u32,
2424 - pub generation: u64,
2425 - pub refresh_success_count: u32,
2426 - pub refresh_failure_count: u32,
2427 - pub connection_state: ClientState,
2428 - /// Monotonic milliseconds of last successful refresh (0 if never).
2429 - pub last_refresh_ts: u64,
2430 -}
2431 -
2432 -/// Default response buffer size for L3 cache refresh.
2433 -const CACHE_RESPONSE_BUF_SIZE: usize = 65536;
2434 -
2435 -#[derive(Debug, Clone, Copy, Default)]
2436 -struct CgroupsHashBucket {
2437 - index: u32,
2438 - used: bool,
2439 -}
2440 -
2441 -fn cache_hash_name(name: &str) -> u32 {
2442 - let mut h: u32 = 5381;
2443 - for b in name.as_bytes() {
2444 - h = ((h << 5).wrapping_add(h)).wrapping_add(*b as u32);
2445 - }
2446 - h
2447 -}
2448 -
2449 -/// L3 client-side cgroups snapshot cache.
2450 -///
2451 -/// Wraps an L2 client and maintains a local owned copy of the most
2452 -/// recent successful snapshot. Lookup by hash+name is O(1) via HashMap.
2453 -///
2454 -/// On refresh failure, the previous cache is preserved. The cache
2455 -/// is empty only if no successful refresh has ever occurred.
2456 -pub struct CgroupsCache {
2457 - client: RawClient,
2458 - items: Vec<CgroupsCacheItem>,
2459 - /// Open-addressing hash table: (hash ^ djb2(name)) -> index into items vec
2460 - buckets: Vec<CgroupsHashBucket>,
2461 - systemd_enabled: u32,
2462 - generation: u64,
2463 - populated: bool,
2464 - refresh_success_count: u32,
2465 - refresh_failure_count: u32,
2466 - /// Monotonic reference point for timestamp calculation
2467 - epoch: std::time::Instant,
2468 - /// Monotonic ms of last successful refresh (0 if never)
2469 - pub last_refresh_ts: u64,
2470 -}
2471 -
2472 -impl CgroupsCache {
2473 - /// Create a new L3 cache. Creates the underlying L2 client context.
2474 - /// Does NOT connect. Does NOT require the server to be running.
2475 - /// Cache starts empty (populated == false).
2476 - pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
2477 - CgroupsCache {
2478 - client: RawClient::new_snapshot(run_dir, service_name, config),
2479 - items: Vec::new(),
2480 - buckets: Vec::new(),
2481 - systemd_enabled: 0,
2482 - generation: 0,
2483 - populated: false,
2484 - refresh_success_count: 0,
2485 - refresh_failure_count: 0,
2486 - epoch: std::time::Instant::now(),
2487 - last_refresh_ts: 0,
2488 - }
2489 - }
2490 -
2491 - /// Refresh the cache. Drives the L2 client (connect/reconnect as
2492 - /// needed) and requests a fresh snapshot. On success, rebuilds the
2493 - /// local cache. On failure, preserves the previous cache.
2494 - ///
2495 - /// Returns true if the cache was updated.
2496 - pub fn refresh(&mut self) -> bool {
2497 - // Drive L2 connection lifecycle
2498 - self.client.refresh();
2499 -
2500 - // Attempt snapshot call
2501 - match self.client.call_snapshot() {
2502 - Ok(view) => {
2503 - // Build new cache from snapshot view
2504 - let mut new_items = Vec::with_capacity(view.item_count as usize);
2505 - for i in 0..view.item_count {
2506 - match view.item(i) {
2507 - Ok(iv) => {
2508 - let name = match iv.name.as_str() {
2509 - Ok(s) => s.to_string(),
2510 - Err(_) => {
2511 - // Non-UTF8 name: use lossy conversion
2512 - String::from_utf8_lossy(iv.name.as_bytes()).into_owned()
2513 - }
2514 - };
2515 - let path = match iv.path.as_str() {
2516 - Ok(s) => s.to_string(),
2517 - Err(_) => String::from_utf8_lossy(iv.path.as_bytes()).into_owned(),
2518 - };
2519 - new_items.push(CgroupsCacheItem {
2520 - hash: iv.hash,
2521 - options: iv.options,
2522 - enabled: iv.enabled,
2523 - name,
2524 - path,
2525 - });
2526 - }
2527 - Err(_) => {
2528 - // Malformed item: abort, preserve old cache
2529 - self.refresh_failure_count += 1;
2530 - return false;
2531 - }
2532 - }
2533 - }
2534 -
2535 - // Rebuild open-addressing lookup table.
2536 - let mut buckets = Vec::new();
2537 - if !new_items.is_empty() {
2538 - let bcount = next_power_of_2_u32((new_items.len() as u32) * 2) as usize;
2539 - buckets.resize(bcount, CgroupsHashBucket::default());
2540 - let mask = (bcount - 1) as u32;
2541 - for (i, item) in new_items.iter().enumerate() {
2542 - let mut slot = (item.hash ^ cache_hash_name(&item.name)) & mask;
2543 - while buckets[slot as usize].used {
2544 - slot = (slot + 1) & mask;
2545 - }
2546 - buckets[slot as usize] = CgroupsHashBucket {
2547 - index: i as u32,
2548 - used: true,
2549 - };
2550 - }
2551 - }
2552 -
2553 - // Replace old cache
2554 - self.items = new_items;
2555 - self.buckets = buckets;
2556 - self.systemd_enabled = view.systemd_enabled;
2557 - self.generation = view.generation;
2558 - self.populated = true;
2559 - self.refresh_success_count += 1;
2560 - self.last_refresh_ts = self.epoch.elapsed().as_millis() as u64;
2561 - true
2562 - }
2563 - Err(_) => {
2564 - // Refresh failed: preserve previous cache
2565 - self.refresh_failure_count += 1;
2566 - false
2567 - }
2568 - }
2569 - }
2570 -
2571 - /// Returns true if at least one successful refresh has occurred.
2572 - /// Cheap cached boolean. No I/O, no syscalls.
2573 - ///
2574 - /// Note: ready means "has cached data", not "is connected."
2575 - #[inline]
2576 - pub fn ready(&self) -> bool {
2577 - self.populated
2578 - }
2579 -
2580 - /// Look up a cached item by hash + name. O(1) via open-addressing hash
2581 - /// table. No I/O.
2582 - pub fn lookup(&self, hash: u32, name: &str) -> Option<&CgroupsCacheItem> {
2583 - if !self.populated {
2584 - return None;
2585 - }
2586 - if !self.buckets.is_empty() {
2587 - let mask = (self.buckets.len() - 1) as u32;
2588 - let mut slot = (hash ^ cache_hash_name(name)) & mask;
2589 - while self.buckets[slot as usize].used {
2590 - let item = &self.items[self.buckets[slot as usize].index as usize];
2591 - if item.hash == hash && item.name == name {
2592 - return Some(item);
2593 - }
2594 - slot = (slot + 1) & mask;
2595 - }
2596 - return None;
2597 - }
2598 -
2599 - self.items
2600 - .iter()
2601 - .find(|item| item.hash == hash && item.name == name)
2602 - }
2603 -
2604 - /// Fill a status snapshot for diagnostics.
2605 - pub fn status(&self) -> CgroupsCacheStatus {
2606 - CgroupsCacheStatus {
2607 - populated: self.populated,
2608 - item_count: self.items.len() as u32,
2609 - systemd_enabled: self.systemd_enabled,
2610 - generation: self.generation,
2611 - refresh_success_count: self.refresh_success_count,
2612 - refresh_failure_count: self.refresh_failure_count,
2613 - connection_state: self.client.state,
2614 - last_refresh_ts: self.last_refresh_ts,
2615 - }
2616 - }
2617 -
2618 - /// Close the cache: free all cached items, close the L2 client.
2619 - pub fn close(&mut self) {
2620 - self.items.clear();
2621 - self.buckets.clear();
2622 - self.populated = false;
2623 - self.client.close();
2624 - }
2625 -}
2626 -
2627 -impl Drop for CgroupsCache {
2628 - fn drop(&mut self) {
2629 - self.close();
2630 - }
2631 -}
2632 -
2633 -// ---------------------------------------------------------------------------
2634 -// Tests
2635 -// ---------------------------------------------------------------------------
59 +#[cfg(all(test, unix))]
60 +use client::{ClientResponseRef, ClientResponseSource};
61 +#[cfg(all(test, unix))]
62 +use common::CACHE_RESPONSE_BUF_SIZE;
63 +#[cfg(all(test, unix))]
64 +use dispatch::dispatch_single;
65 +#[cfg(all(test, unix))]
66 +use server_session_unix::poll_fd;
67
68 #[cfg(all(test, unix))]
69 #[path = "raw_unix_tests.rs"]
src/crates/netipc/src/service/raw/apps_lookup.rs new
+91
@@ -0,0 +1,91 @@
1 +use super::client::{ClientConfig, RawCallKind, RawClient};
2 +use super::dispatch::{DispatchError, DispatchHandler};
3 +use crate::protocol::{
4 + self, AppsLookupBuilder, AppsLookupRequestView, AppsLookupResponseView, NipcError,
5 + APPS_LOOKUP_KEY_SIZE, APPS_LOOKUP_REQ_HDR_SIZE, LOOKUP_DIR_ENTRY_SIZE, METHOD_APPS_LOOKUP,
6 +};
7 +use std::sync::Arc;
8 +
9 +pub type AppsLookupHandler =
10 + Arc<dyn for<'a> Fn(&AppsLookupRequestView, &mut AppsLookupBuilder<'a>) -> bool + Send + Sync>;
11 +
12 +impl RawClient {
13 + /// Create a new client context bound to the apps-lookup service kind.
14 + /// Does NOT connect. Does NOT require the server to be running.
15 + pub fn new_apps_lookup(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
16 + Self::new_bound(run_dir, service_name, METHOD_APPS_LOOKUP, config)
17 + }
18 +
19 + /// Blocking typed call: APPS_LOOKUP method.
20 + ///
21 + /// The returned view is valid until the next typed call on this client.
22 + pub fn call_apps_lookup(
23 + &mut self,
24 + pids: &[u32],
25 + ) -> Result<AppsLookupResponseView<'_>, NipcError> {
26 + self.validate_method(METHOD_APPS_LOOKUP)?;
27 +
28 + let dir_size = pids
29 + .len()
30 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
31 + .ok_or(NipcError::Overflow)?;
32 + let key_size = pids
33 + .len()
34 + .checked_mul(APPS_LOOKUP_KEY_SIZE)
35 + .ok_or(NipcError::Overflow)?;
36 + let req_size = APPS_LOOKUP_REQ_HDR_SIZE
37 + .checked_add(dir_size)
38 + .and_then(|v| v.checked_add(key_size))
39 + .ok_or(NipcError::Overflow)?;
40 + let req_len = {
41 + let req_buf = self.request_scratch(req_size);
42 + protocol::encode_apps_lookup_request(pids, req_buf)?
43 + };
44 + let response =
45 + self.raw_call_with_retry(METHOD_APPS_LOOKUP, req_len, RawCallKind::single())?;
46 + let view = AppsLookupResponseView::decode(self.response_payload(response)?)?;
47 + if view.item_count != pids.len() as u32 {
48 + return Err(NipcError::BadItemCount);
49 + }
50 + for (i, expected) in pids.iter().enumerate() {
51 + let item = view.item(i as u32)?;
52 + if item.pid != *expected {
53 + return Err(NipcError::BadLayout);
54 + }
55 + }
56 + Ok(view)
57 + }
58 +}
59 +
60 +pub fn apps_lookup_dispatch(handler: AppsLookupHandler) -> DispatchHandler {
61 + Arc::new(move |request, response_buf| {
62 + let request =
63 + AppsLookupRequestView::decode(request).map_err(|_| DispatchError::BadEnvelope)?;
64 + let dir_size = (request.item_count as usize)
65 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
66 + .ok_or(DispatchError::Overflow)?;
67 + let min_required = protocol::APPS_LOOKUP_RESP_HDR_SIZE
68 + .checked_add(dir_size)
69 + .ok_or(DispatchError::Overflow)?;
70 + if response_buf.len() < min_required {
71 + return Err(DispatchError::Overflow);
72 + }
73 + let mut builder = AppsLookupBuilder::new(response_buf, request.item_count, 0);
74 + if !handler(&request, &mut builder) {
75 + return Err(DispatchError::HandlerFailed);
76 + }
77 + if let Some(err) = builder.error() {
78 + return match err {
79 + NipcError::Overflow => Err(DispatchError::Overflow),
80 + _ => Err(DispatchError::BadEnvelope),
81 + };
82 + }
83 + if builder.item_count() != request.item_count {
84 + return Err(DispatchError::BadEnvelope);
85 + }
86 + builder.finish().map_err(|err| match err {
87 + NipcError::Overflow => DispatchError::Overflow,
88 + _ => DispatchError::BadEnvelope,
89 + })
90 + })
91 +}
src/crates/netipc/src/service/raw/cgroups_cache.rs new
+215
@@ -0,0 +1,215 @@
1 +use super::client::{ClientConfig, ClientState, RawClient};
2 +use super::common::next_power_of_2_u32;
3 +
4 +/// Cached copy of a single cgroup item. Owns its strings.
5 +/// Built from ephemeral L2 views during cache construction.
6 +#[derive(Debug, Clone)]
7 +pub struct CgroupsCacheItem {
8 + pub hash: u32,
9 + pub options: u32,
10 + pub enabled: u32,
11 + pub name: String,
12 + pub path: String,
13 +}
14 +
15 +/// L3 cache status snapshot (for diagnostics, not hot path).
16 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17 +pub struct CgroupsCacheStatus {
18 + pub populated: bool,
19 + pub item_count: u32,
20 + pub systemd_enabled: u32,
21 + pub generation: u64,
22 + pub refresh_success_count: u32,
23 + pub refresh_failure_count: u32,
24 + pub connection_state: ClientState,
25 + /// Monotonic milliseconds of last successful refresh (0 if never).
26 + pub last_refresh_ts: u64,
27 +}
28 +
29 +#[derive(Debug, Clone, Copy, Default)]
30 +struct CgroupsHashBucket {
31 + index: u32,
32 + used: bool,
33 +}
34 +
35 +fn cache_hash_name(name: &str) -> u32 {
36 + let mut h: u32 = 5381;
37 + for b in name.as_bytes() {
38 + h = ((h << 5).wrapping_add(h)).wrapping_add(*b as u32);
39 + }
40 + h
41 +}
42 +
43 +/// L3 client-side cgroups snapshot cache.
44 +///
45 +/// Wraps an L2 client and maintains a local owned copy of the most
46 +/// recent successful snapshot. Lookup by hash+name is O(1) via HashMap.
47 +///
48 +/// On refresh failure, the previous cache is preserved. The cache
49 +/// is empty only if no successful refresh has ever occurred.
50 +pub struct CgroupsCache {
51 + pub(super) client: RawClient,
52 + items: Vec<CgroupsCacheItem>,
53 + /// Open-addressing hash table: (hash ^ djb2(name)) -> index into items vec
54 + buckets: Vec<CgroupsHashBucket>,
55 + systemd_enabled: u32,
56 + generation: u64,
57 + populated: bool,
58 + refresh_success_count: u32,
59 + refresh_failure_count: u32,
60 + /// Monotonic reference point for timestamp calculation
61 + epoch: std::time::Instant,
62 + /// Monotonic ms of last successful refresh (0 if never)
63 + pub last_refresh_ts: u64,
64 +}
65 +
66 +impl CgroupsCache {
67 + /// Create a new L3 cache. Creates the underlying L2 client context.
68 + /// Does NOT connect. Does NOT require the server to be running.
69 + /// Cache starts empty (populated == false).
70 + pub fn new(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
71 + CgroupsCache {
72 + client: RawClient::new_snapshot(run_dir, service_name, config),
73 + items: Vec::new(),
74 + buckets: Vec::new(),
75 + systemd_enabled: 0,
76 + generation: 0,
77 + populated: false,
78 + refresh_success_count: 0,
79 + refresh_failure_count: 0,
80 + epoch: std::time::Instant::now(),
81 + last_refresh_ts: 0,
82 + }
83 + }
84 +
85 + /// Refresh the cache. Drives the L2 client (connect/reconnect as
86 + /// needed) and requests a fresh snapshot. On success, rebuilds the
87 + /// local cache. On failure, preserves the previous cache.
88 + ///
89 + /// Returns true if the cache was updated.
90 + pub fn refresh(&mut self) -> bool {
91 + self.client.refresh();
92 +
93 + match self.client.call_snapshot() {
94 + Ok(view) => {
95 + let mut new_items = Vec::with_capacity(view.item_count as usize);
96 + for i in 0..view.item_count {
97 + match view.item(i) {
98 + Ok(iv) => {
99 + let name = match iv.name.as_str() {
100 + Ok(s) => s.to_string(),
101 + Err(_) => String::from_utf8_lossy(iv.name.as_bytes()).into_owned(),
102 + };
103 + let path = match iv.path.as_str() {
104 + Ok(s) => s.to_string(),
105 + Err(_) => String::from_utf8_lossy(iv.path.as_bytes()).into_owned(),
106 + };
107 + new_items.push(CgroupsCacheItem {
108 + hash: iv.hash,
109 + options: iv.options,
110 + enabled: iv.enabled,
111 + name,
112 + path,
113 + });
114 + }
115 + Err(_) => {
116 + self.refresh_failure_count += 1;
117 + return false;
118 + }
119 + }
120 + }
121 +
122 + let mut buckets = Vec::new();
123 + if !new_items.is_empty() {
124 + let bcount = next_power_of_2_u32((new_items.len() as u32) * 2) as usize;
125 + buckets.resize(bcount, CgroupsHashBucket::default());
126 + let mask = (bcount - 1) as u32;
127 + for (i, item) in new_items.iter().enumerate() {
128 + let mut slot = (item.hash ^ cache_hash_name(&item.name)) & mask;
129 + while buckets[slot as usize].used {
130 + slot = (slot + 1) & mask;
131 + }
132 + buckets[slot as usize] = CgroupsHashBucket {
133 + index: i as u32,
134 + used: true,
135 + };
136 + }
137 + }
138 +
139 + self.items = new_items;
140 + self.buckets = buckets;
141 + self.systemd_enabled = view.systemd_enabled;
142 + self.generation = view.generation;
143 + self.populated = true;
144 + self.refresh_success_count += 1;
145 + self.last_refresh_ts = self.epoch.elapsed().as_millis() as u64;
146 + true
147 + }
148 + Err(_) => {
149 + self.refresh_failure_count += 1;
150 + false
151 + }
152 + }
153 + }
154 +
155 + /// Returns true if at least one successful refresh has occurred.
156 + /// Cheap cached boolean. No I/O, no syscalls.
157 + ///
158 + /// Note: ready means "has cached data", not "is connected."
159 + #[inline]
160 + pub fn ready(&self) -> bool {
161 + self.populated
162 + }
163 +
164 + /// Look up a cached item by hash + name. O(1) via open-addressing hash
165 + /// table. No I/O.
166 + pub fn lookup(&self, hash: u32, name: &str) -> Option<&CgroupsCacheItem> {
167 + if !self.populated {
168 + return None;
169 + }
170 + if !self.buckets.is_empty() {
171 + let mask = (self.buckets.len() - 1) as u32;
172 + let mut slot = (hash ^ cache_hash_name(name)) & mask;
173 + while self.buckets[slot as usize].used {
174 + let item = &self.items[self.buckets[slot as usize].index as usize];
175 + if item.hash == hash && item.name == name {
176 + return Some(item);
177 + }
178 + slot = (slot + 1) & mask;
179 + }
180 + return None;
181 + }
182 +
183 + self.items
184 + .iter()
185 + .find(|item| item.hash == hash && item.name == name)
186 + }
187 +
188 + /// Fill a status snapshot for diagnostics.
189 + pub fn status(&self) -> CgroupsCacheStatus {
190 + CgroupsCacheStatus {
191 + populated: self.populated,
192 + item_count: self.items.len() as u32,
193 + systemd_enabled: self.systemd_enabled,
194 + generation: self.generation,
195 + refresh_success_count: self.refresh_success_count,
196 + refresh_failure_count: self.refresh_failure_count,
197 + connection_state: self.client.status().state,
198 + last_refresh_ts: self.last_refresh_ts,
199 + }
200 + }
201 +
202 + /// Close the cache: free all cached items, close the L2 client.
203 + pub fn close(&mut self) {
204 + self.items.clear();
205 + self.buckets.clear();
206 + self.populated = false;
207 + self.client.close();
208 + }
209 +}
210 +
211 +impl Drop for CgroupsCache {
212 + fn drop(&mut self) {
213 + self.close();
214 + }
215 +}
src/crates/netipc/src/service/raw/cgroups_lookup.rs new
+100
@@ -0,0 +1,100 @@
1 +use super::client::{ClientConfig, RawCallKind, RawClient};
2 +use super::dispatch::{DispatchError, DispatchHandler};
3 +use crate::protocol::{
4 + self, CgroupsLookupBuilder, CgroupsLookupRequestView, CgroupsLookupResponseView, NipcError,
5 + CGROUPS_LOOKUP_REQ_HDR_SIZE, LOOKUP_DIR_ENTRY_SIZE, METHOD_CGROUPS_LOOKUP,
6 +};
7 +use std::sync::Arc;
8 +
9 +pub type CgroupsLookupHandler = Arc<
10 + dyn for<'a> Fn(&CgroupsLookupRequestView, &mut CgroupsLookupBuilder<'a>) -> bool + Send + Sync,
11 +>;
12 +
13 +impl RawClient {
14 + /// Create a new client context bound to the cgroups-lookup service kind.
15 + /// Does NOT connect. Does NOT require the server to be running.
16 + pub fn new_cgroups_lookup(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
17 + Self::new_bound(run_dir, service_name, METHOD_CGROUPS_LOOKUP, config)
18 + }
19 +
20 + /// Blocking typed call: CGROUPS_LOOKUP method.
21 + ///
22 + /// The returned view is valid until the next typed call on this client.
23 + pub fn call_cgroups_lookup(
24 + &mut self,
25 + paths: &[&[u8]],
26 + ) -> Result<CgroupsLookupResponseView<'_>, NipcError> {
27 + self.validate_method(METHOD_CGROUPS_LOOKUP)?;
28 +
29 + let dir_size = paths
30 + .len()
31 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
32 + .ok_or(NipcError::Overflow)?;
33 + let mut req_size = CGROUPS_LOOKUP_REQ_HDR_SIZE
34 + .checked_add(dir_size)
35 + .ok_or(NipcError::Overflow)?;
36 + let mut data = req_size;
37 + for path in paths {
38 + let aligned = data
39 + .checked_add(7)
40 + .map(|v| v & !7)
41 + .ok_or(NipcError::Overflow)?;
42 + data = aligned
43 + .checked_add(path.len())
44 + .and_then(|v| v.checked_add(1))
45 + .ok_or(NipcError::Overflow)?;
46 + }
47 + req_size = data;
48 +
49 + let req_len = {
50 + let req_buf = self.request_scratch(req_size);
51 + protocol::encode_cgroups_lookup_request(paths, req_buf)?
52 + };
53 + let response =
54 + self.raw_call_with_retry(METHOD_CGROUPS_LOOKUP, req_len, RawCallKind::single())?;
55 + let view = CgroupsLookupResponseView::decode(self.response_payload(response)?)?;
56 + if view.item_count != paths.len() as u32 {
57 + return Err(NipcError::BadItemCount);
58 + }
59 + for (i, expected) in paths.iter().enumerate() {
60 + let item = view.item(i as u32)?;
61 + if item.path.as_bytes() != *expected {
62 + return Err(NipcError::BadLayout);
63 + }
64 + }
65 + Ok(view)
66 + }
67 +}
68 +
69 +pub fn cgroups_lookup_dispatch(handler: CgroupsLookupHandler) -> DispatchHandler {
70 + Arc::new(move |request, response_buf| {
71 + let request =
72 + CgroupsLookupRequestView::decode(request).map_err(|_| DispatchError::BadEnvelope)?;
73 + let dir_size = (request.item_count as usize)
74 + .checked_mul(LOOKUP_DIR_ENTRY_SIZE)
75 + .ok_or(DispatchError::Overflow)?;
76 + let min_required = protocol::CGROUPS_LOOKUP_RESP_HDR_SIZE
77 + .checked_add(dir_size)
78 + .ok_or(DispatchError::Overflow)?;
79 + if response_buf.len() < min_required {
80 + return Err(DispatchError::Overflow);
81 + }
82 + let mut builder = CgroupsLookupBuilder::new(response_buf, request.item_count, 0);
83 + if !handler(&request, &mut builder) {
84 + return Err(DispatchError::HandlerFailed);
85 + }
86 + if let Some(err) = builder.error() {
87 + return match err {
88 + NipcError::Overflow => Err(DispatchError::Overflow),
89 + _ => Err(DispatchError::BadEnvelope),
90 + };
91 + }
92 + if builder.item_count() != request.item_count {
93 + return Err(DispatchError::BadEnvelope);
94 + }
95 + builder.finish().map_err(|err| match err {
96 + NipcError::Overflow => DispatchError::Overflow,
97 + _ => DispatchError::BadEnvelope,
98 + })
99 + })
100 +}
src/crates/netipc/src/service/raw/cgroups_snapshot.rs new
+67
@@ -0,0 +1,67 @@
1 +use super::client::{ClientConfig, RawCallKind, RawClient};
2 +use super::dispatch::{DispatchError, DispatchHandler};
3 +use crate::protocol::{
4 + self, CgroupsRequest, CgroupsResponseView, NipcError, METHOD_CGROUPS_SNAPSHOT,
5 +};
6 +use std::sync::Arc;
7 +
8 +pub type SnapshotHandler =
9 + Arc<dyn for<'a> Fn(&CgroupsRequest, &mut protocol::CgroupsBuilder<'a>) -> bool + Send + Sync>;
10 +
11 +impl RawClient {
12 + /// Create a new client context bound to the cgroups-snapshot service kind.
13 + /// Does NOT connect. Does NOT require the server to be running.
14 + pub fn new_snapshot(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
15 + Self::new_bound(run_dir, service_name, METHOD_CGROUPS_SNAPSHOT, config)
16 + }
17 +
18 + /// Blocking typed call: encode request, send, receive, check
19 + /// transport_status, decode response.
20 + ///
21 + /// The returned view is valid until the next typed call on this client.
22 + pub fn call_snapshot(&mut self) -> Result<CgroupsResponseView<'_>, NipcError> {
23 + self.validate_method(METHOD_CGROUPS_SNAPSHOT)?;
24 + let req = CgroupsRequest {
25 + layout_version: 1,
26 + flags: 0,
27 + };
28 + let req_len = {
29 + let req_buf = self.request_scratch(4);
30 + let req_len = req.encode(req_buf);
31 + if req_len == 0 {
32 + return Err(NipcError::Truncated);
33 + }
34 + req_len
35 + };
36 +
37 + let response =
38 + self.raw_call_with_retry(METHOD_CGROUPS_SNAPSHOT, req_len, RawCallKind::single())?;
39 + CgroupsResponseView::decode(self.response_payload(response)?)
40 + }
41 +}
42 +
43 +pub fn snapshot_max_items(response_buf_size: usize, override_max_items: u32) -> u32 {
44 + if override_max_items != 0 {
45 + return override_max_items;
46 + }
47 + protocol::estimate_cgroups_max_items(response_buf_size)
48 +}
49 +
50 +pub fn snapshot_dispatch(handler: SnapshotHandler, max_items: u32) -> DispatchHandler {
51 + Arc::new(move |request, response_buf| {
52 + let request = CgroupsRequest::decode(request).map_err(|_| DispatchError::BadEnvelope)?;
53 + let item_budget = snapshot_max_items(response_buf.len(), max_items);
54 + if item_budget == 0 {
55 + return Err(DispatchError::Overflow);
56 + }
57 + let mut builder = protocol::CgroupsBuilder::new(response_buf, item_budget, 0, 0);
58 + if !handler(&request, &mut builder) {
59 + return Err(DispatchError::HandlerFailed);
60 + }
61 + let n = builder.finish();
62 + if n == 0 {
63 + return Err(DispatchError::Overflow);
64 + }
65 + Ok(n)
66 + })
67 +}
src/crates/netipc/src/service/raw/client.rs new
+272
@@ -0,0 +1,272 @@
1 +use super::common::{ensure_client_scratch, next_power_of_2_u32};
2 +use crate::protocol::{self, NipcError, MAX_PAYLOAD_CAP};
3 +
4 +#[cfg(unix)]
5 +pub(super) use crate::transport::posix::ClientConfig;
6 +
7 +#[cfg(unix)]
8 +use crate::transport::posix::UdsSession;
9 +
10 +#[cfg(target_os = "linux")]
11 +use crate::transport::shm::ShmContext;
12 +
13 +#[cfg(windows)]
14 +pub(super) use crate::transport::windows::ClientConfig;
15 +
16 +#[cfg(windows)]
17 +use crate::transport::windows::NpSession;
18 +
19 +#[cfg(windows)]
20 +use crate::transport::win_shm::WinShmContext;
21 +
22 +/// Client connection state machine.
23 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24 +pub enum ClientState {
25 + Disconnected,
26 + Connecting,
27 + Ready,
28 + NotFound,
29 + AuthFailed,
30 + Incompatible,
31 + Broken,
32 +}
33 +
34 +/// Diagnostic counters snapshot.
35 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36 +pub struct ClientStatus {
37 + pub state: ClientState,
38 + pub connect_count: u32,
39 + pub reconnect_count: u32,
40 + pub call_count: u32,
41 + pub error_count: u32,
42 +}
43 +
44 +/// L2 client context bound to one service kind.
45 +///
46 +/// Manages connection lifecycle and provides typed blocking calls with
47 +/// at-least-once retry semantics. The outer request code remains only for
48 +/// validation; each client instance is bound to one expected request kind.
49 +pub struct RawClient {
50 + pub(super) state: ClientState,
51 + pub(super) run_dir: String,
52 + pub(super) service_name: String,
53 + pub(super) expected_method_code: u16,
54 + pub(super) transport_config: ClientConfig,
55 +
56 + #[cfg(unix)]
57 + pub(super) session: Option<UdsSession>,
58 + #[cfg(target_os = "linux")]
59 + pub(super) shm: Option<ShmContext>,
60 +
61 + #[cfg(windows)]
62 + pub(super) session: Option<NpSession>,
63 + #[cfg(windows)]
64 + pub(super) shm: Option<WinShmContext>,
65 +
66 + pub(super) request_buf: Vec<u8>,
67 + pub(super) send_buf: Vec<u8>,
68 + pub(super) transport_buf: Vec<u8>,
69 +
70 + pub(super) connect_count: u32,
71 + pub(super) reconnect_count: u32,
72 + pub(super) call_count: u32,
73 + pub(super) error_count: u32,
74 +}
75 +
76 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77 +pub(super) struct RawCallKind {
78 + pub(super) flags: u16,
79 + pub(super) item_count: u32,
80 + pub(super) check_item_count: bool,
81 +}
82 +
83 +impl RawCallKind {
84 + pub(super) fn single() -> Self {
85 + Self {
86 + flags: 0,
87 + item_count: 1,
88 + check_item_count: false,
89 + }
90 + }
91 +
92 + pub(super) fn batch(item_count: u32) -> Self {
93 + Self {
94 + flags: protocol::FLAG_BATCH,
95 + item_count,
96 + check_item_count: true,
97 + }
98 + }
99 +}
100 +
101 +impl RawClient {
102 + pub(super) fn new_bound(
103 + run_dir: &str,
104 + service_name: &str,
105 + expected_method_code: u16,
106 + config: ClientConfig,
107 + ) -> Self {
108 + RawClient {
109 + state: ClientState::Disconnected,
110 + run_dir: run_dir.to_string(),
111 + service_name: service_name.to_string(),
112 + expected_method_code,
113 + transport_config: config,
114 + session: None,
115 + #[cfg(target_os = "linux")]
116 + shm: None,
117 + #[cfg(windows)]
118 + shm: None,
119 + request_buf: Vec::new(),
120 + send_buf: Vec::new(),
121 + transport_buf: Vec::new(),
122 + connect_count: 0,
123 + reconnect_count: 0,
124 + call_count: 0,
125 + error_count: 0,
126 + }
127 + }
128 +
129 + /// Attempt connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
130 + /// Returns true if the state changed.
131 + pub fn refresh(&mut self) -> bool {
132 + let old_state = self.state;
133 +
134 + match self.state {
135 + ClientState::Disconnected | ClientState::NotFound => {
136 + self.state = ClientState::Connecting;
137 + self.state = self.try_connect();
138 + if self.state == ClientState::Ready {
139 + self.connect_count += 1;
140 + }
141 + }
142 + ClientState::Broken => {
143 + self.disconnect();
144 + self.state = ClientState::Connecting;
145 + self.state = self.try_connect();
146 + if self.state == ClientState::Ready {
147 + self.reconnect_count += 1;
148 + }
149 + }
150 + ClientState::Ready
151 + | ClientState::Connecting
152 + | ClientState::AuthFailed
153 + | ClientState::Incompatible => {}
154 + }
155 +
156 + self.state != old_state
157 + }
158 +
159 + /// Cheap cached boolean. No I/O, no syscalls.
160 + #[inline]
161 + pub fn ready(&self) -> bool {
162 + self.state == ClientState::Ready
163 + }
164 +
165 + /// Detailed status snapshot for diagnostics.
166 + pub fn status(&self) -> ClientStatus {
167 + ClientStatus {
168 + state: self.state,
169 + connect_count: self.connect_count,
170 + reconnect_count: self.reconnect_count,
171 + call_count: self.call_count,
172 + error_count: self.error_count,
173 + }
174 + }
175 +
176 + pub(super) fn request_scratch(&mut self, needed: usize) -> &mut [u8] {
177 + ensure_client_scratch(&mut self.request_buf, needed)
178 + }
179 +
180 + pub(super) fn validate_method(&self, method_code: u16) -> Result<(), NipcError> {
181 + if self.expected_method_code == method_code {
182 + Ok(())
183 + } else {
184 + Err(NipcError::BadLayout)
185 + }
186 + }
187 +
188 + pub(super) fn session_max_request_payload_bytes(&self) -> u32 {
189 + #[cfg(unix)]
190 + if let Some(ref session) = self.session {
191 + return session.max_request_payload_bytes;
192 + }
193 +
194 + #[cfg(windows)]
195 + if let Some(ref session) = self.session {
196 + return session.max_request_payload_bytes;
197 + }
198 +
199 + self.transport_config.max_request_payload_bytes
200 + }
201 +
202 + pub(super) fn session_max_response_payload_bytes(&self) -> u32 {
203 + #[cfg(unix)]
204 + if let Some(ref session) = self.session {
205 + return session.max_response_payload_bytes;
206 + }
207 +
208 + #[cfg(windows)]
209 + if let Some(ref session) = self.session {
210 + return session.max_response_payload_bytes;
211 + }
212 +
213 + self.transport_config.max_response_payload_bytes
214 + }
215 +
216 + pub(super) fn client_note_request_capacity(&mut self, payload_len: u32) {
217 + let grown = next_power_of_2_u32(payload_len).min(MAX_PAYLOAD_CAP);
218 + if grown > self.transport_config.max_request_payload_bytes {
219 + self.transport_config.max_request_payload_bytes = grown;
220 + }
221 + }
222 +
223 + pub(super) fn client_note_response_capacity(&mut self, payload_len: u32) {
224 + let grown = next_power_of_2_u32(payload_len).min(MAX_PAYLOAD_CAP);
225 + if grown > self.transport_config.max_response_payload_bytes {
226 + self.transport_config.max_response_payload_bytes = grown;
227 + }
228 + }
229 +
230 + /// Tear down connection and release resources.
231 + pub fn close(&mut self) {
232 + self.disconnect();
233 + self.state = ClientState::Disconnected;
234 + }
235 +
236 + /// Tear down the current connection.
237 + pub(super) fn disconnect(&mut self) {
238 + #[cfg(target_os = "linux")]
239 + {
240 + if let Some(mut shm) = self.shm.take() {
241 + shm.close();
242 + }
243 + }
244 +
245 + #[cfg(windows)]
246 + {
247 + if let Some(mut shm) = self.shm.take() {
248 + shm.close();
249 + }
250 + }
251 +
252 + self.session.take();
253 + }
254 +}
255 +
256 +impl Drop for RawClient {
257 + fn drop(&mut self) {
258 + self.close();
259 + }
260 +}
261 +
262 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263 +pub(super) enum ClientResponseSource {
264 + TransportBuf,
265 + SessionBuf,
266 +}
267 +
268 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269 +pub(super) struct ClientResponseRef {
270 + pub(super) source: ClientResponseSource,
271 + pub(super) len: usize,
272 +}
src/crates/netipc/src/service/raw/client_call.rs new
+386
@@ -0,0 +1,386 @@
1 +use super::client::{ClientResponseRef, ClientResponseSource, ClientState, RawCallKind, RawClient};
2 +use super::common::{ensure_client_scratch, CACHE_RESPONSE_BUF_SIZE};
3 +use crate::protocol::{
4 + self, Header, NipcError, HEADER_SIZE, KIND_REQUEST, KIND_RESPONSE, MAGIC_MSG,
5 + STATUS_LIMIT_EXCEEDED, STATUS_OK, VERSION,
6 +};
7 +
8 +impl RawClient {
9 + /// Reconnect-driven recovery for raw calls.
10 + ///
11 + /// Ordinary failures retry once. Overflow-driven resize recovery may
12 + /// reconnect more than once while negotiated capacities grow.
13 + pub(super) fn raw_call_with_retry(
14 + &mut self,
15 + method_code: u16,
16 + req_len: usize,
17 + call: RawCallKind,
18 + ) -> Result<ClientResponseRef, NipcError> {
19 + if self.state != ClientState::Ready {
20 + self.error_count += 1;
21 + return Err(NipcError::BadLayout);
22 + }
23 +
24 + let mut overflow_retries = 0u32;
25 + loop {
26 + let prev_req = self.session_max_request_payload_bytes();
27 + let prev_resp = self.session_max_response_payload_bytes();
28 + let prev_cfg_req = self.transport_config.max_request_payload_bytes;
29 + let prev_cfg_resp = self.transport_config.max_response_payload_bytes;
30 +
31 + match self.do_raw_call(method_code, req_len, call) {
32 + Ok(payload) => {
33 + self.call_count += 1;
34 + return Ok(payload);
35 + }
36 + Err(first_err) => {
37 + if first_err != NipcError::Overflow {
38 + self.disconnect();
39 + self.state = ClientState::Broken;
40 + self.state = self.try_connect();
41 + if self.state != ClientState::Ready {
42 + self.error_count += 1;
43 + return Err(first_err);
44 + }
45 + self.reconnect_count += 1;
46 +
47 + match self.do_raw_call(method_code, req_len, call) {
48 + Ok(payload) => {
49 + self.call_count += 1;
50 + return Ok(payload);
51 + }
52 + Err(retry_err) => {
53 + self.disconnect();
54 + self.state = ClientState::Broken;
55 + self.error_count += 1;
56 + return Err(retry_err);
57 + }
58 + }
59 + }
60 +
61 + self.disconnect();
62 + self.state = ClientState::Broken;
63 + self.state = self.try_connect();
64 + if self.state != ClientState::Ready {
65 + self.error_count += 1;
66 + return Err(first_err);
67 + }
68 + self.reconnect_count += 1;
69 +
70 + if self.session_max_request_payload_bytes() <= prev_req
71 + && self.session_max_response_payload_bytes() <= prev_resp
72 + && self.transport_config.max_request_payload_bytes <= prev_cfg_req
73 + && self.transport_config.max_response_payload_bytes <= prev_cfg_resp
74 + {
75 + self.disconnect();
76 + self.state = ClientState::Broken;
77 + self.error_count += 1;
78 + return Err(first_err);
79 + }
80 +
81 + overflow_retries += 1;
82 + if overflow_retries >= 8 {
83 + self.disconnect();
84 + self.state = ClientState::Broken;
85 + self.error_count += 1;
86 + return Err(first_err);
87 + }
88 + }
89 + }
90 + }
91 + }
92 +
93 + /// Single attempt at a raw call.
94 + fn do_raw_call(
95 + &mut self,
96 + method_code: u16,
97 + req_len: usize,
98 + call: RawCallKind,
99 + ) -> Result<ClientResponseRef, NipcError> {
100 + let mut hdr = Header {
101 + kind: KIND_REQUEST,
102 + code: method_code,
103 + flags: call.flags,
104 + item_count: call.item_count,
105 + message_id: (self.call_count as u64) + 1,
106 + transport_status: STATUS_OK,
107 + ..Header::default()
108 + };
109 +
110 + self.transport_send_request_buf(&mut hdr, req_len)?;
111 + let (resp_hdr, response) = self.transport_receive()?;
112 +
113 + if resp_hdr.kind != KIND_RESPONSE {
114 + return Err(NipcError::BadKind);
115 + }
116 + if resp_hdr.code != method_code {
117 + return Err(NipcError::BadLayout);
118 + }
119 + if resp_hdr.message_id != hdr.message_id {
120 + return Err(NipcError::BadLayout);
121 + }
122 +
123 + match resp_hdr.transport_status {
124 + STATUS_OK => {}
125 + STATUS_LIMIT_EXCEEDED => {
126 + let current = self.session_max_response_payload_bytes();
127 + if current > 0 {
128 + self.client_note_response_capacity(current.saturating_mul(2));
129 + }
130 + return Err(NipcError::Overflow);
131 + }
132 + _ => return Err(NipcError::BadLayout),
133 + }
134 +
135 + if call.check_item_count && resp_hdr.item_count != call.item_count {
136 + return Err(NipcError::BadItemCount);
137 + }
138 +
139 + Ok(response)
140 + }
141 +
142 + /// Compatibility test seam for sending a borrowed payload through the
143 + /// single shared request-buffer transport path.
144 + #[cfg(test)]
145 + #[allow(dead_code)]
146 + pub(super) fn transport_send(
147 + &mut self,
148 + hdr: &mut Header,
149 + payload: &[u8],
150 + ) -> Result<(), NipcError> {
151 + let req = self.request_scratch(payload.len());
152 + req.copy_from_slice(payload);
153 + self.transport_send_request_buf(hdr, payload.len())
154 + }
155 +
156 + /// Send via the active transport (SHM if available, baseline otherwise).
157 + pub(super) fn transport_send_request_buf(
158 + &mut self,
159 + hdr: &mut Header,
160 + req_len: usize,
161 + ) -> Result<(), NipcError> {
162 + let max_request_payload_bytes = self.session_max_request_payload_bytes();
163 +
164 + #[cfg(target_os = "linux")]
165 + {
166 + if self.shm.is_some() {
167 + if req_len > max_request_payload_bytes as usize {
168 + self.client_note_request_capacity(req_len as u32);
169 + return Err(NipcError::Overflow);
170 + }
171 +
172 + let msg_len = HEADER_SIZE + req_len;
173 + let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
174 +
175 + hdr.magic = MAGIC_MSG;
176 + hdr.version = VERSION;
177 + hdr.header_len = protocol::HEADER_LEN;
178 + hdr.payload_len = req_len as u32;
179 +
180 + hdr.encode(&mut msg[..HEADER_SIZE]);
181 + if req_len > 0 {
182 + msg[HEADER_SIZE..HEADER_SIZE + req_len]
183 + .copy_from_slice(&self.request_buf[..req_len]);
184 + }
185 +
186 + let send_result = self.shm.as_mut().unwrap().send(&msg[..msg_len]);
187 + return match send_result {
188 + Ok(()) => Ok(()),
189 + Err(crate::transport::shm::ShmError::MsgTooLarge) => {
190 + self.client_note_request_capacity(req_len as u32);
191 + Err(NipcError::Overflow)
192 + }
193 + Err(_) => Err(NipcError::Truncated),
194 + };
195 + }
196 + }
197 +
198 + #[cfg(windows)]
199 + {
200 + if self.shm.is_some() {
201 + if req_len > max_request_payload_bytes as usize {
202 + self.client_note_request_capacity(req_len as u32);
203 + return Err(NipcError::Overflow);
204 + }
205 +
206 + let msg_len = HEADER_SIZE + req_len;
207 + let msg = ensure_client_scratch(&mut self.send_buf, msg_len);
208 +
209 + hdr.magic = MAGIC_MSG;
210 + hdr.version = VERSION;
211 + hdr.header_len = protocol::HEADER_LEN;
212 + hdr.payload_len = req_len as u32;
213 +
214 + hdr.encode(&mut msg[..HEADER_SIZE]);
215 + if req_len > 0 {
216 + msg[HEADER_SIZE..HEADER_SIZE + req_len]
217 + .copy_from_slice(&self.request_buf[..req_len]);
218 + }
219 +
220 + let send_result = self.shm.as_mut().unwrap().send(&msg[..msg_len]);
221 + return match send_result {
222 + Ok(()) => Ok(()),
223 + Err(crate::transport::win_shm::WinShmError::MsgTooLarge) => {
224 + self.client_note_request_capacity(req_len as u32);
225 + Err(NipcError::Overflow)
226 + }
227 + Err(_) => Err(NipcError::Truncated),
228 + };
229 + }
230 + }
231 +
232 + let send_result = {
233 + let session = self.session.as_mut().ok_or(NipcError::Truncated)?;
234 + session.send(hdr, &self.request_buf[..req_len])
235 + };
236 + match send_result {
237 + Ok(()) => Ok(()),
238 + #[cfg(unix)]
239 + Err(crate::transport::posix::UdsError::LimitExceeded) => {
240 + self.client_note_request_capacity(req_len as u32);
241 + Err(NipcError::Overflow)
242 + }
243 + #[cfg(windows)]
244 + Err(crate::transport::windows::NpError::LimitExceeded) => {
245 + self.client_note_request_capacity(req_len as u32);
246 + Err(NipcError::Overflow)
247 + }
248 + Err(_) => Err(NipcError::Truncated),
249 + }
250 + }
251 +
252 + /// Receive via the active transport. Returns (header, payload view).
253 + pub(super) fn transport_receive(&mut self) -> Result<(Header, ClientResponseRef), NipcError> {
254 + let needed = self.max_receive_message_bytes();
255 + let scratch = ensure_client_scratch(&mut self.transport_buf, needed);
256 +
257 + #[cfg(target_os = "linux")]
258 + {
259 + if let Some(ref mut shm) = self.shm {
260 + let mlen = shm
261 + .receive(scratch, 30000)
262 + .map_err(|_| NipcError::Truncated)?;
263 +
264 + if mlen < HEADER_SIZE {
265 + return Err(NipcError::Truncated);
266 + }
267 +
268 + let hdr = Header::decode(&scratch[..mlen])?;
269 + return Ok((
270 + hdr,
271 + ClientResponseRef {
272 + source: ClientResponseSource::TransportBuf,
273 + len: mlen - HEADER_SIZE,
274 + },
275 + ));
276 + }
277 + }
278 +
279 + #[cfg(windows)]
280 + {
281 + if let Some(ref mut shm) = self.shm {
282 + let mlen = shm
283 + .receive(scratch, 30000)
284 + .map_err(|_| NipcError::Truncated)?;
285 +
286 + if mlen < HEADER_SIZE {
287 + return Err(NipcError::Truncated);
288 + }
289 +
290 + let hdr = Header::decode(&scratch[..mlen])?;
291 + return Ok((
292 + hdr,
293 + ClientResponseRef {
294 + source: ClientResponseSource::TransportBuf,
295 + len: mlen - HEADER_SIZE,
296 + },
297 + ));
298 + }
299 + }
300 +
301 + let session = self.session.as_mut().ok_or(NipcError::Truncated)?;
302 +
303 + #[cfg(unix)]
304 + {
305 + let scratch_payload_ptr = unsafe { scratch.as_ptr().add(HEADER_SIZE) };
306 + let (hdr, payload) = session.receive(scratch).map_err(|_| NipcError::Truncated)?;
307 + let source = if payload.as_ptr() == scratch_payload_ptr {
308 + ClientResponseSource::TransportBuf
309 + } else {
310 + ClientResponseSource::SessionBuf
311 + };
312 + Ok((
313 + hdr,
314 + ClientResponseRef {
315 + source,
316 + len: payload.len(),
317 + },
318 + ))
319 + }
320 +
321 + #[cfg(windows)]
322 + {
323 + let scratch_payload_ptr = unsafe { scratch.as_ptr().add(HEADER_SIZE) };
324 + let (hdr, payload) = session.receive(scratch).map_err(|_| NipcError::Truncated)?;
325 + let source = if payload.as_ptr() == scratch_payload_ptr {
326 + ClientResponseSource::TransportBuf
327 + } else {
328 + ClientResponseSource::SessionBuf
329 + };
330 + Ok((
331 + hdr,
332 + ClientResponseRef {
333 + source,
334 + len: payload.len(),
335 + },
336 + ))
337 + }
338 + }
339 +
340 + pub(super) fn response_payload(&self, response: ClientResponseRef) -> Result<&[u8], NipcError> {
341 + match response.source {
342 + ClientResponseSource::TransportBuf => {
343 + let start = HEADER_SIZE;
344 + let end = HEADER_SIZE + response.len;
345 + if end > self.transport_buf.len() {
346 + return Err(NipcError::Truncated);
347 + }
348 + Ok(&self.transport_buf[start..end])
349 + }
350 + ClientResponseSource::SessionBuf => {
351 + #[cfg(unix)]
352 + {
353 + let session = self.session.as_ref().ok_or(NipcError::Truncated)?;
354 + return Ok(session.received_payload(response.len));
355 + }
356 + #[cfg(windows)]
357 + {
358 + let session = self.session.as_ref().ok_or(NipcError::Truncated)?;
359 + return Ok(session.received_payload(response.len));
360 + }
361 + #[allow(unreachable_code)]
362 + Err(NipcError::Truncated)
363 + }
364 + }
365 + }
366 +
367 + pub(super) fn max_receive_message_bytes(&self) -> usize {
368 + let mut max_payload = self.transport_config.max_response_payload_bytes as usize;
369 + #[cfg(unix)]
370 + if let Some(ref session) = self.session {
371 + if session.max_response_payload_bytes > 0 {
372 + max_payload = session.max_response_payload_bytes as usize;
373 + }
374 + }
375 + #[cfg(windows)]
376 + if let Some(ref session) = self.session {
377 + if session.max_response_payload_bytes > 0 {
378 + max_payload = session.max_response_payload_bytes as usize;
379 + }
380 + }
381 + if max_payload == 0 {
382 + max_payload = CACHE_RESPONSE_BUF_SIZE;
383 + }
384 + HEADER_SIZE + max_payload
385 + }
386 +}
src/crates/netipc/src/service/raw/client_unix.rs new
+81
@@ -0,0 +1,81 @@
1 +#![cfg(unix)]
2 +
3 +use super::client::{ClientState, RawClient};
4 +use super::common::{CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS, CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS};
5 +#[cfg(target_os = "linux")]
6 +use crate::protocol::{PROFILE_SHM_FUTEX, PROFILE_SHM_HYBRID};
7 +use crate::transport::posix::UdsSession;
8 +#[cfg(target_os = "linux")]
9 +use crate::transport::shm::ShmContext;
10 +
11 +impl RawClient {
12 + /// Attempt a full connection: transport connect + handshake, then SHM
13 + /// upgrade if negotiated.
14 + #[cfg(unix)]
15 + pub(super) fn try_connect(&mut self) -> ClientState {
16 + match UdsSession::connect(&self.run_dir, &self.service_name, &self.transport_config) {
17 + Ok(session) => {
18 + #[cfg(target_os = "linux")]
19 + let selected_profile = session.selected_profile;
20 + #[cfg(target_os = "linux")]
21 + let session_id = session.session_id;
22 +
23 + #[cfg(target_os = "linux")]
24 + {
25 + if selected_profile == PROFILE_SHM_HYBRID
26 + || selected_profile == PROFILE_SHM_FUTEX
27 + {
28 + let mut shm_ok = false;
29 + let deadline = std::time::Instant::now()
30 + + std::time::Duration::from_millis(CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS);
31 + loop {
32 + match ShmContext::client_attach(
33 + &self.run_dir,
34 + &self.service_name,
35 + session_id,
36 + ) {
37 + Ok(ctx) => {
38 + self.shm = Some(ctx);
39 + shm_ok = true;
40 + break;
41 + }
42 + Err(_) => {
43 + if std::time::Instant::now() >= deadline {
44 + break;
45 + }
46 + std::thread::sleep(std::time::Duration::from_millis(
47 + CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS,
48 + ));
49 + }
50 + }
51 + }
52 + if !shm_ok {
53 + drop(session);
54 + self.transport_config.supported_profiles &=
55 + !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
56 + self.transport_config.preferred_profiles &=
57 + !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
58 + if self.transport_config.supported_profiles == 0 {
59 + return ClientState::Disconnected;
60 + }
61 + return self.try_connect();
62 + }
63 + }
64 + }
65 +
66 + self.session = Some(session);
67 + ClientState::Ready
68 + }
69 + Err(e) => {
70 + use crate::transport::posix::UdsError;
71 + match e {
72 + UdsError::Connect(_) => ClientState::NotFound,
73 + UdsError::AuthFailed => ClientState::AuthFailed,
74 + UdsError::NoProfile => ClientState::Incompatible,
75 + UdsError::Incompatible(_) => ClientState::Incompatible,
76 + _ => ClientState::Disconnected,
77 + }
78 + }
79 + }
80 + }
81 +}
src/crates/netipc/src/service/raw/client_windows.rs new
+70
@@ -0,0 +1,70 @@
1 +use super::client::{ClientState, RawClient};
2 +use super::common::{CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS, CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS};
3 +use crate::transport::win_shm::{
4 + WinShmContext, PROFILE_BUSYWAIT as WIN_SHM_PROFILE_BUSYWAIT,
5 + PROFILE_HYBRID as WIN_SHM_PROFILE_HYBRID,
6 +};
7 +use crate::transport::windows::{NpError, NpSession};
8 +
9 +impl RawClient {
10 + /// Windows: attempt a full Named Pipe connection + Win SHM upgrade.
11 + pub(super) fn try_connect(&mut self) -> ClientState {
12 + match NpSession::connect(&self.run_dir, &self.service_name, &self.transport_config) {
13 + Ok(session) => {
14 + let selected_profile = session.selected_profile;
15 +
16 + if selected_profile == WIN_SHM_PROFILE_HYBRID
17 + || selected_profile == WIN_SHM_PROFILE_BUSYWAIT
18 + {
19 + let mut shm_ok = false;
20 + let deadline = std::time::Instant::now()
21 + + std::time::Duration::from_millis(CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS);
22 + loop {
23 + match WinShmContext::client_attach(
24 + &self.run_dir,
25 + &self.service_name,
26 + self.transport_config.auth_token,
27 + session.session_id,
28 + selected_profile,
29 + ) {
30 + Ok(ctx) => {
31 + self.shm = Some(ctx);
32 + shm_ok = true;
33 + break;
34 + }
35 + Err(_) => {
36 + if std::time::Instant::now() >= deadline {
37 + break;
38 + }
39 + std::thread::sleep(std::time::Duration::from_millis(
40 + CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS,
41 + ));
42 + }
43 + }
44 + }
45 + if !shm_ok {
46 + drop(session);
47 + self.transport_config.supported_profiles &=
48 + !(WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
49 + self.transport_config.preferred_profiles &=
50 + !(WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
51 + if self.transport_config.supported_profiles == 0 {
52 + return ClientState::Disconnected;
53 + }
54 + return self.try_connect();
55 + }
56 + }
57 +
58 + self.session = Some(session);
59 + ClientState::Ready
60 + }
61 + Err(e) => match e {
62 + NpError::Connect(_) => ClientState::NotFound,
63 + NpError::AuthFailed => ClientState::AuthFailed,
64 + NpError::NoProfile => ClientState::Incompatible,
65 + NpError::Incompatible(_) => ClientState::Incompatible,
66 + _ => ClientState::Disconnected,
67 + },
68 + }
69 + }
70 +}
src/crates/netipc/src/service/raw/common.rs new
+29
@@ -0,0 +1,29 @@
1 +pub(super) const SERVER_POLL_TIMEOUT_MS: u32 = 100;
2 +pub(super) const CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS: u64 = 5;
3 +pub(super) const CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS: u64 = 5_000;
4 +pub(super) const CACHE_RESPONSE_BUF_SIZE: usize = 65536;
5 +
6 +pub(super) fn next_power_of_2_u32(n: u32) -> u32 {
7 + if n < 16 {
8 + return 16;
9 + }
10 +
11 + if n > (1u32 << 31) {
12 + return 1u32 << 31;
13 + }
14 +
15 + let mut value = n - 1;
16 + value |= value >> 1;
17 + value |= value >> 2;
18 + value |= value >> 4;
19 + value |= value >> 8;
20 + value |= value >> 16;
21 + value + 1
22 +}
23 +
24 +pub(super) fn ensure_client_scratch(buf: &mut Vec<u8>, needed: usize) -> &mut [u8] {
25 + if buf.len() < needed {
26 + buf.resize(needed, 0);
27 + }
28 + &mut buf[..needed]
29 +}
src/crates/netipc/src/service/raw/dispatch.rs new
+71
@@ -0,0 +1,71 @@
1 +use super::common::next_power_of_2_u32;
2 +use std::sync::atomic::{AtomicU32, Ordering};
3 +use std::sync::Arc;
4 +
5 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6 +pub enum DispatchError {
7 + BadEnvelope,
8 + Overflow,
9 + HandlerFailed,
10 +}
11 +
12 +pub type DispatchHandler =
13 + Arc<dyn Fn(&[u8], &mut [u8]) -> Result<usize, DispatchError> + Send + Sync>;
14 +
15 +pub(super) fn dispatch_single_internal(
16 + expected_method_code: u16,
17 + handler: Option<&DispatchHandler>,
18 + method_code: u16,
19 + request: &[u8],
20 + response_buf: &mut [u8],
21 +) -> Result<usize, DispatchError> {
22 + if method_code != expected_method_code {
23 + return Err(DispatchError::HandlerFailed);
24 + }
25 +
26 + match handler {
27 + Some(dispatch) => match dispatch(request, response_buf) {
28 + Ok(n) if n <= response_buf.len() => Ok(n),
29 + Ok(_) => Err(DispatchError::Overflow),
30 + Err(err) => Err(err),
31 + },
32 + None => Err(DispatchError::HandlerFailed),
33 + }
34 +}
35 +
36 +#[cfg(test)]
37 +#[allow(dead_code)]
38 +pub(super) fn dispatch_single(
39 + expected_method_code: u16,
40 + handler: Option<&DispatchHandler>,
41 + method_code: u16,
42 + request: &[u8],
43 + response_buf: &mut [u8],
44 +) -> Result<usize, DispatchError> {
45 + dispatch_single_internal(
46 + expected_method_code,
47 + handler,
48 + method_code,
49 + request,
50 + response_buf,
51 + )
52 +}
53 +
54 +pub(super) fn method_supported_internal(
55 + expected_method_code: u16,
56 + handler: Option<&DispatchHandler>,
57 + method_code: u16,
58 +) -> bool {
59 + handler.is_some() && method_code == expected_method_code
60 +}
61 +
62 +pub(super) fn server_note_payload_capacity(target: &AtomicU32, payload_len: u32) {
63 + let grown = next_power_of_2_u32(payload_len);
64 + let mut current = target.load(Ordering::Relaxed);
65 + while grown > current {
66 + match target.compare_exchange_weak(current, grown, Ordering::Release, Ordering::Relaxed) {
67 + Ok(_) => break,
68 + Err(observed) => current = observed,
69 + }
70 + }
71 +}
src/crates/netipc/src/service/raw/increment.rs new
+91
@@ -0,0 +1,91 @@
1 +use super::client::{ClientConfig, RawCallKind, RawClient};
2 +use super::dispatch::{DispatchError, DispatchHandler};
3 +use crate::protocol::{
4 + self, batch_item_get, increment_decode, increment_encode, BatchBuilder, NipcError,
5 + INCREMENT_PAYLOAD_SIZE, METHOD_INCREMENT,
6 +};
7 +use std::sync::Arc;
8 +
9 +pub type IncrementHandler = Arc<dyn Fn(u64) -> Option<u64> + Send + Sync>;
10 +
11 +impl RawClient {
12 + /// Create a new client context bound to the increment service kind.
13 + /// Does NOT connect. Does NOT require the server to be running.
14 + pub fn new_increment(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
15 + Self::new_bound(run_dir, service_name, METHOD_INCREMENT, config)
16 + }
17 +
18 + /// Blocking typed call: INCREMENT method.
19 + /// Sends a u64 value, receives the incremented u64 back.
20 + pub fn call_increment(&mut self, value: u64) -> Result<u64, NipcError> {
21 + self.validate_method(METHOD_INCREMENT)?;
22 + let req_len = {
23 + let req_buf = self.request_scratch(INCREMENT_PAYLOAD_SIZE);
24 + let req_len = increment_encode(value, req_buf);
25 + if req_len == 0 {
26 + return Err(NipcError::Truncated);
27 + }
28 + req_len
29 + };
30 +
31 + let response =
32 + self.raw_call_with_retry(METHOD_INCREMENT, req_len, RawCallKind::single())?;
33 + increment_decode(self.response_payload(response)?)
34 + }
35 +
36 + /// Blocking typed batch call: INCREMENT method.
37 + /// Sends multiple u64 values, receives the incremented u64s back.
38 + pub fn call_increment_batch(&mut self, values: &[u64]) -> Result<Vec<u64>, NipcError> {
39 + self.validate_method(METHOD_INCREMENT)?;
40 + if values.is_empty() {
41 + return Ok(Vec::new());
42 + }
43 +
44 + if values.len() == 1 {
45 + let r = self.call_increment(values[0])?;
46 + return Ok(vec![r]);
47 + }
48 +
49 + let count = values.len() as u32;
50 + let req_buf_size = protocol::align8(count as usize * 8)
51 + + count as usize * protocol::align8(INCREMENT_PAYLOAD_SIZE)
52 + + 64;
53 + let req_len = {
54 + let req_buf = self.request_scratch(req_buf_size);
55 + let mut bb = BatchBuilder::new(req_buf, count);
56 + for &v in values {
57 + let mut item_buf = [0u8; INCREMENT_PAYLOAD_SIZE];
58 + if increment_encode(v, &mut item_buf) == 0 {
59 + return Err(NipcError::Truncated);
60 + }
61 + bb.add(&item_buf).map_err(|_| NipcError::Overflow)?;
62 + }
63 + let (req_len, _out_count) = bb.finish();
64 + req_len
65 + };
66 +
67 + let response =
68 + self.raw_call_with_retry(METHOD_INCREMENT, req_len, RawCallKind::batch(count))?;
69 + let resp_payload = self.response_payload(response)?;
70 + let mut results = Vec::with_capacity(values.len());
71 + for i in 0..count {
72 + let (item_data, _item_len) = batch_item_get(resp_payload, count, i)?;
73 + let val = increment_decode(item_data)?;
74 + results.push(val);
75 + }
76 +
77 + Ok(results)
78 + }
79 +}
80 +
81 +pub fn increment_dispatch(handler: IncrementHandler) -> DispatchHandler {
82 + Arc::new(move |request, response_buf| {
83 + let value = increment_decode(request).map_err(|_| DispatchError::BadEnvelope)?;
84 + let result = handler(value).ok_or(DispatchError::HandlerFailed)?;
85 + let n = increment_encode(result, response_buf);
86 + if n == 0 {
87 + return Err(DispatchError::Overflow);
88 + }
89 + Ok(n)
90 + })
91 +}
src/crates/netipc/src/service/raw/server.rs new
+113
@@ -0,0 +1,113 @@
1 +use super::dispatch::DispatchHandler;
2 +use crate::protocol::MAX_PAYLOAD_DEFAULT;
3 +
4 +#[cfg(unix)]
5 +pub(super) use crate::transport::posix::ServerConfig;
6 +
7 +#[cfg(windows)]
8 +pub(super) use crate::transport::windows::ServerConfig;
9 +
10 +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
11 +use std::sync::Arc;
12 +
13 +/// L2 managed server. Typed request/response dispatcher.
14 +///
15 +/// Handles accept, spawns a thread per session (up to worker_count),
16 +/// reads requests, dispatches to handler, sends responses.
17 +pub struct ManagedServer {
18 + pub(super) run_dir: String,
19 + pub(super) service_name: String,
20 + pub(super) server_config: ServerConfig,
21 + pub(super) expected_method_code: u16,
22 + pub(super) handler: Option<DispatchHandler>,
23 + pub(super) running: Arc<AtomicBool>,
24 + pub(super) learned_request_payload_bytes: Arc<AtomicU32>,
25 + pub(super) learned_response_payload_bytes: Arc<AtomicU32>,
26 + pub(super) next_session_id: u64,
27 + pub(super) worker_count: usize,
28 + #[cfg(windows)]
29 + pub(super) listener_handle: Arc<std::sync::Mutex<Option<usize>>>,
30 +}
31 +
32 +impl ManagedServer {
33 + /// Create a new managed server for a single service kind.
34 + pub fn new(
35 + run_dir: &str,
36 + service_name: &str,
37 + config: ServerConfig,
38 + expected_method_code: u16,
39 + handler: Option<DispatchHandler>,
40 + ) -> Self {
41 + Self::with_workers(
42 + run_dir,
43 + service_name,
44 + config,
45 + expected_method_code,
46 + handler,
47 + 8,
48 + )
49 + }
50 +
51 + /// Create a managed server with an explicit worker count.
52 + pub fn with_workers(
53 + run_dir: &str,
54 + service_name: &str,
55 + config: ServerConfig,
56 + expected_method_code: u16,
57 + handler: Option<DispatchHandler>,
58 + worker_count: usize,
59 + ) -> Self {
60 + let learned_request = if config.max_request_payload_bytes != 0 {
61 + config.max_request_payload_bytes
62 + } else {
63 + MAX_PAYLOAD_DEFAULT
64 + };
65 + let learned_response = if config.max_response_payload_bytes != 0 {
66 + config.max_response_payload_bytes
67 + } else {
68 + MAX_PAYLOAD_DEFAULT
69 + };
70 +
71 + ManagedServer {
72 + run_dir: run_dir.to_string(),
73 + service_name: service_name.to_string(),
74 + server_config: config,
75 + expected_method_code,
76 + handler,
77 + running: Arc::new(AtomicBool::new(false)),
78 + learned_request_payload_bytes: Arc::new(AtomicU32::new(learned_request)),
79 + learned_response_payload_bytes: Arc::new(AtomicU32::new(learned_response)),
80 + next_session_id: 1,
81 + worker_count: if worker_count < 1 { 1 } else { worker_count },
82 + #[cfg(windows)]
83 + listener_handle: Arc::new(std::sync::Mutex::new(None)),
84 + }
85 + }
86 +
87 + /// Signal shutdown. On Windows, also closes the listener pipe to
88 + /// unblock ConnectNamedPipe in the accept loop.
89 + pub fn stop(&self) {
90 + self.running.store(false, Ordering::Release);
91 +
92 + #[cfg(windows)]
93 + {
94 + let mut guard = self.listener_handle.lock().unwrap();
95 + if let Some(h) = guard.take() {
96 + extern "system" {
97 + fn CloseHandle(h: isize) -> i32;
98 + }
99 + unsafe {
100 + CloseHandle(h as isize);
101 + }
102 + }
103 + }
104 + }
105 +
106 + /// Returns the internal running flag for diagnostics and test helpers.
107 + ///
108 + /// For reliable shutdown, call `stop()`. On Windows, flipping this flag
109 + /// alone does not wake a blocking listener accept.
110 + pub fn running_flag(&self) -> Arc<AtomicBool> {
111 + self.running.clone()
112 + }
113 +}
src/crates/netipc/src/service/raw/server_session_unix.rs new
+304
@@ -0,0 +1,304 @@
1 +use super::common::{ensure_client_scratch, SERVER_POLL_TIMEOUT_MS};
2 +use super::dispatch::{
3 + dispatch_single_internal, method_supported_internal, server_note_payload_capacity,
4 + DispatchError, DispatchHandler,
5 +};
6 +use crate::protocol::{
7 + self, batch_item_get, BatchBuilder, Header, FLAG_BATCH, HEADER_SIZE, KIND_REQUEST,
8 + KIND_RESPONSE, MAGIC_MSG, STATUS_BAD_ENVELOPE, STATUS_INTERNAL_ERROR, STATUS_LIMIT_EXCEEDED,
9 + STATUS_OK, VERSION,
10 +};
11 +use crate::transport::posix::UdsSession;
12 +
13 +#[cfg(target_os = "linux")]
14 +use crate::transport::shm::ShmContext;
15 +
16 +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
17 +use std::sync::Arc;
18 +
19 +/// POSIX: Handle one client session in its own thread.
20 +pub(super) fn handle_session_threaded(
21 + mut session: UdsSession,
22 + #[cfg(target_os = "linux")] mut shm: Option<ShmContext>,
23 + #[cfg(not(target_os = "linux"))] _shm: Option<()>,
24 + expected_method_code: u16,
25 + handler: Option<DispatchHandler>,
26 + running: Arc<AtomicBool>,
27 + learned_request_payload_bytes: Arc<AtomicU32>,
28 + learned_response_payload_bytes: Arc<AtomicU32>,
29 +) {
30 + let mut recv_buf = vec![0u8; HEADER_SIZE + session.max_request_payload_bytes as usize];
31 + let mut resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
32 + let mut item_resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
33 + let mut msg_buf = vec![0u8; HEADER_SIZE + session.max_response_payload_bytes as usize];
34 +
35 + while running.load(Ordering::Acquire) {
36 + let (hdr, payload) = {
37 + #[cfg(target_os = "linux")]
38 + {
39 + if let Some(ref mut shm_ctx) = shm {
40 + match shm_ctx.receive(&mut recv_buf, SERVER_POLL_TIMEOUT_MS) {
41 + Ok(mlen) => {
42 + if mlen < HEADER_SIZE {
43 + break;
44 + }
45 + let hdr = match Header::decode(&recv_buf[..mlen]) {
46 + Ok(h) => h,
47 + Err(_) => break,
48 + };
49 + let payload = &recv_buf[HEADER_SIZE..mlen];
50 + (hdr, payload)
51 + }
52 + Err(crate::transport::shm::ShmError::Timeout) => continue,
53 + Err(_) => break,
54 + }
55 + } else {
56 + let ready = poll_fd(session.fd(), SERVER_POLL_TIMEOUT_MS as i32);
57 + if ready < 0 {
58 + break;
59 + }
60 + if ready == 0 {
61 + continue;
62 + }
63 +
64 + match session.receive(&mut recv_buf) {
65 + Ok((hdr, payload)) => (hdr, payload),
66 + Err(_) => break,
67 + }
68 + }
69 + }
70 +
71 + #[cfg(not(target_os = "linux"))]
72 + {
73 + let ready = poll_fd(session.fd(), SERVER_POLL_TIMEOUT_MS as i32);
74 + if ready < 0 {
75 + break;
76 + }
77 + if ready == 0 {
78 + continue;
79 + }
80 +
81 + match session.receive(&mut recv_buf) {
82 + Ok((hdr, payload)) => (hdr, payload),
83 + Err(_) => break,
84 + }
85 + }
86 + };
87 +
88 + if hdr.kind != KIND_REQUEST {
89 + break;
90 + }
91 +
92 + if payload.len() <= u32::MAX as usize {
93 + server_note_payload_capacity(&learned_request_payload_bytes, payload.len() as u32);
94 + }
95 +
96 + if !method_supported_internal(expected_method_code, handler.as_ref(), hdr.code) {
97 + let mut resp_hdr = Header {
98 + kind: KIND_RESPONSE,
99 + code: hdr.code,
100 + message_id: hdr.message_id,
101 + transport_status: protocol::STATUS_UNSUPPORTED,
102 + item_count: 1,
103 + ..Header::default()
104 + };
105 +
106 + #[cfg(target_os = "linux")]
107 + {
108 + if let Some(ref mut shm_ctx) = shm {
109 + let msg = ensure_client_scratch(&mut msg_buf, HEADER_SIZE);
110 + resp_hdr.magic = MAGIC_MSG;
111 + resp_hdr.version = VERSION;
112 + resp_hdr.header_len = protocol::HEADER_LEN;
113 + resp_hdr.payload_len = 0;
114 + resp_hdr.encode(&mut msg[..HEADER_SIZE]);
115 + if shm_ctx.send(&msg[..HEADER_SIZE]).is_err() {
116 + break;
117 + }
118 + continue;
119 + }
120 + }
121 +
122 + if session.send(&mut resp_hdr, &[]).is_err() {
123 + break;
124 + }
125 + continue;
126 + }
127 +
128 + let is_batch = (hdr.flags & FLAG_BATCH) != 0 && hdr.item_count >= 1;
129 + let response_len;
130 + let dispatch_result = if !is_batch {
131 + dispatch_single_internal(
132 + expected_method_code,
133 + handler.as_ref(),
134 + hdr.code,
135 + payload,
136 + &mut resp_buf,
137 + )
138 + } else {
139 + let mut bb = BatchBuilder::new(&mut resp_buf, hdr.item_count);
140 + let mut batch_result = Ok(0usize);
141 +
142 + for i in 0..hdr.item_count {
143 + let (item_data, _item_len) = match batch_item_get(payload, hdr.item_count, i) {
144 + Ok(v) => v,
145 + Err(_) => {
146 + batch_result = Err(DispatchError::BadEnvelope);
147 + break;
148 + }
149 + };
150 + let item_len = match dispatch_single_internal(
151 + expected_method_code,
152 + handler.as_ref(),
153 + hdr.code,
154 + item_data,
155 + &mut item_resp_buf,
156 + ) {
157 + Ok(n) => n,
158 + Err(err) => {
159 + batch_result = Err(err);
160 + break;
161 + }
162 + };
163 + if bb.add(&item_resp_buf[..item_len]).is_err() {
164 + batch_result = Err(DispatchError::Overflow);
165 + break;
166 + }
167 + }
168 + if batch_result.is_ok() {
169 + let (n, _) = bb.finish();
170 + batch_result = Ok(n);
171 + }
172 + batch_result
173 + };
174 +
175 + let mut resp_hdr = Header {
176 + kind: KIND_RESPONSE,
177 + code: hdr.code,
178 + message_id: hdr.message_id,
179 + ..Header::default()
180 + };
181 +
182 + match dispatch_result {
183 + Ok(n) => {
184 + response_len = n;
185 + if response_len <= u32::MAX as usize {
186 + server_note_payload_capacity(
187 + &learned_response_payload_bytes,
188 + response_len as u32,
189 + );
190 + }
191 + resp_hdr.transport_status = STATUS_OK;
192 + if is_batch {
193 + resp_hdr.flags = FLAG_BATCH;
194 + resp_hdr.item_count = hdr.item_count;
195 + } else {
196 + resp_hdr.flags = 0;
197 + resp_hdr.item_count = 1;
198 + }
199 + }
200 + Err(DispatchError::Overflow) => {
201 + let current = session.max_response_payload_bytes;
202 + if current >= u32::MAX / 2 {
203 + server_note_payload_capacity(&learned_response_payload_bytes, u32::MAX);
204 + } else {
205 + server_note_payload_capacity(&learned_response_payload_bytes, current * 2);
206 + }
207 + resp_hdr.transport_status = STATUS_LIMIT_EXCEEDED;
208 + resp_hdr.item_count = 1;
209 + resp_hdr.flags = 0;
210 + response_len = 0;
211 + }
212 + Err(DispatchError::BadEnvelope) => {
213 + resp_hdr.transport_status = STATUS_BAD_ENVELOPE;
214 + resp_hdr.item_count = 1;
215 + resp_hdr.flags = 0;
216 + response_len = 0;
217 + }
218 + Err(DispatchError::HandlerFailed) => {
219 + resp_hdr.transport_status = STATUS_INTERNAL_ERROR;
220 + resp_hdr.item_count = 1;
221 + resp_hdr.flags = 0;
222 + response_len = 0;
223 + }
224 + }
225 +
226 + #[cfg(target_os = "linux")]
227 + {
228 + if let Some(ref mut shm_ctx) = shm {
229 + let msg_len = HEADER_SIZE + response_len;
230 + let msg = ensure_client_scratch(&mut msg_buf, msg_len);
231 +
232 + resp_hdr.magic = MAGIC_MSG;
233 + resp_hdr.version = VERSION;
234 + resp_hdr.header_len = protocol::HEADER_LEN;
235 + resp_hdr.payload_len = response_len as u32;
236 +
237 + resp_hdr.encode(&mut msg[..HEADER_SIZE]);
238 + if response_len > 0 {
239 + msg[HEADER_SIZE..].copy_from_slice(&resp_buf[..response_len]);
240 + }
241 +
242 + if shm_ctx.send(msg).is_err() {
243 + break;
244 + }
245 + if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
246 + break;
247 + }
248 + continue;
249 + }
250 + }
251 +
252 + if session
253 + .send(&mut resp_hdr, &resp_buf[..response_len])
254 + .is_err()
255 + {
256 + break;
257 + }
258 + if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
259 + break;
260 + }
261 + }
262 +
263 + #[cfg(target_os = "linux")]
264 + {
265 + if let Some(mut shm_ctx) = shm {
266 + shm_ctx.destroy();
267 + }
268 + }
269 + drop(session);
270 +}
271 +
272 +/// Poll a file descriptor for readability with a timeout in milliseconds.
273 +/// Returns: 1 = data ready, 0 = timeout, -1 = error/hangup.
274 +pub(super) fn poll_fd(fd: i32, timeout_ms: i32) -> i32 {
275 + let mut pfd = libc::pollfd {
276 + fd,
277 + events: libc::POLLIN,
278 + revents: 0,
279 + };
280 +
281 + let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
282 +
283 + if ret < 0 {
284 + let errno = unsafe { *libc::__errno_location() };
285 + if errno == libc::EINTR {
286 + return 0;
287 + }
288 + return -1;
289 + }
290 +
291 + if ret == 0 {
292 + return 0;
293 + }
294 +
295 + if pfd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 {
296 + return -1;
297 + }
298 +
299 + if pfd.revents & libc::POLLIN != 0 {
300 + return 1;
301 + }
302 +
303 + 0
304 +}
src/crates/netipc/src/service/raw/server_session_windows.rs new
+233
@@ -0,0 +1,233 @@
1 +use super::common::{ensure_client_scratch, SERVER_POLL_TIMEOUT_MS};
2 +use super::dispatch::{
3 + dispatch_single_internal, method_supported_internal, server_note_payload_capacity,
4 + DispatchError, DispatchHandler,
5 +};
6 +use crate::protocol::{
7 + self, batch_item_get, BatchBuilder, Header, FLAG_BATCH, HEADER_SIZE, KIND_REQUEST,
8 + KIND_RESPONSE, MAGIC_MSG, STATUS_BAD_ENVELOPE, STATUS_INTERNAL_ERROR, STATUS_LIMIT_EXCEEDED,
9 + STATUS_OK, VERSION,
10 +};
11 +use crate::transport::win_shm::WinShmContext;
12 +use crate::transport::windows::NpSession;
13 +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
14 +use std::sync::Arc;
15 +
16 +/// Windows: handle one client session over Named Pipe + optional Win SHM.
17 +/// Standalone function for use in per-session threads.
18 +pub(super) fn handle_session_win_threaded(
19 + mut session: NpSession,
20 + mut shm: Option<WinShmContext>,
21 + expected_method_code: u16,
22 + handler: Option<DispatchHandler>,
23 + running: Arc<AtomicBool>,
24 + learned_request_payload_bytes: Arc<AtomicU32>,
25 + learned_response_payload_bytes: Arc<AtomicU32>,
26 +) {
27 + let mut recv_buf = vec![0u8; HEADER_SIZE + session.max_request_payload_bytes as usize];
28 + let mut resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
29 + let mut item_resp_buf = vec![0u8; session.max_response_payload_bytes as usize];
30 + let mut msg_buf = vec![0u8; HEADER_SIZE + session.max_response_payload_bytes as usize];
31 +
32 + while running.load(Ordering::Acquire) {
33 + let (hdr, payload) = {
34 + if let Some(ref mut shm_ctx) = shm {
35 + match shm_ctx.receive(&mut recv_buf, SERVER_POLL_TIMEOUT_MS) {
36 + Ok(mlen) => {
37 + if mlen < HEADER_SIZE {
38 + break;
39 + }
40 + let hdr = match Header::decode(&recv_buf[..mlen]) {
41 + Ok(h) => h,
42 + Err(_) => break,
43 + };
44 + let payload = &recv_buf[HEADER_SIZE..mlen];
45 + (hdr, payload)
46 + }
47 + Err(crate::transport::win_shm::WinShmError::Timeout) => continue,
48 + Err(_) => break,
49 + }
50 + } else {
51 + match session.wait_readable(SERVER_POLL_TIMEOUT_MS) {
52 + Ok(true) => {}
53 + Ok(false) => continue,
54 + Err(_) => break,
55 + }
56 + match session.receive(&mut recv_buf) {
57 + Ok((hdr, payload)) => (hdr, payload),
58 + Err(_) => break,
59 + }
60 + }
61 + };
62 +
63 + if hdr.kind != KIND_REQUEST {
64 + break;
65 + }
66 +
67 + if payload.len() <= u32::MAX as usize {
68 + server_note_payload_capacity(&learned_request_payload_bytes, payload.len() as u32);
69 + }
70 +
71 + if !method_supported_internal(expected_method_code, handler.as_ref(), hdr.code) {
72 + let mut resp_hdr = Header {
73 + kind: KIND_RESPONSE,
74 + code: hdr.code,
75 + message_id: hdr.message_id,
76 + transport_status: protocol::STATUS_UNSUPPORTED,
77 + item_count: 1,
78 + ..Header::default()
79 + };
80 +
81 + if let Some(ref mut shm_ctx) = shm {
82 + let msg = ensure_client_scratch(&mut msg_buf, HEADER_SIZE);
83 + resp_hdr.magic = MAGIC_MSG;
84 + resp_hdr.version = VERSION;
85 + resp_hdr.header_len = protocol::HEADER_LEN;
86 + resp_hdr.payload_len = 0;
87 + resp_hdr.encode(&mut msg[..HEADER_SIZE]);
88 + if shm_ctx.send(&msg[..HEADER_SIZE]).is_err() {
89 + break;
90 + }
91 + } else if session.send(&mut resp_hdr, &[]).is_err() {
92 + break;
93 + }
94 + continue;
95 + }
96 +
97 + let is_batch = (hdr.flags & FLAG_BATCH) != 0 && hdr.item_count >= 1;
98 + let response_len;
99 + let dispatch_result = if !is_batch {
100 + dispatch_single_internal(
101 + expected_method_code,
102 + handler.as_ref(),
103 + hdr.code,
104 + payload,
105 + &mut resp_buf,
106 + )
107 + } else {
108 + let mut bb = BatchBuilder::new(&mut resp_buf, hdr.item_count);
109 + let mut batch_result = Ok(0usize);
110 +
111 + for i in 0..hdr.item_count {
112 + let (item_data, _item_len) = match batch_item_get(payload, hdr.item_count, i) {
113 + Ok(v) => v,
114 + Err(_) => {
115 + batch_result = Err(DispatchError::BadEnvelope);
116 + break;
117 + }
118 + };
119 + let item_len = match dispatch_single_internal(
120 + expected_method_code,
121 + handler.as_ref(),
122 + hdr.code,
123 + item_data,
124 + &mut item_resp_buf,
125 + ) {
126 + Ok(n) => n,
127 + Err(err) => {
128 + batch_result = Err(err);
129 + break;
130 + }
131 + };
132 + if bb.add(&item_resp_buf[..item_len]).is_err() {
133 + batch_result = Err(DispatchError::Overflow);
134 + break;
135 + }
136 + }
137 + if batch_result.is_ok() {
138 + let (n, _) = bb.finish();
139 + batch_result = Ok(n);
140 + }
141 + batch_result
142 + };
143 +
144 + let mut resp_hdr = Header {
145 + kind: KIND_RESPONSE,
146 + code: hdr.code,
147 + message_id: hdr.message_id,
148 + ..Header::default()
149 + };
150 +
151 + match dispatch_result {
152 + Ok(n) => {
153 + response_len = n;
154 + if response_len <= u32::MAX as usize {
155 + server_note_payload_capacity(
156 + &learned_response_payload_bytes,
157 + response_len as u32,
158 + );
159 + }
160 + resp_hdr.transport_status = STATUS_OK;
161 + if is_batch {
162 + resp_hdr.flags = FLAG_BATCH;
163 + resp_hdr.item_count = hdr.item_count;
164 + } else {
165 + resp_hdr.flags = 0;
166 + resp_hdr.item_count = 1;
167 + }
168 + }
169 + Err(DispatchError::Overflow) => {
170 + let current = session.max_response_payload_bytes;
171 + if current >= u32::MAX / 2 {
172 + server_note_payload_capacity(&learned_response_payload_bytes, u32::MAX);
173 + } else {
174 + server_note_payload_capacity(&learned_response_payload_bytes, current * 2);
175 + }
176 + resp_hdr.transport_status = STATUS_LIMIT_EXCEEDED;
177 + resp_hdr.item_count = 1;
178 + resp_hdr.flags = 0;
179 + response_len = 0;
180 + }
181 + Err(DispatchError::BadEnvelope) => {
182 + resp_hdr.transport_status = STATUS_BAD_ENVELOPE;
183 + resp_hdr.item_count = 1;
184 + resp_hdr.flags = 0;
185 + response_len = 0;
186 + }
187 + Err(DispatchError::HandlerFailed) => {
188 + resp_hdr.transport_status = STATUS_INTERNAL_ERROR;
189 + resp_hdr.item_count = 1;
190 + resp_hdr.flags = 0;
191 + response_len = 0;
192 + }
193 + }
194 +
195 + if let Some(ref mut shm_ctx) = shm {
196 + let msg_len = HEADER_SIZE + response_len;
197 + let msg = ensure_client_scratch(&mut msg_buf, msg_len);
198 +
199 + resp_hdr.magic = MAGIC_MSG;
200 + resp_hdr.version = VERSION;
201 + resp_hdr.header_len = protocol::HEADER_LEN;
202 + resp_hdr.payload_len = response_len as u32;
203 +
204 + resp_hdr.encode(&mut msg[..HEADER_SIZE]);
205 + if response_len > 0 {
206 + msg[HEADER_SIZE..].copy_from_slice(&resp_buf[..response_len]);
207 + }
208 +
209 + if shm_ctx.send(msg).is_err() {
210 + break;
211 + }
212 + if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
213 + break;
214 + }
215 + continue;
216 + }
217 +
218 + if session
219 + .send(&mut resp_hdr, &resp_buf[..response_len])
220 + .is_err()
221 + {
222 + break;
223 + }
224 + if resp_hdr.transport_status == STATUS_LIMIT_EXCEEDED {
225 + break;
226 + }
227 + }
228 +
229 + if let Some(mut shm_ctx) = shm {
230 + shm_ctx.destroy();
231 + }
232 + session.close();
233 +}
src/crates/netipc/src/service/raw/server_unix.rs new
+166
@@ -0,0 +1,166 @@
1 +#![cfg(unix)]
2 +
3 +use super::common::SERVER_POLL_TIMEOUT_MS;
4 +use super::server::{ManagedServer, ServerConfig};
5 +use super::server_session_unix::{handle_session_threaded, poll_fd};
6 +use crate::protocol::NipcError;
7 +#[cfg(target_os = "linux")]
8 +use crate::protocol::{HEADER_SIZE, PROFILE_SHM_FUTEX, PROFILE_SHM_HYBRID};
9 +use crate::transport::posix::UdsListener;
10 +#[cfg(target_os = "linux")]
11 +use crate::transport::shm::ShmContext;
12 +use std::sync::atomic::Ordering;
13 +
14 +impl ManagedServer {
15 + /// Run the acceptor loop. Blocking. Accepts clients, spawns a
16 + /// thread per session (up to worker_count concurrent sessions).
17 + ///
18 + /// Returns when `stop()` is called or on fatal error.
19 + #[cfg(unix)]
20 + pub fn run(&mut self) -> Result<(), NipcError> {
21 + #[cfg(target_os = "linux")]
22 + crate::transport::shm::cleanup_stale(&self.run_dir, &self.service_name);
23 +
24 + let listener = UdsListener::bind(
25 + &self.run_dir,
26 + &self.service_name,
27 + self.server_config.clone(),
28 + )
29 + .map_err(|_| NipcError::BadLayout)?;
30 +
31 + self.running.store(true, Ordering::Release);
32 +
33 + let mut session_threads: Vec<std::thread::JoinHandle<()>> = Vec::new();
34 +
35 + while self.running.load(Ordering::Acquire) {
36 + let ready = poll_fd(listener.fd(), SERVER_POLL_TIMEOUT_MS as i32);
37 + if ready < 0 {
38 + break;
39 + }
40 + if ready == 0 {
41 + session_threads.retain(|t| !t.is_finished());
42 + continue;
43 + }
44 +
45 + let (session_id, accept_cfg, precreated_shm, ready) = self.prepare_unix_accept();
46 + if !ready {
47 + std::thread::sleep(std::time::Duration::from_millis(10));
48 + continue;
49 + }
50 +
51 + let session = match listener.accept_with_config(session_id, accept_cfg) {
52 + Ok(s) => s,
53 + Err(_) => {
54 + #[cfg(target_os = "linux")]
55 + if let Some(mut shm) = precreated_shm {
56 + shm.destroy();
57 + }
58 + if !self.running.load(Ordering::Acquire) {
59 + break;
60 + }
61 + std::thread::sleep(std::time::Duration::from_millis(10));
62 + continue;
63 + }
64 + };
65 +
66 + session_threads.retain(|t| !t.is_finished());
67 + if session_threads.len() >= self.worker_count {
68 + #[cfg(target_os = "linux")]
69 + if let Some(mut shm) = precreated_shm {
70 + shm.destroy();
71 + }
72 + drop(session);
73 + continue;
74 + }
75 +
76 + #[cfg(target_os = "linux")]
77 + let shm = match self.finalize_unix_shm(&session, precreated_shm) {
78 + Some(shm) => Some(shm),
79 + None if session.selected_profile == PROFILE_SHM_HYBRID
80 + || session.selected_profile == PROFILE_SHM_FUTEX =>
81 + {
82 + drop(session);
83 + continue;
84 + }
85 + None => None,
86 + };
87 + #[cfg(not(target_os = "linux"))]
88 + let shm: Option<()> = None;
89 +
90 + let expected_method_code = self.expected_method_code;
91 + let handler = self.handler.clone();
92 + let running = self.running.clone();
93 + let learned_request_payload_bytes = self.learned_request_payload_bytes.clone();
94 + let learned_response_payload_bytes = self.learned_response_payload_bytes.clone();
95 +
96 + let t = std::thread::spawn(move || {
97 + handle_session_threaded(
98 + session,
99 + #[cfg(target_os = "linux")]
100 + shm,
101 + #[cfg(not(target_os = "linux"))]
102 + shm,
103 + expected_method_code,
104 + handler,
105 + running,
106 + learned_request_payload_bytes,
107 + learned_response_payload_bytes,
108 + );
109 + });
110 + session_threads.push(t);
111 + }
112 +
113 + for t in session_threads {
114 + let _ = t.join();
115 + }
116 +
117 + Ok(())
118 + }
119 +
120 + #[cfg(target_os = "linux")]
121 + fn prepare_unix_accept(&mut self) -> (u64, ServerConfig, Option<ShmContext>, bool) {
122 + let session_id = self.next_session_id;
123 + self.next_session_id += 1;
124 +
125 + let mut cfg = self.server_config.clone();
126 + cfg.max_request_payload_bytes = self.learned_request_payload_bytes.load(Ordering::Acquire);
127 + cfg.max_response_payload_bytes =
128 + self.learned_response_payload_bytes.load(Ordering::Acquire);
129 +
130 + let shm_profiles = cfg.supported_profiles & (PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
131 + if shm_profiles == 0 {
132 + return (session_id, cfg, None, true);
133 + }
134 +
135 + match ShmContext::server_create(
136 + &self.run_dir,
137 + &self.service_name,
138 + session_id,
139 + cfg.max_request_payload_bytes + HEADER_SIZE as u32,
140 + cfg.max_response_payload_bytes + HEADER_SIZE as u32,
141 + ) {
142 + Ok(ctx) => (session_id, cfg, Some(ctx), true),
143 + Err(_) => {
144 + cfg.supported_profiles &= !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
145 + cfg.preferred_profiles &= !(PROFILE_SHM_HYBRID | PROFILE_SHM_FUTEX);
146 + (session_id, cfg.clone(), None, cfg.supported_profiles != 0)
147 + }
148 + }
149 + }
150 +
151 + #[cfg(target_os = "linux")]
152 + fn finalize_unix_shm(
153 + &self,
154 + session: &crate::transport::posix::UdsSession,
155 + mut shm: Option<ShmContext>,
156 + ) -> Option<ShmContext> {
157 + let profile = session.selected_profile;
158 + if profile != PROFILE_SHM_HYBRID && profile != PROFILE_SHM_FUTEX {
159 + if let Some(ref mut ctx) = shm {
160 + ctx.destroy();
161 + }
162 + return None;
163 + }
164 + shm
165 + }
166 +}
src/crates/netipc/src/service/raw/server_windows.rs new
+215
@@ -0,0 +1,215 @@
1 +use super::server::{ManagedServer, ServerConfig};
2 +use super::server_session_windows::handle_session_win_threaded;
3 +use crate::protocol::{NipcError, HEADER_SIZE};
4 +use crate::transport::win_shm::{
5 + WinShmContext, PROFILE_BUSYWAIT as WIN_SHM_PROFILE_BUSYWAIT,
6 + PROFILE_HYBRID as WIN_SHM_PROFILE_HYBRID,
7 +};
8 +use crate::transport::windows::{NpListener, NpSession};
9 +use std::sync::atomic::Ordering;
10 +
11 +impl ManagedServer {
12 + /// Windows: run the acceptor loop over Named Pipes.
13 + pub fn run(&mut self) -> Result<(), NipcError> {
14 + let mut listener = NpListener::bind(
15 + &self.run_dir,
16 + &self.service_name,
17 + self.server_config.clone(),
18 + )
19 + .map_err(|_| NipcError::BadLayout)?;
20 +
21 + self.remember_windows_listener_handle(&listener);
22 +
23 + self.running.store(true, Ordering::Release);
24 +
25 + let mut session_threads: Vec<std::thread::JoinHandle<()>> = Vec::new();
26 +
27 + while self.running.load(Ordering::Acquire) {
28 + let (session_id, accept_cfg, prepared_shm, ready) = self.prepare_windows_accept();
29 + if !ready {
30 + std::thread::sleep(std::time::Duration::from_millis(10));
31 + continue;
32 + }
33 +
34 + self.remember_windows_listener_handle(&listener);
35 + let accepted = listener.accept_with_config(session_id, accept_cfg);
36 + self.remember_windows_listener_handle(&listener);
37 +
38 + let session = match accepted {
39 + Ok(s) => s,
40 + Err(_) => {
41 + if let Some(mut prepared) = prepared_shm {
42 + prepared.destroy_all();
43 + }
44 + if !self.running.load(Ordering::Acquire) {
45 + break;
46 + }
47 + std::thread::sleep(std::time::Duration::from_millis(10));
48 + continue;
49 + }
50 + };
51 +
52 + session_threads.retain(|t| !t.is_finished());
53 + if session_threads.len() >= self.worker_count {
54 + if let Some(mut prepared) = prepared_shm {
55 + prepared.destroy_all();
56 + }
57 + drop(session);
58 + continue;
59 + }
60 +
61 + let shm = self.finalize_windows_shm(&session, prepared_shm);
62 + if shm.is_none()
63 + && (session.selected_profile == WIN_SHM_PROFILE_HYBRID
64 + || session.selected_profile == WIN_SHM_PROFILE_BUSYWAIT)
65 + {
66 + drop(session);
67 + continue;
68 + }
69 +
70 + let expected_method_code = self.expected_method_code;
71 + let handler = self.handler.clone();
72 + let running = self.running.clone();
73 + let learned_request_payload_bytes = self.learned_request_payload_bytes.clone();
74 + let learned_response_payload_bytes = self.learned_response_payload_bytes.clone();
75 + let t = std::thread::spawn(move || {
76 + handle_session_win_threaded(
77 + session,
78 + shm,
79 + expected_method_code,
80 + handler,
81 + running,
82 + learned_request_payload_bytes,
83 + learned_response_payload_bytes,
84 + );
85 + });
86 + session_threads.push(t);
87 + }
88 +
89 + *self.listener_handle.lock().unwrap() = None;
90 +
91 + for t in session_threads {
92 + let _ = t.join();
93 + }
94 +
95 + Ok(())
96 + }
97 +
98 + fn remember_windows_listener_handle(&self, listener: &NpListener) {
99 + let handle = listener.handle();
100 + let stored = if handle == 0 || handle == -1 {
101 + None
102 + } else {
103 + Some(handle as usize)
104 + };
105 + *self.listener_handle.lock().unwrap() = stored;
106 + }
107 +
108 + fn prepare_windows_accept(&mut self) -> (u64, ServerConfig, Option<PreparedWinShm>, bool) {
109 + let session_id = self.next_session_id;
110 + self.next_session_id += 1;
111 +
112 + let mut cfg = self.server_config.clone();
113 + cfg.max_request_payload_bytes = self.learned_request_payload_bytes.load(Ordering::Acquire);
114 + cfg.max_response_payload_bytes =
115 + self.learned_response_payload_bytes.load(Ordering::Acquire);
116 +
117 + let shm_profiles =
118 + cfg.supported_profiles & (WIN_SHM_PROFILE_HYBRID | WIN_SHM_PROFILE_BUSYWAIT);
119 + if shm_profiles == 0 {
120 + return (session_id, cfg, None, true);
121 + }
122 +
123 + let mut prepared = PreparedWinShm::default();
124 + for profile in [WIN_SHM_PROFILE_HYBRID, WIN_SHM_PROFILE_BUSYWAIT] {
125 + if cfg.supported_profiles & profile == 0 {
126 + continue;
127 + }
128 +
129 + match WinShmContext::server_create(
130 + &self.run_dir,
131 + &self.service_name,
132 + self.server_config.auth_token,
133 + session_id,
134 + profile,
135 + cfg.max_request_payload_bytes + HEADER_SIZE as u32,
136 + cfg.max_response_payload_bytes + HEADER_SIZE as u32,
137 + ) {
138 + Ok(ctx) => prepared.insert(profile, ctx),
139 + Err(_) => {
140 + cfg.supported_profiles &= !profile;
141 + cfg.preferred_profiles &= !profile;
142 + }
143 + }
144 + }
145 +
146 + if cfg.supported_profiles == 0 {
147 + prepared.destroy_all();
148 + return (session_id, cfg, None, false);
149 + }
150 +
151 + if prepared.is_empty() {
152 + return (session_id, cfg, None, true);
153 + }
154 +
155 + (session_id, cfg, Some(prepared), true)
156 + }
157 +
158 + fn finalize_windows_shm(
159 + &self,
160 + session: &NpSession,
161 + mut prepared: Option<PreparedWinShm>,
162 + ) -> Option<WinShmContext> {
163 + let profile = session.selected_profile;
164 + if profile != WIN_SHM_PROFILE_HYBRID && profile != WIN_SHM_PROFILE_BUSYWAIT {
165 + if let Some(ref mut prepared) = prepared {
166 + prepared.destroy_all();
167 + }
168 + return None;
169 + }
170 + let mut prepared = prepared?;
171 + // Keep the negotiated context and destroy every unused prepared context.
172 + let selected = prepared.take(profile);
173 + prepared.destroy_all();
174 + selected
175 + }
176 +}
177 +
178 +#[derive(Default)]
179 +struct PreparedWinShm {
180 + hybrid: Option<WinShmContext>,
181 + busywait: Option<WinShmContext>,
182 +}
183 +
184 +impl PreparedWinShm {
185 + fn insert(&mut self, profile: u32, ctx: WinShmContext) {
186 + if profile == WIN_SHM_PROFILE_HYBRID {
187 + self.hybrid = Some(ctx);
188 + } else if profile == WIN_SHM_PROFILE_BUSYWAIT {
189 + self.busywait = Some(ctx);
190 + }
191 + }
192 +
193 + fn take(&mut self, profile: u32) -> Option<WinShmContext> {
194 + if profile == WIN_SHM_PROFILE_HYBRID {
195 + self.hybrid.take()
196 + } else if profile == WIN_SHM_PROFILE_BUSYWAIT {
197 + self.busywait.take()
198 + } else {
199 + None
200 + }
201 + }
202 +
203 + fn destroy_all(&mut self) {
204 + if let Some(mut ctx) = self.hybrid.take() {
205 + ctx.destroy();
206 + }
207 + if let Some(mut ctx) = self.busywait.take() {
208 + ctx.destroy();
209 + }
210 + }
211 +
212 + fn is_empty(&self) -> bool {
213 + self.hybrid.is_none() && self.busywait.is_none()
214 + }
215 +}
src/crates/netipc/src/service/raw/string_reverse.rs new
+56
@@ -0,0 +1,56 @@
1 +use super::client::{ClientConfig, RawCallKind, RawClient};
2 +use super::dispatch::{DispatchError, DispatchHandler};
3 +use crate::protocol::{
4 + self, string_reverse_decode, string_reverse_encode, NipcError, METHOD_STRING_REVERSE,
5 + STRING_REVERSE_HDR_SIZE,
6 +};
7 +use std::sync::Arc;
8 +
9 +pub type StringReverseHandler = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
10 +
11 +impl RawClient {
12 + /// Create a new client context bound to the string-reverse service kind.
13 + /// Does NOT connect. Does NOT require the server to be running.
14 + pub fn new_string_reverse(run_dir: &str, service_name: &str, config: ClientConfig) -> Self {
15 + Self::new_bound(run_dir, service_name, METHOD_STRING_REVERSE, config)
16 + }
17 +
18 + /// Blocking typed call: STRING_REVERSE method.
19 + /// Sends a string, receives the reversed string back.
20 + ///
21 + /// The returned view is valid until the next typed call on this client.
22 + pub fn call_string_reverse(
23 + &mut self,
24 + s: &str,
25 + ) -> Result<protocol::StringReverseView<'_>, NipcError> {
26 + self.validate_method(METHOD_STRING_REVERSE)?;
27 + let req_size = STRING_REVERSE_HDR_SIZE
28 + .checked_add(s.len())
29 + .and_then(|size| size.checked_add(1))
30 + .ok_or(NipcError::Overflow)?;
31 + let req_len = {
32 + let req_buf = self.request_scratch(req_size);
33 + let req_len = string_reverse_encode(s.as_bytes(), req_buf);
34 + if req_len == 0 {
35 + return Err(NipcError::Truncated);
36 + }
37 + req_len
38 + };
39 +
40 + let response =
41 + self.raw_call_with_retry(METHOD_STRING_REVERSE, req_len, RawCallKind::single())?;
42 + string_reverse_decode(self.response_payload(response)?)
43 + }
44 +}
45 +
46 +pub fn string_reverse_dispatch(handler: StringReverseHandler) -> DispatchHandler {
47 + Arc::new(move |request, response_buf| {
48 + let view = string_reverse_decode(request).map_err(|_| DispatchError::BadEnvelope)?;
49 + let result = handler(view.as_str()).ok_or(DispatchError::HandlerFailed)?;
50 + let n = string_reverse_encode(result.as_bytes(), response_buf);
51 + if n == 0 {
52 + return Err(DispatchError::Overflow);
53 + }
54 + Ok(n)
55 + })
56 +}
src/crates/netipc/src/service/raw_unix_tests.rs
+198 -1
@@ -1,7 +1,13 @@
1 use super::*;
2 #[cfg(target_os = "linux")]
3 use crate::protocol::PROFILE_SHM_FUTEX;
4 -use crate::protocol::{increment_encode, BatchBuilder, CgroupsBuilder, PROFILE_BASELINE};
4 +use crate::protocol::{
5 + increment_encode, AppsLookupBuilder, AppsLookupRequestView, BatchBuilder, CgroupsBuilder,
6 + CgroupsLookupBuilder, CgroupsLookupRequestView, APPS_CGROUP_HOST_ROOT, APPS_CGROUP_KNOWN,
7 + CGROUP_LOOKUP_KNOWN, CGROUP_LOOKUP_UNKNOWN_RETRY_LATER, METHOD_APPS_LOOKUP,
8 + METHOD_CGROUPS_LOOKUP, NIPC_UID_UNSET, ORCHESTRATOR_DOCKER, ORCHESTRATOR_K8S, PID_LOOKUP_KNOWN,
9 + PID_LOOKUP_UNKNOWN, PROFILE_BASELINE,
10 +};
11 use std::os::fd::RawFd;
12 use std::os::unix::ffi::OsStrExt;
13 use std::path::PathBuf;
@@ -131,6 +137,14 @@ fn snapshot_client(service: &str, config: ClientConfig) -> RawClient {
137 RawClient::new_snapshot(TEST_RUN_DIR, service, config)
138 }
139
140 +fn cgroups_lookup_client(service: &str, config: ClientConfig) -> RawClient {
141 + RawClient::new_cgroups_lookup(TEST_RUN_DIR, service, config)
142 +}
143 +
144 +fn apps_lookup_client(service: &str, config: ClientConfig) -> RawClient {
145 + RawClient::new_apps_lookup(TEST_RUN_DIR, service, config)
146 +}
147 +
148 fn increment_client(service: &str, config: ClientConfig) -> RawClient {
149 RawClient::new_increment(TEST_RUN_DIR, service, config)
150 }
@@ -251,6 +265,111 @@ fn test_cgroups_dispatch() -> DispatchHandler {
265 snapshot_dispatch(test_cgroups_snapshot_handler(), 3)
266 }
267
268 +fn test_cgroups_lookup_handler() -> CgroupsLookupHandler {
269 + Arc::new(
270 + |req: &CgroupsLookupRequestView<'_>, builder: &mut CgroupsLookupBuilder<'_>| {
271 + for i in 0..req.item_count {
272 + let item = match req.item(i) {
273 + Ok(item) => item,
274 + Err(_) => return false,
275 + };
276 + if item.as_bytes() == b"/known" {
277 + if builder
278 + .add(
279 + CGROUP_LOOKUP_KNOWN,
280 + ORCHESTRATOR_K8S,
281 + item.as_bytes(),
282 + b"pod-a",
283 + &[(b"namespace".as_slice(), b"default".as_slice())],
284 + )
285 + .is_err()
286 + {
287 + return false;
288 + }
289 + } else if builder
290 + .add(
291 + CGROUP_LOOKUP_UNKNOWN_RETRY_LATER,
292 + 0,
293 + item.as_bytes(),
294 + b"",
295 + &[],
296 + )
297 + .is_err()
298 + {
299 + return false;
300 + }
301 + }
302 + true
303 + },
304 + )
305 +}
306 +
307 +fn test_cgroups_lookup_dispatch() -> DispatchHandler {
308 + cgroups_lookup_dispatch(test_cgroups_lookup_handler())
309 +}
310 +
311 +fn test_apps_lookup_handler() -> AppsLookupHandler {
312 + Arc::new(
313 + |req: &AppsLookupRequestView<'_>, builder: &mut AppsLookupBuilder<'_>| {
314 + for i in 0..req.item_count {
315 + let pid = match req.item(i) {
316 + Ok(pid) => pid,
317 + Err(_) => return false,
318 + };
319 + let result = match pid {
320 + 1234 => builder.add(
321 + PID_LOOKUP_KNOWN,
322 + APPS_CGROUP_KNOWN,
323 + ORCHESTRATOR_DOCKER,
324 + pid,
325 + 1,
326 + 1000,
327 + 42,
328 + b"nginx",
329 + b"/docker/abc",
330 + b"container-a",
331 + &[(b"image".as_slice(), b"nginx:latest".as_slice())],
332 + ),
333 + 0 => builder.add(
334 + PID_LOOKUP_KNOWN,
335 + APPS_CGROUP_HOST_ROOT,
336 + 0,
337 + pid,
338 + 0,
339 + 0,
340 + 0,
341 + b"swapper",
342 + b"",
343 + b"",
344 + &[],
345 + ),
346 + _ => builder.add(
347 + PID_LOOKUP_UNKNOWN,
348 + APPS_CGROUP_KNOWN,
349 + 0,
350 + pid,
351 + 0,
352 + NIPC_UID_UNSET,
353 + 0,
354 + b"",
355 + b"",
356 + b"",
357 + &[],
358 + ),
359 + };
360 + if result.is_err() {
361 + return false;
362 + }
363 + }
364 + true
365 + },
366 + )
367 +}
368 +
369 +fn test_apps_lookup_dispatch() -> DispatchHandler {
370 + apps_lookup_dispatch(test_apps_lookup_handler())
371 +}
372 +
373 fn increment_handler() -> IncrementHandler {
374 Arc::new(|value| Some(value + 1))
375 }
@@ -804,6 +923,84 @@ fn test_cgroups_call() {
923 cleanup_all(svc);
924 }
925
926 +#[test]
927 +fn test_cgroups_lookup_call() {
928 + let svc = "rs_svc_cgroups_lookup";
929 + ensure_run_dir();
930 + cleanup_all(svc);
931 +
932 + let mut server = TestServer::start(
933 + svc,
934 + METHOD_CGROUPS_LOOKUP,
935 + Some(test_cgroups_lookup_dispatch()),
936 + );
937 +
938 + let mut client = cgroups_lookup_client(svc, client_config());
939 + client.refresh();
940 + assert!(client.ready());
941 +
942 + let view = client
943 + .call_cgroups_lookup(&[b"/known".as_slice(), b"/missing".as_slice()])
944 + .expect("cgroups lookup call");
945 +
946 + assert_eq!(view.item_count, 2);
947 + let item0 = view.item(0).expect("item 0");
948 + assert_eq!(item0.status, CGROUP_LOOKUP_KNOWN);
949 + assert_eq!(item0.orchestrator, ORCHESTRATOR_K8S);
950 + assert_eq!(item0.path.as_bytes(), b"/known");
951 + assert_eq!(item0.name.as_bytes(), b"pod-a");
952 + assert_eq!(item0.label_count, 1);
953 +
954 + let item1 = view.item(1).expect("item 1");
955 + assert_eq!(item1.status, CGROUP_LOOKUP_UNKNOWN_RETRY_LATER);
956 + assert_eq!(item1.path.as_bytes(), b"/missing");
957 + assert_eq!(item1.name.as_bytes(), b"");
958 +
959 + client.close();
960 + server.stop();
961 + cleanup_all(svc);
962 +}
963 +
964 +#[test]
965 +fn test_apps_lookup_call() {
966 + let svc = "rs_svc_apps_lookup";
967 + ensure_run_dir();
968 + cleanup_all(svc);
969 +
970 + let mut server = TestServer::start(svc, METHOD_APPS_LOOKUP, Some(test_apps_lookup_dispatch()));
971 +
972 + let mut client = apps_lookup_client(svc, client_config());
973 + client.refresh();
974 + assert!(client.ready());
975 +
976 + let view = client
977 + .call_apps_lookup(&[1234, 0, 9999])
978 + .expect("apps lookup call");
979 +
980 + assert_eq!(view.item_count, 3);
981 + let item0 = view.item(0).expect("item 0");
982 + assert_eq!(item0.pid, 1234);
983 + assert_eq!(item0.status, PID_LOOKUP_KNOWN);
984 + assert_eq!(item0.cgroup_status, APPS_CGROUP_KNOWN);
985 + assert_eq!(item0.comm.as_bytes(), b"nginx");
986 + assert_eq!(item0.cgroup_path.as_bytes(), b"/docker/abc");
987 + assert_eq!(item0.label_count, 1);
988 +
989 + let item1 = view.item(1).expect("item 1");
990 + assert_eq!(item1.pid, 0);
991 + assert_eq!(item1.cgroup_status, APPS_CGROUP_HOST_ROOT);
992 + assert_eq!(item1.cgroup_path.as_bytes(), b"");
993 +
994 + let item2 = view.item(2).expect("item 2");
995 + assert_eq!(item2.pid, 9999);
996 + assert_eq!(item2.status, PID_LOOKUP_UNKNOWN);
997 + assert_eq!(item2.uid, NIPC_UID_UNSET);
998 +
999 + client.close();
1000 + server.stop();
1001 + cleanup_all(svc);
1002 +}
1003 +
1004 #[cfg(target_os = "linux")]
1005 #[test]
1006 fn test_cgroups_call_shm() {
src/crates/netipc/src/service/raw_windows_tests.rs
+185 -4
@@ -1,9 +1,13 @@
1 use super::*;
2 use crate::protocol::{
3 - increment_encode, BatchBuilder, CgroupsBuilder, CgroupsRequest, Header, HelloAck, NipcError,
4 - CODE_HELLO_ACK, FLAG_BATCH, HEADER_SIZE, INCREMENT_PAYLOAD_SIZE, KIND_CONTROL, KIND_REQUEST,
5 - KIND_RESPONSE, METHOD_CGROUPS_SNAPSHOT, METHOD_INCREMENT, METHOD_STRING_REVERSE,
6 - PROFILE_BASELINE, PROFILE_SHM_HYBRID, STATUS_INTERNAL_ERROR, STATUS_OK, VERSION,
3 + increment_encode, AppsLookupBuilder, BatchBuilder, CgroupsBuilder, CgroupsLookupBuilder,
4 + CgroupsRequest, Header, HelloAck, NipcError, APPS_CGROUP_HOST_ROOT, APPS_CGROUP_KNOWN,
5 + CGROUP_LOOKUP_KNOWN, CGROUP_LOOKUP_UNKNOWN_RETRY_LATER, CODE_HELLO_ACK, FLAG_BATCH,
6 + HEADER_SIZE, INCREMENT_PAYLOAD_SIZE, KIND_CONTROL, KIND_REQUEST, KIND_RESPONSE,
7 + METHOD_APPS_LOOKUP, METHOD_CGROUPS_LOOKUP, METHOD_CGROUPS_SNAPSHOT, METHOD_INCREMENT,
8 + METHOD_STRING_REVERSE, NIPC_UID_UNSET, ORCHESTRATOR_DOCKER, ORCHESTRATOR_K8S, PID_LOOKUP_KNOWN,
9 + PID_LOOKUP_UNKNOWN, PROFILE_BASELINE, PROFILE_SHM_HYBRID, STATUS_INTERNAL_ERROR, STATUS_OK,
10 + VERSION,
11 };
12 use crate::transport::windows::build_pipe_name;
13 use std::ptr;
@@ -116,6 +120,14 @@ fn string_reverse_client(service: &str, config: ClientConfig) -> RawClient {
120 RawClient::new_string_reverse(TEST_RUN_DIR, service, config)
121 }
122
123 +fn cgroups_lookup_client(service: &str, config: ClientConfig) -> RawClient {
124 + RawClient::new_cgroups_lookup(TEST_RUN_DIR, service, config)
125 +}
126 +
127 +fn apps_lookup_client(service: &str, config: ClientConfig) -> RawClient {
128 + RawClient::new_apps_lookup(TEST_RUN_DIR, service, config)
129 +}
130 +
131 fn fill_test_cgroups_snapshot(builder: &mut CgroupsBuilder<'_>) -> bool {
132 let items = [
133 (
@@ -161,6 +173,96 @@ fn increment_dispatch_handler() -> DispatchHandler {
173 increment_dispatch(Arc::new(|value| Some(value + 1)))
174 }
175
176 +fn cgroups_lookup_dispatch_handler() -> DispatchHandler {
177 + cgroups_lookup_dispatch(Arc::new(|req, builder: &mut CgroupsLookupBuilder<'_>| {
178 + builder.set_generation(55);
179 + for i in 0..req.item_count {
180 + let path = match req.item(i) {
181 + Ok(path) => path,
182 + Err(_) => return false,
183 + };
184 + let result = if path.as_bytes() == b"/docker/abc" {
185 + builder.add(
186 + CGROUP_LOOKUP_KNOWN,
187 + ORCHESTRATOR_DOCKER,
188 + path.as_bytes(),
189 + b"container-a",
190 + &[(b"role".as_slice(), b"web".as_slice())],
191 + )
192 + } else {
193 + builder.add(
194 + CGROUP_LOOKUP_UNKNOWN_RETRY_LATER,
195 + 0,
196 + path.as_bytes(),
197 + b"",
198 + &[],
199 + )
200 + };
201 + if result.is_err() {
202 + return false;
203 + }
204 + }
205 + true
206 + }))
207 +}
208 +
209 +fn apps_lookup_dispatch_handler() -> DispatchHandler {
210 + apps_lookup_dispatch(Arc::new(|req, builder: &mut AppsLookupBuilder<'_>| {
211 + builder.set_generation(77);
212 + for i in 0..req.item_count {
213 + let pid = match req.item(i) {
214 + Ok(pid) => pid,
215 + Err(_) => return false,
216 + };
217 + let result = match pid {
218 + 123 => builder.add(
219 + PID_LOOKUP_KNOWN,
220 + APPS_CGROUP_KNOWN,
221 + ORCHESTRATOR_K8S,
222 + pid,
223 + 1,
224 + 1000,
225 + 42,
226 + b"nginx",
227 + b"/kubepods/pod-a",
228 + b"pod-a",
229 + &[(b"namespace".as_slice(), b"default".as_slice())],
230 + ),
231 + 124 => builder.add(
232 + PID_LOOKUP_KNOWN,
233 + APPS_CGROUP_HOST_ROOT,
234 + 0,
235 + pid,
236 + 1,
237 + 0,
238 + 43,
239 + b"sshd",
240 + b"",
241 + b"",
242 + &[],
243 + ),
244 + _ => builder.add(
245 + PID_LOOKUP_UNKNOWN,
246 + APPS_CGROUP_KNOWN,
247 + 0,
248 + pid,
249 + 0,
250 + NIPC_UID_UNSET,
251 + 0,
252 + b"",
253 + b"",
254 + b"",
255 + &[],
256 + ),
257 + };
258 + if result.is_err() {
259 + return false;
260 + }
261 + }
262 + true
263 + }))
264 +}
265 +
266 fn connect_ready(client: &mut RawClient) {
267 for _ in 0..200 {
268 client.refresh();
@@ -590,6 +692,85 @@ fn test_cgroups_call_windows_shm() {
692 server.stop();
693 }
694
695 +#[test]
696 +fn test_cgroups_lookup_call_windows_baseline() {
697 + let svc = unique_service("rs_win_cgroups_lookup");
698 + let mut server = TestServer::start(
699 + &svc,
700 + METHOD_CGROUPS_LOOKUP,
701 + cgroups_lookup_dispatch_handler(),
702 + );
703 +
704 + let mut client = cgroups_lookup_client(&svc, client_config());
705 + connect_ready(&mut client);
706 +
707 + let paths: [&[u8]; 2] = [b"/docker/abc".as_slice(), b"/missing".as_slice()];
708 + let view = client.call_cgroups_lookup(&paths).expect("cgroups lookup");
709 + assert_eq!(view.item_count, 2);
710 + assert_eq!(view.generation, 55);
711 +
712 + let known = view.item(0).expect("known item");
713 + assert_eq!(known.status, CGROUP_LOOKUP_KNOWN);
714 + assert_eq!(known.orchestrator, ORCHESTRATOR_DOCKER);
715 + assert_eq!(known.path.as_bytes(), b"/docker/abc");
716 + assert_eq!(known.name.as_bytes(), b"container-a");
717 + let label = known.label(0).expect("known label");
718 + assert_eq!(label.key.as_bytes(), b"role");
719 + assert_eq!(label.value.as_bytes(), b"web");
720 +
721 + let unknown = view.item(1).expect("unknown item");
722 + assert_eq!(unknown.status, CGROUP_LOOKUP_UNKNOWN_RETRY_LATER);
723 + assert_eq!(unknown.path.as_bytes(), b"/missing");
724 + assert_eq!(unknown.name.as_bytes(), b"");
725 + assert_eq!(client.status().call_count, 1);
726 +
727 + client.close();
728 + server.stop();
729 +}
730 +
731 +#[test]
732 +fn test_apps_lookup_call_windows_baseline() {
733 + let svc = unique_service("rs_win_apps_lookup");
734 + let mut server = TestServer::start(&svc, METHOD_APPS_LOOKUP, apps_lookup_dispatch_handler());
735 +
736 + let mut client = apps_lookup_client(&svc, client_config());
737 + connect_ready(&mut client);
738 +
739 + let view = client
740 + .call_apps_lookup(&[123, 124, 999])
741 + .expect("apps lookup");
742 + assert_eq!(view.item_count, 3);
743 + assert_eq!(view.generation, 77);
744 +
745 + let known = view.item(0).expect("known pid");
746 + assert_eq!(known.status, PID_LOOKUP_KNOWN);
747 + assert_eq!(known.cgroup_status, APPS_CGROUP_KNOWN);
748 + assert_eq!(known.orchestrator, ORCHESTRATOR_K8S);
749 + assert_eq!(known.pid, 123);
750 + assert_eq!(known.comm.as_bytes(), b"nginx");
751 + assert_eq!(known.cgroup_path.as_bytes(), b"/kubepods/pod-a");
752 + assert_eq!(known.cgroup_name.as_bytes(), b"pod-a");
753 + let label = known.label(0).expect("known pid label");
754 + assert_eq!(label.key.as_bytes(), b"namespace");
755 + assert_eq!(label.value.as_bytes(), b"default");
756 +
757 + let host = view.item(1).expect("host pid");
758 + assert_eq!(host.status, PID_LOOKUP_KNOWN);
759 + assert_eq!(host.cgroup_status, APPS_CGROUP_HOST_ROOT);
760 + assert_eq!(host.comm.as_bytes(), b"sshd");
761 + assert_eq!(host.cgroup_path.as_bytes(), b"");
762 +
763 + let unknown = view.item(2).expect("unknown pid");
764 + assert_eq!(unknown.status, PID_LOOKUP_UNKNOWN);
765 + assert_eq!(unknown.pid, 999);
766 + assert_eq!(unknown.uid, NIPC_UID_UNSET);
767 + assert_eq!(unknown.comm.as_bytes(), b"");
768 + assert_eq!(client.status().call_count, 1);
769 +
770 + client.close();
771 + server.stop();
772 +}
773 +
774 #[test]
775 fn test_retry_on_failure_windows() {
776 let svc = unique_service("rs_win_svc_retry");
src/crates/netipc/src/transport/posix.rs
+37 -10
@@ -12,6 +12,7 @@ use crate::protocol::{
12 use std::collections::HashSet;
13 use std::ffi::CString;
14 use std::io;
15 +use std::os::unix::fs::MetadataExt;
16 use std::os::unix::io::RawFd;
17 use std::path::{Path, PathBuf};
18 use std::sync::atomic::{AtomicU64, Ordering};
@@ -292,7 +293,9 @@ impl UdsSession {
293 self.max_response_batch_items,
294 )
295 };
295 - 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 + {
299 return Err(UdsError::LimitExceeded);
300 }
301 if hdr.item_count > max_items {
@@ -352,7 +355,7 @@ impl UdsSession {
355 let remaining_after_first = payload.len() - first_chunk_payload;
356
357 let continuation_chunks = if remaining_after_first > 0 {
355 - (remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
358 + 1 + ((remaining_after_first - 1) / chunk_payload_budget)
359 } else {
360 0
361 };
@@ -445,8 +448,14 @@ impl UdsSession {
448
449 let total_msg = HEADER_SIZE + hdr.payload_len as usize;
450
451 + if n > total_msg {
452 + return Err(UdsError::Protocol(
453 + "packet exceeds declared payload_len".into(),
454 + ));
455 + }
456 +
457 // Non-chunked: entire message in one packet
449 - if n >= total_msg {
458 + if n == total_msg {
459 let payload = &buf[HEADER_SIZE..HEADER_SIZE + hdr.payload_len as usize];
460
461 // Validate batch directory
@@ -487,7 +496,7 @@ impl UdsSession {
496 // Expected chunk count
497 let remaining_after_first = hdr.payload_len as usize - first_payload_bytes;
498 let expected_continuations = if remaining_after_first > 0 && chunk_payload_budget > 0 {
490 - (remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
499 + 1 + ((remaining_after_first - 1) / chunk_payload_budget)
500 } else {
501 0
502 };
@@ -593,7 +602,7 @@ impl UdsListener {
602 let path = build_socket_path(run_dir, service_name)?;
603
604 // Stale recovery
596 - match check_and_recover_stale(&path) {
605 + match check_and_recover_stale(&path, run_dir_allows_stale_unlink(run_dir)) {
606 StaleResult::LiveServer => return Err(UdsError::AddrInUse),
607 StaleResult::Stale | StaleResult::NotExist => { /* proceed */ }
608 }
@@ -910,7 +919,17 @@ enum StaleResult {
919 LiveServer,
920 }
921
913 -fn check_and_recover_stale(path: &str) -> StaleResult {
922 +fn run_dir_allows_stale_unlink(run_dir: &str) -> bool {
923 + let metadata = match std::fs::metadata(run_dir) {
924 + Ok(m) => m,
925 + Err(_) => return false,
926 + };
927 + metadata.is_dir()
928 + && metadata.uid() == unsafe { libc::geteuid() }
929 + && metadata.mode() & 0o022 == 0
930 +}
931 +
932 +fn check_and_recover_stale(path: &str, allow_stale_unlink: bool) -> StaleResult {
933 if !Path::new(path).exists() {
934 return StaleResult::NotExist;
935 }
@@ -926,10 +945,18 @@ fn check_and_recover_stale(path: &str) -> StaleResult {
945 // Connected => live server
946 StaleResult::LiveServer
947 }
929 - Err(UdsError::Connect(e)) if e == libc::ECONNREFUSED || e == libc::ENOENT => {
930 - // Connection refused or no such socket => stale, unlink
931 - let _ = std::fs::remove_file(path);
932 - StaleResult::Stale
948 + Err(UdsError::Connect(e)) if e == libc::ENOENT => StaleResult::NotExist,
949 + Err(UdsError::Connect(e)) if e == libc::ECONNREFUSED => {
950 + if !allow_stale_unlink {
951 + StaleResult::LiveServer
952 + } else {
953 + // Connection refused means stale; unlink only in a private run dir.
954 + match std::fs::remove_file(path) {
955 + Ok(()) => StaleResult::Stale,
956 + Err(err) if err.kind() == io::ErrorKind::NotFound => StaleResult::NotExist,
957 + Err(_) => StaleResult::LiveServer,
958 + }
959 + }
960 }
961 Err(_) => {
962 // Other errors (EACCES, etc.) — can't determine ownership,
src/crates/netipc/src/transport/posix_tests.rs
+34
@@ -1461,6 +1461,40 @@ fn test_receive_packet_too_short_for_header() {
1461 unsafe { libc::close(fd1) };
1462 }
1463
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 +
1498 #[test]
1499 fn test_receive_batch_directory_too_short_nonchunked() {
1500 let (fd0, fd1) = socketpair_seqpacket();
src/crates/netipc/src/transport/shm.rs
+36 -11
@@ -6,6 +6,8 @@
6 //!
7 //! Wire-compatible with the C implementation in netipc_shm.c.
8
9 +use std::ffi::CString;
10 +use std::os::unix::fs::MetadataExt;
11 use std::path::{Path, PathBuf};
12 use std::ptr;
13
@@ -245,7 +247,7 @@ impl ShmContext {
247
248 // If O_EXCL failed (file exists), do stale recovery and retry.
249 if fd < 0 && unsafe { *libc::__errno_location() } == libc::EEXIST {
248 - let stale = check_shm_stale(&path);
250 + let stale = check_shm_stale(&path, run_dir_allows_stale_unlink(run_dir));
251 if stale == StaleResult::LiveServer {
252 return Err(ShmError::AddrInUse);
253 }
@@ -740,6 +742,7 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
742 Ok(e) => e,
743 Err(_) => return,
744 };
745 + let allow_stale_unlink = run_dir_allows_stale_unlink(run_dir);
746
747 for entry in entries.flatten() {
748 let name = match entry.file_name().into_string() {
@@ -764,7 +767,7 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
767 // target disappeared) — remove the stale directory entry. Any other
768 // open failure is ambiguous, so leave the entry alone.
769 if should_unlink_cleanup_open_failure(errno()) {
767 - unsafe { libc::unlink(c_path.as_ptr()) };
770 + let _ = unlink_stale_path(&c_path, allow_stale_unlink);
771 }
772 continue;
773 }
@@ -773,8 +776,8 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
776 if unsafe { libc::fstat(fd, &mut st) } != 0 || (st.st_size as usize) < HEADER_LEN as usize {
777 unsafe {
778 libc::close(fd);
776 - libc::unlink(c_path.as_ptr());
779 }
780 + let _ = unlink_stale_path(&c_path, allow_stale_unlink);
781 continue;
782 }
783
@@ -791,7 +794,7 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
794 unsafe { libc::close(fd) };
795
796 if map == libc::MAP_FAILED {
794 - unsafe { libc::unlink(c_path.as_ptr()) };
797 + let _ = unlink_stale_path(&c_path, allow_stale_unlink);
798 continue;
799 }
800
@@ -800,8 +803,8 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
803 if magic != REGION_MAGIC {
804 unsafe {
805 libc::munmap(map, HEADER_LEN as usize);
803 - libc::unlink(c_path.as_ptr());
806 }
807 + let _ = unlink_stale_path(&c_path, allow_stale_unlink);
808 continue;
809 }
810
@@ -811,7 +814,7 @@ pub fn cleanup_stale(run_dir: &str, service_name: &str) {
814
815 // If owner is dead (or generation is zero / legacy), unlink
816 if !pid_alive(owner) || gen == 0 {
814 - unsafe { libc::unlink(c_path.as_ptr()) };
817 + let _ = unlink_stale_path(&c_path, allow_stale_unlink);
818 }
819 }
820 }
@@ -970,6 +973,20 @@ fn should_unlink_cleanup_open_failure(err: i32) -> bool {
973 err == libc::ENOENT
974 }
975
976 +fn run_dir_allows_stale_unlink(run_dir: &str) -> bool {
977 + let metadata = match std::fs::metadata(run_dir) {
978 + Ok(m) => m,
979 + Err(_) => return false,
980 + };
981 + metadata.is_dir()
982 + && metadata.uid() == unsafe { libc::geteuid() }
983 + && metadata.mode() & 0o022 == 0
984 +}
985 +
986 +fn unlink_stale_path(c_path: &CString, allow_stale_unlink: bool) -> bool {
987 + allow_stale_unlink && (unsafe { libc::unlink(c_path.as_ptr()) } == 0 || errno() == libc::ENOENT)
988 +}
989 +
990 fn classify_stale_open_failure(err: i32) -> StaleResult {
991 if err == libc::ENOENT {
992 StaleResult::NotExist
@@ -979,7 +996,7 @@ fn classify_stale_open_failure(err: i32) -> StaleResult {
996 }
997
998 #[allow(dead_code)]
982 -fn check_shm_stale(path: &Path) -> StaleResult {
999 +fn check_shm_stale(path: &Path, allow_stale_unlink: bool) -> StaleResult {
1000 let c_path = match path_to_cstring(path) {
1001 Ok(c) => c,
1002 Err(_) => return StaleResult::NotExist,
@@ -991,7 +1008,9 @@ fn check_shm_stale(path: &Path) -> StaleResult {
1008 }
1009
1010 if (st.st_size as usize) < HEADER_LEN as usize {
994 - unsafe { libc::unlink(c_path.as_ptr()) };
1011 + if !unlink_stale_path(&c_path, allow_stale_unlink) {
1012 + return StaleResult::LiveServer;
1013 + }
1014 return StaleResult::Invalid;
1015 }
1016
@@ -1013,7 +1032,9 @@ fn check_shm_stale(path: &Path) -> StaleResult {
1032 unsafe { libc::close(fd) };
1033
1034 if map == libc::MAP_FAILED {
1016 - unsafe { libc::unlink(c_path.as_ptr()) };
1035 + if !unlink_stale_path(&c_path, allow_stale_unlink) {
1036 + return StaleResult::LiveServer;
1037 + }
1038 return StaleResult::Invalid;
1039 }
1040
@@ -1022,7 +1043,9 @@ fn check_shm_stale(path: &Path) -> StaleResult {
1043 if magic != REGION_MAGIC {
1044 unsafe {
1045 libc::munmap(map, HEADER_LEN as usize);
1025 - libc::unlink(c_path.as_ptr());
1046 + }
1047 + if !unlink_stale_path(&c_path, allow_stale_unlink) {
1048 + return StaleResult::LiveServer;
1049 }
1050 return StaleResult::Invalid;
1051 }
@@ -1036,7 +1059,9 @@ fn check_shm_stale(path: &Path) -> StaleResult {
1059 }
1060
1061 // Dead owner or zero generation (PID reuse / legacy) — stale
1039 - unsafe { libc::unlink(c_path.as_ptr()) };
1062 + if !unlink_stale_path(&c_path, allow_stale_unlink) {
1063 + return StaleResult::LiveServer;
1064 + }
1065 StaleResult::Recovered
1066 }
1067
src/crates/netipc/src/transport/shm_tests.rs
+16 -7
@@ -1125,7 +1125,10 @@ fn test_check_shm_stale_nonexistent_returns_not_exist() {
1125 cleanup_shm(svc, sid);
1126
1127 let path = build_shm_path(TEST_RUN_DIR, svc, sid).expect("path");
1128 - assert!(matches!(check_shm_stale(&path), StaleResult::NotExist));
1128 + assert!(matches!(
1129 + check_shm_stale(&path, true),
1130 + StaleResult::NotExist
1131 + ));
1132 }
1133
1134 #[test]
@@ -1133,7 +1136,10 @@ fn test_check_shm_stale_invalid_cstring_returns_not_exist() {
1136 let bad_path = PathBuf::from(OsString::from_vec(vec![
1137 b'/', b't', b'm', b'p', b'/', b'n', b'i', b'p', b'c', 0, b'b',
1138 ]));
1136 - assert!(matches!(check_shm_stale(&bad_path), StaleResult::NotExist));
1139 + assert!(matches!(
1140 + check_shm_stale(&bad_path, true),
1141 + StaleResult::NotExist
1142 + ));
1143 }
1144
1145 #[test]
@@ -1259,7 +1265,7 @@ fn test_check_shm_stale_short_file_invalid() {
1265 let path = build_shm_path(TEST_RUN_DIR, svc, sid).expect("path");
1266 std::fs::write(&path, [0u8; 8]).expect("write short file");
1267
1262 - assert!(matches!(check_shm_stale(&path), StaleResult::Invalid));
1268 + assert!(matches!(check_shm_stale(&path, true), StaleResult::Invalid));
1269 assert!(!path.exists(), "short stale file should be removed");
1270 }
1271
@@ -1277,7 +1283,7 @@ fn test_check_shm_stale_bad_magic_invalid() {
1283 server.close();
1284
1285 let path = build_shm_path(TEST_RUN_DIR, svc, sid).expect("path");
1280 - assert!(matches!(check_shm_stale(&path), StaleResult::Invalid));
1286 + assert!(matches!(check_shm_stale(&path, true), StaleResult::Invalid));
1287 assert!(!path.exists(), "bad magic stale file should be removed");
1288 }
1289
@@ -1295,7 +1301,10 @@ fn test_check_shm_stale_zero_generation_recovers() {
1301 server.close();
1302
1303 let path = build_shm_path(TEST_RUN_DIR, svc, sid).expect("path");
1298 - assert!(matches!(check_shm_stale(&path), StaleResult::Recovered));
1304 + assert!(matches!(
1305 + check_shm_stale(&path, true),
1306 + StaleResult::Recovered
1307 + ));
1308 assert!(
1309 !path.exists(),
1310 "zero-generation stale file should be removed"
@@ -1333,7 +1342,7 @@ fn test_check_shm_stale_open_failure_invalid() {
1342 errno()
1343 );
1344
1336 - assert!(matches!(check_shm_stale(&path), StaleResult::Invalid));
1345 + assert!(matches!(check_shm_stale(&path, true), StaleResult::Invalid));
1346 // Under non-root: file preserved (EACCES). Under root: chmod 000
1347 // has no effect, so the file is opened, inspected, and removed.
1348 if unsafe { libc::geteuid() } != 0 {
@@ -1359,7 +1368,7 @@ fn test_check_shm_stale_directory_symlink_invalid() {
1368 std::fs::create_dir_all(&target).expect("create target dir");
1369 std::os::unix::fs::symlink(&target, &path).expect("create symlink");
1370
1362 - assert!(matches!(check_shm_stale(&path), StaleResult::Invalid));
1371 + assert!(matches!(check_shm_stale(&path, true), StaleResult::Invalid));
1372 assert!(
1373 !path.exists(),
1374 "directory symlink stale entry should be removed"
src/crates/netipc/src/transport/windows.rs
+66 -4
@@ -689,7 +689,9 @@ impl NpSession {
689 self.max_response_batch_items,
690 )
691 };
692 - if payload.len() > max_payload as usize || payload.len() > u32::MAX as usize {
692 + if payload.len() > max_payload as usize
693 + || payload.len() > (u32::MAX as usize).saturating_sub(HEADER_SIZE)
694 + {
695 return Err(NpError::LimitExceeded);
696 }
697 if hdr.item_count > max_items {
@@ -748,7 +750,7 @@ impl NpSession {
750 let remaining_after_first = payload.len() - first_chunk_payload;
751
752 let continuation_chunks = if remaining_after_first > 0 {
751 - (remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
753 + 1 + ((remaining_after_first - 1) / chunk_payload_budget)
754 } else {
755 0
756 };
@@ -847,8 +849,14 @@ impl NpSession {
849
850 let total_msg = HEADER_SIZE + hdr.payload_len as usize;
851
852 + if n > total_msg {
853 + return Err(NpError::Protocol(
854 + "packet exceeds declared payload_len".into(),
855 + ));
856 + }
857 +
858 // Non-chunked
851 - if n >= total_msg {
859 + if n == total_msg {
860 let payload = &buf[HEADER_SIZE..HEADER_SIZE + hdr.payload_len as usize];
861
862 // Validate batch directory
@@ -885,7 +893,7 @@ impl NpSession {
893
894 let remaining_after_first = hdr.payload_len as usize - first_payload_bytes;
895 let expected_continuations = if remaining_after_first > 0 && chunk_payload_budget > 0 {
888 - (remaining_after_first + chunk_payload_budget - 1) / chunk_payload_budget
896 + 1 + ((remaining_after_first - 1) / chunk_payload_budget)
897 } else {
898 0
899 };
@@ -1792,6 +1800,60 @@ mod tests {
1800 server.join().expect("server join");
1801 }
1802
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 +
1857 #[cfg(windows)]
1858 #[test]
1859 fn test_chunking_and_received_payload() {
src/go/pkg/netipc/protocol/apps_lookup.go new
+691
@@ -0,0 +1,691 @@
1 +package protocol
2 +
3 +const (
4 + NipcUIDUnset uint32 = ^uint32(0)
5 +
6 + PidLookupKnown uint16 = 0
7 + PidLookupUnknown uint16 = 1
8 +
9 + AppsCgroupKnown uint16 = 0
10 + AppsCgroupUnknownRetryLater uint16 = 1
11 + AppsCgroupUnknownPermanent uint16 = 2
12 + AppsCgroupHostRoot uint16 = 3
13 +
14 + AppsLookupReqHdr = 16
15 + AppsLookupRespHdr = 16
16 + AppsLookupItemHdr = 60
17 + AppsLookupKeySize = 8
18 +
19 + appsLookupUnknownItemSize = AppsLookupItemHdr + 3
20 +)
21 +
22 +type AppsLookupRequestView struct {
23 + ItemCount uint32
24 + payload []byte
25 +}
26 +
27 +type AppsLookupResponseView struct {
28 + LayoutVersion uint16
29 + Flags uint16
30 + ItemCount uint32
31 + Generation uint64
32 + payload []byte
33 +}
34 +
35 +type AppsLookupItemView struct {
36 + Status uint16
37 + Orchestrator uint16
38 + CgroupStatus uint16
39 + Pid uint32
40 + Ppid uint32
41 + Uid uint32
42 + Starttime uint64
43 + Comm CStringView
44 + CgroupPath CStringView
45 + CgroupName CStringView
46 + LabelCount uint16
47 + item []byte
48 + labelTableOffset int
49 +}
50 +
51 +type appsLookupSemantics struct {
52 + status uint16
53 + cgroupStatus uint16
54 + orchestrator uint16
55 + ppid uint32
56 + uid uint32
57 + starttime uint64
58 + commLen int
59 + pathLen int
60 + nameLen int
61 + labelCount int
62 +}
63 +
64 +func validateAppsLookupSemantics(v appsLookupSemantics) error {
65 + if err := validateAppsLookupDomains(v.status, v.cgroupStatus, v.commLen); err != nil {
66 + return err
67 + }
68 + if v.status == PidLookupUnknown {
69 + return validateAppsLookupUnknown(v)
70 + }
71 + return validateAppsLookupKnown(v)
72 +}
73 +
74 +func validateAppsLookupDomains(status, cgroupStatus uint16, commLen int) error {
75 + if status != PidLookupKnown && status != PidLookupUnknown {
76 + return ErrBadLayout
77 + }
78 + if cgroupStatus != AppsCgroupKnown && cgroupStatus != AppsCgroupUnknownRetryLater &&
79 + cgroupStatus != AppsCgroupUnknownPermanent && cgroupStatus != AppsCgroupHostRoot {
80 + return ErrBadLayout
81 + }
82 + if commLen > 15 {
83 + return ErrBadLayout
84 + }
85 + return nil
86 +}
87 +
88 +func validateAppsLookupUnknown(v appsLookupSemantics) error {
89 + if v.orchestrator != 0 || v.cgroupStatus != 0 || v.ppid != 0 || v.uid != NipcUIDUnset ||
90 + v.starttime != 0 || v.commLen != 0 || v.pathLen != 0 || v.nameLen != 0 || v.labelCount != 0 {
91 + return ErrBadLayout
92 + }
93 + return nil
94 +}
95 +
96 +func validateAppsLookupKnown(v appsLookupSemantics) error {
97 + if v.commLen == 0 {
98 + return ErrBadLayout
99 + }
100 + switch v.cgroupStatus {
101 + case AppsCgroupKnown:
102 + if v.pathLen == 0 {
103 + return ErrBadLayout
104 + }
105 + case AppsCgroupUnknownRetryLater:
106 + if v.orchestrator != 0 || v.nameLen != 0 || v.labelCount != 0 {
107 + return ErrBadLayout
108 + }
109 + case AppsCgroupUnknownPermanent:
110 + if v.pathLen == 0 || v.orchestrator != 0 || v.nameLen != 0 || v.labelCount != 0 {
111 + return ErrBadLayout
112 + }
113 + case AppsCgroupHostRoot:
114 + if v.orchestrator != 0 || v.pathLen != 0 || v.nameLen != 0 || v.labelCount != 0 {
115 + return ErrBadLayout
116 + }
117 + }
118 + return nil
119 +}
120 +
121 +func EncodeAppsLookupRequest(pids []uint32, buf []byte) (int, error) {
122 + count := len(pids)
123 + if uint64(count) > uint64(^uint32(0)) {
124 + return 0, ErrOverflow
125 + }
126 + dirSize, ok := checkedMulInt(count, LookupDirEntrySize)
127 + if !ok {
128 + return 0, ErrOverflow
129 + }
130 + keySize, ok := checkedMulInt(count, AppsLookupKeySize)
131 + if !ok {
132 + return 0, ErrOverflow
133 + }
134 + packedStart, ok := checkedAddInt(AppsLookupReqHdr, dirSize)
135 + if !ok {
136 + return 0, ErrOverflow
137 + }
138 + total, ok := checkedAddInt(packedStart, keySize)
139 + if !ok {
140 + return 0, ErrOverflow
141 + }
142 + if total > len(buf) {
143 + return 0, ErrOverflow
144 + }
145 + for i, pid := range pids {
146 + offset32, ok := checkedU32Int(i * AppsLookupKeySize)
147 + if !ok {
148 + return 0, ErrOverflow
149 + }
150 + base := AppsLookupReqHdr + i*LookupDirEntrySize
151 + ne.PutUint32(buf[base:base+4], offset32)
152 + ne.PutUint32(buf[base+4:base+8], AppsLookupKeySize)
153 + key := packedStart + i*AppsLookupKeySize
154 + ne.PutUint32(buf[key:key+4], pid)
155 + ne.PutUint32(buf[key+4:key+8], 0)
156 + }
157 + ne.PutUint16(buf[0:2], 1)
158 + ne.PutUint16(buf[2:4], 0)
159 + ne.PutUint32(buf[4:8], uint32(count))
160 + ne.PutUint32(buf[8:12], 0)
161 + ne.PutUint32(buf[12:16], 0)
162 + return total, nil
163 +}
164 +
165 +func DecodeAppsLookupRequest(buf []byte) (*AppsLookupRequestView, error) {
166 + if len(buf) < AppsLookupReqHdr {
167 + return nil, ErrTruncated
168 + }
169 + if ne.Uint16(buf[0:2]) != 1 || ne.Uint16(buf[2:4]) != 0 ||
170 + ne.Uint32(buf[8:12]) != 0 || ne.Uint32(buf[12:16]) != 0 {
171 + return nil, ErrBadLayout
172 + }
173 + itemCount := ne.Uint32(buf[4:8])
174 + dirSize64 := uint64(itemCount) * uint64(LookupDirEntrySize)
175 + dirEnd, ok := checkedInt(uint64(AppsLookupReqHdr) + dirSize64)
176 + if !ok {
177 + return nil, ErrBadItemCount
178 + }
179 + if dirEnd > len(buf) {
180 + return nil, ErrTruncated
181 + }
182 + if err := validateLookupDir(buf, AppsLookupReqHdr, itemCount, len(buf)-dirEnd, 0, AppsLookupKeySize); err != nil {
183 + return nil, err
184 + }
185 + for i := range itemCount {
186 + base := AppsLookupReqHdr + int(i)*LookupDirEntrySize
187 + off, _, err := lookupDirEntry(buf, base)
188 + if err != nil {
189 + return nil, err
190 + }
191 + key, err := lookupPayloadSlice(buf, dirEnd, off, AppsLookupKeySize)
192 + if err != nil {
193 + return nil, err
194 + }
195 + if ne.Uint32(key[4:8]) != 0 {
196 + return nil, ErrBadLayout
197 + }
198 + }
199 + return &AppsLookupRequestView{ItemCount: itemCount, payload: buf}, nil
200 +}
201 +
202 +func (v *AppsLookupRequestView) Item(index uint32) (uint32, error) {
203 + if index >= v.ItemCount {
204 + return 0, ErrOutOfBounds
205 + }
206 + dirEnd, ok := lookupBuilderDataOffset(AppsLookupReqHdr, v.ItemCount)
207 + if !ok {
208 + return 0, ErrOverflow
209 + }
210 + base, ok := lookupDirOffset(AppsLookupReqHdr, index)
211 + if !ok {
212 + return 0, ErrOverflow
213 + }
214 + off, _, err := lookupDirEntry(v.payload, base)
215 + if err != nil {
216 + return 0, err
217 + }
218 + key, err := lookupPayloadSlice(v.payload, dirEnd, off, AppsLookupKeySize)
219 + if err != nil {
220 + return 0, err
221 + }
222 + return ne.Uint32(key[0:4]), nil
223 +}
224 +
225 +func DecodeAppsLookupResponse(buf []byte) (*AppsLookupResponseView, error) {
226 + if len(buf) < AppsLookupRespHdr {
227 + return nil, ErrTruncated
228 + }
229 + if ne.Uint16(buf[0:2]) != 1 || ne.Uint16(buf[2:4]) != 0 {
230 + return nil, ErrBadLayout
231 + }
232 + itemCount := ne.Uint32(buf[4:8])
233 + dirEnd, ok := checkedInt(uint64(AppsLookupRespHdr) + uint64(itemCount)*uint64(LookupDirEntrySize))
234 + if !ok {
235 + return nil, ErrBadItemCount
236 + }
237 + if dirEnd > len(buf) {
238 + return nil, ErrTruncated
239 + }
240 + if err := validateLookupDir(buf, AppsLookupRespHdr, itemCount, len(buf)-dirEnd, AppsLookupItemHdr, -1); err != nil {
241 + return nil, err
242 + }
243 + for i := range itemCount {
244 + base := AppsLookupRespHdr + int(i)*LookupDirEntrySize
245 + off, length, err := lookupDirEntry(buf, base)
246 + if err != nil {
247 + return nil, err
248 + }
249 + item, err := lookupPayloadSlice(buf, dirEnd, off, length)
250 + if err != nil {
251 + return nil, err
252 + }
253 + if _, err := decodeAppsLookupItem(item); err != nil {
254 + return nil, err
255 + }
256 + }
257 + return &AppsLookupResponseView{
258 + LayoutVersion: 1,
259 + Flags: 0,
260 + ItemCount: itemCount,
261 + Generation: ne.Uint64(buf[8:16]),
262 + payload: buf,
263 + }, nil
264 +}
265 +
266 +func (v *AppsLookupResponseView) Item(index uint32) (*AppsLookupItemView, error) {
267 + if index >= v.ItemCount {
268 + return nil, ErrOutOfBounds
269 + }
270 + dirEnd, ok := lookupBuilderDataOffset(AppsLookupRespHdr, v.ItemCount)
271 + if !ok {
272 + return nil, ErrOverflow
273 + }
274 + base, ok := lookupDirOffset(AppsLookupRespHdr, index)
275 + if !ok {
276 + return nil, ErrOverflow
277 + }
278 + off, length, err := lookupDirEntry(v.payload, base)
279 + if err != nil {
280 + return nil, err
281 + }
282 + item, err := lookupPayloadSlice(v.payload, dirEnd, off, length)
283 + if err != nil {
284 + return nil, err
285 + }
286 + return decodeAppsLookupItem(item)
287 +}
288 +
289 +func decodeAppsLookupItem(item []byte) (*AppsLookupItemView, error) {
290 + if len(item) < AppsLookupItemHdr {
291 + return nil, ErrTruncated
292 + }
293 + status := ne.Uint16(item[2:4])
294 + orchestrator := ne.Uint16(item[4:6])
295 + cgroupStatus := ne.Uint16(item[6:8])
296 + pid := ne.Uint32(item[8:12])
297 + ppid := ne.Uint32(item[12:16])
298 + uid := ne.Uint32(item[16:20])
299 + starttime := ne.Uint64(item[24:32])
300 + commOff, err := checkedWireU32Int(item, 32)
301 + if err != nil {
302 + return nil, err
303 + }
304 + commLen, err := checkedWireU32Int(item, 36)
305 + if err != nil {
306 + return nil, err
307 + }
308 + pathOff, err := checkedWireU32Int(item, 40)
309 + if err != nil {
310 + return nil, err
311 + }
312 + pathLen, err := checkedWireU32Int(item, 44)
313 + if err != nil {
314 + return nil, err
315 + }
316 + nameOff, err := checkedWireU32Int(item, 48)
317 + if err != nil {
318 + return nil, err
319 + }
320 + nameLen, err := checkedWireU32Int(item, 52)
321 + if err != nil {
322 + return nil, err
323 + }
324 + labelCount := ne.Uint16(item[56:58])
325 + if ne.Uint16(item[0:2]) != 1 || ne.Uint32(item[20:24]) != 0 || ne.Uint16(item[58:60]) != 0 {
326 + return nil, ErrBadLayout
327 + }
328 + if err := validateAppsLookupSemantics(appsLookupSemantics{
329 + status: status,
330 + cgroupStatus: cgroupStatus,
331 + orchestrator: orchestrator,
332 + ppid: ppid,
333 + uid: uid,
334 + starttime: starttime,
335 + commLen: commLen,
336 + pathLen: pathLen,
337 + nameLen: nameLen,
338 + labelCount: int(labelCount),
339 + }); err != nil {
340 + return nil, err
341 + }
342 + if status == PidLookupUnknown {
343 + return decodeAppsLookupUnknownItem(item, pid, commOff, pathOff, nameOff)
344 + }
345 + comm, commEnd, err := lookupString(item, AppsLookupItemHdr, commOff, commLen)
346 + if err != nil {
347 + return nil, err
348 + }
349 + path, pathEnd, err := lookupString(item, AppsLookupItemHdr, pathOff, pathLen)
350 + if err != nil {
351 + return nil, err
352 + }
353 + name, nameEnd, err := lookupString(item, AppsLookupItemHdr, nameOff, nameLen)
354 + if err != nil {
355 + return nil, err
356 + }
357 + if overlap(commOff, commEnd, pathOff, pathEnd) || overlap(commOff, commEnd, nameOff, nameEnd) ||
358 + overlap(pathOff, pathEnd, nameOff, nameEnd) {
359 + return nil, ErrBadLayout
360 + }
361 + table, err := validateLabels(item, AppsLookupItemHdr, labelCount, max(commEnd, max(pathEnd, nameEnd)))
362 + if err != nil {
363 + return nil, err
364 + }
365 + return &AppsLookupItemView{
366 + Status: status,
367 + Orchestrator: orchestrator,
368 + CgroupStatus: cgroupStatus,
369 + Pid: pid,
370 + Ppid: ppid,
371 + Uid: uid,
372 + Starttime: starttime,
373 + Comm: comm,
374 + CgroupPath: path,
375 + CgroupName: name,
376 + LabelCount: labelCount,
377 + item: item,
378 + labelTableOffset: table,
379 + }, nil
380 +}
381 +
382 +func decodeAppsLookupUnknownItem(item []byte, pid uint32, commOff, pathOff, nameOff int) (*AppsLookupItemView, error) {
383 + comm, commEnd, err := lookupEmptyString(item, AppsLookupItemHdr, commOff)
384 + if err != nil {
385 + return nil, err
386 + }
387 + path, pathEnd, err := lookupEmptyString(item, AppsLookupItemHdr, pathOff)
388 + if err != nil {
389 + return nil, err
390 + }
391 + name, nameEnd, err := lookupEmptyString(item, AppsLookupItemHdr, nameOff)
392 + if err != nil {
393 + return nil, err
394 + }
395 + if overlap(commOff, commEnd, pathOff, pathEnd) || overlap(commOff, commEnd, nameOff, nameEnd) ||
396 + overlap(pathOff, pathEnd, nameOff, nameEnd) {
397 + return nil, ErrBadLayout
398 + }
399 + labelTableOffset := max(commEnd, max(pathEnd, nameEnd))
400 + if labelTableOffset != len(item) {
401 + return nil, ErrBadLayout
402 + }
403 + return &AppsLookupItemView{
404 + Status: PidLookupUnknown,
405 + CgroupStatus: AppsCgroupKnown,
406 + Pid: pid,
407 + Uid: NipcUIDUnset,
408 + Comm: comm,
409 + CgroupPath: path,
410 + CgroupName: name,
411 + item: item,
412 + labelTableOffset: labelTableOffset,
413 + }, nil
414 +}
415 +
416 +func (v *AppsLookupItemView) Label(index uint32) (LookupLabelView, error) {
417 + return lookupLabelAt(v.item, AppsLookupItemHdr, v.LabelCount, v.labelTableOffset, index)
418 +}
419 +
420 +type AppsLookupBuilder struct {
421 + buf []byte
422 + generation uint64
423 + itemCount uint32
424 + maxItems uint32
425 + dataOffset int
426 + err error
427 +}
428 +
429 +func NewAppsLookupBuilder(buf []byte, maxItems uint32, generation uint64) *AppsLookupBuilder {
430 + minRequired, ok := lookupBuilderDataOffset(AppsLookupRespHdr, maxItems)
431 + if !ok {
432 + panic("AppsLookupBuilder buffer too small")
433 + }
434 + if len(buf) < minRequired {
435 + panic("AppsLookupBuilder buffer too small")
436 + }
437 + return &AppsLookupBuilder{buf: buf, generation: generation, maxItems: maxItems, dataOffset: minRequired}
438 +}
439 +
440 +func (b *AppsLookupBuilder) SetGeneration(generation uint64) {
441 + b.generation = generation
442 +}
443 +
444 +// Add appends one APPS_LOOKUP wire item; parameters mirror the fixed protocol fields.
445 +func (b *AppsLookupBuilder) Add(status, cgroupStatus, orchestrator uint16, pid, ppid, uid uint32, starttime uint64, comm, cgroupPath, cgroupName []byte, labels []struct{ Key, Value []byte }) error { //NOSONAR
446 + if b.itemCount >= b.maxItems {
447 + b.err = ErrOverflow
448 + return ErrOverflow
449 + }
450 + if err := validateAppsLookupSemantics(appsLookupSemantics{
451 + status: status,
452 + cgroupStatus: cgroupStatus,
453 + orchestrator: orchestrator,
454 + ppid: ppid,
455 + uid: uid,
456 + starttime: starttime,
457 + commLen: len(comm),
458 + pathLen: len(cgroupPath),
459 + nameLen: len(cgroupName),
460 + labelCount: len(labels),
461 + }); err != nil {
462 + b.err = err
463 + return err
464 + }
465 + if status == PidLookupUnknown {
466 + return b.addUnknown(pid)
467 + }
468 + if invalidSourceString(comm, status == PidLookupKnown) ||
469 + invalidSourceString(cgroupPath, false) || invalidSourceString(cgroupName, false) {
470 + b.err = ErrBadLayout
471 + return ErrBadLayout
472 + }
473 + labelCount, ok := checkedU16Int(len(labels))
474 + if !ok {
475 + b.err = ErrOverflow
476 + return ErrOverflow
477 + }
478 + itemStart, ok := checkedAlign8(b.dataOffset)
479 + if !ok {
480 + b.err = ErrOverflow
481 + return ErrOverflow
482 + }
483 + commOff := AppsLookupItemHdr
484 + pathOff, ok := checkedAddInt(commOff, len(comm))
485 + if ok {
486 + pathOff, ok = checkedAddInt(pathOff, 1)
487 + }
488 + if !ok {
489 + b.err = ErrOverflow
490 + return ErrOverflow
491 + }
492 + nameOff, ok := checkedAddInt(pathOff, len(cgroupPath))
493 + if ok {
494 + nameOff, ok = checkedAddInt(nameOff, 1)
495 + }
496 + if !ok {
497 + b.err = ErrOverflow
498 + return ErrOverflow
499 + }
500 + fixedEnd, ok := checkedAddInt(nameOff, len(cgroupName))
501 + if ok {
502 + fixedEnd, ok = checkedAddInt(fixedEnd, 1)
503 + }
504 + if !ok {
505 + b.err = ErrOverflow
506 + return ErrOverflow
507 + }
508 + tableStart, tableBytes, itemSize, err := labelLayoutGo(fixedEnd, labels)
509 + if err != nil {
510 + b.err = err
511 + return err
512 + }
513 + itemEnd, ok := checkedAddInt(itemStart, itemSize)
514 + if !ok {
515 + b.err = ErrOverflow
516 + return ErrOverflow
517 + }
518 + if itemEnd > len(b.buf) {
519 + b.err = ErrOverflow
520 + return ErrOverflow
521 + }
522 + commOff32, ok := checkedU32Int(commOff)
523 + if !ok {
524 + b.err = ErrOverflow
525 + return ErrOverflow
526 + }
527 + commLen32, ok := checkedU32Int(len(comm))
528 + if !ok {
529 + b.err = ErrOverflow
530 + return ErrOverflow
531 + }
532 + pathOff32, ok := checkedU32Int(pathOff)
533 + if !ok {
534 + b.err = ErrOverflow
535 + return ErrOverflow
536 + }
537 + pathLen32, ok := checkedU32Int(len(cgroupPath))
538 + if !ok {
539 + b.err = ErrOverflow
540 + return ErrOverflow
541 + }
542 + nameOff32, ok := checkedU32Int(nameOff)
543 + if !ok {
544 + b.err = ErrOverflow
545 + return ErrOverflow
546 + }
547 + nameLen32, ok := checkedU32Int(len(cgroupName))
548 + if !ok {
549 + b.err = ErrOverflow
550 + return ErrOverflow
551 + }
552 + itemStart32, ok := checkedU32Int(itemStart)
553 + if !ok {
554 + b.err = ErrOverflow
555 + return ErrOverflow
556 + }
557 + itemSize32, ok := checkedU32Int(itemSize)
558 + if !ok {
559 + b.err = ErrOverflow
560 + return ErrOverflow
561 + }
562 + clear(b.buf[b.dataOffset:itemStart])
563 + item := b.buf[itemStart:itemEnd]
564 + ne.PutUint16(item[0:2], 1)
565 + ne.PutUint16(item[2:4], status)
566 + ne.PutUint16(item[4:6], orchestrator)
567 + ne.PutUint16(item[6:8], cgroupStatus)
568 + ne.PutUint32(item[8:12], pid)
569 + ne.PutUint32(item[12:16], ppid)
570 + ne.PutUint32(item[16:20], uid)
571 + ne.PutUint32(item[20:24], 0)
572 + ne.PutUint64(item[24:32], starttime)
573 + ne.PutUint32(item[32:36], commOff32)
574 + ne.PutUint32(item[36:40], commLen32)
575 + ne.PutUint32(item[40:44], pathOff32)
576 + ne.PutUint32(item[44:48], pathLen32)
577 + ne.PutUint32(item[48:52], nameOff32)
578 + ne.PutUint32(item[52:56], nameLen32)
579 + ne.PutUint16(item[56:58], labelCount)
580 + ne.PutUint16(item[58:60], 0)
581 + copy(item[commOff:], comm)
582 + item[commOff+len(comm)] = 0
583 + copy(item[pathOff:], cgroupPath)
584 + item[pathOff+len(cgroupPath)] = 0
585 + copy(item[nameOff:], cgroupName)
586 + item[nameOff+len(cgroupName)] = 0
587 + if len(labels) > 0 {
588 + clear(item[fixedEnd:tableStart])
589 + next, err := writeLookupLabels(item, tableStart, tableBytes, labels)
590 + if err != nil {
591 + b.err = err
592 + return err
593 + }
594 + itemSize = next
595 + }
596 + dir, ok := lookupDirOffset(AppsLookupRespHdr, b.itemCount)
597 + if !ok {
598 + b.err = ErrOverflow
599 + return ErrOverflow
600 + }
601 + ne.PutUint32(b.buf[dir:dir+4], itemStart32)
602 + ne.PutUint32(b.buf[dir+4:dir+8], itemSize32)
603 + b.dataOffset = itemStart + itemSize
604 + b.itemCount++
605 + return nil
606 +}
607 +
608 +func (b *AppsLookupBuilder) addUnknown(pid uint32) error {
609 + itemStart, ok := checkedAlign8(b.dataOffset)
610 + if !ok {
611 + b.err = ErrOverflow
612 + return ErrOverflow
613 + }
614 + itemEnd, ok := checkedAddInt(itemStart, appsLookupUnknownItemSize)
615 + if !ok || itemEnd > len(b.buf) {
616 + b.err = ErrOverflow
617 + return ErrOverflow
618 + }
619 + itemStart32, ok := checkedU32Int(itemStart)
620 + if !ok {
621 + b.err = ErrOverflow
622 + return ErrOverflow
623 + }
624 + itemSize32, ok := checkedU32Int(appsLookupUnknownItemSize)
625 + if !ok {
626 + b.err = ErrOverflow
627 + return ErrOverflow
628 + }
629 + clear(b.buf[b.dataOffset:itemStart])
630 + item := b.buf[itemStart:itemEnd]
631 + clear(item)
632 + ne.PutUint16(item[0:2], 1)
633 + ne.PutUint16(item[2:4], PidLookupUnknown)
634 + ne.PutUint32(item[8:12], pid)
635 + ne.PutUint32(item[16:20], NipcUIDUnset)
636 + ne.PutUint32(item[32:36], AppsLookupItemHdr)
637 + ne.PutUint32(item[40:44], AppsLookupItemHdr+1)
638 + ne.PutUint32(item[48:52], AppsLookupItemHdr+2)
639 +
640 + dir, ok := lookupDirOffset(AppsLookupRespHdr, b.itemCount)
641 + if !ok {
642 + b.err = ErrOverflow
643 + return ErrOverflow
644 + }
645 + ne.PutUint32(b.buf[dir:dir+4], itemStart32)
646 + ne.PutUint32(b.buf[dir+4:dir+8], itemSize32)
647 + b.dataOffset = itemEnd
648 + b.itemCount++
649 + return nil
650 +}
651 +
652 +func (b *AppsLookupBuilder) Finish() int {
653 + return finishLookupResponse(b.buf, AppsLookupRespHdr, b.itemCount, b.dataOffset, b.generation)
654 +}
655 +
656 +func (b *AppsLookupBuilder) Error() error {
657 + return b.err
658 +}
659 +
660 +func (b *AppsLookupBuilder) ItemCount() uint32 {
661 + return b.itemCount
662 +}
663 +
664 +func DispatchAppsLookup(req []byte, resp []byte, handler func(*AppsLookupRequestView, *AppsLookupBuilder) bool) (int, error) {
665 + request, err := DecodeAppsLookupRequest(req)
666 + if err != nil {
667 + return 0, err
668 + }
669 + minRequired, ok := lookupBuilderDataOffset(AppsLookupRespHdr, request.ItemCount)
670 + if !ok || len(resp) < minRequired {
671 + return 0, ErrOverflow
672 + }
673 + builder := NewAppsLookupBuilder(resp, request.ItemCount, 0)
674 + if !handler(request, builder) {
675 + if builder.Error() != nil {
676 + return 0, builder.Error()
677 + }
678 + return 0, ErrBadLayout
679 + }
680 + if builder.Error() != nil {
681 + return 0, builder.Error()
682 + }
683 + if builder.itemCount != request.ItemCount {
684 + return 0, ErrBadItemCount
685 + }
686 + n := builder.Finish()
687 + if n == 0 {
688 + return 0, ErrOverflow
689 + }
690 + return n, nil
691 +}
src/go/pkg/netipc/protocol/cgroups_lookup.go new
+593
@@ -0,0 +1,593 @@
1 +package protocol
2 +
3 +import "bytes"
4 +
5 +const (
6 + CgroupLookupKnown uint16 = 0
7 + CgroupLookupUnknownRetryLater uint16 = 1
8 + CgroupLookupUnknownPermanent uint16 = 2
9 +
10 + CgroupsLookupReqHdr = 16
11 + CgroupsLookupRespHdr = 16
12 + CgroupsLookupItemHdr = 28
13 +
14 + cgroupsLookupUnknownFixedBytes = CgroupsLookupItemHdr + 1
15 +)
16 +
17 +type CgroupsLookupRequestView struct {
18 + ItemCount uint32
19 + payload []byte
20 +}
21 +
22 +type CgroupsLookupResponseView struct {
23 + LayoutVersion uint16
24 + Flags uint16
25 + ItemCount uint32
26 + Generation uint64
27 + payload []byte
28 +}
29 +
30 +type CgroupsLookupItemView struct {
31 + Status uint16
32 + Orchestrator uint16
33 + Path CStringView
34 + Name CStringView
35 + LabelCount uint16
36 + item []byte
37 + labelTableOffset int
38 +}
39 +
40 +func validateCgroupsLookupSemantics(status, orchestrator uint16, pathLen, nameLen, labelCount int) error {
41 + if status != CgroupLookupKnown && status != CgroupLookupUnknownRetryLater && status != CgroupLookupUnknownPermanent {
42 + return ErrBadLayout
43 + }
44 + if pathLen == 0 {
45 + return ErrBadLayout
46 + }
47 + if status != CgroupLookupKnown && (orchestrator != 0 || nameLen != 0 || labelCount != 0) {
48 + return ErrBadLayout
49 + }
50 + return nil
51 +}
52 +
53 +func EncodeCgroupsLookupRequest(paths [][]byte, buf []byte) (int, error) {
54 + count := len(paths)
55 + if uint64(count) > uint64(^uint32(0)) {
56 + return 0, ErrOverflow
57 + }
58 + dirSize, ok := checkedMulInt(count, LookupDirEntrySize)
59 + if !ok {
60 + return 0, ErrOverflow
61 + }
62 + packedStart, ok := checkedAddInt(CgroupsLookupReqHdr, dirSize)
63 + if !ok {
64 + return 0, ErrOverflow
65 + }
66 + if len(buf) < packedStart {
67 + return 0, ErrOverflow
68 + }
69 + data := packedStart
70 + for i, path := range paths {
71 + if invalidSourceString(path, true) {
72 + return 0, ErrBadLayout
73 + }
74 + aligned, ok := checkedAlign8(data)
75 + if !ok {
76 + return 0, ErrOverflow
77 + }
78 + keyLen, ok := checkedAddInt(len(path), 1)
79 + if !ok {
80 + return 0, ErrOverflow
81 + }
82 + end, ok := checkedAddInt(aligned, keyLen)
83 + if !ok {
84 + return 0, ErrOverflow
85 + }
86 + if end > len(buf) {
87 + return 0, ErrOverflow
88 + }
89 + clear(buf[data:aligned])
90 + offset32, ok := checkedU32Int(aligned - packedStart)
91 + if !ok {
92 + return 0, ErrOverflow
93 + }
94 + keyLen32, ok := checkedU32Int(keyLen)
95 + if !ok {
96 + return 0, ErrOverflow
97 + }
98 + base := CgroupsLookupReqHdr + i*LookupDirEntrySize
99 + ne.PutUint32(buf[base:base+4], offset32)
100 + ne.PutUint32(buf[base+4:base+8], keyLen32)
101 + copy(buf[aligned:], path)
102 + buf[aligned+len(path)] = 0
103 + data = end
104 + }
105 + ne.PutUint16(buf[0:2], 1)
106 + ne.PutUint16(buf[2:4], 0)
107 + ne.PutUint32(buf[4:8], uint32(count))
108 + ne.PutUint32(buf[8:12], 0)
109 + ne.PutUint32(buf[12:16], 0)
110 + return data, nil
111 +}
112 +
113 +func DecodeCgroupsLookupRequest(buf []byte) (*CgroupsLookupRequestView, error) {
114 + if len(buf) < CgroupsLookupReqHdr {
115 + return nil, ErrTruncated
116 + }
117 + if ne.Uint16(buf[0:2]) != 1 || ne.Uint16(buf[2:4]) != 0 ||
118 + ne.Uint32(buf[8:12]) != 0 || ne.Uint32(buf[12:16]) != 0 {
119 + return nil, ErrBadLayout
120 + }
121 + itemCount := ne.Uint32(buf[4:8])
122 + dirSize64 := uint64(itemCount) * uint64(LookupDirEntrySize)
123 + dirEnd, ok := checkedInt(uint64(CgroupsLookupReqHdr) + dirSize64)
124 + if !ok {
125 + return nil, ErrBadItemCount
126 + }
127 + if dirEnd > len(buf) {
128 + return nil, ErrTruncated
129 + }
130 + if err := validateLookupDir(buf, CgroupsLookupReqHdr, itemCount, len(buf)-dirEnd, 2, -1); err != nil {
131 + return nil, err
132 + }
133 + for i := range itemCount {
134 + base := CgroupsLookupReqHdr + int(i)*LookupDirEntrySize
135 + off, length, err := lookupDirEntry(buf, base)
136 + if err != nil {
137 + return nil, err
138 + }
139 + key, err := lookupPayloadSlice(buf, dirEnd, off, length)
140 + if err != nil {
141 + return nil, err
142 + }
143 + if key[length-1] != 0 {
144 + return nil, ErrMissingNul
145 + }
146 + if bytes.Contains(key[:length-1], []byte{0}) {
147 + return nil, ErrBadLayout
148 + }
149 + }
150 + return &CgroupsLookupRequestView{ItemCount: itemCount, payload: buf}, nil
151 +}
152 +
153 +func (v *CgroupsLookupRequestView) Item(index uint32) (CStringView, error) {
154 + if index >= v.ItemCount {
155 + return CStringView{}, ErrOutOfBounds
156 + }
157 + dirEnd, ok := lookupBuilderDataOffset(CgroupsLookupReqHdr, v.ItemCount)
158 + if !ok {
159 + return CStringView{}, ErrOverflow
160 + }
161 + base, ok := lookupDirOffset(CgroupsLookupReqHdr, index)
162 + if !ok {
163 + return CStringView{}, ErrOverflow
164 + }
165 + off, length, err := lookupDirEntry(v.payload, base)
166 + if err != nil {
167 + return CStringView{}, err
168 + }
169 + item, err := lookupPayloadSlice(v.payload, dirEnd, off, length)
170 + if err != nil {
171 + return CStringView{}, err
172 + }
173 + stringLen, ok := checkedU32Int(length - 1)
174 + if !ok {
175 + return CStringView{}, ErrOutOfBounds
176 + }
177 + return NewCStringView(item, stringLen), nil
178 +}
179 +
180 +func DecodeCgroupsLookupResponse(buf []byte) (*CgroupsLookupResponseView, error) {
181 + if len(buf) < CgroupsLookupRespHdr {
182 + return nil, ErrTruncated
183 + }
184 + if ne.Uint16(buf[0:2]) != 1 || ne.Uint16(buf[2:4]) != 0 {
185 + return nil, ErrBadLayout
186 + }
187 + itemCount := ne.Uint32(buf[4:8])
188 + dirEnd, ok := checkedInt(uint64(CgroupsLookupRespHdr) + uint64(itemCount)*uint64(LookupDirEntrySize))
189 + if !ok {
190 + return nil, ErrBadItemCount
191 + }
192 + if dirEnd > len(buf) {
193 + return nil, ErrTruncated
194 + }
195 + if err := validateLookupDir(buf, CgroupsLookupRespHdr, itemCount, len(buf)-dirEnd, CgroupsLookupItemHdr, -1); err != nil {
196 + return nil, err
197 + }
198 + for i := range itemCount {
199 + base := CgroupsLookupRespHdr + int(i)*LookupDirEntrySize
200 + off, length, err := lookupDirEntry(buf, base)
201 + if err != nil {
202 + return nil, err
203 + }
204 + item, err := lookupPayloadSlice(buf, dirEnd, off, length)
205 + if err != nil {
206 + return nil, err
207 + }
208 + if _, err := decodeCgroupsLookupItem(item); err != nil {
209 + return nil, err
210 + }
211 + }
212 + return &CgroupsLookupResponseView{
213 + LayoutVersion: 1,
214 + Flags: 0,
215 + ItemCount: itemCount,
216 + Generation: ne.Uint64(buf[8:16]),
217 + payload: buf,
218 + }, nil
219 +}
220 +
221 +func (v *CgroupsLookupResponseView) Item(index uint32) (*CgroupsLookupItemView, error) {
222 + if index >= v.ItemCount {
223 + return nil, ErrOutOfBounds
224 + }
225 + dirEnd, ok := lookupBuilderDataOffset(CgroupsLookupRespHdr, v.ItemCount)
226 + if !ok {
227 + return nil, ErrOverflow
228 + }
229 + base, ok := lookupDirOffset(CgroupsLookupRespHdr, index)
230 + if !ok {
231 + return nil, ErrOverflow
232 + }
233 + off, length, err := lookupDirEntry(v.payload, base)
234 + if err != nil {
235 + return nil, err
236 + }
237 + item, err := lookupPayloadSlice(v.payload, dirEnd, off, length)
238 + if err != nil {
239 + return nil, err
240 + }
241 + return decodeCgroupsLookupItem(item)
242 +}
243 +
244 +func decodeCgroupsLookupItem(item []byte) (*CgroupsLookupItemView, error) {
245 + if len(item) < CgroupsLookupItemHdr {
246 + return nil, ErrTruncated
247 + }
248 + status := ne.Uint16(item[2:4])
249 + orchestrator := ne.Uint16(item[4:6])
250 + pathOff, err := checkedWireU32Int(item, 8)
251 + if err != nil {
252 + return nil, err
253 + }
254 + pathLen, err := checkedWireU32Int(item, 12)
255 + if err != nil {
256 + return nil, err
257 + }
258 + nameOff, err := checkedWireU32Int(item, 16)
259 + if err != nil {
260 + return nil, err
261 + }
262 + nameLen, err := checkedWireU32Int(item, 20)
263 + if err != nil {
264 + return nil, err
265 + }
266 + labelCount := ne.Uint16(item[24:26])
267 + if ne.Uint16(item[0:2]) != 1 || ne.Uint16(item[6:8]) != 0 || ne.Uint16(item[26:28]) != 0 {
268 + return nil, ErrBadLayout
269 + }
270 + if err := validateCgroupsLookupSemantics(status, orchestrator, pathLen, nameLen, int(labelCount)); err != nil {
271 + return nil, err
272 + }
273 + if status != CgroupLookupKnown {
274 + return decodeCgroupsLookupUnknownItem(item, status, pathOff, pathLen, nameOff)
275 + }
276 + path, pathEnd, err := lookupString(item, CgroupsLookupItemHdr, pathOff, pathLen)
277 + if err != nil {
278 + return nil, err
279 + }
280 + name, nameEnd, err := lookupString(item, CgroupsLookupItemHdr, nameOff, nameLen)
281 + if err != nil {
282 + return nil, err
283 + }
284 + if overlap(pathOff, pathEnd, nameOff, nameEnd) {
285 + return nil, ErrBadLayout
286 + }
287 + table, err := validateLabels(item, CgroupsLookupItemHdr, labelCount, max(pathEnd, nameEnd))
288 + if err != nil {
289 + return nil, err
290 + }
291 + return &CgroupsLookupItemView{
292 + Status: status,
293 + Orchestrator: orchestrator,
294 + Path: path,
295 + Name: name,
296 + LabelCount: labelCount,
297 + item: item,
298 + labelTableOffset: table,
299 + }, nil
300 +}
301 +
302 +func decodeCgroupsLookupUnknownItem(item []byte, status uint16, pathOff, pathLen, nameOff int) (*CgroupsLookupItemView, error) {
303 + path, pathEnd, err := lookupString(item, CgroupsLookupItemHdr, pathOff, pathLen)
304 + if err != nil {
305 + return nil, err
306 + }
307 + name, nameEnd, err := lookupEmptyString(item, CgroupsLookupItemHdr, nameOff)
308 + if err != nil {
309 + return nil, err
310 + }
311 + if overlap(pathOff, pathEnd, nameOff, nameEnd) {
312 + return nil, ErrBadLayout
313 + }
314 + labelTableOffset := max(pathEnd, nameEnd)
315 + if labelTableOffset != len(item) {
316 + return nil, ErrBadLayout
317 + }
318 + return &CgroupsLookupItemView{
319 + Status: status,
320 + Path: path,
321 + Name: name,
322 + item: item,
323 + labelTableOffset: labelTableOffset,
324 + }, nil
325 +}
326 +
327 +func (v *CgroupsLookupItemView) Label(index uint32) (LookupLabelView, error) {
328 + return lookupLabelAt(v.item, CgroupsLookupItemHdr, v.LabelCount, v.labelTableOffset, index)
329 +}
330 +
331 +type CgroupsLookupBuilder struct {
332 + buf []byte
333 + generation uint64
334 + itemCount uint32
335 + maxItems uint32
336 + dataOffset int
337 + err error
338 +}
339 +
340 +func NewCgroupsLookupBuilder(buf []byte, maxItems uint32, generation uint64) *CgroupsLookupBuilder {
341 + minRequired, ok := lookupBuilderDataOffset(CgroupsLookupRespHdr, maxItems)
342 + if !ok {
343 + panic("CgroupsLookupBuilder buffer too small")
344 + }
345 + if len(buf) < minRequired {
346 + panic("CgroupsLookupBuilder buffer too small")
347 + }
348 + return &CgroupsLookupBuilder{buf: buf, generation: generation, maxItems: maxItems, dataOffset: minRequired}
349 +}
350 +
351 +func (b *CgroupsLookupBuilder) SetGeneration(generation uint64) {
352 + b.generation = generation
353 +}
354 +
355 +func (b *CgroupsLookupBuilder) Add(status, orchestrator uint16, path, name []byte, labels []struct{ Key, Value []byte }) error {
356 + if b.itemCount >= b.maxItems {
357 + b.err = ErrOverflow
358 + return ErrOverflow
359 + }
360 + if err := validateCgroupsLookupSemantics(status, orchestrator, len(path), len(name), len(labels)); err != nil {
361 + b.err = err
362 + return err
363 + }
364 + if invalidSourceString(path, true) || invalidSourceString(name, false) {
365 + b.err = ErrBadLayout
366 + return ErrBadLayout
367 + }
368 + if status != CgroupLookupKnown {
369 + return b.addUnknown(status, path)
370 + }
371 + labelCount, ok := checkedU16Int(len(labels))
372 + if !ok {
373 + b.err = ErrOverflow
374 + return ErrOverflow
375 + }
376 + itemStart, ok := checkedAlign8(b.dataOffset)
377 + if !ok {
378 + b.err = ErrOverflow
379 + return ErrOverflow
380 + }
381 + pathOff := CgroupsLookupItemHdr
382 + nameOff, ok := checkedAddInt(pathOff, len(path))
383 + if ok {
384 + nameOff, ok = checkedAddInt(nameOff, 1)
385 + }
386 + if !ok {
387 + b.err = ErrOverflow
388 + return ErrOverflow
389 + }
390 + fixedEnd, ok := checkedAddInt(nameOff, len(name))
391 + if ok {
392 + fixedEnd, ok = checkedAddInt(fixedEnd, 1)
393 + }
394 + if !ok {
395 + b.err = ErrOverflow
396 + return ErrOverflow
397 + }
398 + tableStart, tableBytes, itemSize, err := labelLayoutGo(fixedEnd, labels)
399 + if err != nil {
400 + b.err = err
401 + return err
402 + }
403 + itemEnd, ok := checkedAddInt(itemStart, itemSize)
404 + if !ok {
405 + b.err = ErrOverflow
406 + return ErrOverflow
407 + }
408 + if itemEnd > len(b.buf) {
409 + b.err = ErrOverflow
410 + return ErrOverflow
411 + }
412 + pathOff32, ok := checkedU32Int(pathOff)
413 + if !ok {
414 + b.err = ErrOverflow
415 + return ErrOverflow
416 + }
417 + pathLen32, ok := checkedU32Int(len(path))
418 + if !ok {
419 + b.err = ErrOverflow
420 + return ErrOverflow
421 + }
422 + nameOff32, ok := checkedU32Int(nameOff)
423 + if !ok {
424 + b.err = ErrOverflow
425 + return ErrOverflow
426 + }
427 + nameLen32, ok := checkedU32Int(len(name))
428 + if !ok {
429 + b.err = ErrOverflow
430 + return ErrOverflow
431 + }
432 + itemStart32, ok := checkedU32Int(itemStart)
433 + if !ok {
434 + b.err = ErrOverflow
435 + return ErrOverflow
436 + }
437 + itemSize32, ok := checkedU32Int(itemSize)
438 + if !ok {
439 + b.err = ErrOverflow
440 + return ErrOverflow
441 + }
442 + clear(b.buf[b.dataOffset:itemStart])
443 + item := b.buf[itemStart:itemEnd]
444 + ne.PutUint16(item[0:2], 1)
445 + ne.PutUint16(item[2:4], status)
446 + ne.PutUint16(item[4:6], orchestrator)
447 + ne.PutUint16(item[6:8], 0)
448 + ne.PutUint32(item[8:12], pathOff32)
449 + ne.PutUint32(item[12:16], pathLen32)
450 + ne.PutUint32(item[16:20], nameOff32)
451 + ne.PutUint32(item[20:24], nameLen32)
452 + ne.PutUint16(item[24:26], labelCount)
453 + ne.PutUint16(item[26:28], 0)
454 + copy(item[pathOff:], path)
455 + item[pathOff+len(path)] = 0
456 + copy(item[nameOff:], name)
457 + item[nameOff+len(name)] = 0
458 + if len(labels) > 0 {
459 + clear(item[fixedEnd:tableStart])
460 + next, err := writeLookupLabels(item, tableStart, tableBytes, labels)
461 + if err != nil {
462 + b.err = err
463 + return err
464 + }
465 + itemSize = next
466 + }
467 + dir, ok := lookupDirOffset(CgroupsLookupRespHdr, b.itemCount)
468 + if !ok {
469 + b.err = ErrOverflow
470 + return ErrOverflow
471 + }
472 + ne.PutUint32(b.buf[dir:dir+4], itemStart32)
473 + ne.PutUint32(b.buf[dir+4:dir+8], itemSize32)
474 + b.dataOffset = itemStart + itemSize
475 + b.itemCount++
476 + return nil
477 +}
478 +
479 +func (b *CgroupsLookupBuilder) addUnknown(status uint16, path []byte) error {
480 + itemStart, ok := checkedAlign8(b.dataOffset)
481 + if !ok {
482 + b.err = ErrOverflow
483 + return ErrOverflow
484 + }
485 + nameOff, ok := checkedAddInt(CgroupsLookupItemHdr, len(path))
486 + if ok {
487 + nameOff, ok = checkedAddInt(nameOff, 1)
488 + }
489 + if !ok {
490 + b.err = ErrOverflow
491 + return ErrOverflow
492 + }
493 + itemSize, ok := checkedAddInt(nameOff, 1)
494 + if !ok {
495 + b.err = ErrOverflow
496 + return ErrOverflow
497 + }
498 + itemEnd, ok := checkedAddInt(itemStart, itemSize)
499 + if !ok {
500 + b.err = ErrOverflow
501 + return ErrOverflow
502 + }
503 + if itemEnd > len(b.buf) {
504 + b.err = ErrOverflow
505 + return ErrOverflow
506 + }
507 + pathLen32, ok := checkedU32Int(len(path))
508 + if !ok {
509 + b.err = ErrOverflow
510 + return ErrOverflow
511 + }
512 + nameOff32, ok := checkedU32Int(nameOff)
513 + if !ok {
514 + b.err = ErrOverflow
515 + return ErrOverflow
516 + }
517 + itemStart32, ok := checkedU32Int(itemStart)
518 + if !ok {
519 + b.err = ErrOverflow
520 + return ErrOverflow
521 + }
522 + itemSize32, ok := checkedU32Int(itemSize)
523 + if !ok {
524 + b.err = ErrOverflow
525 + return ErrOverflow
526 + }
527 + clear(b.buf[b.dataOffset:itemStart])
528 + item := b.buf[itemStart:itemEnd]
529 + ne.PutUint16(item[0:2], 1)
530 + ne.PutUint16(item[2:4], status)
531 + ne.PutUint16(item[4:6], 0)
532 + ne.PutUint16(item[6:8], 0)
533 + ne.PutUint32(item[8:12], CgroupsLookupItemHdr)
534 + ne.PutUint32(item[12:16], pathLen32)
535 + ne.PutUint32(item[16:20], nameOff32)
536 + ne.PutUint32(item[20:24], 0)
537 + ne.PutUint16(item[24:26], 0)
538 + ne.PutUint16(item[26:28], 0)
539 + copy(item[CgroupsLookupItemHdr:], path)
540 + item[CgroupsLookupItemHdr+len(path)] = 0
541 + item[nameOff] = 0
542 + dir, ok := lookupDirOffset(CgroupsLookupRespHdr, b.itemCount)
543 + if !ok {
544 + b.err = ErrOverflow
545 + return ErrOverflow
546 + }
547 + ne.PutUint32(b.buf[dir:dir+4], itemStart32)
548 + ne.PutUint32(b.buf[dir+4:dir+8], itemSize32)
549 + b.dataOffset = itemStart + itemSize
550 + b.itemCount++
551 + return nil
552 +}
553 +
554 +func (b *CgroupsLookupBuilder) Finish() int {
555 + return finishLookupResponse(b.buf, CgroupsLookupRespHdr, b.itemCount, b.dataOffset, b.generation)
556 +}
557 +
558 +func (b *CgroupsLookupBuilder) Error() error {
559 + return b.err
560 +}
561 +
562 +func (b *CgroupsLookupBuilder) ItemCount() uint32 {
563 + return b.itemCount
564 +}
565 +
566 +func DispatchCgroupsLookup(req []byte, resp []byte, handler func(*CgroupsLookupRequestView, *CgroupsLookupBuilder) bool) (int, error) {
567 + request, err := DecodeCgroupsLookupRequest(req)
568 + if err != nil {
569 + return 0, err
570 + }
571 + minRequired, ok := lookupBuilderDataOffset(CgroupsLookupRespHdr, request.ItemCount)
572 + if !ok || len(resp) < minRequired {
573 + return 0, ErrOverflow
574 + }
575 + builder := NewCgroupsLookupBuilder(resp, request.ItemCount, 0)
576 + if !handler(request, builder) {
577 + if builder.Error() != nil {
578 + return 0, builder.Error()
579 + }
580 + return 0, ErrBadLayout
581 + }
582 + if builder.Error() != nil {
583 + return 0, builder.Error()
584 + }
585 + if builder.itemCount != request.ItemCount {
586 + return 0, ErrBadItemCount
587 + }
588 + n := builder.Finish()
589 + if n == 0 {
590 + return 0, ErrOverflow
591 + }
592 + return n, nil
593 +}
src/go/pkg/netipc/protocol/cgroups_snapshot.go renamed
+203 -62
@@ -6,6 +6,13 @@ import (
6 "fmt"
7 )
8
9 +const (
10 + cgroupsReqSize = 4
11 + cgroupsRespHdr = 24
12 + cgroupsDirEntry = 8
13 + cgroupsItemHdr = 32
14 +)
15 +
16 // ---------------------------------------------------------------------------
17 // Cgroups snapshot request (4 bytes)
18 // ---------------------------------------------------------------------------
@@ -142,30 +149,42 @@ func DecodeCgroupsResponse(buf []byte) (CgroupsResponseView, error) {
149 return CgroupsResponseView{}, ErrBadLayout
150 }
151
145 - // Validate directory fits (use uint64 to prevent int overflow on 32-bit).
152 dirSize64 := uint64(itemCount) * uint64(cgroupsDirEntry)
153 dirEnd64 := uint64(cgroupsRespHdr) + dirSize64
148 - if dirEnd64 > uint64(len(buf)) {
154 + dirEnd, ok := checkedInt(dirEnd64)
155 + if !ok {
156 + return CgroupsResponseView{}, ErrBadItemCount
157 + }
158 + if dirEnd > len(buf) {
159 return CgroupsResponseView{}, ErrTruncated
160 }
151 - dirEnd := int(dirEnd64)
161
162 packedAreaLen := len(buf) - dirEnd
163
164 // Validate each directory entry.
156 - dirSize := int(dirSize64)
165 + dirSize, ok := checkedInt(dirSize64)
166 + if !ok {
167 + return CgroupsResponseView{}, ErrBadItemCount
168 + }
169 for i := 0; i < dirSize; i += cgroupsDirEntry {
170 base := cgroupsRespHdr + i
159 - off := ne.Uint32(buf[base : base+4])
160 - length := ne.Uint32(buf[base+4 : base+8])
171 + off, err := checkedWireU32Int(buf, base)
172 + if err != nil {
173 + return CgroupsResponseView{}, err
174 + }
175 + length, err := checkedWireU32Int(buf, base+4)
176 + if err != nil {
177 + return CgroupsResponseView{}, err
178 + }
179
162 - if int(off)%Alignment != 0 {
180 + if off%Alignment != 0 {
181 return CgroupsResponseView{}, ErrBadAlignment
182 }
165 - if uint64(off)+uint64(length) > uint64(packedAreaLen) {
183 + end, ok := checkedAddInt(off, length)
184 + if !ok || end > packedAreaLen {
185 return CgroupsResponseView{}, ErrOutOfBounds
186 }
168 - if int(length) < cgroupsItemHdr {
187 + if length < cgroupsItemHdr {
188 return CgroupsResponseView{}, ErrTruncated
189 }
190 }
@@ -188,15 +207,41 @@ func (v *CgroupsResponseView) Item(index uint32) (CgroupsItemView, error) {
207 }
208
209 dirStart := cgroupsRespHdr
191 - dirSize := int(uint64(v.ItemCount) * uint64(cgroupsDirEntry))
192 - packedAreaStart := dirStart + dirSize
210 + dirSize, ok := checkedInt(uint64(v.ItemCount) * uint64(cgroupsDirEntry))
211 + if !ok {
212 + return CgroupsItemView{}, ErrBadItemCount
213 + }
214 + packedAreaStart, ok := checkedAddInt(dirStart, dirSize)
215 + if !ok {
216 + return CgroupsItemView{}, ErrOutOfBounds
217 + }
218
194 - dirBase := dirStart + int(index)*cgroupsDirEntry
195 - itemOff := int(ne.Uint32(v.payload[dirBase : dirBase+4]))
196 - itemLen := int(ne.Uint32(v.payload[dirBase+4 : dirBase+8]))
219 + dirIndexOff, ok := checkedInt(uint64(index) * uint64(cgroupsDirEntry))
220 + if !ok {
221 + return CgroupsItemView{}, ErrOutOfBounds
222 + }
223 + dirBase, ok := checkedAddInt(dirStart, dirIndexOff)
224 + if !ok {
225 + return CgroupsItemView{}, ErrOutOfBounds
226 + }
227 + itemOff, err := checkedWireU32Int(v.payload, dirBase)
228 + if err != nil {
229 + return CgroupsItemView{}, err
230 + }
231 + itemLen, err := checkedWireU32Int(v.payload, dirBase+4)
232 + if err != nil {
233 + return CgroupsItemView{}, err
234 + }
235
198 - itemStart := packedAreaStart + itemOff
199 - item := v.payload[itemStart : itemStart+itemLen]
236 + itemStart, ok := checkedAddInt(packedAreaStart, itemOff)
237 + if !ok {
238 + return CgroupsItemView{}, ErrOutOfBounds
239 + }
240 + itemEnd, ok := checkedAddInt(itemStart, itemLen)
241 + if !ok || itemEnd > len(v.payload) {
242 + return CgroupsItemView{}, ErrOutOfBounds
243 + }
244 + item := v.payload[itemStart:itemEnd]
245
246 layoutVersion := ne.Uint16(item[0:2])
247 flags := ne.Uint16(item[2:4])
@@ -204,10 +249,24 @@ func (v *CgroupsResponseView) Item(index uint32) (CgroupsItemView, error) {
249 options := ne.Uint32(item[8:12])
250 enabled := ne.Uint32(item[12:16])
251
207 - nameOff := int(ne.Uint32(item[16:20]))
208 - nameLen := ne.Uint32(item[20:24])
209 - pathOff := int(ne.Uint32(item[24:28]))
210 - pathLen := ne.Uint32(item[28:32])
252 + nameOff, err := checkedWireU32Int(item, 16)
253 + if err != nil {
254 + return CgroupsItemView{}, err
255 + }
256 + nameLen, err := checkedWireU32Int(item, 20)
257 + if err != nil {
258 + return CgroupsItemView{}, err
259 + }
260 + nameLen32 := ne.Uint32(item[20:24])
261 + pathOff, err := checkedWireU32Int(item, 24)
262 + if err != nil {
263 + return CgroupsItemView{}, err
264 + }
265 + pathLen, err := checkedWireU32Int(item, 28)
266 + if err != nil {
267 + return CgroupsItemView{}, err
268 + }
269 + pathLen32 := ne.Uint32(item[28:32])
270
271 if layoutVersion != 1 {
272 return CgroupsItemView{}, ErrBadLayout
@@ -222,10 +281,15 @@ func (v *CgroupsResponseView) Item(index uint32) (CgroupsItemView, error) {
281 if nameOff < cgroupsItemHdr {
282 return CgroupsItemView{}, ErrOutOfBounds
283 }
225 - if uint64(nameOff)+uint64(nameLen)+1 > uint64(itemLen) {
284 + nameEnd, ok := checkedAddInt(nameOff, nameLen)
285 + if !ok {
286 + return CgroupsItemView{}, ErrOutOfBounds
287 + }
288 + nameNulEnd, ok := checkedAddInt(nameEnd, 1)
289 + if !ok || nameNulEnd > itemLen {
290 return CgroupsItemView{}, ErrOutOfBounds
291 }
228 - if item[nameOff+int(nameLen)] != 0 {
292 + if item[nameEnd] != 0 {
293 return CgroupsItemView{}, ErrMissingNul
294 }
295
@@ -233,26 +297,27 @@ func (v *CgroupsResponseView) Item(index uint32) (CgroupsItemView, error) {
297 if pathOff < cgroupsItemHdr {
298 return CgroupsItemView{}, ErrOutOfBounds
299 }
236 - if uint64(pathOff)+uint64(pathLen)+1 > uint64(itemLen) {
300 + pathEnd, ok := checkedAddInt(pathOff, pathLen)
301 + if !ok {
302 + return CgroupsItemView{}, ErrOutOfBounds
303 + }
304 + pathNulEnd, ok := checkedAddInt(pathEnd, 1)
305 + if !ok || pathNulEnd > itemLen {
306 return CgroupsItemView{}, ErrOutOfBounds
307 }
239 - if item[pathOff+int(pathLen)] != 0 {
308 + if item[pathEnd] != 0 {
309 return CgroupsItemView{}, ErrMissingNul
310 }
311
312 // Reject overlapping name and path regions (including NUL)
313 {
245 - nameStart := uint64(nameOff)
246 - nameEnd := nameStart + uint64(nameLen) + 1
247 - pathStart := uint64(pathOff)
248 - pathEnd := pathStart + uint64(pathLen) + 1
249 - if nameStart < pathEnd && pathStart < nameEnd {
314 + if overlap(nameOff, nameNulEnd, pathOff, pathNulEnd) {
315 return CgroupsItemView{}, ErrBadLayout
316 }
317 }
318
254 - name := NewCStringView(item[nameOff:nameOff+int(nameLen)+1], nameLen)
255 - path := NewCStringView(item[pathOff:pathOff+int(pathLen)+1], pathLen)
319 + name := NewCStringView(item[nameOff:nameNulEnd], nameLen32)
320 + path := NewCStringView(item[pathOff:pathNulEnd], pathLen32)
321
322 return CgroupsItemView{
323 LayoutVersion: layoutVersion,
@@ -309,11 +374,7 @@ func NewCgroupsBuilder(buf []byte, maxItems uint32, systemdEnabled uint32, gener
374 // reserve directory slots for maxItems before packed item data is appended.
375 func CgroupsBuilderMinBytes(maxItems uint32) (int, bool) {
376 minRequired := uint64(cgroupsRespHdr) + uint64(maxItems)*uint64(cgroupsDirEntry)
312 - maxInt := uint64(int(^uint(0) >> 1))
313 - if minRequired > maxInt {
314 - return 0, false
315 - }
316 - return int(minRequired), true
377 + return checkedInt(minRequired)
378 }
379
380 // SetHeader updates the response header fields written by Finish().
@@ -333,7 +394,12 @@ func EstimateCgroupsMaxItems(bufSize int) uint32 {
394 }
395
396 minAlignedItem := Align8(cgroupsItemHdr + 2)
336 - return uint32((bufSize - cgroupsRespHdr) / (cgroupsDirEntry + minAlignedItem))
397 + items := (bufSize - cgroupsRespHdr) / (cgroupsDirEntry + minAlignedItem)
398 + items32, ok := checkedU32Int(items)
399 + if !ok {
400 + return ^uint32(0)
401 + }
402 + return items32
403 }
404
405 // Add adds one cgroup item. Handles offset bookkeeping, NUL termination,
@@ -343,13 +409,30 @@ func (b *CgroupsBuilder) Add(hash, options, enabled uint32, name, path []byte) e
409 return ErrOverflow
410 }
411
346 - // Align item start to 8 bytes.
347 - itemStart := Align8(b.dataOffset)
412 + itemStart, ok := checkedAlign8(b.dataOffset)
413 + if !ok {
414 + return ErrOverflow
415 + }
416
349 - // Item payload: 32-byte header + name + NUL + path + NUL.
350 - itemSize := cgroupsItemHdr + len(name) + 1 + len(path) + 1
417 + nameSize, ok := checkedAddInt(len(name), 1)
418 + if !ok {
419 + return ErrOverflow
420 + }
421 + pathSize, ok := checkedAddInt(len(path), 1)
422 + if !ok {
423 + return ErrOverflow
424 + }
425 + itemSize, ok := checkedAddInt(cgroupsItemHdr, nameSize)
426 + if !ok {
427 + return ErrOverflow
428 + }
429 + itemSize, ok = checkedAddInt(itemSize, pathSize)
430 + if !ok {
431 + return ErrOverflow
432 + }
433
352 - if itemStart+itemSize > len(b.buf) {
434 + itemEnd, ok := checkedAddInt(itemStart, itemSize)
435 + if !ok || itemEnd > len(b.buf) {
436 return ErrOverflow
437 }
438
@@ -358,8 +441,31 @@ func (b *CgroupsBuilder) Add(hash, options, enabled uint32, name, path []byte) e
441 clear(b.buf[b.dataOffset:itemStart])
442 }
443
361 - nameOffset := uint32(cgroupsItemHdr)
362 - pathOffset := uint32(cgroupsItemHdr) + uint32(len(name)) + 1
444 + nameLen32, ok := checkedU32Int(len(name))
445 + if !ok {
446 + return ErrOverflow
447 + }
448 + pathLen32, ok := checkedU32Int(len(path))
449 + if !ok {
450 + return ErrOverflow
451 + }
452 + nameOffset32 := uint32(cgroupsItemHdr)
453 + pathOffset, ok := checkedAddInt(cgroupsItemHdr, nameSize)
454 + if !ok {
455 + return ErrOverflow
456 + }
457 + pathOffset32, ok := checkedU32Int(pathOffset)
458 + if !ok {
459 + return ErrOverflow
460 + }
461 + itemStart32, ok := checkedU32Int(itemStart)
462 + if !ok {
463 + return ErrOverflow
464 + }
465 + itemSize32, ok := checkedU32Int(itemSize)
466 + if !ok {
467 + return ErrOverflow
468 + }
469
470 // Write item header.
471 p := itemStart
@@ -368,26 +474,33 @@ func (b *CgroupsBuilder) Add(hash, options, enabled uint32, name, path []byte) e
474 ne.PutUint32(b.buf[p+4:p+8], hash)
475 ne.PutUint32(b.buf[p+8:p+12], options)
476 ne.PutUint32(b.buf[p+12:p+16], enabled)
371 - ne.PutUint32(b.buf[p+16:p+20], nameOffset)
372 - ne.PutUint32(b.buf[p+20:p+24], uint32(len(name)))
373 - ne.PutUint32(b.buf[p+24:p+28], pathOffset)
374 - ne.PutUint32(b.buf[p+28:p+32], uint32(len(path)))
477 + ne.PutUint32(b.buf[p+16:p+20], nameOffset32)
478 + ne.PutUint32(b.buf[p+20:p+24], nameLen32)
479 + ne.PutUint32(b.buf[p+24:p+28], pathOffset32)
480 + ne.PutUint32(b.buf[p+28:p+32], pathLen32)
481
482 // Write strings with NUL terminators.
377 - ns := p + int(nameOffset)
483 + ns := p + cgroupsItemHdr
484 copy(b.buf[ns:], name)
485 b.buf[ns+len(name)] = 0
486
381 - ps := p + int(pathOffset)
487 + ps := p + pathOffset
488 copy(b.buf[ps:], path)
489 b.buf[ps+len(path)] = 0
490
491 // Write directory entry (absolute offset stored temporarily).
386 - dirEntry := cgroupsRespHdr + int(b.itemCount)*cgroupsDirEntry
387 - ne.PutUint32(b.buf[dirEntry:dirEntry+4], uint32(itemStart))
388 - ne.PutUint32(b.buf[dirEntry+4:dirEntry+8], uint32(itemSize))
492 + dirEntryOff, ok := checkedInt(uint64(b.itemCount) * uint64(cgroupsDirEntry))
493 + if !ok {
494 + return ErrOverflow
495 + }
496 + dirEntry, ok := checkedAddInt(cgroupsRespHdr, dirEntryOff)
497 + if !ok {
498 + return ErrOverflow
499 + }
500 + ne.PutUint32(b.buf[dirEntry:dirEntry+4], itemStart32)
501 + ne.PutUint32(b.buf[dirEntry+4:dirEntry+8], itemSize32)
502
390 - b.dataOffset = itemStart + itemSize
503 + b.dataOffset = itemEnd
504 b.itemCount++
505 return nil
506 }
@@ -407,25 +520,49 @@ func (b *CgroupsBuilder) Finish() int {
520 return cgroupsRespHdr
521 }
522
410 - // Where the decoder expects packed data to start.
411 - finalPackedStart := cgroupsRespHdr + int(b.itemCount)*cgroupsDirEntry
523 + dirSize, ok := checkedInt(uint64(b.itemCount) * uint64(cgroupsDirEntry))
524 + if !ok {
525 + return 0
526 + }
527 + finalPackedStart, ok := checkedAddInt(cgroupsRespHdr, dirSize)
528 + if !ok {
529 + return 0
530 + }
531
532 // Read the first directory entry to find where packed data begins.
414 - firstItemAbs := int(ne.Uint32(p[cgroupsRespHdr : cgroupsRespHdr+4]))
533 + firstItemAbs32 := ne.Uint32(p[cgroupsRespHdr : cgroupsRespHdr+4])
534 + firstItemAbs, ok := checkedInt(uint64(firstItemAbs32))
535 + if !ok {
536 + return 0
537 + }
538
539 packedDataLen := b.dataOffset - firstItemAbs
540
541 if finalPackedStart < firstItemAbs {
542 + packedDataEnd, ok := checkedAddInt(firstItemAbs, packedDataLen)
543 + if !ok {
544 + return 0
545 + }
546 // Shift packed data left.
420 - copy(p[finalPackedStart:], p[firstItemAbs:firstItemAbs+packedDataLen])
547 + copy(p[finalPackedStart:], p[firstItemAbs:packedDataEnd])
548 }
549
550 // Convert directory entries from absolute to relative offsets.
551 dirBase := cgroupsRespHdr
425 - for i := 0; i < int(b.itemCount); i++ {
426 - entry := dirBase + i*cgroupsDirEntry
552 + for i := uint32(0); i < b.itemCount; i++ {
553 + entryOff, ok := checkedInt(uint64(i) * uint64(cgroupsDirEntry))
554 + if !ok {
555 + return 0
556 + }
557 + entry, ok := checkedAddInt(dirBase, entryOff)
558 + if !ok {
559 + return 0
560 + }
561 absOff := ne.Uint32(p[entry : entry+4])
428 - relOff := absOff - uint32(firstItemAbs)
562 + if absOff < firstItemAbs32 {
563 + return 0
564 + }
565 + relOff := absOff - firstItemAbs32
566 ne.PutUint32(p[entry:entry+4], relOff)
567 // length stays the same.
568 }
@@ -438,7 +575,11 @@ func (b *CgroupsBuilder) Finish() int {
575 ne.PutUint32(p[12:16], 0)
576 ne.PutUint64(p[16:24], b.generation)
577
441 - return finalPackedStart + packedDataLen
578 + total, ok := checkedAddInt(finalPackedStart, packedDataLen)
579 + if !ok {
580 + return 0
581 + }
582 + return total
583 }
584
585 // DispatchCgroupsSnapshot decodes request, builds response via handler.
src/go/pkg/netipc/protocol/frame.go
+128 -36
@@ -48,6 +48,8 @@ const (
48 MethodIncrement uint16 = 1
49 MethodCgroupsSnapshot uint16 = 2
50 MethodStringReverse uint16 = 3
51 + MethodCgroupsLookup uint16 = 4
52 + MethodAppsLookup uint16 = 5
53
54 // Profile bits.
55 ProfileBaseline uint32 = 0x01
@@ -62,16 +64,12 @@ const (
64 // (1 MiB) to prevent excessive memory allocation from a compromised peer.
65 MaxPayloadCap uint32 = 1024 * 1024
66
65 - // Alignment for batch items and cgroups items.
67 + // Alignment for batch items and typed codec items.
68 Alignment = 8
69
70 // Payload sizes.
69 - helloSize = 44
70 - helloAckSize = 48
71 - cgroupsReqSize = 4
72 - cgroupsRespHdr = 24
73 - cgroupsDirEntry = 8
74 - cgroupsItemHdr = 32
71 + helloSize = 44
72 + helloAckSize = 48
73 )
74
75 var ne = binary.NativeEndian
@@ -265,8 +263,14 @@ func BatchDirEncode(entries []BatchEntry, buf []byte) int {
263 // BatchDirDecode decodes itemCount directory entries from buf. Validates
264 // alignment and that each entry falls within packedAreaLen.
265 func BatchDirDecode(buf []byte, itemCount uint32, packedAreaLen uint32) ([]BatchEntry, error) {
268 - count := int(itemCount)
269 - dirSize := count * 8
266 + count, ok := checkedInt(uint64(itemCount))
267 + if !ok {
268 + return nil, ErrBadItemCount
269 + }
270 + dirSize, ok := checkedMulInt(count, 8)
271 + if !ok {
272 + return nil, ErrBadItemCount
273 + }
274 if len(buf) < dirSize {
275 return nil, ErrTruncated
276 }
@@ -277,7 +281,7 @@ func BatchDirDecode(buf []byte, itemCount uint32, packedAreaLen uint32) ([]Batch
281 off := ne.Uint32(buf[base : base+4])
282 length := ne.Uint32(buf[base+4 : base+8])
283
280 - if int(off)%Alignment != 0 {
284 + if off%uint32(Alignment) != 0 {
285 return nil, ErrBadAlignment
286 }
287 if uint64(off)+uint64(length) > uint64(packedAreaLen) {
@@ -291,8 +295,14 @@ func BatchDirDecode(buf []byte, itemCount uint32, packedAreaLen uint32) ([]Batch
295 // BatchDirValidate validates the batch directory without allocating.
296 // Checks alignment and that each entry falls within packedAreaLen.
297 func BatchDirValidate(buf []byte, itemCount uint32, packedAreaLen uint32) error {
294 - count := int(itemCount)
295 - dirSize := count * 8
298 + count, ok := checkedInt(uint64(itemCount))
299 + if !ok {
300 + return ErrBadItemCount
301 + }
302 + dirSize, ok := checkedMulInt(count, 8)
303 + if !ok {
304 + return ErrBadItemCount
305 + }
306 if len(buf) < dirSize {
307 return ErrTruncated
308 }
@@ -300,7 +310,7 @@ func BatchDirValidate(buf []byte, itemCount uint32, packedAreaLen uint32) error
310 base := i * 8
311 off := ne.Uint32(buf[base : base+4])
312 length := ne.Uint32(buf[base+4 : base+8])
303 - if int(off)%Alignment != 0 {
313 + if off%uint32(Alignment) != 0 {
314 return ErrBadAlignment
315 }
316 if uint64(off)+uint64(length) > uint64(packedAreaLen) {
@@ -317,30 +327,55 @@ func BatchItemGet(payload []byte, itemCount uint32, index uint32) ([]byte, error
327 return nil, ErrOutOfBounds
328 }
329
320 - dirSize := int(itemCount) * 8
321 - dirAligned := Align8(dirSize)
330 + dirSize, ok := checkedInt(uint64(itemCount) * 8)
331 + if !ok {
332 + return nil, ErrBadItemCount
333 + }
334 + dirAligned, ok := checkedAlign8(dirSize)
335 + if !ok {
336 + return nil, ErrBadItemCount
337 + }
338
339 if len(payload) < dirAligned {
340 return nil, ErrTruncated
341 }
342
327 - idx := int(index)
328 - base := idx * 8
329 - off := ne.Uint32(payload[base : base+4])
330 - length := ne.Uint32(payload[base+4 : base+8])
343 + idx, ok := checkedInt(uint64(index))
344 + if !ok {
345 + return nil, ErrOutOfBounds
346 + }
347 + base, ok := checkedMulInt(idx, 8)
348 + if !ok {
349 + return nil, ErrOutOfBounds
350 + }
351 + off, err := checkedWireU32Int(payload, base)
352 + if err != nil {
353 + return nil, err
354 + }
355 + length, err := checkedWireU32Int(payload, base+4)
356 + if err != nil {
357 + return nil, err
358 + }
359
360 packedAreaStart := dirAligned
361 packedAreaLen := len(payload) - packedAreaStart
362
335 - if int(off)%Alignment != 0 {
363 + if off%Alignment != 0 {
364 return nil, ErrBadAlignment
365 }
338 - if uint64(off)+uint64(length) > uint64(packedAreaLen) {
366 + relEnd, ok := checkedAddInt(off, length)
367 + if !ok || relEnd > packedAreaLen {
368 return nil, ErrOutOfBounds
369 }
370
342 - start := packedAreaStart + int(off)
343 - end := start + int(length)
371 + start, ok := checkedAddInt(packedAreaStart, off)
372 + if !ok {
373 + return nil, ErrOutOfBounds
374 + }
375 + end, ok := checkedAddInt(start, length)
376 + if !ok {
377 + return nil, ErrOutOfBounds
378 + }
379 return payload[start:end], nil
380 }
381
@@ -364,7 +399,19 @@ func (b *BatchBuilder) Reset(buf []byte, maxItems uint32) {
399 b.buf = buf
400 b.itemCount = 0
401 b.maxItems = maxItems
367 - b.dirEnd = Align8(int(maxItems) * 8)
402 + dirSize, ok := checkedInt(uint64(maxItems) * 8)
403 + if !ok {
404 + b.dirEnd = maxIntValue()
405 + b.dataOffset = 0
406 + return
407 + }
408 + dirEnd, ok := checkedAlign8(dirSize)
409 + if !ok {
410 + b.dirEnd = maxIntValue()
411 + b.dataOffset = 0
412 + return
413 + }
414 + b.dirEnd = dirEnd
415 b.dataOffset = 0
416 }
417
@@ -382,28 +429,55 @@ func (b *BatchBuilder) Add(item []byte) error {
429 return ErrOverflow
430 }
431
385 - alignedOff := Align8(b.dataOffset)
386 - absPos := b.dirEnd + alignedOff
432 + alignedOff, ok := checkedAlign8(b.dataOffset)
433 + if !ok {
434 + return ErrOverflow
435 + }
436 + absPos, ok := checkedAddInt(b.dirEnd, alignedOff)
437 + if !ok {
438 + return ErrOverflow
439 + }
440
388 - if absPos+len(item) > len(b.buf) {
441 + itemEnd, ok := checkedAddInt(absPos, len(item))
442 + if !ok || itemEnd > len(b.buf) {
443 return ErrOverflow
444 }
445
446 // Zero alignment padding.
447 if alignedOff > b.dataOffset {
394 - padStart := b.dirEnd + b.dataOffset
395 - padEnd := b.dirEnd + alignedOff
448 + padStart, ok := checkedAddInt(b.dirEnd, b.dataOffset)
449 + if !ok {
450 + return ErrOverflow
451 + }
452 + padEnd, ok := checkedAddInt(b.dirEnd, alignedOff)
453 + if !ok {
454 + return ErrOverflow
455 + }
456 clear(b.buf[padStart:padEnd])
457 }
458
459 copy(b.buf[absPos:], item)
460
461 // Write directory entry.
402 - idx := int(b.itemCount) * 8
403 - ne.PutUint32(b.buf[idx:idx+4], uint32(alignedOff))
404 - ne.PutUint32(b.buf[idx+4:idx+8], uint32(len(item)))
462 + idx, ok := checkedInt(uint64(b.itemCount) * 8)
463 + if !ok {
464 + return ErrOverflow
465 + }
466 + alignedOff32, ok := checkedU32Int(alignedOff)
467 + if !ok {
468 + return ErrOverflow
469 + }
470 + itemLen32, ok := checkedU32Int(len(item))
471 + if !ok {
472 + return ErrOverflow
473 + }
474 + ne.PutUint32(b.buf[idx:idx+4], alignedOff32)
475 + ne.PutUint32(b.buf[idx+4:idx+8], itemLen32)
476
406 - b.dataOffset = alignedOff + len(item)
477 + b.dataOffset, ok = checkedAddInt(alignedOff, len(item))
478 + if !ok {
479 + return ErrOverflow
480 + }
481 b.itemCount++
482 return nil
483 }
@@ -412,14 +486,32 @@ func (b *BatchBuilder) Add(item []byte) error {
486 // Compacts if fewer items were added than maxItems.
487 func (b *BatchBuilder) Finish() (int, uint32) {
488 count := b.itemCount
415 - finalDirAligned := Align8(int(count) * 8)
489 + dirSize, ok := checkedInt(uint64(count) * 8)
490 + if !ok {
491 + return 0, count
492 + }
493 + finalDirAligned, ok := checkedAlign8(dirSize)
494 + if !ok {
495 + return 0, count
496 + }
497
498 if finalDirAligned < b.dirEnd && b.dataOffset > 0 {
499 + dataEnd, ok := checkedAddInt(b.dirEnd, b.dataOffset)
500 + if !ok {
501 + return 0, count
502 + }
503 // Shift packed data left.
419 - copy(b.buf[finalDirAligned:], b.buf[b.dirEnd:b.dirEnd+b.dataOffset])
504 + copy(b.buf[finalDirAligned:], b.buf[b.dirEnd:dataEnd])
505 }
506
422 - total := finalDirAligned + Align8(b.dataOffset)
507 + alignedData, ok := checkedAlign8(b.dataOffset)
508 + if !ok {
509 + return 0, count
510 + }
511 + total, ok := checkedAddInt(finalDirAligned, alignedData)
512 + if !ok {
513 + return 0, count
514 + }
515 return total, count
516 }
517
src/go/pkg/netipc/protocol/fuzz_test.go
+146
@@ -213,6 +213,152 @@ func FuzzDecodeCgroupsResponse(f *testing.F) {
213 })
214 }
215
216 +func FuzzDecodeCgroupsLookupRequest(f *testing.F) {
217 + var seed [128]byte
218 + n, err := EncodeCgroupsLookupRequest([][]byte{[]byte("/a"), []byte("/b/c")}, seed[:])
219 + if err == nil {
220 + f.Add(seed[:n])
221 + }
222 + f.Add([]byte{})
223 + f.Add(make([]byte, CgroupsLookupReqHdr-1))
224 + f.Add(make([]byte, CgroupsLookupReqHdr))
225 +
226 + f.Fuzz(func(t *testing.T, data []byte) {
227 + view, err := DecodeCgroupsLookupRequest(data)
228 + if err != nil {
229 + return
230 + }
231 + for i := uint32(0); i < view.ItemCount; i++ {
232 + item, ierr := view.Item(i)
233 + if ierr == nil {
234 + _ = item.Bytes()
235 + _ = item.String()
236 + }
237 + }
238 + _, _ = view.Item(view.ItemCount)
239 + })
240 +}
241 +
242 +func FuzzDecodeCgroupsLookupResponse(f *testing.F) {
243 + var empty [128]byte
244 + eb := NewCgroupsLookupBuilder(empty[:], 0, 0)
245 + f.Add(empty[:eb.Finish()])
246 +
247 + var seed [512]byte
248 + builder := NewCgroupsLookupBuilder(seed[:], 1, 1)
249 + if err := builder.Add(
250 + CgroupLookupKnown,
251 + OrchestratorK8s,
252 + []byte("/a"),
253 + []byte("pod-a"),
254 + []struct{ Key, Value []byte }{{Key: []byte("namespace"), Value: []byte("default")}},
255 + ); err == nil {
256 + f.Add(seed[:builder.Finish()])
257 + }
258 + f.Add([]byte{})
259 + f.Add(make([]byte, CgroupsLookupRespHdr-1))
260 + f.Add(make([]byte, CgroupsLookupRespHdr))
261 +
262 + f.Fuzz(func(t *testing.T, data []byte) {
263 + view, err := DecodeCgroupsLookupResponse(data)
264 + if err != nil {
265 + return
266 + }
267 + for i := uint32(0); i < view.ItemCount; i++ {
268 + item, ierr := view.Item(i)
269 + if ierr != nil {
270 + continue
271 + }
272 + _ = item.Path.Bytes()
273 + _ = item.Name.String()
274 + for j := uint32(0); j < uint32(item.LabelCount); j++ {
275 + label, lerr := item.Label(j)
276 + if lerr == nil {
277 + _ = label.Key.String()
278 + _ = label.Value.String()
279 + }
280 + }
281 + }
282 + _, _ = view.Item(view.ItemCount)
283 + })
284 +}
285 +
286 +func FuzzDecodeAppsLookupRequest(f *testing.F) {
287 + var seed [128]byte
288 + n, err := EncodeAppsLookupRequest([]uint32{0, 1234}, seed[:])
289 + if err == nil {
290 + f.Add(seed[:n])
291 + }
292 + f.Add([]byte{})
293 + f.Add(make([]byte, AppsLookupReqHdr-1))
294 + f.Add(make([]byte, AppsLookupReqHdr))
295 +
296 + f.Fuzz(func(t *testing.T, data []byte) {
297 + view, err := DecodeAppsLookupRequest(data)
298 + if err != nil {
299 + return
300 + }
301 + for i := uint32(0); i < view.ItemCount; i++ {
302 + pid, ierr := view.Item(i)
303 + if ierr == nil {
304 + _ = pid
305 + }
306 + }
307 + _, _ = view.Item(view.ItemCount)
308 + })
309 +}
310 +
311 +func FuzzDecodeAppsLookupResponse(f *testing.F) {
312 + var empty [128]byte
313 + eb := NewAppsLookupBuilder(empty[:], 0, 0)
314 + f.Add(empty[:eb.Finish()])
315 +
316 + var seed [1024]byte
317 + builder := NewAppsLookupBuilder(seed[:], 1, 1)
318 + if err := builder.Add(
319 + PidLookupKnown,
320 + AppsCgroupKnown,
321 + OrchestratorDocker,
322 + 1234,
323 + 1,
324 + 1000,
325 + 42,
326 + []byte("nginx"),
327 + []byte("/docker/abc"),
328 + []byte("container-a"),
329 + []struct{ Key, Value []byte }{{Key: []byte("image"), Value: []byte("nginx:latest")}},
330 + ); err == nil {
331 + f.Add(seed[:builder.Finish()])
332 + }
333 + f.Add([]byte{})
334 + f.Add(make([]byte, AppsLookupRespHdr-1))
335 + f.Add(make([]byte, AppsLookupRespHdr))
336 +
337 + f.Fuzz(func(t *testing.T, data []byte) {
338 + view, err := DecodeAppsLookupResponse(data)
339 + if err != nil {
340 + return
341 + }
342 + for i := uint32(0); i < view.ItemCount; i++ {
343 + item, ierr := view.Item(i)
344 + if ierr != nil {
345 + continue
346 + }
347 + _ = item.Comm.String()
348 + _ = item.CgroupPath.Bytes()
349 + _ = item.CgroupName.String()
350 + for j := uint32(0); j < uint32(item.LabelCount); j++ {
351 + label, lerr := item.Label(j)
352 + if lerr == nil {
353 + _ = label.Key.String()
354 + _ = label.Value.String()
355 + }
356 + }
357 + }
358 + _, _ = view.Item(view.ItemCount)
359 + })
360 +}
361 +
362 func FuzzBatchDirDecode(f *testing.F) {
363 // Seed: valid 2-entry directory.
364 var seed [16]byte
src/go/pkg/netipc/protocol/lookup.go new
+1
@@ -0,0 +1 @@
1 +package protocol
src/go/pkg/netipc/protocol/lookup_common.go new
+492
@@ -0,0 +1,492 @@
1 +package protocol
2 +
3 +import "bytes"
4 +
5 +const (
6 + OrchestratorUnknown uint16 = 0
7 + OrchestratorSystemd uint16 = 1
8 + OrchestratorDocker uint16 = 2
9 + OrchestratorK8s uint16 = 3
10 + OrchestratorKvm uint16 = 4
11 + OrchestratorLxc uint16 = 5
12 + OrchestratorPodman uint16 = 6
13 + OrchestratorNspawn uint16 = 7
14 +
15 + LookupDirEntrySize = 8
16 + LookupLabelEntrySize = 16
17 +)
18 +
19 +// LookupLabelView represents a key-value label pair view in the lookup wire format.
20 +type LookupLabelView struct {
21 + Key CStringView
22 + Value CStringView
23 +}
24 +
25 +func invalidSourceString(data []byte, requireNonEmpty bool) bool {
26 + return (requireNonEmpty && len(data) == 0) || bytes.IndexByte(data, 0) >= 0
27 +}
28 +
29 +// maxIntValue returns the maximum value representable by int on this platform.
30 +func maxIntValue() int {
31 + return int(^uint(0) >> 1)
32 +}
33 +
34 +func checkedU32Int(value int) (uint32, bool) {
35 + if value < 0 || uint64(value) > uint64(^uint32(0)) {
36 + return 0, false
37 + }
38 + return uint32(value), true
39 +}
40 +
41 +func checkedU16Int(value int) (uint16, bool) {
42 + if value < 0 || value > int(^uint16(0)) {
43 + return 0, false
44 + }
45 + return uint16(value), true
46 +}
47 +
48 +func checkedWireU32Int(buf []byte, off int) (int, error) {
49 + value, ok := checkedInt(uint64(ne.Uint32(buf[off : off+4])))
50 + if !ok {
51 + return 0, ErrOutOfBounds
52 + }
53 + return value, nil
54 +}
55 +
56 +func lookupDirEntry(buf []byte, base int) (int, int, error) {
57 + off, err := checkedWireU32Int(buf, base)
58 + if err != nil {
59 + return 0, 0, err
60 + }
61 + length, err := checkedWireU32Int(buf, base+4)
62 + if err != nil {
63 + return 0, 0, err
64 + }
65 + return off, length, nil
66 +}
67 +
68 +func lookupPayloadSlice(buf []byte, start int, off int, length int) ([]byte, error) {
69 + abs, ok := checkedAddInt(start, off)
70 + if !ok {
71 + return nil, ErrOutOfBounds
72 + }
73 + end, ok := checkedAddInt(abs, length)
74 + if !ok || end > len(buf) {
75 + return nil, ErrOutOfBounds
76 + }
77 + return buf[abs:end], nil
78 +}
79 +
80 +func lookupBuilderDataOffset(hdrSize int, maxItems uint32) (int, bool) {
81 + dirSize, ok := checkedInt(uint64(maxItems) * uint64(LookupDirEntrySize))
82 + if !ok {
83 + return 0, false
84 + }
85 + return checkedAddInt(hdrSize, dirSize)
86 +}
87 +
88 +func lookupDirOffset(hdrSize int, index uint32) (int, bool) {
89 + dirOff, ok := checkedInt(uint64(index) * uint64(LookupDirEntrySize))
90 + if !ok {
91 + return 0, false
92 + }
93 + return checkedAddInt(hdrSize, dirOff)
94 +}
95 +
96 +func checkedInt(value uint64) (int, bool) {
97 + maxInt := uint64(maxIntValue()) // #nosec G115 -- maxIntValue is non-negative and intentionally widened for the bounds check.
98 + if value > maxInt {
99 + return 0, false
100 + }
101 + return int(value), true // #nosec G115 -- value is bounded by maxInt above.
102 +}
103 +
104 +func checkedAddInt(a, b int) (int, bool) {
105 + if a < 0 || b < 0 {
106 + return 0, false
107 + }
108 + maxInt := maxIntValue()
109 + if a > maxInt-b {
110 + return 0, false
111 + }
112 + return a + b, true
113 +}
114 +
115 +func checkedMulInt(a, b int) (int, bool) {
116 + if a < 0 || b < 0 {
117 + return 0, false
118 + }
119 + maxInt := maxIntValue()
120 + if a != 0 && b > maxInt/a {
121 + return 0, false
122 + }
123 + return a * b, true
124 +}
125 +
126 +func checkedAlign8(v int) (int, bool) {
127 + if v < 0 {
128 + return 0, false
129 + }
130 + maxInt := maxIntValue()
131 + if v > maxInt-7 {
132 + return 0, false
133 + }
134 + return Align8(v), true
135 +}
136 +
137 +func validateLookupDir(buf []byte, dirStart int, itemCount uint32, packedAreaLen int, minLen int, exactLen int) error {
138 + var minLen32 uint32
139 + var exactLen32 uint32
140 + if minLen >= 0 {
141 + converted, ok := checkedU32Int(minLen)
142 + if !ok {
143 + return ErrBadLayout
144 + }
145 + minLen32 = converted
146 + }
147 + if exactLen >= 0 {
148 + converted, ok := checkedU32Int(exactLen)
149 + if !ok {
150 + return ErrBadLayout
151 + }
152 + exactLen32 = converted
153 + }
154 +
155 + dirSize, ok := checkedInt(uint64(itemCount) * uint64(LookupDirEntrySize))
156 + if !ok {
157 + return ErrBadItemCount
158 + }
159 + dirEnd, ok := checkedAddInt(dirStart, dirSize)
160 + if !ok {
161 + return ErrBadItemCount
162 + }
163 + if dirEnd > len(buf) {
164 + return ErrTruncated
165 + }
166 +
167 + prevEnd := 0
168 + for i := range itemCount {
169 + base, ok := lookupDirOffset(dirStart, i)
170 + if !ok {
171 + return ErrBadItemCount
172 + }
173 + off, length, err := lookupDirEntry(buf, base)
174 + if err != nil {
175 + return err
176 + }
177 + if off%Alignment != 0 {
178 + return ErrBadAlignment
179 + }
180 + length32, ok := checkedU32Int(length)
181 + if !ok {
182 + return ErrOutOfBounds
183 + }
184 + if exactLen >= 0 {
185 + if length32 != exactLen32 {
186 + return ErrBadLayout
187 + }
188 + } else if length32 < minLen32 {
189 + return ErrBadLayout
190 + }
191 + end, ok := checkedAddInt(off, length)
192 + if !ok || end > packedAreaLen {
193 + return ErrOutOfBounds
194 + }
195 + if i > 0 && off < prevEnd {
196 + return ErrBadLayout
197 + }
198 + prevEnd = end
199 + }
200 + return nil
201 +}
202 +
203 +func lookupString(item []byte, hdrSize int, off int, length int) (CStringView, int, error) {
204 + if off < hdrSize {
205 + return CStringView{}, 0, ErrOutOfBounds
206 + }
207 + nul, ok := checkedAddInt(off, length)
208 + if !ok || nul >= len(item) {
209 + return CStringView{}, 0, ErrOutOfBounds
210 + }
211 + if item[nul] != 0 {
212 + return CStringView{}, 0, ErrMissingNul
213 + }
214 + if bytes.Contains(item[off:nul], []byte{0}) {
215 + return CStringView{}, 0, ErrBadLayout
216 + }
217 + length32, ok := checkedU32Int(length)
218 + if !ok {
219 + return CStringView{}, 0, ErrOutOfBounds
220 + }
221 + return NewCStringView(item[off:nul+1], length32), nul + 1, nil
222 +}
223 +
224 +func lookupEmptyString(item []byte, hdrSize int, off int) (CStringView, int, error) {
225 + if off < hdrSize || off >= len(item) {
226 + return CStringView{}, 0, ErrOutOfBounds
227 + }
228 + if item[off] != 0 {
229 + return CStringView{}, 0, ErrMissingNul
230 + }
231 + return NewCStringView(item[off:off+1], 0), off + 1, nil
232 +}
233 +
234 +func overlap(aStart, aEnd, bStart, bEnd int) bool {
235 + return aStart < bEnd && bStart < aEnd
236 +}
237 +
238 +func validateLabels(item []byte, hdrSize int, labelCount uint16, fixedEnd int) (int, error) {
239 + if labelCount == 0 {
240 + if fixedEnd != len(item) {
241 + return 0, ErrBadLayout
242 + }
243 + return fixedEnd, nil
244 + }
245 +
246 + tableStart, ok := checkedAlign8(fixedEnd)
247 + if !ok {
248 + return 0, ErrOutOfBounds
249 + }
250 + if tableStart > len(item) {
251 + return 0, ErrOutOfBounds
252 + }
253 + for _, b := range item[fixedEnd:tableStart] {
254 + if b != 0 {
255 + return 0, ErrBadLayout
256 + }
257 + }
258 +
259 + tableBytes, ok := checkedInt(uint64(labelCount) * uint64(LookupLabelEntrySize))
260 + if !ok {
261 + return 0, ErrOutOfBounds
262 + }
263 + expected, ok := checkedAddInt(tableStart, tableBytes)
264 + if !ok || expected > len(item) {
265 + return 0, ErrOutOfBounds
266 + }
267 +
268 + for i := range labelCount {
269 + entryRel, ok := checkedInt(uint64(i) * uint64(LookupLabelEntrySize))
270 + if !ok {
271 + return 0, ErrOutOfBounds
272 + }
273 + base, ok := checkedAddInt(tableStart, entryRel)
274 + if !ok {
275 + return 0, ErrOutOfBounds
276 + }
277 + keyOff, err := checkedWireU32Int(item, base)
278 + if err != nil {
279 + return 0, err
280 + }
281 + keyLen, err := checkedWireU32Int(item, base+4)
282 + if err != nil {
283 + return 0, err
284 + }
285 + valueOff, err := checkedWireU32Int(item, base+8)
286 + if err != nil {
287 + return 0, err
288 + }
289 + valueLen, err := checkedWireU32Int(item, base+12)
290 + if err != nil {
291 + return 0, err
292 + }
293 + if keyLen == 0 || keyOff != expected {
294 + return 0, ErrBadLayout
295 + }
296 + _, keyEnd, err := lookupString(item, hdrSize, keyOff, keyLen)
297 + if err != nil {
298 + return 0, err
299 + }
300 + expected = keyEnd
301 + if valueOff != expected {
302 + return 0, ErrBadLayout
303 + }
304 + _, valueEnd, err := lookupString(item, hdrSize, valueOff, valueLen)
305 + if err != nil {
306 + return 0, err
307 + }
308 + expected = valueEnd
309 + }
310 + if expected != len(item) {
311 + return 0, ErrBadLayout
312 + }
313 + return tableStart, nil
314 +}
315 +
316 +func lookupLabelAt(item []byte, hdrSize int, labelCount uint16, tableOffset int, index uint32) (LookupLabelView, error) {
317 + if index >= uint32(labelCount) {
318 + return LookupLabelView{}, ErrOutOfBounds
319 + }
320 + entryRel, ok := checkedInt(uint64(index) * uint64(LookupLabelEntrySize))
321 + if !ok {
322 + return LookupLabelView{}, ErrOutOfBounds
323 + }
324 + base, ok := checkedAddInt(tableOffset, entryRel)
325 + if !ok {
326 + return LookupLabelView{}, ErrOutOfBounds
327 + }
328 + keyOff, err := checkedWireU32Int(item, base)
329 + if err != nil {
330 + return LookupLabelView{}, err
331 + }
332 + keyLen, err := checkedWireU32Int(item, base+4)
333 + if err != nil {
334 + return LookupLabelView{}, err
335 + }
336 + valueOff, err := checkedWireU32Int(item, base+8)
337 + if err != nil {
338 + return LookupLabelView{}, err
339 + }
340 + valueLen, err := checkedWireU32Int(item, base+12)
341 + if err != nil {
342 + return LookupLabelView{}, err
343 + }
344 + key, _, err := lookupString(item, hdrSize, keyOff, keyLen)
345 + if err != nil {
346 + return LookupLabelView{}, err
347 + }
348 + value, _, err := lookupString(item, hdrSize, valueOff, valueLen)
349 + if err != nil {
350 + return LookupLabelView{}, err
351 + }
352 + return LookupLabelView{Key: key, Value: value}, nil
353 +}
354 +
355 +func writeLookupLabels(item []byte, tableStart, tableBytes int, labels []struct{ Key, Value []byte }) (int, error) {
356 + next, ok := checkedAddInt(tableStart, tableBytes)
357 + if !ok {
358 + return 0, ErrOverflow
359 + }
360 + for i, label := range labels {
361 + keyOff32, ok := checkedU32Int(next)
362 + if !ok {
363 + return 0, ErrOverflow
364 + }
365 + keyLen32, ok := checkedU32Int(len(label.Key))
366 + if !ok {
367 + return 0, ErrOverflow
368 + }
369 + valueOff, ok := checkedAddInt(next, len(label.Key))
370 + if ok {
371 + valueOff, ok = checkedAddInt(valueOff, 1)
372 + }
373 + if !ok {
374 + return 0, ErrOverflow
375 + }
376 + valueOff32, ok := checkedU32Int(valueOff)
377 + if !ok {
378 + return 0, ErrOverflow
379 + }
380 + valueLen32, ok := checkedU32Int(len(label.Value))
381 + if !ok {
382 + return 0, ErrOverflow
383 + }
384 + entryRel, ok := checkedMulInt(i, LookupLabelEntrySize)
385 + if !ok {
386 + return 0, ErrOverflow
387 + }
388 + entry, ok := checkedAddInt(tableStart, entryRel)
389 + if !ok {
390 + return 0, ErrOverflow
391 + }
392 + ne.PutUint32(item[entry:entry+4], keyOff32)
393 + ne.PutUint32(item[entry+4:entry+8], keyLen32)
394 + ne.PutUint32(item[entry+8:entry+12], valueOff32)
395 + ne.PutUint32(item[entry+12:entry+16], valueLen32)
396 + copy(item[next:], label.Key)
397 + item[next+len(label.Key)] = 0
398 + next = valueOff
399 + copy(item[next:], label.Value)
400 + item[next+len(label.Value)] = 0
401 + next, ok = checkedAddInt(next, len(label.Value))
402 + if ok {
403 + next, ok = checkedAddInt(next, 1)
404 + }
405 + if !ok {
406 + return 0, ErrOverflow
407 + }
408 + }
409 + return next, nil
410 +}
411 +
412 +func labelLayoutGo(fixedEnd int, labels []struct{ Key, Value []byte }) (int, int, int, error) {
413 + if len(labels) == 0 {
414 + return fixedEnd, 0, fixedEnd, nil
415 + }
416 + tableStart, ok := checkedAlign8(fixedEnd)
417 + if !ok {
418 + return 0, 0, 0, ErrOverflow
419 + }
420 + tableBytes, ok := checkedMulInt(len(labels), LookupLabelEntrySize)
421 + if !ok {
422 + return 0, 0, 0, ErrOverflow
423 + }
424 + itemSize, ok := checkedAddInt(tableStart, tableBytes)
425 + if !ok {
426 + return 0, 0, 0, ErrOverflow
427 + }
428 + for _, label := range labels {
429 + if invalidSourceString(label.Key, true) || invalidSourceString(label.Value, false) {
430 + return 0, 0, 0, ErrBadLayout
431 + }
432 + keySize, ok := checkedAddInt(len(label.Key), 1)
433 + if ok {
434 + valueSize, okValue := checkedAddInt(len(label.Value), 1)
435 + if okValue {
436 + keySize, ok = checkedAddInt(keySize, valueSize)
437 + } else {
438 + ok = false
439 + }
440 + }
441 + if ok {
442 + itemSize, ok = checkedAddInt(itemSize, keySize)
443 + }
444 + if !ok {
445 + return 0, 0, 0, ErrOverflow
446 + }
447 + }
448 + return tableStart, tableBytes, itemSize, nil
449 +}
450 +
451 +func finishLookupResponse(buf []byte, hdrSize int, itemCount uint32, dataOffset int, generation uint64) int {
452 + ne.PutUint16(buf[0:2], 1)
453 + ne.PutUint16(buf[2:4], 0)
454 + ne.PutUint32(buf[4:8], itemCount)
455 + ne.PutUint64(buf[8:16], generation)
456 + if itemCount == 0 {
457 + return hdrSize
458 + }
459 + dirSize, ok := checkedInt(uint64(itemCount) * uint64(LookupDirEntrySize))
460 + if !ok {
461 + return 0
462 + }
463 + count := int(itemCount)
464 + finalPackedStart, ok := checkedAddInt(hdrSize, dirSize)
465 + if !ok {
466 + return 0
467 + }
468 + firstItemAbs, ok := checkedInt(uint64(ne.Uint32(buf[hdrSize : hdrSize+4])))
469 + if !ok {
470 + return 0
471 + }
472 + if dataOffset < firstItemAbs {
473 + return 0
474 + }
475 + packedDataLen := dataOffset - firstItemAbs
476 + if finalPackedStart < firstItemAbs {
477 + copy(buf[finalPackedStart:], buf[firstItemAbs:firstItemAbs+packedDataLen])
478 + }
479 + for i := range count {
480 + entry := hdrSize + i*LookupDirEntrySize
481 + abs, ok := checkedInt(uint64(ne.Uint32(buf[entry : entry+4])))
482 + if !ok || abs < firstItemAbs {
483 + return 0
484 + }
485 + rel, ok := checkedU32Int(abs - firstItemAbs)
486 + if !ok {
487 + return 0
488 + }
489 + ne.PutUint32(buf[entry:entry+4], rel)
490 + }
491 + return finalPackedStart + packedDataLen
492 +}
src/go/pkg/netipc/protocol/lookup_test.go new
+1555
@@ -0,0 +1,1555 @@
1 +package protocol
2 +
3 +import "testing"
4 +
5 +func labels(items ...struct{ Key, Value []byte }) []struct{ Key, Value []byte } {
6 + return items
7 +}
8 +
9 +func TestCgroupsLookupRoundTrip(t *testing.T) {
10 + var req [256]byte
11 + n, err := EncodeCgroupsLookupRequest([][]byte{
12 + []byte("/sys/fs/cgroup/a"),
13 + []byte("/system.slice/docker-abc.scope"),
14 + }, req[:])
15 + if err != nil {
16 + t.Fatalf("encode request: %v", err)
17 + }
18 + reqView, err := DecodeCgroupsLookupRequest(req[:n])
19 + if err != nil {
20 + t.Fatalf("decode request: %v", err)
21 + }
22 + if reqView.ItemCount != 2 {
23 + t.Fatalf("item count = %d, want 2", reqView.ItemCount)
24 + }
25 + item0, err := reqView.Item(0)
26 + if err != nil || item0.String() != "/sys/fs/cgroup/a" {
27 + t.Fatalf("item 0 = %q, err=%v", item0.String(), err)
28 + }
29 +
30 + var resp [1024]byte
31 + builder := NewCgroupsLookupBuilder(resp[:], 2, 123)
32 + if err := builder.Add(
33 + CgroupLookupKnown,
34 + OrchestratorK8s,
35 + []byte("/sys/fs/cgroup/a"),
36 + []byte("pod-a"),
37 + labels(struct{ Key, Value []byte }{[]byte("namespace"), []byte("default")}),
38 + ); err != nil {
39 + t.Fatalf("add known: %v", err)
40 + }
41 + if err := builder.Add(
42 + CgroupLookupUnknownPermanent,
43 + 0,
44 + []byte("/system.slice/docker-abc.scope"),
45 + nil,
46 + nil,
47 + ); err != nil {
48 + t.Fatalf("add unknown: %v", err)
49 + }
50 + total := builder.Finish()
51 + view, err := DecodeCgroupsLookupResponse(resp[:total])
52 + if err != nil {
53 + t.Fatalf("decode response: %v", err)
54 + }
55 + if view.ItemCount != 2 || view.Generation != 123 {
56 + t.Fatalf("header = count %d generation %d", view.ItemCount, view.Generation)
57 + }
58 + got, err := view.Item(0)
59 + if err != nil {
60 + t.Fatalf("response item 0: %v", err)
61 + }
62 + if got.Status != CgroupLookupKnown || got.Orchestrator != OrchestratorK8s ||
63 + got.Path.String() != "/sys/fs/cgroup/a" || got.Name.String() != "pod-a" ||
64 + got.LabelCount != 1 {
65 + t.Fatalf("bad known item: %+v", got)
66 + }
67 + label, err := got.Label(0)
68 + if err != nil {
69 + t.Fatalf("label 0: %v", err)
70 + }
71 + if label.Key.String() != "namespace" || label.Value.String() != "default" {
72 + t.Fatalf("bad label: %q=%q", label.Key.String(), label.Value.String())
73 + }
74 + got, err = view.Item(1)
75 + if err != nil {
76 + t.Fatalf("response item 1: %v", err)
77 + }
78 + if got.Status != CgroupLookupUnknownPermanent || got.Name.Len() != 0 {
79 + t.Fatalf("bad unknown item: %+v", got)
80 + }
81 +}
82 +
83 +func TestAppsLookupRoundTrip(t *testing.T) {
84 + var req [128]byte
85 + n, err := EncodeAppsLookupRequest([]uint32{0, 1234, 9999}, req[:])
86 + if err != nil {
87 + t.Fatalf("encode request: %v", err)
88 + }
89 + reqView, err := DecodeAppsLookupRequest(req[:n])
90 + if err != nil {
91 + t.Fatalf("decode request: %v", err)
92 + }
93 + pid, err := reqView.Item(0)
94 + if err != nil || pid != 0 {
95 + t.Fatalf("item 0 pid = %d, err=%v", pid, err)
96 + }
97 +
98 + var resp [2048]byte
99 + builder := NewAppsLookupBuilder(resp[:], 3, 77)
100 + if err := builder.Add(
101 + PidLookupKnown,
102 + AppsCgroupKnown,
103 + OrchestratorDocker,
104 + 1234,
105 + 1,
106 + 1000,
107 + ^uint64(0),
108 + []byte("nginx"),
109 + []byte("/docker/abc"),
110 + []byte("container-a"),
111 + labels(struct{ Key, Value []byte }{[]byte("image"), []byte("nginx:latest")}),
112 + ); err != nil {
113 + t.Fatalf("add known: %v", err)
114 + }
115 + if err := builder.Add(
116 + PidLookupKnown,
117 + AppsCgroupHostRoot,
118 + 0,
119 + 0,
120 + 0,
121 + 0,
122 + 0,
123 + []byte("swapper"),
124 + nil,
125 + nil,
126 + nil,
127 + ); err != nil {
128 + t.Fatalf("add host root: %v", err)
129 + }
130 + if err := builder.Add(
131 + PidLookupUnknown,
132 + AppsCgroupKnown,
133 + 0,
134 + 9999,
135 + 0,
136 + NipcUIDUnset,
137 + 0,
138 + nil,
139 + nil,
140 + nil,
141 + nil,
142 + ); err != nil {
143 + t.Fatalf("add unknown: %v", err)
144 + }
145 + total := builder.Finish()
146 + view, err := DecodeAppsLookupResponse(resp[:total])
147 + if err != nil {
148 + t.Fatalf("decode response: %v", err)
149 + }
150 + if view.ItemCount != 3 || view.Generation != 77 {
151 + t.Fatalf("header = count %d generation %d", view.ItemCount, view.Generation)
152 + }
153 + item0, err := view.Item(0)
154 + if err != nil {
155 + t.Fatalf("item 0: %v", err)
156 + }
157 + if item0.Pid != 1234 || item0.Status != PidLookupKnown ||
158 + item0.CgroupStatus != AppsCgroupKnown ||
159 + item0.Comm.String() != "nginx" ||
160 + item0.CgroupPath.String() != "/docker/abc" ||
161 + item0.Starttime != ^uint64(0) {
162 + t.Fatalf("bad known item: %+v", item0)
163 + }
164 + item1, err := view.Item(1)
165 + if err != nil {
166 + t.Fatalf("item 1: %v", err)
167 + }
168 + if item1.Pid != 0 || item1.CgroupStatus != AppsCgroupHostRoot || item1.CgroupPath.Len() != 0 {
169 + t.Fatalf("bad host-root item: %+v", item1)
170 + }
171 + item2, err := view.Item(2)
172 + if err != nil {
173 + t.Fatalf("item 2: %v", err)
174 + }
175 + if item2.Pid != 9999 || item2.Status != PidLookupUnknown || item2.Uid != NipcUIDUnset {
176 + t.Fatalf("bad unknown item: %+v", item2)
177 + }
178 +}
179 +
180 +func TestLookupValidationEdges(t *testing.T) {
181 + var req [128]byte
182 + if _, err := EncodeCgroupsLookupRequest([][]byte{[]byte("bad\x00path")}, req[:]); err != ErrBadLayout {
183 + t.Fatalf("interior NUL request error = %v, want ErrBadLayout", err)
184 + }
185 +
186 + var resp [256]byte
187 + builder := NewAppsLookupBuilder(resp[:], 1, 0)
188 + if err := builder.Add(
189 + PidLookupKnown,
190 + AppsCgroupHostRoot,
191 + 0,
192 + 1,
193 + 0,
194 + 0,
195 + 1,
196 + []byte("1234567890123456"),
197 + nil,
198 + nil,
199 + nil,
200 + ); err != ErrBadLayout {
201 + t.Fatalf("comm len 16 error = %v, want ErrBadLayout", err)
202 + }
203 +
204 + cg := NewCgroupsLookupBuilder(resp[:], 1, 0)
205 + if err := cg.Add(CgroupLookupKnown, 99, []byte("/x"), nil, nil); err != nil {
206 + t.Fatalf("unknown orchestrator should be accepted: %v", err)
207 + }
208 + total := cg.Finish()
209 + itemStart := CgroupsLookupRespHdr + LookupDirEntrySize + int(ne.Uint32(resp[CgroupsLookupRespHdr:CgroupsLookupRespHdr+4]))
210 + ne.PutUint16(resp[itemStart+2:itemStart+4], 99)
211 + if _, err := DecodeCgroupsLookupResponse(resp[:total]); err != ErrBadLayout {
212 + t.Fatalf("bad status error = %v, want ErrBadLayout", err)
213 + }
214 +}
215 +
216 +func TestLookupDispatchRejectsShortResponseBuffer(t *testing.T) {
217 + var req [128]byte
218 + reqLen, err := EncodeCgroupsLookupRequest([][]byte{[]byte("/x")}, req[:])
219 + if err != nil {
220 + t.Fatalf("encode cgroups request: %v", err)
221 + }
222 + shortCgroups := make([]byte, CgroupsLookupRespHdr+LookupDirEntrySize-1)
223 + n, err := DispatchCgroupsLookup(req[:reqLen], shortCgroups, func(*CgroupsLookupRequestView, *CgroupsLookupBuilder) bool {
224 + t.Fatal("handler should not run with undersized response buffer")
225 + return false
226 + })
227 + if err != ErrOverflow || n != 0 {
228 + t.Fatalf("cgroups dispatch = n %d err %v, want 0 ErrOverflow", n, err)
229 + }
230 +
231 + reqLen, err = EncodeAppsLookupRequest([]uint32{1234}, req[:])
232 + if err != nil {
233 + t.Fatalf("encode apps request: %v", err)
234 + }
235 + shortApps := make([]byte, AppsLookupRespHdr+LookupDirEntrySize-1)
236 + n, err = DispatchAppsLookup(req[:reqLen], shortApps, func(*AppsLookupRequestView, *AppsLookupBuilder) bool {
237 + t.Fatal("handler should not run with undersized response buffer")
238 + return false
239 + })
240 + if err != ErrOverflow || n != 0 {
241 + t.Fatalf("apps dispatch = n %d err %v, want 0 ErrOverflow", n, err)
242 + }
243 +}
244 +
245 +func TestLookupDecodeRejectsMaxUint32DirectoryOffset(t *testing.T) {
246 + var resp [512]byte
247 + cg := NewCgroupsLookupBuilder(resp[:], 1, 0)
248 + if err := cg.Add(CgroupLookupKnown, OrchestratorDocker, []byte("/x"), nil, nil); err != nil {
249 + t.Fatalf("add cgroups item: %v", err)
250 + }
251 + total := cg.Finish()
252 + ne.PutUint32(resp[CgroupsLookupRespHdr:CgroupsLookupRespHdr+4], ^uint32(0)-7)
253 + if _, err := DecodeCgroupsLookupResponse(resp[:total]); err != ErrOutOfBounds {
254 + t.Fatalf("cgroups response with max offset error = %v, want ErrOutOfBounds", err)
255 + }
256 +
257 + apps := NewAppsLookupBuilder(resp[:], 1, 0)
258 + if err := apps.Add(
259 + PidLookupKnown,
260 + AppsCgroupHostRoot,
261 + 0,
262 + 1234,
263 + 1,
264 + 1000,
265 + 42,
266 + []byte("nginx"),
267 + nil,
268 + nil,
269 + nil,
270 + ); err != nil {
271 + t.Fatalf("add apps item: %v", err)
272 + }
273 + total = apps.Finish()
274 + ne.PutUint32(resp[AppsLookupRespHdr:AppsLookupRespHdr+4], ^uint32(0)-7)
275 + if _, err := DecodeAppsLookupResponse(resp[:total]); err != ErrOutOfBounds {
276 + t.Fatalf("apps response with max offset error = %v, want ErrOutOfBounds", err)
277 + }
278 +}
279 +
280 +func TestLookupLabelLayoutOverflow(t *testing.T) {
281 + sample := labels(struct{ Key, Value []byte }{[]byte("k"), []byte("v")})
282 + maxInt := int(^uint(0) >> 1)
283 +
284 + if _, _, _, err := labelLayoutGo(maxInt-3, sample); err != ErrOverflow {
285 + t.Fatalf("align overflow error = %v, want ErrOverflow", err)
286 + }
287 + if _, _, _, err := labelLayoutGo(maxInt-15, sample); err != ErrOverflow {
288 + t.Fatalf("label table overflow error = %v, want ErrOverflow", err)
289 + }
290 +}
291 +
292 +func expectPanic(t *testing.T, name string, fn func()) {
293 + t.Helper()
294 + defer func() {
295 + if recover() == nil {
296 + t.Fatalf("%s did not panic", name)
297 + }
298 + }()
299 + fn()
300 +}
301 +
302 +func cgroupsLookupItemStart(t *testing.T, buf []byte) int {
303 + t.Helper()
304 + return CgroupsLookupRespHdr + LookupDirEntrySize +
305 + int(ne.Uint32(buf[CgroupsLookupRespHdr:CgroupsLookupRespHdr+4]))
306 +}
307 +
308 +func appsLookupItemStart(t *testing.T, buf []byte) int {
309 + t.Helper()
310 + return AppsLookupRespHdr + LookupDirEntrySize +
311 + int(ne.Uint32(buf[AppsLookupRespHdr:AppsLookupRespHdr+4]))
312 +}
313 +
314 +func validCgroupsLookupItemBytes(t *testing.T) []byte {
315 + t.Helper()
316 + var resp [512]byte
317 + builder := NewCgroupsLookupBuilder(resp[:], 1, 0)
318 + if err := builder.Add(
319 + CgroupLookupKnown,
320 + OrchestratorDocker,
321 + []byte("/x"),
322 + []byte("name"),
323 + labels(struct{ Key, Value []byte }{[]byte("k"), []byte("v")}),
324 + ); err != nil {
325 + t.Fatalf("add cgroups item: %v", err)
326 + }
327 + total := builder.Finish()
328 + start := cgroupsLookupItemStart(t, resp[:])
329 + length := int(ne.Uint32(resp[CgroupsLookupRespHdr+4 : CgroupsLookupRespHdr+8]))
330 + return append([]byte(nil), resp[start:start+length]...)[:min(length, total-start)]
331 +}
332 +
333 +func validAppsLookupItemBytes(t *testing.T) []byte {
334 + t.Helper()
335 + var resp [1024]byte
336 + builder := NewAppsLookupBuilder(resp[:], 1, 0)
337 + if err := builder.Add(
338 + PidLookupKnown,
339 + AppsCgroupKnown,
340 + OrchestratorDocker,
341 + 1234, 1, 1000, 42,
342 + []byte("nginx"),
343 + []byte("/docker/abc"),
344 + []byte("container-a"),
345 + labels(struct{ Key, Value []byte }{[]byte("k"), []byte("v")}),
346 + ); err != nil {
347 + t.Fatalf("add apps item: %v", err)
348 + }
349 + total := builder.Finish()
350 + start := appsLookupItemStart(t, resp[:])
351 + length := int(ne.Uint32(resp[AppsLookupRespHdr+4 : AppsLookupRespHdr+8]))
352 + return append([]byte(nil), resp[start:start+length]...)[:min(length, total-start)]
353 +}
354 +
355 +func TestLookupRequestValidationCoverage(t *testing.T) {
356 + var req [128]byte
357 +
358 + n, err := EncodeCgroupsLookupRequest(nil, req[:])
359 + if err != nil {
360 + t.Fatalf("encode empty cgroups request: %v", err)
361 + }
362 + cgView, err := DecodeCgroupsLookupRequest(req[:n])
363 + if err != nil || cgView.ItemCount != 0 {
364 + t.Fatalf("decode empty cgroups request = count %d err %v", cgView.ItemCount, err)
365 + }
366 + if _, err := cgView.Item(0); err != ErrOutOfBounds {
367 + t.Fatalf("empty cgroups item error = %v, want ErrOutOfBounds", err)
368 + }
369 + if _, err := EncodeCgroupsLookupRequest([][]byte{[]byte("/x")}, req[:CgroupsLookupReqHdr]); err != ErrOverflow {
370 + t.Fatalf("short cgroups request buffer error = %v, want ErrOverflow", err)
371 + }
372 + if _, err := EncodeCgroupsLookupRequest([][]byte{[]byte("/xy")}, make([]byte, CgroupsLookupReqHdr+LookupDirEntrySize+1)); err != ErrOverflow {
373 + t.Fatalf("packed cgroups request overflow error = %v, want ErrOverflow", err)
374 + }
375 +
376 + n, err = EncodeCgroupsLookupRequest([][]byte{[]byte("/xy")}, req[:])
377 + if err != nil {
378 + t.Fatalf("encode cgroups request: %v", err)
379 + }
380 + if _, err := DecodeCgroupsLookupRequest(req[:CgroupsLookupReqHdr]); err != ErrTruncated {
381 + t.Fatalf("cgroups request truncated dir error = %v, want ErrTruncated", err)
382 + }
383 + cgManualReq := make([]byte, CgroupsLookupReqHdr+LookupDirEntrySize)
384 + ne.PutUint32(cgManualReq[CgroupsLookupReqHdr+4:CgroupsLookupReqHdr+8], 8)
385 + if _, err := (&CgroupsLookupRequestView{ItemCount: 1, payload: cgManualReq}).Item(0); err != ErrOutOfBounds {
386 + t.Fatalf("manual cgroups request item error = %v, want ErrOutOfBounds", err)
387 + }
388 + for _, tc := range []struct {
389 + name string
390 + edit func([]byte)
391 + want error
392 + }{
393 + {
394 + name: "bad layout",
395 + edit: func(b []byte) {
396 + ne.PutUint16(b[0:2], 2)
397 + },
398 + want: ErrBadLayout,
399 + },
400 + {
401 + name: "bad flags",
402 + edit: func(b []byte) {
403 + ne.PutUint16(b[2:4], 1)
404 + },
405 + want: ErrBadLayout,
406 + },
407 + {
408 + name: "bad reserved",
409 + edit: func(b []byte) {
410 + ne.PutUint32(b[8:12], 1)
411 + },
412 + want: ErrBadLayout,
413 + },
414 + {
415 + name: "bad alignment",
416 + edit: func(b []byte) {
417 + ne.PutUint32(b[CgroupsLookupReqHdr:CgroupsLookupReqHdr+4], 1)
418 + },
419 + want: ErrBadAlignment,
420 + },
421 + {
422 + name: "too short item",
423 + edit: func(b []byte) {
424 + ne.PutUint32(b[CgroupsLookupReqHdr+4:CgroupsLookupReqHdr+8], 1)
425 + },
426 + want: ErrBadLayout,
427 + },
428 + {
429 + name: "missing nul",
430 + edit: func(b []byte) {
431 + dirEnd := CgroupsLookupReqHdr + LookupDirEntrySize
432 + b[dirEnd+3] = '!'
433 + },
434 + want: ErrMissingNul,
435 + },
436 + {
437 + name: "interior nul",
438 + edit: func(b []byte) {
439 + dirEnd := CgroupsLookupReqHdr + LookupDirEntrySize
440 + b[dirEnd+1] = 0
441 + },
442 + want: ErrBadLayout,
443 + },
444 + } {
445 + bad := append([]byte(nil), req[:n]...)
446 + tc.edit(bad)
447 + if _, err := DecodeCgroupsLookupRequest(bad); err != tc.want {
448 + t.Fatalf("decode cgroups request %s error = %v, want %v", tc.name, err, tc.want)
449 + }
450 + }
451 +
452 + n, err = EncodeAppsLookupRequest(nil, req[:])
453 + if err != nil {
454 + t.Fatalf("encode empty apps request: %v", err)
455 + }
456 + appsView, err := DecodeAppsLookupRequest(req[:n])
457 + if err != nil || appsView.ItemCount != 0 {
458 + t.Fatalf("decode empty apps request = count %d err %v", appsView.ItemCount, err)
459 + }
460 + if _, err := appsView.Item(0); err != ErrOutOfBounds {
461 + t.Fatalf("empty apps item error = %v, want ErrOutOfBounds", err)
462 + }
463 + if _, err := EncodeAppsLookupRequest([]uint32{1234}, req[:AppsLookupReqHdr]); err != ErrOverflow {
464 + t.Fatalf("short apps request buffer error = %v, want ErrOverflow", err)
465 + }
466 + if _, err := EncodeAppsLookupRequest([]uint32{1234}, make([]byte, AppsLookupReqHdr+LookupDirEntrySize+AppsLookupKeySize-1)); err != ErrOverflow {
467 + t.Fatalf("packed apps request overflow error = %v, want ErrOverflow", err)
468 + }
469 +
470 + n, err = EncodeAppsLookupRequest([]uint32{1234}, req[:])
471 + if err != nil {
472 + t.Fatalf("encode apps request: %v", err)
473 + }
474 + if _, err := DecodeAppsLookupRequest(req[:AppsLookupReqHdr]); err != ErrTruncated {
475 + t.Fatalf("apps request truncated dir error = %v, want ErrTruncated", err)
476 + }
477 + appsManualReq := make([]byte, AppsLookupReqHdr+LookupDirEntrySize)
478 + ne.PutUint32(appsManualReq[AppsLookupReqHdr+4:AppsLookupReqHdr+8], AppsLookupKeySize)
479 + if _, err := (&AppsLookupRequestView{ItemCount: 1, payload: appsManualReq}).Item(0); err != ErrOutOfBounds {
480 + t.Fatalf("manual apps request item error = %v, want ErrOutOfBounds", err)
481 + }
482 + for _, tc := range []struct {
483 + name string
484 + edit func([]byte)
485 + want error
486 + }{
487 + {
488 + name: "bad layout",
489 + edit: func(b []byte) {
490 + ne.PutUint16(b[0:2], 2)
491 + },
492 + want: ErrBadLayout,
493 + },
494 + {
495 + name: "bad item length",
496 + edit: func(b []byte) {
497 + ne.PutUint32(b[AppsLookupReqHdr+4:AppsLookupReqHdr+8], AppsLookupKeySize-1)
498 + },
499 + want: ErrBadLayout,
500 + },
501 + {
502 + name: "bad key reserved",
503 + edit: func(b []byte) {
504 + dirEnd := AppsLookupReqHdr + LookupDirEntrySize
505 + ne.PutUint32(b[dirEnd+4:dirEnd+8], 1)
506 + },
507 + want: ErrBadLayout,
508 + },
509 + } {
510 + bad := append([]byte(nil), req[:n]...)
511 + tc.edit(bad)
512 + if _, err := DecodeAppsLookupRequest(bad); err != tc.want {
513 + t.Fatalf("decode apps request %s error = %v, want %v", tc.name, err, tc.want)
514 + }
515 + }
516 +}
517 +
518 +func TestCgroupsLookupBuilderGuardCoverage(t *testing.T) {
519 + expectPanic(t, "small cgroups builder", func() {
520 + _ = NewCgroupsLookupBuilder(make([]byte, CgroupsLookupRespHdr+LookupDirEntrySize-1), 1, 0)
521 + })
522 +
523 + var empty [CgroupsLookupRespHdr]byte
524 + emptyBuilder := NewCgroupsLookupBuilder(empty[:], 0, 1)
525 + emptyBuilder.SetGeneration(2)
526 + if total := emptyBuilder.Finish(); total != CgroupsLookupRespHdr {
527 + t.Fatalf("empty cgroups finish = %d", total)
528 + }
529 + emptyView, err := DecodeCgroupsLookupResponse(empty[:])
530 + if err != nil || emptyView.Generation != 2 || emptyView.ItemCount != 0 {
531 + t.Fatalf("empty cgroups decode = generation %d count %d err %v", emptyView.Generation, emptyView.ItemCount, err)
532 + }
533 +
534 + var resp [512]byte
535 + builder := NewCgroupsLookupBuilder(resp[:], 1, 10)
536 + builder.SetGeneration(11)
537 + if builder.ItemCount() != 0 || builder.Error() != nil {
538 + t.Fatalf("fresh cgroups builder count/error = %d/%v", builder.ItemCount(), builder.Error())
539 + }
540 + err = builder.Add(
541 + CgroupLookupKnown,
542 + OrchestratorK8s,
543 + []byte("/x"),
544 + []byte("pod"),
545 + labels(struct{ Key, Value []byte }{[]byte("namespace"), []byte("default")}),
546 + )
547 + if err != nil {
548 + t.Fatalf("add cgroups known: %v", err)
549 + }
550 + if builder.ItemCount() != 1 || builder.Error() != nil {
551 + t.Fatalf("used cgroups builder count/error = %d/%v", builder.ItemCount(), builder.Error())
552 + }
553 + if err := builder.Add(CgroupLookupKnown, 0, []byte("/overflow"), nil, nil); err != ErrOverflow {
554 + t.Fatalf("cgroups overflow error = %v, want ErrOverflow", err)
555 + }
556 + if builder.Error() != ErrOverflow {
557 + t.Fatalf("cgroups builder error = %v, want ErrOverflow", builder.Error())
558 + }
559 + total := builder.Finish()
560 + view, err := DecodeCgroupsLookupResponse(resp[:total])
561 + if err != nil || view.Generation != 11 {
562 + t.Fatalf("decode cgroups builder response = generation %d err %v", view.Generation, err)
563 + }
564 + item, err := view.Item(0)
565 + if err != nil {
566 + t.Fatalf("cgroups response item: %v", err)
567 + }
568 + if _, err := item.Label(1); err != ErrOutOfBounds {
569 + t.Fatalf("cgroups label out-of-bounds error = %v, want ErrOutOfBounds", err)
570 + }
571 + if _, err := view.Item(1); err != ErrOutOfBounds {
572 + t.Fatalf("cgroups item out-of-bounds error = %v, want ErrOutOfBounds", err)
573 + }
574 +
575 + for _, tc := range []struct {
576 + name string
577 + add func(*CgroupsLookupBuilder) error
578 + want error
579 + }{
580 + {
581 + name: "bad status",
582 + add: func(b *CgroupsLookupBuilder) error {
583 + return b.Add(99, 0, []byte("/x"), nil, nil)
584 + },
585 + want: ErrBadLayout,
586 + },
587 + {
588 + name: "empty path",
589 + add: func(b *CgroupsLookupBuilder) error {
590 + return b.Add(CgroupLookupKnown, 0, nil, nil, nil)
591 + },
592 + want: ErrBadLayout,
593 + },
594 + {
595 + name: "bad name",
596 + add: func(b *CgroupsLookupBuilder) error {
597 + return b.Add(CgroupLookupKnown, 0, []byte("/x"), []byte("bad\x00name"), nil)
598 + },
599 + want: ErrBadLayout,
600 + },
601 + {
602 + name: "unknown with name",
603 + add: func(b *CgroupsLookupBuilder) error {
604 + return b.Add(CgroupLookupUnknownRetryLater, 0, []byte("/x"), []byte("name"), nil)
605 + },
606 + want: ErrBadLayout,
607 + },
608 + {
609 + name: "bad label",
610 + add: func(b *CgroupsLookupBuilder) error {
611 + return b.Add(CgroupLookupKnown, 0, []byte("/x"), nil,
612 + labels(struct{ Key, Value []byte }{[]byte{}, []byte("v")}))
613 + },
614 + want: ErrBadLayout,
615 + },
616 + } {
617 + b := NewCgroupsLookupBuilder(resp[:], 1, 0)
618 + if err := tc.add(b); err != tc.want {
619 + t.Fatalf("cgroups builder %s error = %v, want %v", tc.name, err, tc.want)
620 + }
621 + }
622 + tooManyLabels := make([]struct{ Key, Value []byte }, int(^uint16(0))+1)
623 + if err := NewCgroupsLookupBuilder(resp[:], 1, 0).Add(CgroupLookupKnown, 0, []byte("/x"), nil, tooManyLabels); err != ErrOverflow {
624 + t.Fatalf("cgroups too-many-labels error = %v, want ErrOverflow", err)
625 + }
626 +
627 + small := make([]byte, CgroupsLookupRespHdr+LookupDirEntrySize)
628 + smallBuilder := NewCgroupsLookupBuilder(small, 1, 0)
629 + if err := smallBuilder.Add(CgroupLookupKnown, 0, []byte("/x"), nil, nil); err != ErrOverflow {
630 + t.Fatalf("small cgroups builder add error = %v, want ErrOverflow", err)
631 + }
632 + negativeOffsetBuilder := &CgroupsLookupBuilder{buf: make([]byte, 512), maxItems: 1, dataOffset: -1}
633 + if err := negativeOffsetBuilder.Add(CgroupLookupKnown, 0, []byte("/x"), nil, nil); err != ErrOverflow {
634 + t.Fatalf("negative-offset cgroups builder add error = %v, want ErrOverflow", err)
635 + }
636 + overflowOffsetBuilder := &CgroupsLookupBuilder{buf: make([]byte, 512), maxItems: 1, dataOffset: maxIntValue() - 32}
637 + if err := overflowOffsetBuilder.Add(CgroupLookupKnown, 0, []byte("/x"), nil, nil); err != ErrOverflow {
638 + t.Fatalf("overflow-offset cgroups builder add error = %v, want ErrOverflow", err)
639 + }
640 +}
641 +
642 +func TestAppsLookupBuilderGuardCoverage(t *testing.T) {
643 + expectPanic(t, "small apps builder", func() {
644 + _ = NewAppsLookupBuilder(make([]byte, AppsLookupRespHdr+LookupDirEntrySize-1), 1, 0)
645 + })
646 +
647 + var empty [AppsLookupRespHdr]byte
648 + emptyBuilder := NewAppsLookupBuilder(empty[:], 0, 1)
649 + emptyBuilder.SetGeneration(2)
650 + if total := emptyBuilder.Finish(); total != AppsLookupRespHdr {
651 + t.Fatalf("empty apps finish = %d", total)
652 + }
653 + emptyView, err := DecodeAppsLookupResponse(empty[:])
654 + if err != nil || emptyView.Generation != 2 || emptyView.ItemCount != 0 {
655 + t.Fatalf("empty apps decode = generation %d count %d err %v", emptyView.Generation, emptyView.ItemCount, err)
656 + }
657 +
658 + var resp [1024]byte
659 + builder := NewAppsLookupBuilder(resp[:], 4, 20)
660 + builder.SetGeneration(21)
661 + if builder.ItemCount() != 0 || builder.Error() != nil {
662 + t.Fatalf("fresh apps builder count/error = %d/%v", builder.ItemCount(), builder.Error())
663 + }
664 + if err := builder.Add(
665 + PidLookupKnown,
666 + AppsCgroupKnown,
667 + OrchestratorDocker,
668 + 1234, 1, 1000, 42,
669 + []byte("nginx"),
670 + []byte("/docker/abc"),
671 + []byte("container-a"),
672 + labels(struct{ Key, Value []byte }{[]byte("image"), []byte("nginx")}),
673 + ); err != nil {
674 + t.Fatalf("add apps known: %v", err)
675 + }
676 + if err := builder.Add(
677 + PidLookupKnown,
678 + AppsCgroupUnknownRetryLater,
679 + 0,
680 + 1235, 1, 1000, 43,
681 + []byte("worker"),
682 + nil,
683 + nil,
684 + nil,
685 + ); err != nil {
686 + t.Fatalf("add apps retry: %v", err)
687 + }
688 + if err := builder.Add(
689 + PidLookupKnown,
690 + AppsCgroupUnknownPermanent,
691 + 0,
692 + 1236, 1, 1000, 44,
693 + []byte("worker2"),
694 + []byte("/gone"),
695 + nil,
696 + nil,
697 + ); err != nil {
698 + t.Fatalf("add apps permanent: %v", err)
699 + }
700 + if err := builder.Add(
701 + PidLookupUnknown,
702 + 0,
703 + 0,
704 + 9999, 0, NipcUIDUnset, 0,
705 + nil, nil, nil, nil,
706 + ); err != nil {
707 + t.Fatalf("add apps unknown: %v", err)
708 + }
709 + if builder.ItemCount() != 4 || builder.Error() != nil {
710 + t.Fatalf("used apps builder count/error = %d/%v", builder.ItemCount(), builder.Error())
711 + }
712 + total := builder.Finish()
713 + view, err := DecodeAppsLookupResponse(resp[:total])
714 + if err != nil || view.Generation != 21 {
715 + t.Fatalf("decode apps builder response = generation %d err %v", view.Generation, err)
716 + }
717 + item, err := view.Item(0)
718 + if err != nil {
719 + t.Fatalf("apps response item: %v", err)
720 + }
721 + if _, err := item.Label(1); err != ErrOutOfBounds {
722 + t.Fatalf("apps label out-of-bounds error = %v, want ErrOutOfBounds", err)
723 + }
724 + if _, err := view.Item(4); err != ErrOutOfBounds {
725 + t.Fatalf("apps item out-of-bounds error = %v, want ErrOutOfBounds", err)
726 + }
727 + item, err = view.Item(1)
728 + if err != nil || item.CgroupStatus != AppsCgroupUnknownRetryLater {
729 + t.Fatalf("apps retry item = %+v err %v", item, err)
730 + }
731 + if len(item.CgroupPath.Bytes()) != 0 {
732 + t.Fatalf("apps retry cgroup path = %q, want empty", item.CgroupPath.Bytes())
733 + }
734 + item, err = view.Item(2)
735 + if err != nil || item.CgroupStatus != AppsCgroupUnknownPermanent {
736 + t.Fatalf("apps permanent item = %+v err %v", item, err)
737 + }
738 + item, err = view.Item(3)
739 + if err != nil || item.Status != PidLookupUnknown {
740 + t.Fatalf("apps unknown item = %+v err %v", item, err)
741 + }
742 + if err := builder.Add(PidLookupUnknown, 0, 0, 10000, 0, NipcUIDUnset, 0, nil, nil, nil, nil); err != ErrOverflow {
743 + t.Fatalf("apps overflow error = %v, want ErrOverflow", err)
744 + }
745 + if builder.Error() != ErrOverflow {
746 + t.Fatalf("apps builder error = %v, want ErrOverflow", builder.Error())
747 + }
748 +
749 + for _, tc := range []struct {
750 + name string
751 + add func(*AppsLookupBuilder) error
752 + want error
753 + }{
754 + {
755 + name: "bad status",
756 + add: func(b *AppsLookupBuilder) error {
757 + return b.Add(99, 0, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil)
758 + },
759 + want: ErrBadLayout,
760 + },
761 + {
762 + name: "bad cgroup status",
763 + add: func(b *AppsLookupBuilder) error {
764 + return b.Add(PidLookupKnown, 99, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil)
765 + },
766 + want: ErrBadLayout,
767 + },
768 + {
769 + name: "bad comm",
770 + add: func(b *AppsLookupBuilder) error {
771 + return b.Add(PidLookupKnown, AppsCgroupHostRoot, 0, 1, 0, 0, 0, []byte("bad\x00comm"), nil, nil, nil)
772 + },
773 + want: ErrBadLayout,
774 + },
775 + {
776 + name: "unknown with data",
777 + add: func(b *AppsLookupBuilder) error {
778 + return b.Add(PidLookupUnknown, 0, 0, 1, 2, NipcUIDUnset, 0, nil, nil, nil, nil)
779 + },
780 + want: ErrBadLayout,
781 + },
782 + {
783 + name: "known missing path",
784 + add: func(b *AppsLookupBuilder) error {
785 + return b.Add(PidLookupKnown, AppsCgroupKnown, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil)
786 + },
787 + want: ErrBadLayout,
788 + },
789 + {
790 + name: "retry with labels",
791 + add: func(b *AppsLookupBuilder) error {
792 + return b.Add(PidLookupKnown, AppsCgroupUnknownRetryLater, 0, 1, 0, 0, 0,
793 + []byte("x"), []byte("/x"), nil,
794 + labels(struct{ Key, Value []byte }{[]byte("k"), []byte("v")}))
795 + },
796 + want: ErrBadLayout,
797 + },
798 + {
799 + name: "permanent missing path",
800 + add: func(b *AppsLookupBuilder) error {
801 + return b.Add(PidLookupKnown, AppsCgroupUnknownPermanent, 0, 1, 0, 0, 0,
802 + []byte("x"), nil, nil, nil)
803 + },
804 + want: ErrBadLayout,
805 + },
806 + {
807 + name: "host root with path",
808 + add: func(b *AppsLookupBuilder) error {
809 + return b.Add(PidLookupKnown, AppsCgroupHostRoot, 0, 1, 0, 0, 0, []byte("x"), []byte("/x"), nil, nil)
810 + },
811 + want: ErrBadLayout,
812 + },
813 + {
814 + name: "bad label",
815 + add: func(b *AppsLookupBuilder) error {
816 + return b.Add(PidLookupKnown, AppsCgroupKnown, 0, 1, 0, 0, 0,
817 + []byte("x"), []byte("/x"), nil,
818 + labels(struct{ Key, Value []byte }{[]byte("bad\x00key"), []byte("v")}))
819 + },
820 + want: ErrBadLayout,
821 + },
822 + } {
823 + b := NewAppsLookupBuilder(resp[:], 1, 0)
824 + if err := tc.add(b); err != tc.want {
825 + t.Fatalf("apps builder %s error = %v, want %v", tc.name, err, tc.want)
826 + }
827 + }
828 + tooManyLabels := make([]struct{ Key, Value []byte }, int(^uint16(0))+1)
829 + if err := NewAppsLookupBuilder(resp[:], 1, 0).Add(PidLookupKnown, AppsCgroupKnown, 0, 1, 0, 0, 0, []byte("x"), []byte("/x"), nil, tooManyLabels); err != ErrOverflow {
830 + t.Fatalf("apps too-many-labels error = %v, want ErrOverflow", err)
831 + }
832 +
833 + small := make([]byte, AppsLookupRespHdr+LookupDirEntrySize)
834 + smallBuilder := NewAppsLookupBuilder(small, 1, 0)
835 + if err := smallBuilder.Add(PidLookupKnown, AppsCgroupHostRoot, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil); err != ErrOverflow {
836 + t.Fatalf("small apps builder add error = %v, want ErrOverflow", err)
837 + }
838 + negativeOffsetBuilder := &AppsLookupBuilder{buf: make([]byte, 512), maxItems: 1, dataOffset: -1}
839 + if err := negativeOffsetBuilder.Add(PidLookupKnown, AppsCgroupHostRoot, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil); err != ErrOverflow {
840 + t.Fatalf("negative-offset apps builder add error = %v, want ErrOverflow", err)
841 + }
842 + overflowOffsetBuilder := &AppsLookupBuilder{buf: make([]byte, 512), maxItems: 1, dataOffset: maxIntValue() - 32}
843 + if err := overflowOffsetBuilder.Add(PidLookupKnown, AppsCgroupHostRoot, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil); err != ErrOverflow {
844 + t.Fatalf("overflow-offset apps builder add error = %v, want ErrOverflow", err)
845 + }
846 +}
847 +
848 +func TestAppsLookupUnknownItemCanonicalLayout(t *testing.T) {
849 + var resp [128]byte
850 + builder := NewAppsLookupBuilder(resp[:], 1, 7)
851 + if err := builder.Add(PidLookupUnknown, AppsCgroupKnown, 0, 4321, 0, NipcUIDUnset, 0, nil, nil, nil, nil); err != nil {
852 + t.Fatalf("add unknown apps item: %v", err)
853 + }
854 + total := builder.Finish()
855 + itemStart := appsLookupItemStart(t, resp[:total])
856 + item := resp[itemStart:total]
857 +
858 + if got := len(item); got != appsLookupUnknownItemSize {
859 + t.Fatalf("unknown item size = %d, want %d", got, appsLookupUnknownItemSize)
860 + }
861 + if got := ne.Uint32(resp[AppsLookupRespHdr+4 : AppsLookupRespHdr+8]); got != appsLookupUnknownItemSize {
862 + t.Fatalf("directory item length = %d, want %d", got, appsLookupUnknownItemSize)
863 + }
864 + if got := ne.Uint32(item[32:36]); got != AppsLookupItemHdr {
865 + t.Fatalf("comm offset = %d, want %d", got, AppsLookupItemHdr)
866 + }
867 + if got := ne.Uint32(item[40:44]); got != AppsLookupItemHdr+1 {
868 + t.Fatalf("path offset = %d, want %d", got, AppsLookupItemHdr+1)
869 + }
870 + if got := ne.Uint32(item[48:52]); got != AppsLookupItemHdr+2 {
871 + t.Fatalf("name offset = %d, want %d", got, AppsLookupItemHdr+2)
872 + }
873 + if item[AppsLookupItemHdr] != 0 || item[AppsLookupItemHdr+1] != 0 || item[AppsLookupItemHdr+2] != 0 {
874 + t.Fatalf("unknown item NUL bytes = %v, want all zero", item[AppsLookupItemHdr:AppsLookupItemHdr+3])
875 + }
876 +
877 + view, err := DecodeAppsLookupResponse(resp[:total])
878 + if err != nil {
879 + t.Fatalf("decode unknown apps item: %v", err)
880 + }
881 + got, err := view.Item(0)
882 + if err != nil {
883 + t.Fatalf("read unknown apps item: %v", err)
884 + }
885 + if got.Status != PidLookupUnknown || got.Pid != 4321 ||
886 + len(got.Comm.Bytes()) != 0 || len(got.CgroupPath.Bytes()) != 0 || len(got.CgroupName.Bytes()) != 0 {
887 + t.Fatalf("decoded unknown apps item = %+v", got)
888 + }
889 +
890 + bad := append([]byte(nil), resp[:total]...)
891 + bad[itemStart+AppsLookupItemHdr+1] = 'x'
892 + if _, err := DecodeAppsLookupResponse(bad); err != ErrMissingNul {
893 + t.Fatalf("decode missing unknown path NUL error = %v, want ErrMissingNul", err)
894 + }
895 +}
896 +
897 +func TestCgroupsLookupUnknownItemCanonicalLayout(t *testing.T) {
898 + path := []byte("/sys/fs/cgroup/bench/cg-001")
899 + var resp [128]byte
900 + builder := NewCgroupsLookupBuilder(resp[:], 1, 9)
901 + if err := builder.Add(CgroupLookupUnknownRetryLater, 0, path, nil, nil); err != nil {
902 + t.Fatalf("add unknown cgroups item: %v", err)
903 + }
904 + total := builder.Finish()
905 + itemStart := cgroupsLookupItemStart(t, resp[:total])
906 + item := resp[itemStart:total]
907 + wantSize := cgroupsLookupUnknownFixedBytes + len(path) + 1
908 +
909 + if got := len(item); got != wantSize {
910 + t.Fatalf("unknown item size = %d, want %d", got, wantSize)
911 + }
912 + if got := ne.Uint32(resp[CgroupsLookupRespHdr+4 : CgroupsLookupRespHdr+8]); got != uint32(wantSize) {
913 + t.Fatalf("directory item length = %d, want %d", got, wantSize)
914 + }
915 + if got := ne.Uint32(item[8:12]); got != CgroupsLookupItemHdr {
916 + t.Fatalf("path offset = %d, want %d", got, CgroupsLookupItemHdr)
917 + }
918 + wantNameOff := CgroupsLookupItemHdr + len(path) + 1
919 + if got := ne.Uint32(item[16:20]); got != uint32(wantNameOff) {
920 + t.Fatalf("name offset = %d, want %d", got, wantNameOff)
921 + }
922 + if item[CgroupsLookupItemHdr+len(path)] != 0 || item[wantNameOff] != 0 {
923 + t.Fatalf("unknown item NUL bytes are not canonical")
924 + }
925 +
926 + view, err := DecodeCgroupsLookupResponse(resp[:total])
927 + if err != nil {
928 + t.Fatalf("decode unknown cgroups item: %v", err)
929 + }
930 + got, err := view.Item(0)
931 + if err != nil {
932 + t.Fatalf("read unknown cgroups item: %v", err)
933 + }
934 + if got.Status != CgroupLookupUnknownRetryLater || got.Path.String() != string(path) ||
935 + len(got.Name.Bytes()) != 0 || got.LabelCount != 0 {
936 + t.Fatalf("decoded unknown cgroups item = %+v", got)
937 + }
938 +
939 + bad := append([]byte(nil), resp[:total]...)
940 + bad[itemStart+wantNameOff] = 'x'
941 + if _, err := DecodeCgroupsLookupResponse(bad); err != ErrMissingNul {
942 + t.Fatalf("decode missing unknown name NUL error = %v, want ErrMissingNul", err)
943 + }
944 +}
945 +
946 +func TestLookupInternalGuardCoverage(t *testing.T) {
947 + if _, ok := checkedU32Int(-1); ok {
948 + t.Fatalf("checkedU32Int(-1) succeeded")
949 + }
950 + if _, ok := checkedU32Int(int(uint64(^uint32(0)) + 1)); ok {
951 + t.Fatalf("checkedU32Int(uint32 max + 1) succeeded")
952 + }
953 + if _, ok := checkedU16Int(-1); ok {
954 + t.Fatalf("checkedU16Int(-1) succeeded")
955 + }
956 + if _, ok := checkedU16Int(int(^uint16(0)) + 1); ok {
957 + t.Fatalf("checkedU16Int(uint16 max + 1) succeeded")
958 + }
959 + if _, ok := checkedInt(uint64(maxIntValue()) + 1); ok {
960 + t.Fatalf("checkedInt(max + 1) succeeded")
961 + }
962 + if _, ok := checkedAddInt(-1, 0); ok {
963 + t.Fatalf("checkedAddInt negative succeeded")
964 + }
965 + if _, ok := checkedAddInt(maxIntValue(), 1); ok {
966 + t.Fatalf("checkedAddInt overflow succeeded")
967 + }
968 + if _, ok := checkedMulInt(-1, 1); ok {
969 + t.Fatalf("checkedMulInt negative succeeded")
970 + }
971 + if _, ok := checkedMulInt(maxIntValue(), 2); ok {
972 + t.Fatalf("checkedMulInt overflow succeeded")
973 + }
974 + if _, ok := checkedAlign8(-1); ok {
975 + t.Fatalf("checkedAlign8 negative succeeded")
976 + }
977 + if _, ok := checkedAlign8(maxIntValue() - 6); ok {
978 + t.Fatalf("checkedAlign8 overflow succeeded")
979 + }
980 + if _, err := lookupPayloadSlice([]byte{0}, -1, 0, 0); err != ErrOutOfBounds {
981 + t.Fatalf("lookupPayloadSlice negative error = %v, want ErrOutOfBounds", err)
982 + }
983 + if _, err := lookupPayloadSlice([]byte{0}, 0, 0, 2); err != ErrOutOfBounds {
984 + t.Fatalf("lookupPayloadSlice too long error = %v, want ErrOutOfBounds", err)
985 + }
986 + if err := validateLookupDir(make([]byte, 8), maxIntValue(), 1, 0, 0, -1); err != ErrBadItemCount {
987 + t.Fatalf("validateLookupDir bad item count error = %v, want ErrBadItemCount", err)
988 + }
989 + if err := validateLookupDir(make([]byte, 0), 0, 1, 0, 0, -1); err != ErrTruncated {
990 + t.Fatalf("validateLookupDir truncated error = %v, want ErrTruncated", err)
991 + }
992 + var dir [16]byte
993 + ne.PutUint32(dir[0:4], 8)
994 + ne.PutUint32(dir[4:8], 8)
995 + ne.PutUint32(dir[8:12], 0)
996 + ne.PutUint32(dir[12:16], 8)
997 + if err := validateLookupDir(dir[:], 0, 2, 32, 1, -1); err != ErrBadLayout {
998 + t.Fatalf("validateLookupDir overlap error = %v, want ErrBadLayout", err)
999 + }
1000 +
1001 + item := []byte{'a', 0, 'b', 0}
1002 + if _, _, err := lookupString(item, 1, 0, 1); err != ErrOutOfBounds {
1003 + t.Fatalf("lookupString below header error = %v, want ErrOutOfBounds", err)
1004 + }
1005 + if _, _, err := lookupString(item, 0, 3, 2); err != ErrOutOfBounds {
1006 + t.Fatalf("lookupString oob error = %v, want ErrOutOfBounds", err)
1007 + }
1008 + if _, _, err := lookupString(item, 0, 0, 2); err != ErrMissingNul {
1009 + t.Fatalf("lookupString missing nul error = %v, want ErrMissingNul", err)
1010 + }
1011 + if _, _, err := lookupString([]byte{'a', 0, 0}, 0, 0, 2); err != ErrBadLayout {
1012 + t.Fatalf("lookupString interior nul error = %v, want ErrBadLayout", err)
1013 + }
1014 +
1015 + if _, err := lookupLabelAt(make([]byte, LookupLabelEntrySize), 0, 1, -1, 0); err != ErrOutOfBounds {
1016 + t.Fatalf("lookupLabelAt bad table offset error = %v, want ErrOutOfBounds", err)
1017 + }
1018 +
1019 + labelItem := make([]byte, 52)
1020 + ne.PutUint32(labelItem[32:36], 48)
1021 + ne.PutUint32(labelItem[36:40], 1)
1022 + ne.PutUint32(labelItem[40:44], 50)
1023 + ne.PutUint32(labelItem[44:48], 1)
1024 + labelItem[48] = 'k'
1025 + labelItem[49] = 0
1026 + labelItem[50] = 'v'
1027 + labelItem[51] = 0
1028 + if _, err := validateLabels(labelItem, 28, 0, 28); err != ErrBadLayout {
1029 + t.Fatalf("validateLabels zero count wrong fixedEnd error = %v, want ErrBadLayout", err)
1030 + }
1031 + if _, err := validateLabels(labelItem, 28, 1, maxIntValue()-6); err != ErrOutOfBounds {
1032 + t.Fatalf("validateLabels align overflow error = %v, want ErrOutOfBounds", err)
1033 + }
1034 + if _, err := validateLabels(labelItem, 28, 1, 60); err != ErrOutOfBounds {
1035 + t.Fatalf("validateLabels table after end error = %v, want ErrOutOfBounds", err)
1036 + }
1037 + badPadding := append([]byte(nil), labelItem...)
1038 + badPadding[28] = 1
1039 + if _, err := validateLabels(badPadding, 28, 1, 28); err != ErrBadLayout {
1040 + t.Fatalf("validateLabels bad padding error = %v, want ErrBadLayout", err)
1041 + }
1042 + if _, err := validateLabels(labelItem[:40], 28, 1, 28); err != ErrOutOfBounds {
1043 + t.Fatalf("validateLabels table oob error = %v, want ErrOutOfBounds", err)
1044 + }
1045 + badKeyLen := append([]byte(nil), labelItem...)
1046 + ne.PutUint32(badKeyLen[36:40], 0)
1047 + if _, err := validateLabels(badKeyLen, 28, 1, 28); err != ErrBadLayout {
1048 + t.Fatalf("validateLabels bad key length error = %v, want ErrBadLayout", err)
1049 + }
1050 + badKeyOff := append([]byte(nil), labelItem...)
1051 + ne.PutUint32(badKeyOff[32:36], 49)
1052 + if _, err := validateLabels(badKeyOff, 28, 1, 28); err != ErrBadLayout {
1053 + t.Fatalf("validateLabels bad key offset error = %v, want ErrBadLayout", err)
1054 + }
1055 + badValueOff := append([]byte(nil), labelItem...)
1056 + ne.PutUint32(badValueOff[40:44], 49)
1057 + if _, err := validateLabels(badValueOff, 28, 1, 28); err != ErrBadLayout {
1058 + t.Fatalf("validateLabels bad value offset error = %v, want ErrBadLayout", err)
1059 + }
1060 + badValueNul := append([]byte(nil), labelItem...)
1061 + badValueNul[51] = '!'
1062 + if _, err := validateLabels(badValueNul, 28, 1, 28); err != ErrMissingNul {
1063 + t.Fatalf("validateLabels bad value nul error = %v, want ErrMissingNul", err)
1064 + }
1065 + extra := append([]byte(nil), labelItem...)
1066 + extra = append(extra, 0)
1067 + if _, err := validateLabels(extra, 28, 1, 28); err != ErrBadLayout {
1068 + t.Fatalf("validateLabels extra byte error = %v, want ErrBadLayout", err)
1069 + }
1070 +
1071 + var compact [80]byte
1072 + ne.PutUint32(compact[16:20], 40)
1073 + copy(compact[40:44], []byte{1, 2, 3, 4})
1074 + if n := finishLookupResponse(compact[:], 16, 1, 44, 7); n != 28 {
1075 + t.Fatalf("finishLookupResponse compact = %d, want 28", n)
1076 + }
1077 + var badFinish [80]byte
1078 + ne.PutUint32(badFinish[16:20], 40)
1079 + ne.PutUint32(badFinish[24:28], 32)
1080 + if n := finishLookupResponse(badFinish[:], 16, 2, 48, 7); n != 0 {
1081 + t.Fatalf("finishLookupResponse bad second offset = %d, want 0", n)
1082 + }
1083 + if n := finishLookupResponse(badFinish[:], maxIntValue(), 1, 0, 7); n != 0 {
1084 + t.Fatalf("finishLookupResponse header overflow = %d, want 0", n)
1085 + }
1086 +}
1087 +
1088 +func TestLookupDecodeAndItemErrorCoverage(t *testing.T) {
1089 + if _, err := DecodeCgroupsLookupResponse(make([]byte, CgroupsLookupRespHdr-1)); err != ErrTruncated {
1090 + t.Fatalf("short cgroups response error = %v, want ErrTruncated", err)
1091 + }
1092 + cgResp := make([]byte, CgroupsLookupRespHdr+LookupDirEntrySize)
1093 + ne.PutUint16(cgResp[0:2], 1)
1094 + ne.PutUint32(cgResp[4:8], 1)
1095 + ne.PutUint32(cgResp[CgroupsLookupRespHdr+4:CgroupsLookupRespHdr+8], CgroupsLookupItemHdr)
1096 + if _, err := DecodeCgroupsLookupResponse(cgResp[:CgroupsLookupRespHdr]); err != ErrTruncated {
1097 + t.Fatalf("cgroups response truncated dir error = %v, want ErrTruncated", err)
1098 + }
1099 + if _, err := DecodeCgroupsLookupResponse(cgResp); err != ErrOutOfBounds {
1100 + t.Fatalf("cgroups response missing item error = %v, want ErrOutOfBounds", err)
1101 + }
1102 + cgShortItem := append([]byte(nil), cgResp...)
1103 + ne.PutUint32(cgShortItem[CgroupsLookupRespHdr+4:CgroupsLookupRespHdr+8], CgroupsLookupItemHdr-1)
1104 + cgShortItem = append(cgShortItem, make([]byte, CgroupsLookupItemHdr-1)...)
1105 + if _, err := DecodeCgroupsLookupResponse(cgShortItem); err != ErrBadLayout {
1106 + t.Fatalf("cgroups response short item error = %v, want ErrBadLayout", err)
1107 + }
1108 + if _, err := (&CgroupsLookupResponseView{ItemCount: 1, payload: cgResp}).Item(0); err != ErrOutOfBounds {
1109 + t.Fatalf("manual cgroups item error = %v, want ErrOutOfBounds", err)
1110 + }
1111 +
1112 + cgItem := validCgroupsLookupItemBytes(t)
1113 + for _, tc := range []struct {
1114 + name string
1115 + edit func([]byte)
1116 + want error
1117 + }{
1118 + {
1119 + name: "short",
1120 + edit: func(b []byte) {
1121 + b = b[:CgroupsLookupItemHdr-1]
1122 + copy(cgItem, b)
1123 + },
1124 + want: ErrTruncated,
1125 + },
1126 + {
1127 + name: "bad reserved",
1128 + edit: func(b []byte) {
1129 + ne.PutUint16(b[6:8], 1)
1130 + },
1131 + want: ErrBadLayout,
1132 + },
1133 + {
1134 + name: "path below header",
1135 + edit: func(b []byte) {
1136 + ne.PutUint32(b[8:12], 0)
1137 + },
1138 + want: ErrOutOfBounds,
1139 + },
1140 + {
1141 + name: "name below header",
1142 + edit: func(b []byte) {
1143 + ne.PutUint32(b[16:20], 0)
1144 + },
1145 + want: ErrOutOfBounds,
1146 + },
1147 + {
1148 + name: "label table missing",
1149 + edit: func(b []byte) {
1150 + ne.PutUint16(b[24:26], 2)
1151 + },
1152 + want: ErrOutOfBounds,
1153 + },
1154 + } {
1155 + item := validCgroupsLookupItemBytes(t)
1156 + if tc.name == "short" {
1157 + if _, err := decodeCgroupsLookupItem(item[:CgroupsLookupItemHdr-1]); err != tc.want {
1158 + t.Fatalf("decode cgroups item %s error = %v, want %v", tc.name, err, tc.want)
1159 + }
1160 + continue
1161 + }
1162 + tc.edit(item)
1163 + if _, err := decodeCgroupsLookupItem(item); err != tc.want {
1164 + t.Fatalf("decode cgroups item %s error = %v, want %v", tc.name, err, tc.want)
1165 + }
1166 + }
1167 +
1168 + if _, err := DecodeAppsLookupResponse(make([]byte, AppsLookupRespHdr-1)); err != ErrTruncated {
1169 + t.Fatalf("short apps response error = %v, want ErrTruncated", err)
1170 + }
1171 + appsResp := make([]byte, AppsLookupRespHdr+LookupDirEntrySize)
1172 + ne.PutUint16(appsResp[0:2], 1)
1173 + ne.PutUint32(appsResp[4:8], 1)
1174 + ne.PutUint32(appsResp[AppsLookupRespHdr+4:AppsLookupRespHdr+8], AppsLookupItemHdr)
1175 + if _, err := DecodeAppsLookupResponse(appsResp[:AppsLookupRespHdr]); err != ErrTruncated {
1176 + t.Fatalf("apps response truncated dir error = %v, want ErrTruncated", err)
1177 + }
1178 + if _, err := DecodeAppsLookupResponse(appsResp); err != ErrOutOfBounds {
1179 + t.Fatalf("apps response missing item error = %v, want ErrOutOfBounds", err)
1180 + }
1181 + appsShortItem := append([]byte(nil), appsResp...)
1182 + ne.PutUint32(appsShortItem[AppsLookupRespHdr+4:AppsLookupRespHdr+8], AppsLookupItemHdr-1)
1183 + appsShortItem = append(appsShortItem, make([]byte, AppsLookupItemHdr-1)...)
1184 + if _, err := DecodeAppsLookupResponse(appsShortItem); err != ErrBadLayout {
1185 + t.Fatalf("apps response short item error = %v, want ErrBadLayout", err)
1186 + }
1187 + if _, err := (&AppsLookupResponseView{ItemCount: 1, payload: appsResp}).Item(0); err != ErrOutOfBounds {
1188 + t.Fatalf("manual apps item error = %v, want ErrOutOfBounds", err)
1189 + }
1190 +
1191 + for _, tc := range []struct {
1192 + name string
1193 + edit func([]byte)
1194 + want error
1195 + }{
1196 + {
1197 + name: "short",
1198 + edit: nil,
1199 + want: ErrTruncated,
1200 + },
1201 + {
1202 + name: "bad reserved",
1203 + edit: func(b []byte) {
1204 + ne.PutUint32(b[20:24], 1)
1205 + },
1206 + want: ErrBadLayout,
1207 + },
1208 + {
1209 + name: "comm below header",
1210 + edit: func(b []byte) {
1211 + ne.PutUint32(b[32:36], 0)
1212 + },
1213 + want: ErrOutOfBounds,
1214 + },
1215 + {
1216 + name: "path below header",
1217 + edit: func(b []byte) {
1218 + ne.PutUint32(b[40:44], 0)
1219 + },
1220 + want: ErrOutOfBounds,
1221 + },
1222 + {
1223 + name: "name below header",
1224 + edit: func(b []byte) {
1225 + ne.PutUint32(b[48:52], 0)
1226 + },
1227 + want: ErrOutOfBounds,
1228 + },
1229 + {
1230 + name: "retry with orchestrator",
1231 + edit: func(b []byte) {
1232 + ne.PutUint16(b[6:8], AppsCgroupUnknownRetryLater)
1233 + },
1234 + want: ErrBadLayout,
1235 + },
1236 + {
1237 + name: "permanent missing path",
1238 + edit: func(b []byte) {
1239 + ne.PutUint16(b[4:6], 0)
1240 + ne.PutUint16(b[6:8], AppsCgroupUnknownPermanent)
1241 + ne.PutUint32(b[44:48], 0)
1242 + ne.PutUint32(b[52:56], 0)
1243 + ne.PutUint16(b[56:58], 0)
1244 + },
1245 + want: ErrBadLayout,
1246 + },
1247 + {
1248 + name: "label table missing",
1249 + edit: func(b []byte) {
1250 + ne.PutUint16(b[56:58], 2)
1251 + },
1252 + want: ErrOutOfBounds,
1253 + },
1254 + } {
1255 + item := validAppsLookupItemBytes(t)
1256 + if tc.name == "short" {
1257 + if _, err := decodeAppsLookupItem(item[:AppsLookupItemHdr-1]); err != tc.want {
1258 + t.Fatalf("decode apps item %s error = %v, want %v", tc.name, err, tc.want)
1259 + }
1260 + continue
1261 + }
1262 + tc.edit(item)
1263 + if _, err := decodeAppsLookupItem(item); err != tc.want {
1264 + t.Fatalf("decode apps item %s error = %v, want %v", tc.name, err, tc.want)
1265 + }
1266 + }
1267 + _ = cgItem
1268 +}
1269 +
1270 +func TestLookupResponseDecodeValidationCoverage(t *testing.T) {
1271 + var resp [1024]byte
1272 +
1273 + cg := NewCgroupsLookupBuilder(resp[:], 1, 0)
1274 + if err := cg.Add(CgroupLookupKnown, OrchestratorDocker, []byte("/x"), []byte("name"), nil); err != nil {
1275 + t.Fatalf("add cgroups response: %v", err)
1276 + }
1277 + cgTotal := cg.Finish()
1278 + cgItem := cgroupsLookupItemStart(t, resp[:])
1279 +
1280 + for _, tc := range []struct {
1281 + name string
1282 + edit func([]byte)
1283 + want error
1284 + }{
1285 + {
1286 + name: "bad flags",
1287 + edit: func(b []byte) {
1288 + ne.PutUint16(b[2:4], 1)
1289 + },
1290 + want: ErrBadLayout,
1291 + },
1292 + {
1293 + name: "bad item layout",
1294 + edit: func(b []byte) {
1295 + ne.PutUint16(b[cgItem:cgItem+2], 2)
1296 + },
1297 + want: ErrBadLayout,
1298 + },
1299 + {
1300 + name: "empty path",
1301 + edit: func(b []byte) {
1302 + ne.PutUint32(b[cgItem+12:cgItem+16], 0)
1303 + },
1304 + want: ErrBadLayout,
1305 + },
1306 + {
1307 + name: "unknown with metadata",
1308 + edit: func(b []byte) {
1309 + ne.PutUint16(b[cgItem+2:cgItem+4], CgroupLookupUnknownRetryLater)
1310 + },
1311 + want: ErrBadLayout,
1312 + },
1313 + {
1314 + name: "overlap",
1315 + edit: func(b []byte) {
1316 + ne.PutUint32(b[cgItem+16:cgItem+20], uint32(CgroupsLookupItemHdr+1))
1317 + ne.PutUint32(b[cgItem+20:cgItem+24], 1)
1318 + },
1319 + want: ErrBadLayout,
1320 + },
1321 + } {
1322 + bad := append([]byte(nil), resp[:cgTotal]...)
1323 + tc.edit(bad)
1324 + if _, err := DecodeCgroupsLookupResponse(bad); err != tc.want {
1325 + t.Fatalf("decode cgroups response %s error = %v, want %v", tc.name, err, tc.want)
1326 + }
1327 + }
1328 +
1329 + apps := NewAppsLookupBuilder(resp[:], 1, 0)
1330 + if err := apps.Add(
1331 + PidLookupKnown,
1332 + AppsCgroupKnown,
1333 + OrchestratorDocker,
1334 + 1234, 1, 1000, 42,
1335 + []byte("nginx"),
1336 + []byte("/docker/abc"),
1337 + []byte("container-a"),
1338 + nil,
1339 + ); err != nil {
1340 + t.Fatalf("add apps response: %v", err)
1341 + }
1342 + appsTotal := apps.Finish()
1343 + appsItem := appsLookupItemStart(t, resp[:])
1344 +
1345 + for _, tc := range []struct {
1346 + name string
1347 + edit func([]byte)
1348 + want error
1349 + }{
1350 + {
1351 + name: "bad flags",
1352 + edit: func(b []byte) {
1353 + ne.PutUint16(b[2:4], 1)
1354 + },
1355 + want: ErrBadLayout,
1356 + },
1357 + {
1358 + name: "bad item layout",
1359 + edit: func(b []byte) {
1360 + ne.PutUint16(b[appsItem:appsItem+2], 2)
1361 + },
1362 + want: ErrBadLayout,
1363 + },
1364 + {
1365 + name: "bad status",
1366 + edit: func(b []byte) {
1367 + ne.PutUint16(b[appsItem+2:appsItem+4], 99)
1368 + },
1369 + want: ErrBadLayout,
1370 + },
1371 + {
1372 + name: "bad cgroup status",
1373 + edit: func(b []byte) {
1374 + ne.PutUint16(b[appsItem+6:appsItem+8], 99)
1375 + },
1376 + want: ErrBadLayout,
1377 + },
1378 + {
1379 + name: "comm too long",
1380 + edit: func(b []byte) {
1381 + ne.PutUint32(b[appsItem+36:appsItem+40], 16)
1382 + },
1383 + want: ErrBadLayout,
1384 + },
1385 + {
1386 + name: "unknown with data",
1387 + edit: func(b []byte) {
1388 + ne.PutUint16(b[appsItem+2:appsItem+4], PidLookupUnknown)
1389 + },
1390 + want: ErrBadLayout,
1391 + },
1392 + {
1393 + name: "known empty comm",
1394 + edit: func(b []byte) {
1395 + ne.PutUint32(b[appsItem+36:appsItem+40], 0)
1396 + },
1397 + want: ErrBadLayout,
1398 + },
1399 + {
1400 + name: "known empty path",
1401 + edit: func(b []byte) {
1402 + ne.PutUint32(b[appsItem+44:appsItem+48], 0)
1403 + },
1404 + want: ErrBadLayout,
1405 + },
1406 + {
1407 + name: "host root with metadata",
1408 + edit: func(b []byte) {
1409 + ne.PutUint16(b[appsItem+6:appsItem+8], AppsCgroupHostRoot)
1410 + },
1411 + want: ErrBadLayout,
1412 + },
1413 + {
1414 + name: "overlap",
1415 + edit: func(b []byte) {
1416 + ne.PutUint32(b[appsItem+40:appsItem+44], uint32(AppsLookupItemHdr+1))
1417 + ne.PutUint32(b[appsItem+44:appsItem+48], 4)
1418 + },
1419 + want: ErrBadLayout,
1420 + },
1421 + } {
1422 + bad := append([]byte(nil), resp[:appsTotal]...)
1423 + tc.edit(bad)
1424 + if _, err := DecodeAppsLookupResponse(bad); err != tc.want {
1425 + t.Fatalf("decode apps response %s error = %v, want %v", tc.name, err, tc.want)
1426 + }
1427 + }
1428 +}
1429 +
1430 +func TestLookupDispatchCoverage(t *testing.T) {
1431 + var req [128]byte
1432 + var resp [512]byte
1433 +
1434 + reqLen, err := EncodeCgroupsLookupRequest([][]byte{[]byte("/x"), []byte("/y")}, req[:])
1435 + if err != nil {
1436 + t.Fatalf("encode cgroups dispatch request: %v", err)
1437 + }
1438 + n, err := DispatchCgroupsLookup(req[:reqLen], resp[:], func(request *CgroupsLookupRequestView, builder *CgroupsLookupBuilder) bool {
1439 + for i := uint32(0); i < request.ItemCount; i++ {
1440 + path, err := request.Item(i)
1441 + if err != nil {
1442 + return false
1443 + }
1444 + if err := builder.Add(CgroupLookupKnown, OrchestratorDocker, path.Bytes(), []byte("name"), nil); err != nil {
1445 + return false
1446 + }
1447 + }
1448 + return true
1449 + })
1450 + if err != nil || n == 0 {
1451 + t.Fatalf("dispatch cgroups success = n %d err %v", n, err)
1452 + }
1453 + if _, err := DecodeCgroupsLookupResponse(resp[:n]); err != nil {
1454 + t.Fatalf("decode dispatched cgroups response: %v", err)
1455 + }
1456 + n, err = DispatchCgroupsLookup(req[:reqLen], resp[:], func(*CgroupsLookupRequestView, *CgroupsLookupBuilder) bool {
1457 + return false
1458 + })
1459 + if err != ErrBadLayout || n != 0 {
1460 + t.Fatalf("dispatch cgroups handler false = n %d err %v", n, err)
1461 + }
1462 + n, err = DispatchCgroupsLookup(req[:reqLen], resp[:], func(*CgroupsLookupRequestView, *CgroupsLookupBuilder) bool {
1463 + return true
1464 + })
1465 + if err != ErrBadItemCount || n != 0 {
1466 + t.Fatalf("dispatch cgroups bad count = n %d err %v", n, err)
1467 + }
1468 + n, err = DispatchCgroupsLookup(req[:reqLen], resp[:], func(_ *CgroupsLookupRequestView, builder *CgroupsLookupBuilder) bool {
1469 + _ = builder.Add(99, 0, []byte("/x"), nil, nil)
1470 + return false
1471 + })
1472 + if err != ErrBadLayout || n != 0 {
1473 + t.Fatalf("dispatch cgroups builder error = n %d err %v", n, err)
1474 + }
1475 + n, err = DispatchCgroupsLookup(req[:reqLen], resp[:], func(_ *CgroupsLookupRequestView, builder *CgroupsLookupBuilder) bool {
1476 + _ = builder.Add(99, 0, []byte("/x"), nil, nil)
1477 + return true
1478 + })
1479 + if err != ErrBadLayout || n != 0 {
1480 + t.Fatalf("dispatch cgroups post-handler builder error = n %d err %v", n, err)
1481 + }
1482 + n, err = DispatchCgroupsLookup(req[:reqLen], resp[:], func(request *CgroupsLookupRequestView, builder *CgroupsLookupBuilder) bool {
1483 + builder.itemCount = request.ItemCount
1484 + builder.dataOffset = 0
1485 + ne.PutUint32(builder.buf[CgroupsLookupRespHdr:CgroupsLookupRespHdr+4], 8)
1486 + return true
1487 + })
1488 + if err != ErrOverflow || n != 0 {
1489 + t.Fatalf("dispatch cgroups finish error = n %d err %v", n, err)
1490 + }
1491 + if n, err := DispatchCgroupsLookup(req[:CgroupsLookupReqHdr-1], resp[:], nil); err != ErrTruncated || n != 0 {
1492 + t.Fatalf("dispatch cgroups bad request = n %d err %v", n, err)
1493 + }
1494 +
1495 + reqLen, err = EncodeAppsLookupRequest([]uint32{1, 2}, req[:])
1496 + if err != nil {
1497 + t.Fatalf("encode apps dispatch request: %v", err)
1498 + }
1499 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(request *AppsLookupRequestView, builder *AppsLookupBuilder) bool {
1500 + for i := uint32(0); i < request.ItemCount; i++ {
1501 + pid, err := request.Item(i)
1502 + if err != nil {
1503 + return false
1504 + }
1505 + if err := builder.Add(PidLookupKnown, AppsCgroupHostRoot, 0, pid, 0, 0, 0, []byte("proc"), nil, nil, nil); err != nil {
1506 + return false
1507 + }
1508 + }
1509 + return true
1510 + })
1511 + if err != nil || n == 0 {
1512 + t.Fatalf("dispatch apps success = n %d err %v", n, err)
1513 + }
1514 + if _, err := DecodeAppsLookupResponse(resp[:n]); err != nil {
1515 + t.Fatalf("decode dispatched apps response: %v", err)
1516 + }
1517 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(*AppsLookupRequestView, *AppsLookupBuilder) bool {
1518 + return false
1519 + })
1520 + if err != ErrBadLayout || n != 0 {
1521 + t.Fatalf("dispatch apps handler false = n %d err %v", n, err)
1522 + }
1523 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(*AppsLookupRequestView, *AppsLookupBuilder) bool {
1524 + return true
1525 + })
1526 + if err != ErrBadItemCount || n != 0 {
1527 + t.Fatalf("dispatch apps bad count = n %d err %v", n, err)
1528 + }
1529 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(_ *AppsLookupRequestView, builder *AppsLookupBuilder) bool {
1530 + _ = builder.Add(99, 0, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil)
1531 + return false
1532 + })
1533 + if err != ErrBadLayout || n != 0 {
1534 + t.Fatalf("dispatch apps builder error = n %d err %v", n, err)
1535 + }
1536 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(_ *AppsLookupRequestView, builder *AppsLookupBuilder) bool {
1537 + _ = builder.Add(99, 0, 0, 1, 0, 0, 0, []byte("x"), nil, nil, nil)
1538 + return true
1539 + })
1540 + if err != ErrBadLayout || n != 0 {
1541 + t.Fatalf("dispatch apps post-handler builder error = n %d err %v", n, err)
1542 + }
1543 + n, err = DispatchAppsLookup(req[:reqLen], resp[:], func(request *AppsLookupRequestView, builder *AppsLookupBuilder) bool {
1544 + builder.itemCount = request.ItemCount
1545 + builder.dataOffset = 0
1546 + ne.PutUint32(builder.buf[AppsLookupRespHdr:AppsLookupRespHdr+4], 8)
1547 + return true
1548 + })
1549 + if err != ErrOverflow || n != 0 {
1550 + t.Fatalf("dispatch apps finish error = n %d err %v", n, err)
1551 + }
1552 + if n, err := DispatchAppsLookup(req[:AppsLookupReqHdr-1], resp[:], nil); err != ErrTruncated || n != 0 {
1553 + t.Fatalf("dispatch apps bad request = n %d err %v", n, err)
1554 + }
1555 +}
src/go/pkg/netipc/protocol/string_reverse.go
+23 -7
@@ -21,8 +21,12 @@ func StringReverseEncode(s string, buf []byte) int {
21 if len(buf) < total {
22 return 0
23 }
24 + strLen, ok := checkedU32Int(len(s))
25 + if !ok {
26 + return 0
27 + }
28 ne.PutUint32(buf[0:4], uint32(StringReverseHdrSize)) // str_offset
25 - ne.PutUint32(buf[4:8], uint32(len(s))) // str_length
29 + ne.PutUint32(buf[4:8], strLen) // str_length
30 if len(s) > 0 {
31 copy(buf[8:8+len(s)], s)
32 }
@@ -35,17 +39,29 @@ func StringReverseDecode(buf []byte) (StringReverseView, error) {
39 if len(buf) < StringReverseHdrSize {
40 return StringReverseView{}, ErrTruncated
41 }
38 - strOffset := int(ne.Uint32(buf[0:4]))
39 - strLength := int(ne.Uint32(buf[4:8]))
40 - if strOffset+strLength+1 > len(buf) {
42 + strOffset, err := checkedWireU32Int(buf, 0)
43 + if err != nil {
44 + return StringReverseView{}, err
45 + }
46 + strLength, err := checkedWireU32Int(buf, 4)
47 + if err != nil {
48 + return StringReverseView{}, err
49 + }
50 + strLength32 := ne.Uint32(buf[4:8])
51 + strEnd, ok := checkedAddInt(strOffset, strLength)
52 + if !ok {
53 + return StringReverseView{}, ErrOutOfBounds
54 + }
55 + strNulEnd, ok := checkedAddInt(strEnd, 1)
56 + if !ok || strNulEnd > len(buf) {
57 return StringReverseView{}, ErrOutOfBounds
58 }
43 - if buf[strOffset+strLength] != 0 {
59 + if buf[strEnd] != 0 {
60 return StringReverseView{}, ErrMissingNul
61 }
62 return StringReverseView{
47 - Str: string(buf[strOffset : strOffset+strLength]),
48 - StrLen: uint32(strLength),
63 + Str: string(buf[strOffset:strEnd]),
64 + StrLen: strLength32,
65 }, nil
66 }
67
src/go/pkg/netipc/service/apps_lookup/client.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build unix
2 +
3 +package apps_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) posix.ClientConfig {
11 + return transportconfig.PosixClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) posix.ServerConfig {
15 + return transportconfig.PosixServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/apps_lookup/client_common.go new
+39
@@ -0,0 +1,39 @@
1 +package apps_lookup
2 +
3 +import (
4 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
5 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
6 +)
7 +
8 +type Client struct {
9 + inner *raw.Client
10 +}
11 +
12 +func NewClient(runDir, serviceName string, config ClientConfig) *Client {
13 + return &Client{inner: raw.NewAppsLookupClient(runDir, serviceName, clientConfigToTransport(config))}
14 +}
15 +
16 +func (c *Client) Refresh() bool { return c.inner.Refresh() }
17 +func (c *Client) Ready() bool { return c.inner.Ready() }
18 +func (c *Client) Status() ClientStatus {
19 + return c.inner.Status()
20 +}
21 +func (c *Client) Call(pids []uint32) (*protocol.AppsLookupResponseView, error) {
22 + return c.inner.CallAppsLookup(pids)
23 +}
24 +func (c *Client) Close() { c.inner.Close() }
25 +
26 +type Server struct {
27 + inner *raw.Server
28 +}
29 +
30 +func NewServer(runDir, serviceName string, config ServerConfig, handler Handler) *Server {
31 + return &Server{inner: raw.NewServer(runDir, serviceName, serverConfigToTransport(config), protocol.MethodAppsLookup, raw.AppsLookupDispatch(handler.Handle))}
32 +}
33 +
34 +func NewServerWithWorkers(runDir, serviceName string, config ServerConfig, handler Handler, workerCount int) *Server {
35 + return &Server{inner: raw.NewServerWithWorkers(runDir, serviceName, serverConfigToTransport(config), protocol.MethodAppsLookup, raw.AppsLookupDispatch(handler.Handle), workerCount)}
36 +}
37 +
38 +func (s *Server) Run() error { return s.inner.Run() }
39 +func (s *Server) Stop() { s.inner.Stop() }
src/go/pkg/netipc/service/apps_lookup/client_windows.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build windows
2 +
3 +package apps_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) windows.ClientConfig {
11 + return transportconfig.WindowsClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) windows.ServerConfig {
15 + return transportconfig.WindowsServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/apps_lookup/types.go new
+33
@@ -0,0 +1,33 @@
1 +// Package apps_lookup provides the public single-kind L2 surface for the
2 +// apps-lookup service.
3 +package apps_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
8 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
9 +)
10 +
11 +type ClientState = raw.ClientState
12 +
13 +const (
14 + StateDisconnected = raw.StateDisconnected
15 + StateConnecting = raw.StateConnecting
16 + StateReady = raw.StateReady
17 + StateNotFound = raw.StateNotFound
18 + StateAuthFailed = raw.StateAuthFailed
19 + StateIncompatible = raw.StateIncompatible
20 + StateBroken = raw.StateBroken
21 +)
22 +
23 +type ClientStatus = raw.ClientStatus
24 +
25 +type ClientConfig transportconfig.TypedConfig
26 +
27 +type ServerConfig transportconfig.TypedConfig
28 +
29 +type HandlerFunc = func(*protocol.AppsLookupRequestView, *protocol.AppsLookupBuilder) bool
30 +
31 +type Handler struct {
32 + Handle HandlerFunc
33 +}
src/go/pkg/netipc/service/cgroups/cache_windows.go deleted
-42
@@ -1,42 +0,0 @@
1 -//go:build windows
2 -
3 -package cgroups
4 -
5 -import (
6 - raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
7 -)
8 -
9 -// Cache is the public L3 client-side cgroups snapshot cache.
10 -type Cache struct {
11 - inner *raw.Cache
12 -}
13 -
14 -// NewCache creates a new L3 cache. Does NOT connect.
15 -func NewCache(runDir, serviceName string, config ClientConfig) *Cache {
16 - return &Cache{inner: raw.NewCache(runDir, serviceName, clientConfigToTransport(config))}
17 -}
18 -
19 -// Refresh drives the L2 client and requests a fresh snapshot.
20 -func (c *Cache) Refresh() bool {
21 - return c.inner.Refresh()
22 -}
23 -
24 -// Ready returns true if at least one successful refresh has occurred.
25 -func (c *Cache) Ready() bool {
26 - return c.inner.Ready()
27 -}
28 -
29 -// Lookup finds a cached item by hash + name. O(1), no I/O.
30 -func (c *Cache) Lookup(hash uint32, name string) (CacheItem, bool) {
31 - return c.inner.Lookup(hash, name)
32 -}
33 -
34 -// Status returns a diagnostic snapshot for the L3 cache.
35 -func (c *Cache) Status() CacheStatus {
36 - return c.inner.Status()
37 -}
38 -
39 -// Close frees all cached items and closes the L2 client.
40 -func (c *Cache) Close() {
41 - c.inner.Close()
42 -}
src/go/pkg/netipc/service/cgroups/client_windows.go deleted
-113
@@ -1,113 +0,0 @@
1 -//go:build windows
2 -
3 -package cgroups
4 -
5 -import (
6 - "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 - raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
8 - windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
9 -)
10 -
11 -func snapshotDispatch(handler Handler) raw.DispatchHandler {
12 - return raw.SnapshotDispatch(handler.Handle, handler.SnapshotMaxItems)
13 -}
14 -
15 -func clientConfigToTransport(config ClientConfig) windows.ClientConfig {
16 - return windows.ClientConfig{
17 - SupportedProfiles: config.SupportedProfiles,
18 - PreferredProfiles: config.PreferredProfiles,
19 - MaxRequestBatchItems: config.MaxRequestBatchItems,
20 - MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
21 - MaxResponseBatchItems: config.MaxRequestBatchItems,
22 - AuthToken: config.AuthToken,
23 - }
24 -}
25 -
26 -func serverConfigToTransport(config ServerConfig) windows.ServerConfig {
27 - return windows.ServerConfig{
28 - SupportedProfiles: config.SupportedProfiles,
29 - PreferredProfiles: config.PreferredProfiles,
30 - MaxRequestBatchItems: config.MaxRequestBatchItems,
31 - MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
32 - MaxResponseBatchItems: config.MaxRequestBatchItems,
33 - AuthToken: config.AuthToken,
34 - }
35 -}
36 -
37 -// Client is the public L2 client context for the cgroups-snapshot service.
38 -type Client struct {
39 - inner *raw.Client
40 -}
41 -
42 -// NewClient creates a new client context. Does NOT connect.
43 -func NewClient(runDir, serviceName string, config ClientConfig) *Client {
44 - return &Client{inner: raw.NewSnapshotClient(runDir, serviceName, clientConfigToTransport(config))}
45 -}
46 -
47 -// Refresh attempts connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
48 -func (c *Client) Refresh() bool {
49 - return c.inner.Refresh()
50 -}
51 -
52 -// Ready returns true only if the client is in the READY state.
53 -func (c *Client) Ready() bool {
54 - return c.inner.Ready()
55 -}
56 -
57 -// Status returns a diagnostic counters snapshot.
58 -func (c *Client) Status() ClientStatus {
59 - return c.inner.Status()
60 -}
61 -
62 -// CallSnapshot performs a blocking typed cgroups snapshot call.
63 -func (c *Client) CallSnapshot() (*protocol.CgroupsResponseView, error) {
64 - return c.inner.CallSnapshot()
65 -}
66 -
67 -// Close tears down the connection and releases resources.
68 -func (c *Client) Close() {
69 - c.inner.Close()
70 -}
71 -
72 -// Server is the public managed server for the cgroups-snapshot service kind.
73 -type Server struct {
74 - inner *raw.Server
75 -}
76 -
77 -// NewServer creates a new managed server.
78 -func NewServer(runDir, serviceName string, config ServerConfig, handler Handler) *Server {
79 - return &Server{
80 - inner: raw.NewServer(
81 - runDir,
82 - serviceName,
83 - serverConfigToTransport(config),
84 - protocol.MethodCgroupsSnapshot,
85 - snapshotDispatch(handler),
86 - ),
87 - }
88 -}
89 -
90 -// NewServerWithWorkers creates a server with an explicit worker count limit.
91 -func NewServerWithWorkers(runDir, serviceName string, config ServerConfig,
92 - handler Handler, workerCount int) *Server {
93 - return &Server{
94 - inner: raw.NewServerWithWorkers(
95 - runDir,
96 - serviceName,
97 - serverConfigToTransport(config),
98 - protocol.MethodCgroupsSnapshot,
99 - snapshotDispatch(handler),
100 - workerCount,
101 - ),
102 - }
103 -}
104 -
105 -// Run starts the acceptor loop. Blocking.
106 -func (s *Server) Run() error {
107 - return s.inner.Run()
108 -}
109 -
110 -// Stop signals the server to stop.
111 -func (s *Server) Stop() {
112 - s.inner.Stop()
113 -}
src/go/pkg/netipc/service/cgroups/compat.go new
+35
@@ -0,0 +1,35 @@
1 +// Package cgroups preserves the historical import path for the
2 +// cgroups-snapshot service.
3 +//
4 +// New code should prefer package cgroups_snapshot.
5 +package cgroups
6 +
7 +import snapshot "github.com/netdata/netdata/go/plugins/pkg/netipc/service/cgroups_snapshot"
8 +
9 +type ClientState = snapshot.ClientState
10 +
11 +const (
12 + StateDisconnected = snapshot.StateDisconnected
13 + StateConnecting = snapshot.StateConnecting
14 + StateReady = snapshot.StateReady
15 + StateNotFound = snapshot.StateNotFound
16 + StateAuthFailed = snapshot.StateAuthFailed
17 + StateIncompatible = snapshot.StateIncompatible
18 + StateBroken = snapshot.StateBroken
19 +)
20 +
21 +type ClientStatus = snapshot.ClientStatus
22 +type ClientConfig = snapshot.ClientConfig
23 +type ServerConfig = snapshot.ServerConfig
24 +type SnapshotHandler = snapshot.SnapshotHandler
25 +type Handler = snapshot.Handler
26 +type Client = snapshot.Client
27 +type Server = snapshot.Server
28 +type Cache = snapshot.Cache
29 +type CacheItem = snapshot.CacheItem
30 +type CacheStatus = snapshot.CacheStatus
31 +
32 +var NewClient = snapshot.NewClient
33 +var NewServer = snapshot.NewServer
34 +var NewServerWithWorkers = snapshot.NewServerWithWorkers
35 +var NewCache = snapshot.NewCache
src/go/pkg/netipc/service/cgroups_lookup/client.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build unix
2 +
3 +package cgroups_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) posix.ClientConfig {
11 + return transportconfig.PosixClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) posix.ServerConfig {
15 + return transportconfig.PosixServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/cgroups_lookup/client_common.go new
+39
@@ -0,0 +1,39 @@
1 +package cgroups_lookup
2 +
3 +import (
4 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
5 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
6 +)
7 +
8 +type Client struct {
9 + inner *raw.Client
10 +}
11 +
12 +func NewClient(runDir, serviceName string, config ClientConfig) *Client {
13 + return &Client{inner: raw.NewCgroupsLookupClient(runDir, serviceName, clientConfigToTransport(config))}
14 +}
15 +
16 +func (c *Client) Refresh() bool { return c.inner.Refresh() }
17 +func (c *Client) Ready() bool { return c.inner.Ready() }
18 +func (c *Client) Status() ClientStatus {
19 + return c.inner.Status()
20 +}
21 +func (c *Client) Call(paths [][]byte) (*protocol.CgroupsLookupResponseView, error) {
22 + return c.inner.CallCgroupsLookup(paths)
23 +}
24 +func (c *Client) Close() { c.inner.Close() }
25 +
26 +type Server struct {
27 + inner *raw.Server
28 +}
29 +
30 +func NewServer(runDir, serviceName string, config ServerConfig, handler Handler) *Server {
31 + return &Server{inner: raw.NewServer(runDir, serviceName, serverConfigToTransport(config), protocol.MethodCgroupsLookup, raw.CgroupsLookupDispatch(handler.Handle))}
32 +}
33 +
34 +func NewServerWithWorkers(runDir, serviceName string, config ServerConfig, handler Handler, workerCount int) *Server {
35 + return &Server{inner: raw.NewServerWithWorkers(runDir, serviceName, serverConfigToTransport(config), protocol.MethodCgroupsLookup, raw.CgroupsLookupDispatch(handler.Handle), workerCount)}
36 +}
37 +
38 +func (s *Server) Run() error { return s.inner.Run() }
39 +func (s *Server) Stop() { s.inner.Stop() }
src/go/pkg/netipc/service/cgroups_lookup/client_windows.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build windows
2 +
3 +package cgroups_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) windows.ClientConfig {
11 + return transportconfig.WindowsClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) windows.ServerConfig {
15 + return transportconfig.WindowsServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/cgroups_lookup/types.go new
+33
@@ -0,0 +1,33 @@
1 +// Package cgroups_lookup provides the public single-kind L2 surface for the
2 +// cgroups-lookup service.
3 +package cgroups_lookup
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
8 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
9 +)
10 +
11 +type ClientState = raw.ClientState
12 +
13 +const (
14 + StateDisconnected = raw.StateDisconnected
15 + StateConnecting = raw.StateConnecting
16 + StateReady = raw.StateReady
17 + StateNotFound = raw.StateNotFound
18 + StateAuthFailed = raw.StateAuthFailed
19 + StateIncompatible = raw.StateIncompatible
20 + StateBroken = raw.StateBroken
21 +)
22 +
23 +type ClientStatus = raw.ClientStatus
24 +
25 +type ClientConfig transportconfig.TypedConfig
26 +
27 +type ServerConfig transportconfig.TypedConfig
28 +
29 +type HandlerFunc = func(*protocol.CgroupsLookupRequestView, *protocol.CgroupsLookupBuilder) bool
30 +
31 +type Handler struct {
32 + Handle HandlerFunc
33 +}
src/go/pkg/netipc/service/cgroups_snapshot/cache.go new
+12
@@ -0,0 +1,12 @@
1 +//go:build unix
2 +
3 +package cgroups_snapshot
4 +
5 +import (
6 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
7 +)
8 +
9 +// NewCache creates a new L3 cache. Does NOT connect.
10 +func NewCache(runDir, serviceName string, config ClientConfig) *Cache {
11 + return &Cache{inner: raw.NewCache(runDir, serviceName, clientConfigToTransport(config))}
12 +}
src/go/pkg/netipc/service/cgroups_snapshot/cache_common.go renamed
+1 -8
@@ -1,6 +1,4 @@
1 -//go:build unix
2 -
3 -package cgroups
1 +package cgroups_snapshot
2
3 import (
4 raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
@@ -11,11 +9,6 @@ type Cache struct {
9 inner *raw.Cache
10 }
11
14 -// NewCache creates a new L3 cache. Does NOT connect.
15 -func NewCache(runDir, serviceName string, config ClientConfig) *Cache {
16 - return &Cache{inner: raw.NewCache(runDir, serviceName, clientConfigToTransport(config))}
17 -}
18 -
12 // Refresh drives the L2 client and requests a fresh snapshot.
13 func (c *Cache) Refresh() bool {
14 return c.inner.Refresh()
src/go/pkg/netipc/service/cgroups_snapshot/cache_windows.go new
+12
@@ -0,0 +1,12 @@
1 +//go:build windows
2 +
3 +package cgroups_snapshot
4 +
5 +import (
6 + raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
7 +)
8 +
9 +// NewCache creates a new L3 cache. Does NOT connect.
10 +func NewCache(runDir, serviceName string, config ClientConfig) *Cache {
11 + return &Cache{inner: raw.NewCache(runDir, serviceName, clientConfigToTransport(config))}
12 +}
src/go/pkg/netipc/service/cgroups_snapshot/cgroups_unix_test.go renamed
+18 -1
@@ -1,6 +1,6 @@
1 //go:build unix
2
3 -package cgroups
3 +package cgroups_snapshot
4
5 import (
6 "fmt"
@@ -201,3 +201,20 @@ func TestClientNotReadyReturnsErrorUnix(t *testing.T) {
201 t.Fatalf("CallSnapshot err = %v, want %v", err, protocol.ErrBadLayout)
202 }
203 }
204 +
205 +func TestClientStatusAndServerWorkersUnix(t *testing.T) {
206 + service := uniqueUnixService("status_workers")
207 + cleanupUnix(service)
208 +
209 + client := NewClient(testRunDirUnix, service, testUnixClientConfig())
210 + defer client.Close()
211 + if status := client.Status(); status.State == 0 && status.ConnectCount != 0 {
212 + t.Fatalf("unexpected initial status: %+v", status)
213 + }
214 +
215 + server := NewServerWithWorkers(testRunDirUnix, service, testUnixServerConfig(), testUnixHandler(), 2)
216 + if server == nil || server.inner == nil {
217 + t.Fatal("NewServerWithWorkers returned nil server")
218 + }
219 + server.Stop()
220 +}
src/go/pkg/netipc/service/cgroups_snapshot/cgroups_windows_test.go renamed
+1 -1
@@ -1,6 +1,6 @@
1 //go:build windows
2
3 -package cgroups
3 +package cgroups_snapshot
4
5 import (
6 "fmt"
src/go/pkg/netipc/service/cgroups_snapshot/client.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build unix
2 +
3 +package cgroups_snapshot
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) posix.ClientConfig {
11 + return transportconfig.PosixClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) posix.ServerConfig {
15 + return transportconfig.PosixServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/cgroups_snapshot/client_common.go renamed
+1 -26
@@ -1,39 +1,14 @@
1 -//go:build unix
2 -
3 -package cgroups
1 +package cgroups_snapshot
2
3 import (
4 "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
5 raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
8 - "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
6 )
7
8 func snapshotDispatch(handler Handler) raw.DispatchHandler {
9 return raw.SnapshotDispatch(handler.Handle, handler.SnapshotMaxItems)
10 }
11
15 -func clientConfigToTransport(config ClientConfig) posix.ClientConfig {
16 - return posix.ClientConfig{
17 - SupportedProfiles: config.SupportedProfiles,
18 - PreferredProfiles: config.PreferredProfiles,
19 - MaxRequestBatchItems: config.MaxRequestBatchItems,
20 - MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
21 - MaxResponseBatchItems: config.MaxRequestBatchItems,
22 - AuthToken: config.AuthToken,
23 - }
24 -}
25 -
26 -func serverConfigToTransport(config ServerConfig) posix.ServerConfig {
27 - return posix.ServerConfig{
28 - SupportedProfiles: config.SupportedProfiles,
29 - PreferredProfiles: config.PreferredProfiles,
30 - MaxRequestBatchItems: config.MaxRequestBatchItems,
31 - MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
32 - MaxResponseBatchItems: config.MaxRequestBatchItems,
33 - AuthToken: config.AuthToken,
34 - }
35 -}
36 -
12 // Client is the public L2 client context for the cgroups-snapshot service.
13 type Client struct {
14 inner *raw.Client
src/go/pkg/netipc/service/cgroups_snapshot/client_windows.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build windows
2 +
3 +package cgroups_snapshot
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +func clientConfigToTransport(config ClientConfig) windows.ClientConfig {
11 + return transportconfig.WindowsClient(transportconfig.TypedConfig(config))
12 +}
13 +
14 +func serverConfigToTransport(config ServerConfig) windows.ServerConfig {
15 + return transportconfig.WindowsServer(transportconfig.TypedConfig(config))
16 +}
src/go/pkg/netipc/service/cgroups_snapshot/types.go renamed
+4 -15
@@ -4,10 +4,11 @@
4 // Clients connect to a service kind, not to a plugin identity. One service
5 // endpoint serves one request kind only. The outer request code remains part
6 // of the envelope for validation, not public multi-method dispatch.
7 -package cgroups
7 +package cgroups_snapshot
8
9 import (
10 "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
11 + "github.com/netdata/netdata/go/plugins/pkg/netipc/service/internal/transportconfig"
12 raw "github.com/netdata/netdata/go/plugins/pkg/netipc/service/raw"
13 )
14
@@ -31,25 +32,13 @@ type ClientStatus = raw.ClientStatus
32 // cgroups-snapshot service.
33 //
34 // Transport-only tuning stays below the public typed API.
34 -type ClientConfig struct {
35 - SupportedProfiles uint32
36 - PreferredProfiles uint32
37 - MaxRequestBatchItems uint32
38 - MaxResponsePayloadBytes uint32
39 - AuthToken uint64
40 -}
35 +type ClientConfig transportconfig.TypedConfig
36
37 // ServerConfig is the public typed-server configuration for the
38 // cgroups-snapshot service.
39 //
40 // Transport-only tuning stays below the public typed API.
46 -type ServerConfig struct {
47 - SupportedProfiles uint32
48 - PreferredProfiles uint32
49 - MaxRequestBatchItems uint32
50 - MaxResponsePayloadBytes uint32
51 - AuthToken uint64
52 -}
41 +type ServerConfig transportconfig.TypedConfig
42
43 // SnapshotHandler is the typed callback used by the cgroups-snapshot service.
44 type SnapshotHandler = func(*protocol.CgroupsRequest, *protocol.CgroupsBuilder) bool
src/go/pkg/netipc/service/internal/transportconfig/posix.go new
+27
@@ -0,0 +1,27 @@
1 +//go:build unix
2 +
3 +package transportconfig
4 +
5 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
6 +
7 +func PosixClient(config TypedConfig) posix.ClientConfig {
8 + return posix.ClientConfig{
9 + SupportedProfiles: config.SupportedProfiles,
10 + PreferredProfiles: config.PreferredProfiles,
11 + MaxRequestBatchItems: config.MaxRequestBatchItems,
12 + MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
13 + MaxResponseBatchItems: responseBatchItems(config),
14 + AuthToken: config.AuthToken,
15 + }
16 +}
17 +
18 +func PosixServer(config TypedConfig) posix.ServerConfig {
19 + return posix.ServerConfig{
20 + SupportedProfiles: config.SupportedProfiles,
21 + PreferredProfiles: config.PreferredProfiles,
22 + MaxRequestBatchItems: config.MaxRequestBatchItems,
23 + MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
24 + MaxResponseBatchItems: responseBatchItems(config),
25 + AuthToken: config.AuthToken,
26 + }
27 +}
src/go/pkg/netipc/service/internal/transportconfig/types.go new
+13
@@ -0,0 +1,13 @@
1 +package transportconfig
2 +
3 +type TypedConfig struct {
4 + SupportedProfiles uint32
5 + PreferredProfiles uint32
6 + MaxRequestBatchItems uint32
7 + MaxResponsePayloadBytes uint32
8 + AuthToken uint64
9 +}
10 +
11 +func responseBatchItems(config TypedConfig) uint32 {
12 + return config.MaxRequestBatchItems
13 +}
src/go/pkg/netipc/service/internal/transportconfig/windows.go new
+27
@@ -0,0 +1,27 @@
1 +//go:build windows
2 +
3 +package transportconfig
4 +
5 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
6 +
7 +func WindowsClient(config TypedConfig) windows.ClientConfig {
8 + return windows.ClientConfig{
9 + SupportedProfiles: config.SupportedProfiles,
10 + PreferredProfiles: config.PreferredProfiles,
11 + MaxRequestBatchItems: config.MaxRequestBatchItems,
12 + MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
13 + MaxResponseBatchItems: responseBatchItems(config),
14 + AuthToken: config.AuthToken,
15 + }
16 +}
17 +
18 +func WindowsServer(config TypedConfig) windows.ServerConfig {
19 + return windows.ServerConfig{
20 + SupportedProfiles: config.SupportedProfiles,
21 + PreferredProfiles: config.PreferredProfiles,
22 + MaxRequestBatchItems: config.MaxRequestBatchItems,
23 + MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
24 + MaxResponseBatchItems: responseBatchItems(config),
25 + AuthToken: config.AuthToken,
26 + }
27 +}
src/go/pkg/netipc/service/raw/apps_lookup.go new
+109
@@ -0,0 +1,109 @@
1 +package raw
2 +
3 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
4 +
5 +// AppsLookupHandler serves a single APPS_LOOKUP service kind.
6 +type AppsLookupHandler func(*protocol.AppsLookupRequestView, *protocol.AppsLookupBuilder) bool
7 +
8 +func appsLookupRequestSize(pids []uint32) (int, error) {
9 + dirSize, err := checkedLookupMul(len(pids), protocol.LookupDirEntrySize)
10 + if err != nil {
11 + return 0, err
12 + }
13 + keySize, err := checkedLookupMul(len(pids), protocol.AppsLookupKeySize)
14 + if err != nil {
15 + return 0, err
16 + }
17 + size, err := checkedLookupAdd(protocol.AppsLookupReqHdr, dirSize)
18 + if err != nil {
19 + return 0, err
20 + }
21 + return checkedLookupAdd(size, keySize)
22 +}
23 +
24 +// CallAppsLookup performs a blocking typed APPS_LOOKUP call.
25 +// The returned view is valid until the next typed call on this client.
26 +func (c *Client) CallAppsLookup(pids []uint32) (*protocol.AppsLookupResponseView, error) {
27 + if err := c.validateMethod(protocol.MethodAppsLookup); err != nil {
28 + return nil, err
29 + }
30 +
31 + var result *protocol.AppsLookupResponseView
32 + err := c.callWithRetry(func() error {
33 + reqSize, err := appsLookupRequestSize(pids)
34 + if err != nil {
35 + return err
36 + }
37 + reqBuf := ensureClientScratch(&c.requestBuf, reqSize)
38 + reqLen, err := protocol.EncodeAppsLookupRequest(pids, reqBuf)
39 + if err != nil {
40 + return err
41 + }
42 +
43 + _, payload, rerr := c.doRawCall(protocol.MethodAppsLookup, reqBuf[:reqLen])
44 + if rerr != nil {
45 + return rerr
46 + }
47 + view, derr := protocol.DecodeAppsLookupResponse(payload)
48 + if derr != nil {
49 + return derr
50 + }
51 + expectedCount, err := checkedLookupU32(len(pids))
52 + if err != nil {
53 + return err
54 + }
55 + if view.ItemCount != expectedCount {
56 + return protocol.ErrBadItemCount
57 + }
58 + for i, expected := range pids {
59 + itemIndex, ierr := checkedLookupU32(i)
60 + if ierr != nil {
61 + return ierr
62 + }
63 + item, ierr := view.Item(itemIndex)
64 + if ierr != nil {
65 + return ierr
66 + }
67 + if item.Pid != expected {
68 + return protocol.ErrBadLayout
69 + }
70 + }
71 + result = view
72 + return nil
73 + })
74 + if err != nil {
75 + return nil, err
76 + }
77 + return result, nil
78 +}
79 +
80 +// AppsLookupDispatch adapts a typed apps lookup handler to the raw dispatch shape.
81 +func AppsLookupDispatch(handle AppsLookupHandler) DispatchHandler {
82 + if handle == nil {
83 + return nil
84 + }
85 + return func(request []byte, responseBuf []byte) (int, error) {
86 + req, err := protocol.DecodeAppsLookupRequest(request)
87 + if err != nil {
88 + return 0, err
89 + }
90 + minRequired, err := lookupMinRequired(protocol.AppsLookupRespHdr, req.ItemCount)
91 + if err != nil {
92 + return 0, err
93 + }
94 + if len(responseBuf) < minRequired {
95 + return 0, protocol.ErrOverflow
96 + }
97 + builder := protocol.NewAppsLookupBuilder(responseBuf, req.ItemCount, 0)
98 + if !handle(req, builder) {
99 + return 0, errHandlerFailed
100 + }
101 + if builder.Error() != nil {
102 + return 0, builder.Error()
103 + }
104 + if builder.ItemCount() != req.ItemCount {
105 + return 0, protocol.ErrBadItemCount
106 + }
107 + return builder.Finish(), nil
108 + }
109 +}
src/go/pkg/netipc/service/raw/apps_lookup_unix.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +// NewAppsLookupClient creates a raw client bound to the apps-lookup service kind.
11 +func NewAppsLookupClient(runDir, serviceName string, config posix.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodAppsLookup)
13 +}
src/go/pkg/netipc/service/raw/apps_lookup_windows.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +// NewAppsLookupClient creates a raw client bound to the apps-lookup service kind.
11 +func NewAppsLookupClient(runDir, serviceName string, config windows.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodAppsLookup)
13 +}
src/go/pkg/netipc/service/raw/cache_bucket_test.go new
+41
@@ -0,0 +1,41 @@
1 +package raw
2 +
3 +import (
4 + "errors"
5 + "testing"
6 +
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
8 +)
9 +
10 +func TestCacheBucketCountForItemCount(t *testing.T) {
11 + tests := []struct {
12 + name string
13 + itemCount uint32
14 + want uint32
15 + wantErr bool
16 + }{
17 + {name: "empty", itemCount: 0, want: 0},
18 + {name: "one", itemCount: 1, want: 16},
19 + {name: "eight", itemCount: 8, want: 16},
20 + {name: "nine", itemCount: 9, want: 32},
21 + {name: "overflow", itemCount: (1 << 30) + 1, wantErr: true},
22 + }
23 +
24 + for _, tt := range tests {
25 + t.Run(tt.name, func(t *testing.T) {
26 + got, err := cacheBucketCountForItemCount(tt.itemCount)
27 + if tt.wantErr {
28 + if !errors.Is(err, protocol.ErrOverflow) {
29 + t.Fatalf("expected ErrOverflow, got %v", err)
30 + }
31 + return
32 + }
33 + if err != nil {
34 + t.Fatalf("unexpected error: %v", err)
35 + }
36 + if got != tt.want {
37 + t.Fatalf("bucket count = %d, want %d", got, tt.want)
38 + }
39 + })
40 + }
41 +}
src/go/pkg/netipc/service/raw/cache_windows.go deleted
-159
@@ -1,159 +0,0 @@
1 -//go:build windows
2 -
3 -// L3: Client-side cgroups snapshot cache (Windows).
4 -//
5 -// Identical cache logic as the POSIX version. Uses Windows Client.
6 -//
7 -// Pure Go — no cgo. Works with CGO_ENABLED=0.
8 -
9 -package raw
10 -
11 -import (
12 - "time"
13 -
14 - windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
15 -)
16 -
17 -// cacheBucket is one open-addressing bucket for hash+name lookup.
18 -type cacheBucket struct {
19 - index int
20 - used bool
21 -}
22 -
23 -func cacheHashName(name string) uint32 {
24 - h := uint32(5381)
25 - for i := 0; i < len(name); i++ {
26 - h = ((h << 5) + h) + uint32(name[i])
27 - }
28 - return h
29 -}
30 -
31 -// Cache is an L3 client-side cgroups snapshot cache.
32 -type Cache struct {
33 - client *Client
34 - items []CacheItem
35 - // Open-addressing hash table: (hash ^ djb2(name)) -> index into items slice.
36 - buckets []cacheBucket
37 -
38 - systemdEnabled uint32
39 - generation uint64
40 - populated bool
41 - refreshSuccessCount uint32
42 - refreshFailureCount uint32
43 - epoch time.Time
44 - lastRefreshTs int64
45 -}
46 -
47 -// NewCache creates a new L3 cache.
48 -func NewCache(runDir, serviceName string, config windows.ClientConfig) *Cache {
49 - return &Cache{
50 - client: NewSnapshotClient(runDir, serviceName, config),
51 - epoch: time.Now(),
52 - }
53 -}
54 -
55 -// Refresh drives the L2 client and requests a fresh snapshot.
56 -func (c *Cache) Refresh() bool {
57 - c.client.Refresh()
58 -
59 - view, err := c.client.CallSnapshot()
60 - if err != nil {
61 - c.refreshFailureCount++
62 - return false
63 - }
64 -
65 - newItems := make([]CacheItem, 0, view.ItemCount)
66 - for i := uint32(0); i < view.ItemCount; i++ {
67 - iv, ierr := view.Item(i)
68 - if ierr != nil {
69 - c.refreshFailureCount++
70 - return false
71 - }
72 - newItems = append(newItems, CacheItem{
73 - Hash: iv.Hash,
74 - Options: iv.Options,
75 - Enabled: iv.Enabled,
76 - Name: iv.Name.String(),
77 - Path: iv.Path.String(),
78 - })
79 - }
80 -
81 - // Rebuild open-addressing lookup table.
82 - var buckets []cacheBucket
83 - if len(newItems) > 0 {
84 - bcount := nextPowerOf2U32(uint32(len(newItems)) * 2)
85 - buckets = make([]cacheBucket, bcount)
86 - mask := bcount - 1
87 - for i := range newItems {
88 - slot := (newItems[i].Hash ^ cacheHashName(newItems[i].Name)) & mask
89 - for buckets[slot].used {
90 - slot = (slot + 1) & mask
91 - }
92 - buckets[slot].index = i
93 - buckets[slot].used = true
94 - }
95 - }
96 -
97 - c.items = newItems
98 - c.buckets = buckets
99 - c.systemdEnabled = view.SystemdEnabled
100 - c.generation = view.Generation
101 - c.populated = true
102 - c.refreshSuccessCount++
103 - c.lastRefreshTs = time.Since(c.epoch).Milliseconds()
104 -
105 - return true
106 -}
107 -
108 -// Ready returns true if at least one successful refresh has occurred.
109 -func (c *Cache) Ready() bool {
110 - return c.populated
111 -}
112 -
113 -// Lookup finds a cached item by hash + name. O(1) via open-addressing hash
114 -// table. No I/O.
115 -func (c *Cache) Lookup(hash uint32, name string) (CacheItem, bool) {
116 - if !c.populated {
117 - return CacheItem{}, false
118 - }
119 - if len(c.buckets) > 0 {
120 - mask := uint32(len(c.buckets) - 1)
121 - slot := (hash ^ cacheHashName(name)) & mask
122 - for c.buckets[slot].used {
123 - item := c.items[c.buckets[slot].index]
124 - if item.Hash == hash && item.Name == name {
125 - return item, true
126 - }
127 - slot = (slot + 1) & mask
128 - }
129 - return CacheItem{}, false
130 - }
131 - for i := range c.items {
132 - if c.items[i].Hash == hash && c.items[i].Name == name {
133 - return c.items[i], true
134 - }
135 - }
136 - return CacheItem{}, false
137 -}
138 -
139 -// Status returns a diagnostic snapshot for the L3 cache.
140 -func (c *Cache) Status() CacheStatus {
141 - return CacheStatus{
142 - Populated: c.populated,
143 - ItemCount: uint32(len(c.items)),
144 - SystemdEnabled: c.systemdEnabled,
145 - Generation: c.generation,
146 - RefreshSuccessCount: c.refreshSuccessCount,
147 - RefreshFailureCount: c.refreshFailureCount,
148 - ConnectionState: c.client.state,
149 - LastRefreshTs: c.lastRefreshTs,
150 - }
151 -}
152 -
153 -// Close frees all cached items and closes the L2 client.
154 -func (c *Cache) Close() {
155 - c.items = nil
156 - c.buckets = nil
157 - c.populated = false
158 - c.client.Close()
159 -}
src/go/pkg/netipc/service/raw/cgroups_cache.go renamed
+63 -19
@@ -1,15 +1,36 @@
1 -//go:build unix
2 -
3 -// L3: Client-side cgroups snapshot cache (POSIX).
4 -
1 package raw
2
3 import (
4 "time"
5
10 - "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 )
8
9 +// Default response buffer size for L3 cache refresh.
10 +const cacheResponseBufSize = 65536
11 +
12 +// CacheItem is an owned copy of a single cgroup item.
13 +// Built from ephemeral L2 views during cache construction.
14 +type CacheItem struct {
15 + Hash uint32
16 + Options uint32
17 + Enabled uint32
18 + Name string // owned copy
19 + Path string // owned copy
20 +}
21 +
22 +// CacheStatus is a diagnostic snapshot for the L3 cache.
23 +type CacheStatus struct {
24 + Populated bool
25 + ItemCount uint32
26 + SystemdEnabled uint32
27 + Generation uint64
28 + RefreshSuccessCount uint32
29 + RefreshFailureCount uint32
30 + ConnectionState ClientState // underlying L2 client state
31 + LastRefreshTs int64 // monotonic timestamp (ms) of last successful refresh, 0 if never
32 +}
33 +
34 // cacheBucket is one open-addressing bucket for hash+name lookup.
35 type cacheBucket struct {
36 index int
@@ -40,21 +61,15 @@ type Cache struct {
61 lastRefreshTs int64 // elapsed ms since epoch
62 }
63
43 -// NewCache creates a new L3 cache. Creates the underlying L2 client
44 -// context. Does NOT connect. Does NOT require the server to be running.
45 -// Cache starts empty (populated == false).
46 -func NewCache(runDir, serviceName string, config posix.ClientConfig) *Cache {
64 +func newCache(client *Client) *Cache {
65 return &Cache{
48 - client: NewSnapshotClient(runDir, serviceName, config),
66 + client: client,
67 epoch: time.Now(),
68 }
69 }
70
53 -// Refresh drives the L2 client (connect/reconnect as needed) and
54 -// requests a fresh snapshot. On success, rebuilds the local cache.
55 -// On failure, preserves the previous cache.
56 -//
57 -// Returns true if the cache was updated.
71 +// Refresh drives the L2 client and requests a fresh snapshot. On success,
72 +// it rebuilds the local cache. On failure, it preserves the previous cache.
73 func (c *Cache) Refresh() bool {
74 c.client.Refresh()
75
@@ -80,10 +95,18 @@ func (c *Cache) Refresh() bool {
95 })
96 }
97
83 - // Rebuild open-addressing lookup table.
98 var buckets []cacheBucket
99 if len(newItems) > 0 {
86 - bcount := nextPowerOf2U32(uint32(len(newItems)) * 2)
100 + itemCount, err := checkedLookupU32(len(newItems))
101 + if err != nil {
102 + c.refreshFailureCount++
103 + return false
104 + }
105 + bcount, err := cacheBucketCountForItemCount(itemCount)
106 + if err != nil {
107 + c.refreshFailureCount++
108 + return false
109 + }
110 buckets = make([]cacheBucket, bcount)
111 mask := bcount - 1
112 for i := range newItems {
@@ -107,6 +130,20 @@ func (c *Cache) Refresh() bool {
130 return true
131 }
132
133 +func cacheBucketCountForItemCount(itemCount uint32) (uint32, error) {
134 + if itemCount == 0 {
135 + return 0, nil
136 + }
137 + if itemCount > 1<<30 {
138 + return 0, protocol.ErrOverflow
139 + }
140 + bcount := nextPowerOf2U32(itemCount * 2)
141 + if bcount == 0 || uint64(bcount) > uint64(int(^uint(0)>>1)) {
142 + return 0, protocol.ErrOverflow
143 + }
144 + return bcount, nil
145 +}
146 +
147 // Ready returns true if at least one successful refresh has occurred.
148 func (c *Cache) Ready() bool {
149 return c.populated
@@ -120,7 +157,10 @@ func (c *Cache) Lookup(hash uint32, name string) (CacheItem, bool) {
157 }
158
159 if len(c.buckets) > 0 {
123 - mask := uint32(len(c.buckets) - 1)
160 + mask, err := checkedLookupU32(len(c.buckets) - 1)
161 + if err != nil {
162 + return CacheItem{}, false
163 + }
164 slot := (hash ^ cacheHashName(name)) & mask
165 for c.buckets[slot].used {
166 item := &c.items[c.buckets[slot].index]
@@ -142,9 +182,13 @@ func (c *Cache) Lookup(hash uint32, name string) (CacheItem, bool) {
182
183 // Status returns a diagnostic snapshot for the L3 cache.
184 func (c *Cache) Status() CacheStatus {
185 + itemCount, err := checkedLookupU32(len(c.items))
186 + if err != nil {
187 + itemCount = ^uint32(0)
188 + }
189 return CacheStatus{
190 Populated: c.populated,
147 - ItemCount: uint32(len(c.items)),
191 + ItemCount: itemCount,
192 SystemdEnabled: c.systemdEnabled,
193 Generation: c.generation,
194 RefreshSuccessCount: c.refreshSuccessCount,
src/go/pkg/netipc/service/raw/cgroups_cache_unix.go new
+16
@@ -0,0 +1,16 @@
1 +//go:build unix
2 +
3 +// L3: Client-side cgroups snapshot cache (POSIX).
4 +
5 +package raw
6 +
7 +import (
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
9 +)
10 +
11 +// NewCache creates a new L3 cache. Creates the underlying L2 client
12 +// context. Does NOT connect. Does NOT require the server to be running.
13 +// Cache starts empty (populated == false).
14 +func NewCache(runDir, serviceName string, config posix.ClientConfig) *Cache {
15 + return newCache(NewSnapshotClient(runDir, serviceName, config))
16 +}
src/go/pkg/netipc/service/raw/cgroups_cache_windows.go new
+18
@@ -0,0 +1,18 @@
1 +//go:build windows
2 +
3 +// L3: Client-side cgroups snapshot cache (Windows).
4 +//
5 +// Identical cache logic as the POSIX version. Uses Windows Client.
6 +//
7 +// Pure Go — no cgo. Works with CGO_ENABLED=0.
8 +
9 +package raw
10 +
11 +import (
12 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
13 +)
14 +
15 +// NewCache creates a new L3 cache.
16 +func NewCache(runDir, serviceName string, config windows.ClientConfig) *Cache {
17 + return newCache(NewSnapshotClient(runDir, serviceName, config))
18 +}
src/go/pkg/netipc/service/raw/cgroups_lookup.go new
+124
@@ -0,0 +1,124 @@
1 +package raw
2 +
3 +import (
4 + "bytes"
5 +
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 +)
8 +
9 +// CgroupsLookupHandler serves a single CGROUPS_LOOKUP service kind.
10 +type CgroupsLookupHandler func(*protocol.CgroupsLookupRequestView, *protocol.CgroupsLookupBuilder) bool
11 +
12 +func cgroupsLookupRequestSize(paths [][]byte) (int, error) {
13 + dirSize, err := checkedLookupMul(len(paths), protocol.LookupDirEntrySize)
14 + if err != nil {
15 + return 0, err
16 + }
17 + size, err := checkedLookupAdd(protocol.CgroupsLookupReqHdr, dirSize)
18 + if err != nil {
19 + return 0, err
20 + }
21 + data := size
22 + for _, path := range paths {
23 + data, err = checkedLookupAlign8(data)
24 + if err != nil {
25 + return 0, err
26 + }
27 + data, err = checkedLookupAdd(data, len(path))
28 + if err != nil {
29 + return 0, err
30 + }
31 + data, err = checkedLookupAdd(data, 1)
32 + if err != nil {
33 + return 0, err
34 + }
35 + }
36 + return data, nil
37 +}
38 +
39 +// CallCgroupsLookup performs a blocking typed CGROUPS_LOOKUP call.
40 +// The returned view is valid until the next typed call on this client.
41 +func (c *Client) CallCgroupsLookup(paths [][]byte) (*protocol.CgroupsLookupResponseView, error) {
42 + if err := c.validateMethod(protocol.MethodCgroupsLookup); err != nil {
43 + return nil, err
44 + }
45 +
46 + var result *protocol.CgroupsLookupResponseView
47 + err := c.callWithRetry(func() error {
48 + reqSize, err := cgroupsLookupRequestSize(paths)
49 + if err != nil {
50 + return err
51 + }
52 + reqBuf := ensureClientScratch(&c.requestBuf, reqSize)
53 + reqLen, err := protocol.EncodeCgroupsLookupRequest(paths, reqBuf)
54 + if err != nil {
55 + return err
56 + }
57 +
58 + _, payload, rerr := c.doRawCall(protocol.MethodCgroupsLookup, reqBuf[:reqLen])
59 + if rerr != nil {
60 + return rerr
61 + }
62 + view, derr := protocol.DecodeCgroupsLookupResponse(payload)
63 + if derr != nil {
64 + return derr
65 + }
66 + expectedCount, err := checkedLookupU32(len(paths))
67 + if err != nil {
68 + return err
69 + }
70 + if view.ItemCount != expectedCount {
71 + return protocol.ErrBadItemCount
72 + }
73 + for i, expected := range paths {
74 + itemIndex, ierr := checkedLookupU32(i)
75 + if ierr != nil {
76 + return ierr
77 + }
78 + item, ierr := view.Item(itemIndex)
79 + if ierr != nil {
80 + return ierr
81 + }
82 + if !bytes.Equal(item.Path.Bytes(), expected) {
83 + return protocol.ErrBadLayout
84 + }
85 + }
86 + result = view
87 + return nil
88 + })
89 + if err != nil {
90 + return nil, err
91 + }
92 + return result, nil
93 +}
94 +
95 +// CgroupsLookupDispatch adapts a typed cgroups lookup handler to the raw dispatch shape.
96 +func CgroupsLookupDispatch(handle CgroupsLookupHandler) DispatchHandler {
97 + if handle == nil {
98 + return nil
99 + }
100 + return func(request []byte, responseBuf []byte) (int, error) {
101 + req, err := protocol.DecodeCgroupsLookupRequest(request)
102 + if err != nil {
103 + return 0, err
104 + }
105 + minRequired, err := lookupMinRequired(protocol.CgroupsLookupRespHdr, req.ItemCount)
106 + if err != nil {
107 + return 0, err
108 + }
109 + if len(responseBuf) < minRequired {
110 + return 0, protocol.ErrOverflow
111 + }
112 + builder := protocol.NewCgroupsLookupBuilder(responseBuf, req.ItemCount, 0)
113 + if !handle(req, builder) {
114 + return 0, errHandlerFailed
115 + }
116 + if builder.Error() != nil {
117 + return 0, builder.Error()
118 + }
119 + if builder.ItemCount() != req.ItemCount {
120 + return 0, protocol.ErrBadItemCount
121 + }
122 + return builder.Finish(), nil
123 + }
124 +}
src/go/pkg/netipc/service/raw/cgroups_lookup_unix.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +// NewCgroupsLookupClient creates a raw client bound to the cgroups-lookup service kind.
11 +func NewCgroupsLookupClient(runDir, serviceName string, config posix.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodCgroupsLookup)
13 +}
src/go/pkg/netipc/service/raw/cgroups_lookup_windows.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +// NewCgroupsLookupClient creates a raw client bound to the cgroups-lookup service kind.
11 +func NewCgroupsLookupClient(runDir, serviceName string, config windows.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodCgroupsLookup)
13 +}
src/go/pkg/netipc/service/raw/cgroups_snapshot.go new
+78
@@ -0,0 +1,78 @@
1 +package raw
2 +
3 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
4 +
5 +// SnapshotHandler serves a single CGROUPS_SNAPSHOT service kind.
6 +type SnapshotHandler func(*protocol.CgroupsRequest, *protocol.CgroupsBuilder) bool
7 +
8 +// SnapshotMaxItems returns the item budget for a single snapshot service kind.
9 +func SnapshotMaxItems(responseBufSize int, override uint32) uint32 {
10 + if override != 0 {
11 + return override
12 + }
13 + return protocol.EstimateCgroupsMaxItems(responseBufSize)
14 +}
15 +
16 +// CallSnapshot performs a blocking typed cgroups snapshot call.
17 +// The returned view is valid until the next typed call on this client.
18 +func (c *Client) CallSnapshot() (*protocol.CgroupsResponseView, error) {
19 + if err := c.validateMethod(protocol.MethodCgroupsSnapshot); err != nil {
20 + return nil, err
21 + }
22 +
23 + var result *protocol.CgroupsResponseView
24 +
25 + err := c.callWithRetry(func() error {
26 + req := protocol.CgroupsRequest{LayoutVersion: 1, Flags: 0}
27 + var reqBuf [4]byte
28 + if req.Encode(reqBuf[:]) == 0 {
29 + return protocol.ErrTruncated
30 + }
31 +
32 + _, payload, rerr := c.doRawCall(protocol.MethodCgroupsSnapshot, reqBuf[:])
33 + if rerr != nil {
34 + return rerr
35 + }
36 +
37 + view, derr := protocol.DecodeCgroupsResponse(payload)
38 + if derr != nil {
39 + return derr
40 + }
41 + result = &view
42 + return nil
43 + })
44 + if err != nil {
45 + return nil, err
46 + }
47 + return result, nil
48 +}
49 +
50 +// SnapshotDispatch adapts a typed snapshot handler to the raw dispatch shape.
51 +func SnapshotDispatch(handle SnapshotHandler, maxItems uint32) DispatchHandler {
52 + if handle == nil {
53 + return nil
54 + }
55 + return func(request []byte, responseBuf []byte) (int, error) {
56 + req, err := protocol.DecodeCgroupsRequest(request)
57 + if err != nil {
58 + return 0, err
59 + }
60 + itemBudget := SnapshotMaxItems(len(responseBuf), maxItems)
61 + if itemBudget == 0 {
62 + return 0, protocol.ErrOverflow
63 + }
64 + minRequired, ok := protocol.CgroupsBuilderMinBytes(itemBudget)
65 + if !ok || len(responseBuf) < minRequired {
66 + return 0, protocol.ErrOverflow
67 + }
68 + builder := protocol.NewCgroupsBuilder(responseBuf, itemBudget, 0, 0)
69 + if !handle(&req, builder) {
70 + return 0, errHandlerFailed
71 + }
72 + n := builder.Finish()
73 + if n == 0 {
74 + return 0, protocol.ErrOverflow
75 + }
76 + return n, nil
77 + }
78 +}
src/go/pkg/netipc/service/raw/cgroups_snapshot_unix.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +// NewSnapshotClient creates a raw client bound to the cgroups-snapshot service kind.
11 +func NewSnapshotClient(runDir, serviceName string, config posix.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodCgroupsSnapshot)
13 +}
src/go/pkg/netipc/service/raw/cgroups_snapshot_windows.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +// NewSnapshotClient creates a raw client bound to the cgroups-snapshot service kind.
11 +func NewSnapshotClient(runDir, serviceName string, config windows.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodCgroupsSnapshot)
13 +}
src/go/pkg/netipc/service/raw/client.go
-907
@@ -1,78 +1,15 @@
1 -//go:build unix
2 -
1 package raw
2
3 import (
6 - "encoding/binary"
4 "errors"
8 - "sync"
9 - "sync/atomic"
10 - "syscall"
5 "time"
12 - "unsafe"
6
7 "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
15 - "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 )
9
10 const clientShmAttachRetryInterval = 5 * time.Millisecond
11 const clientShmAttachRetryTimeout = 5 * time.Second
12
21 -// ---------------------------------------------------------------------------
22 -// Client context
23 -// ---------------------------------------------------------------------------
24 -
25 -// Client is an internal L2 client context bound to one service kind.
26 -// It manages connection lifecycle and provides typed blocking calls with
27 -// at-least-once retry semantics.
28 -type Client struct {
29 - state ClientState
30 - runDir string
31 - serviceName string
32 - expectedMethodCode uint16
33 - config posix.ClientConfig
34 -
35 - // Connection (managed internally)
36 - session *posix.Session
37 - shm *posix.ShmContext
38 -
39 - // Reusable scratch buffers owned by the client for hot request paths.
40 - requestBuf []byte
41 - sendBuf []byte
42 - transportBuf []byte
43 -
44 - // Stats
45 - connectCount uint32
46 - reconnectCount uint32
47 - callCount uint32
48 - errorCount uint32
49 -}
50 -
51 -func newClient(runDir, serviceName string, config posix.ClientConfig, expectedMethodCode uint16) *Client {
52 - return &Client{
53 - state: StateDisconnected,
54 - runDir: runDir,
55 - serviceName: serviceName,
56 - expectedMethodCode: expectedMethodCode,
57 - config: config,
58 - }
59 -}
60 -
61 -// NewSnapshotClient creates a raw client bound to the cgroups-snapshot service kind.
62 -func NewSnapshotClient(runDir, serviceName string, config posix.ClientConfig) *Client {
63 - return newClient(runDir, serviceName, config, protocol.MethodCgroupsSnapshot)
64 -}
65 -
66 -// NewIncrementClient creates a raw client bound to the increment service kind.
67 -func NewIncrementClient(runDir, serviceName string, config posix.ClientConfig) *Client {
68 - return newClient(runDir, serviceName, config, protocol.MethodIncrement)
69 -}
70 -
71 -// NewStringReverseClient creates a raw client bound to the string-reverse service kind.
72 -func NewStringReverseClient(runDir, serviceName string, config posix.ClientConfig) *Client {
73 - return newClient(runDir, serviceName, config, protocol.MethodStringReverse)
74 -}
75 -
13 func nextPowerOf2U32(n uint32) uint32 {
14 if n < 16 {
15 return 16
@@ -292,852 +229,8 @@ func (c *Client) doRawCall(methodCode uint16, reqPayload []byte) (protocol.Heade
229 return respHdr, payload, nil
230 }
231
295 -// CallSnapshot performs a blocking typed cgroups snapshot call.
296 -// The returned view is valid until the next typed call on this client.
297 -func (c *Client) CallSnapshot() (*protocol.CgroupsResponseView, error) {
298 - if err := c.validateMethod(protocol.MethodCgroupsSnapshot); err != nil {
299 - return nil, err
300 - }
301 -
302 - var result *protocol.CgroupsResponseView
303 -
304 - err := c.callWithRetry(func() error {
305 - req := protocol.CgroupsRequest{LayoutVersion: 1, Flags: 0}
306 - var reqBuf [4]byte
307 - if req.Encode(reqBuf[:]) == 0 {
308 - return protocol.ErrTruncated
309 - }
310 -
311 - _, payload, rerr := c.doRawCall(protocol.MethodCgroupsSnapshot, reqBuf[:])
312 - if rerr != nil {
313 - return rerr
314 - }
315 -
316 - view, derr := protocol.DecodeCgroupsResponse(payload)
317 - if derr != nil {
318 - return derr
319 - }
320 - result = &view
321 - return nil
322 - })
323 - if err != nil {
324 - return nil, err
325 - }
326 - return result, nil
327 -}
328 -
329 -// CallIncrement performs a blocking INCREMENT call.
330 -// Sends requestValue, returns the server's response value.
331 -func (c *Client) CallIncrement(requestValue uint64) (uint64, error) {
332 - if err := c.validateMethod(protocol.MethodIncrement); err != nil {
333 - return 0, err
334 - }
335 -
336 - var result uint64
337 -
338 - err := c.callWithRetry(func() error {
339 - var reqBuf [protocol.IncrementPayloadSize]byte
340 - if protocol.IncrementEncode(requestValue, reqBuf[:]) == 0 {
341 - return protocol.ErrTruncated
342 - }
343 -
344 - _, payload, rerr := c.doRawCall(protocol.MethodIncrement, reqBuf[:])
345 - if rerr != nil {
346 - return rerr
347 - }
348 -
349 - val, derr := protocol.IncrementDecode(payload)
350 - if derr != nil {
351 - return derr
352 - }
353 - result = val
354 - return nil
355 - })
356 - return result, err
357 -}
358 -
359 -// CallStringReverse performs a blocking STRING_REVERSE call.
360 -// The returned view is valid until the next typed call on this client.
361 -func (c *Client) CallStringReverse(requestStr string) (*protocol.StringReverseView, error) {
362 - if err := c.validateMethod(protocol.MethodStringReverse); err != nil {
363 - return nil, err
364 - }
365 -
366 - var result *protocol.StringReverseView
367 -
368 - err := c.callWithRetry(func() error {
369 - reqBuf := ensureClientScratch(&c.requestBuf, protocol.StringReverseHdrSize+len(requestStr)+1)
370 - if protocol.StringReverseEncode(requestStr, reqBuf) == 0 {
371 - return protocol.ErrTruncated
372 - }
373 -
374 - _, payload, rerr := c.doRawCall(protocol.MethodStringReverse, reqBuf)
375 - if rerr != nil {
376 - return rerr
377 - }
378 -
379 - view, derr := protocol.StringReverseDecode(payload)
380 - if derr != nil {
381 - return derr
382 - }
383 - result = &view
384 - return nil
385 - })
386 - if err != nil {
387 - return nil, err
388 - }
389 - return result, nil
390 -}
391 -
392 -// CallIncrementBatch performs a blocking batch INCREMENT call.
393 -// Sends multiple values, returns the server's response values.
394 -func (c *Client) CallIncrementBatch(values []uint64) ([]uint64, error) {
395 - if err := c.validateMethod(protocol.MethodIncrement); err != nil {
396 - return nil, err
397 - }
398 -
399 - if len(values) == 0 {
400 - return nil, nil
401 - }
402 -
403 - var results []uint64
404 - itemCount := uint32(len(values))
405 -
406 - err := c.callWithRetry(func() error {
407 - // Build batch request payload
408 - batchBufSize := protocol.Align8(int(itemCount)*8) + int(itemCount)*protocol.IncrementPayloadSize + int(itemCount)*protocol.Alignment
409 - batchBuf := ensureClientScratch(&c.requestBuf, batchBufSize)
410 - bb := protocol.NewBatchBuilder(batchBuf, itemCount)
411 -
412 - for _, v := range values {
413 - var item [protocol.IncrementPayloadSize]byte
414 - if protocol.IncrementEncode(v, item[:]) == 0 {
415 - return protocol.ErrTruncated
416 - }
417 - if err := bb.Add(item[:]); err != nil {
418 - return err
419 - }
420 - }
421 -
422 - totalPayloadLen, _ := bb.Finish()
423 - reqPayload := batchBuf[:totalPayloadLen]
424 -
425 - // Build and send header with batch flags
426 - hdr := protocol.Header{
427 - Kind: protocol.KindRequest,
428 - Code: protocol.MethodIncrement,
429 - Flags: protocol.FlagBatch,
430 - ItemCount: itemCount,
431 - MessageID: uint64(c.callCount) + 1,
432 - TransportStatus: protocol.StatusOK,
433 - }
434 -
435 - if err := c.transportSend(&hdr, reqPayload); err != nil {
436 - return err
437 - }
438 -
439 - // Receive response
440 - respHdr, respPayload, err := c.transportReceive()
441 - if err != nil {
442 - return err
443 - }
444 -
445 - if respHdr.Kind != protocol.KindResponse {
446 - return protocol.ErrBadKind
447 - }
448 - if respHdr.Code != protocol.MethodIncrement {
449 - return protocol.ErrBadLayout
450 - }
451 - if respHdr.MessageID != hdr.MessageID {
452 - return protocol.ErrBadLayout
453 - }
454 - switch respHdr.TransportStatus {
455 - case protocol.StatusOK:
456 - case protocol.StatusLimitExceeded:
457 - if current := c.sessionMaxResponsePayloadBytes(); current > 0 {
458 - if current >= ^uint32(0)/2 {
459 - c.noteResponseCapacity(^uint32(0))
460 - } else {
461 - c.noteResponseCapacity(current * 2)
462 - }
463 - }
464 - return protocol.ErrOverflow
465 - default:
466 - return protocol.ErrBadLayout
467 - }
468 - if respHdr.Flags&protocol.FlagBatch == 0 || respHdr.ItemCount != itemCount {
469 - return protocol.ErrBadItemCount
470 - }
471 -
472 - // Extract each response item
473 - out := make([]uint64, itemCount)
474 - for i := range itemCount {
475 - itemData, gerr := protocol.BatchItemGet(respPayload, itemCount, i)
476 - if gerr != nil {
477 - return gerr
478 - }
479 - val, derr := protocol.IncrementDecode(itemData)
480 - if derr != nil {
481 - return derr
482 - }
483 - out[i] = val
484 - }
485 - results = out
486 - return nil
487 - })
488 - return results, err
489 -}
490 -
232 // Close tears down the connection and releases resources.
233 func (c *Client) Close() {
234 c.disconnect()
235 c.state = StateDisconnected
236 }
496 -
497 -// ------------------------------------------------------------------
498 -// Internal helpers
499 -// ------------------------------------------------------------------
500 -
501 -func (c *Client) disconnect() {
502 - if c.shm != nil {
503 - c.shm.ShmClose()
504 - c.shm = nil
505 - }
506 - if c.session != nil {
507 - c.session.Close()
508 - c.session = nil
509 - }
510 -}
511 -
512 -func (c *Client) tryConnect() ClientState {
513 - session, err := posix.Connect(c.runDir, c.serviceName, &c.config)
514 - if err != nil {
515 - switch {
516 - case isConnectError(err):
517 - return StateNotFound
518 - case isAuthError(err):
519 - return StateAuthFailed
520 - case isIncompatibleError(err):
521 - return StateIncompatible
522 - default:
523 - return StateDisconnected
524 - }
525 - }
526 -
527 - // SHM upgrade if negotiated
528 - if session.SelectedProfile == protocol.ProfileSHMHybrid ||
529 - session.SelectedProfile == protocol.ProfileSHMFutex {
530 - // Retry attach: the server prepared SHM before handshake, but the
531 - // client may still race slightly with the peer exposing the region.
532 - deadline := time.Now().Add(clientShmAttachRetryTimeout)
533 - for {
534 - shm, serr := posix.ShmClientAttach(c.runDir, c.serviceName, session.SessionID)
535 - if serr == nil {
536 - c.shm = shm
537 - break
538 - }
539 - if !time.Now().Before(deadline) {
540 - break
541 - }
542 - time.Sleep(clientShmAttachRetryInterval)
543 - }
544 - if c.shm == nil {
545 - // SHM attach failed after negotiation. Close that session,
546 - // blacklist SHM for this client context, and retry baseline.
547 - session.Close()
548 - c.config.SupportedProfiles &^= posixShmProfiles
549 - c.config.PreferredProfiles &^= posixShmProfiles
550 - if c.config.SupportedProfiles == 0 {
551 - return StateDisconnected
552 - }
553 - return c.tryConnect()
554 - }
555 - }
556 -
557 - c.session = session
558 - return StateReady
559 -}
560 -
561 -func (c *Client) transportSend(hdr *protocol.Header, payload []byte) error {
562 - if c.shm != nil {
563 - if len(payload) > int(c.sessionMaxRequestPayloadBytes()) {
564 - c.noteRequestCapacity(uint32(len(payload)))
565 - return protocol.ErrOverflow
566 - }
567 -
568 - msgLen := protocol.HeaderSize + len(payload)
569 - msg := ensureClientScratch(&c.sendBuf, msgLen)
570 -
571 - hdr.Magic = protocol.MagicMsg
572 - hdr.Version = protocol.Version
573 - hdr.HeaderLen = protocol.HeaderLen
574 - hdr.PayloadLen = uint32(len(payload))
575 -
576 - hdr.Encode(msg[:protocol.HeaderSize])
577 - if len(payload) > 0 {
578 - copy(msg[protocol.HeaderSize:], payload)
579 - }
580 -
581 - if err := c.shm.ShmSend(msg[:msgLen]); err != nil {
582 - if errors.Is(err, posix.ErrShmMsgTooLarge) {
583 - c.noteRequestCapacity(uint32(len(payload)))
584 - return protocol.ErrOverflow
585 - }
586 - return protocol.ErrTruncated
587 - }
588 - return nil
589 - }
590 -
591 - // UDS path
592 - if c.session == nil {
593 - return protocol.ErrTruncated
594 - }
595 - if err := c.session.Send(hdr, payload); err != nil {
596 - if errors.Is(err, posix.ErrLimitExceeded) {
597 - c.noteRequestCapacity(uint32(len(payload)))
598 - return protocol.ErrOverflow
599 - }
600 - return protocol.ErrTruncated
601 - }
602 - return nil
603 -}
604 -
605 -func (c *Client) transportReceive() (protocol.Header, []byte, error) {
606 - scratch := ensureClientScratch(&c.transportBuf, c.maxReceiveMessageBytes())
607 -
608 - if c.shm != nil {
609 - mlen, err := c.shm.ShmReceive(scratch, 30000)
610 - if err != nil {
611 - return protocol.Header{}, nil, protocol.ErrTruncated
612 - }
613 - if mlen < protocol.HeaderSize {
614 - return protocol.Header{}, nil, protocol.ErrTruncated
615 - }
616 -
617 - hdr, err := protocol.DecodeHeader(scratch[:mlen])
618 - if err != nil {
619 - return protocol.Header{}, nil, err
620 - }
621 - return hdr, scratch[protocol.HeaderSize:mlen], nil
622 - }
623 -
624 - // UDS path: receive returns (Header, payload, error)
625 - if c.session == nil {
626 - return protocol.Header{}, nil, protocol.ErrTruncated
627 - }
628 -
629 - hdr, payload, err := c.session.Receive(scratch)
630 - if err != nil {
631 - return protocol.Header{}, nil, protocol.ErrTruncated
632 - }
633 - return hdr, payload, nil
634 -}
635 -
636 -func (c *Client) maxReceiveMessageBytes() int {
637 - maxPayload := c.config.MaxResponsePayloadBytes
638 - if c.session != nil && c.session.MaxResponsePayloadBytes > 0 {
639 - maxPayload = c.session.MaxResponsePayloadBytes
640 - }
641 - if maxPayload == 0 {
642 - maxPayload = cacheResponseBufSize
643 - }
644 - return protocol.HeaderSize + int(maxPayload)
645 -}
646 -
647 -// Error classification helpers
648 -func isConnectError(err error) bool {
649 - return errors.Is(err, posix.ErrConnect) || errors.Is(err, posix.ErrSocket)
650 -}
651 -
652 -func isAuthError(err error) bool {
653 - return errors.Is(err, posix.ErrAuthFailed)
654 -}
655 -
656 -func isIncompatibleError(err error) bool {
657 - return errors.Is(err, posix.ErrNoProfile) || errors.Is(err, posix.ErrIncompatible)
658 -}
659 -
660 -// ---------------------------------------------------------------------------
661 -// Managed server
662 -// ---------------------------------------------------------------------------
663 -
664 -// Server is an internal managed server bound to one expected request kind.
665 -// Supports multiple concurrent client sessions up to workerCount.
666 -type Server struct {
667 - runDir string
668 - serviceName string
669 - config posix.ServerConfig
670 - expectedMethodCode uint16
671 - handler DispatchHandler
672 - running atomic.Bool
673 - learnedRequestPayloadBytes atomic.Uint32
674 - learnedResponsePayloadBytes atomic.Uint32
675 - nextSessionID atomic.Uint64
676 - workerCount int
677 - wg sync.WaitGroup
678 -}
679 -
680 -// NewServer creates a new managed server. workerCount limits the
681 -// maximum number of concurrent client sessions (default 1 if <= 0).
682 -func NewServer(
683 - runDir, serviceName string,
684 - config posix.ServerConfig,
685 - expectedMethodCode uint16,
686 - handler DispatchHandler,
687 -) *Server {
688 - return NewServerWithWorkers(runDir, serviceName, config, expectedMethodCode, handler, 8)
689 -}
690 -
691 -// NewServerWithWorkers creates a server with an explicit worker count limit.
692 -func NewServerWithWorkers(
693 - runDir, serviceName string,
694 - config posix.ServerConfig,
695 - expectedMethodCode uint16,
696 - handler DispatchHandler,
697 - workerCount int,
698 -) *Server {
699 - if workerCount < 1 {
700 - workerCount = 1
701 - }
702 - learnedRequest := config.MaxRequestPayloadBytes
703 - if learnedRequest == 0 {
704 - learnedRequest = protocol.MaxPayloadDefault
705 - }
706 - learnedResponse := config.MaxResponsePayloadBytes
707 - if learnedResponse == 0 {
708 - learnedResponse = protocol.MaxPayloadDefault
709 - }
710 - s := &Server{
711 - runDir: runDir,
712 - serviceName: serviceName,
713 - config: config,
714 - expectedMethodCode: expectedMethodCode,
715 - handler: handler,
716 - workerCount: workerCount,
717 - }
718 - s.learnedRequestPayloadBytes.Store(learnedRequest)
719 - s.learnedResponsePayloadBytes.Store(learnedResponse)
720 - // Session ids are 1-based; prepareAcceptConfig() allocates with Add(1).
721 - s.nextSessionID.Store(0)
722 - return s
723 -}
724 -
725 -func (s *Server) dispatchSingle(methodCode uint16, request []byte, responseBuf []byte) (int, error) {
726 - if methodCode != s.expectedMethodCode || s.handler == nil {
727 - return 0, errHandlerFailed
728 - }
729 -
730 - return s.handler(request, responseBuf)
731 -}
732 -
733 -func (s *Server) methodSupported(methodCode uint16) bool {
734 - return s.handler != nil && methodCode == s.expectedMethodCode
735 -}
736 -
737 -func serverNotePayloadCapacity(target *atomic.Uint32, payloadLen uint32) {
738 - grown := nextPowerOf2U32(payloadLen)
739 - for {
740 - current := target.Load()
741 - if grown <= current {
742 - return
743 - }
744 - if target.CompareAndSwap(current, grown) {
745 - return
746 - }
747 - }
748 -}
749 -
750 -const posixShmProfiles = protocol.ProfileSHMHybrid | protocol.ProfileSHMFutex
751 -
752 -func (s *Server) prepareAcceptConfig() (uint64, posix.ServerConfig, *posix.ShmContext, bool) {
753 - sessionID := s.nextSessionID.Add(1)
754 - cfg := s.config
755 - cfg.MaxRequestPayloadBytes = s.learnedRequestPayloadBytes.Load()
756 - cfg.MaxResponsePayloadBytes = s.learnedResponsePayloadBytes.Load()
757 -
758 - if cfg.SupportedProfiles&posixShmProfiles == 0 {
759 - return sessionID, cfg, nil, true
760 - }
761 -
762 - shm, err := posix.ShmServerCreate(
763 - s.runDir, s.serviceName, sessionID,
764 - cfg.MaxRequestPayloadBytes+uint32(protocol.HeaderSize),
765 - cfg.MaxResponsePayloadBytes+uint32(protocol.HeaderSize),
766 - )
767 - if err == nil {
768 - return sessionID, cfg, shm, true
769 - }
770 -
771 - cfg.SupportedProfiles &^= posixShmProfiles
772 - cfg.PreferredProfiles &^= posixShmProfiles
773 - if cfg.SupportedProfiles == 0 {
774 - return sessionID, cfg, nil, false
775 - }
776 -
777 - return sessionID, cfg, nil, true
778 -}
779 -
780 -// Run starts the acceptor loop. Blocking. Accepts clients, spawns a
781 -// goroutine per session (up to workerCount concurrently).
782 -// Returns when Stop() is called or on fatal error.
783 -func (s *Server) Run() error {
784 - posix.ShmCleanupStale(s.runDir, s.serviceName)
785 -
786 - listener, err := posix.Listen(s.runDir, s.serviceName, s.config)
787 - if err != nil {
788 - return err
789 - }
790 - defer listener.Close()
791 -
792 - s.running.Store(true)
793 -
794 - /* Semaphore channel limits concurrent sessions */
795 - sem := make(chan struct{}, s.workerCount)
796 -
797 - for s.running.Load() {
798 - // Poll the listener fd before blocking on accept
799 - ready := pollFd(listener.Fd(), serverPollTimeoutMs)
800 - if ready < 0 {
801 - break
802 - }
803 - if ready == 0 {
804 - continue
805 - }
806 -
807 - sessionID, acceptCfg, precreatedShm, ok := s.prepareAcceptConfig()
808 - if !ok {
809 - time.Sleep(10 * time.Millisecond)
810 - continue
811 - }
812 -
813 - session, err := listener.AcceptWithConfig(sessionID, acceptCfg)
814 - if err != nil {
815 - if precreatedShm != nil {
816 - precreatedShm.ShmDestroy()
817 - }
818 - if !s.running.Load() {
819 - break
820 - }
821 - time.Sleep(10 * time.Millisecond)
822 - continue
823 - }
824 -
825 - // Try to acquire a worker slot (non-blocking check)
826 - select {
827 - case sem <- struct{}{}:
828 - // Got a slot
829 - default:
830 - // At capacity: reject client
831 - if precreatedShm != nil {
832 - precreatedShm.ShmDestroy()
833 - }
834 - session.Close()
835 - continue
836 - }
837 -
838 - var shm *posix.ShmContext
839 - if session.SelectedProfile == protocol.ProfileSHMHybrid ||
840 - session.SelectedProfile == protocol.ProfileSHMFutex {
841 - if precreatedShm == nil {
842 - session.Close()
843 - <-sem
844 - continue
845 - }
846 - shm = precreatedShm
847 - } else if precreatedShm != nil {
848 - precreatedShm.ShmDestroy()
849 - }
850 -
851 - // Handle this session in a goroutine
852 - s.wg.Add(1)
853 - go func(sess *posix.Session, shmCtx *posix.ShmContext) {
854 - defer func() {
855 - if r := recover(); r != nil {
856 - // Session handler panicked; log but don't crash the server
857 - }
858 - <-sem // release worker slot
859 - s.wg.Done()
860 - }()
861 - s.handleSession(sess, shmCtx)
862 - }(session, shm)
863 - }
864 -
865 - // Wait for all active session goroutines to finish
866 - s.wg.Wait()
867 -
868 - return nil
869 -}
870 -
871 -// Stop signals the server to stop.
872 -func (s *Server) Stop() {
873 - s.running.Store(false)
874 -}
875 -
876 -func (s *Server) handleSession(session *posix.Session, shm *posix.ShmContext) {
877 - recvBuf := make([]byte, protocol.HeaderSize+int(session.MaxRequestPayloadBytes))
878 - respBuf := make([]byte, int(session.MaxResponsePayloadBytes))
879 - itemRespBuf := make([]byte, int(session.MaxResponsePayloadBytes))
880 - msgBuf := make([]byte, int(session.MaxResponsePayloadBytes)+protocol.HeaderSize)
881 -
882 - defer func() {
883 - if shm != nil {
884 - shm.ShmDestroy()
885 - }
886 - session.Close()
887 - }()
888 -
889 - for s.running.Load() {
890 - var hdr protocol.Header
891 - var payload []byte
892 -
893 - if shm != nil {
894 - mlen, err := shm.ShmReceive(recvBuf, serverPollTimeoutMs)
895 - if err != nil {
896 - if err == posix.ErrShmTimeout {
897 - continue
898 - }
899 - return
900 - }
901 - if mlen < protocol.HeaderSize {
902 - return
903 - }
904 - h, err := protocol.DecodeHeader(recvBuf[:mlen])
905 - if err != nil {
906 - return
907 - }
908 - hdr = h
909 - payload = recvBuf[protocol.HeaderSize:mlen]
910 - } else {
911 - // Poll the session fd before blocking on receive
912 - ready := pollFd(session.Fd(), serverPollTimeoutMs)
913 - if ready < 0 {
914 - return
915 - }
916 - if ready == 0 {
917 - continue
918 - }
919 -
920 - h, p, err := session.Receive(recvBuf)
921 - if err != nil {
922 - return
923 - }
924 - hdr = h
925 - payload = p
926 - }
927 -
928 - // Protocol violation: unexpected message kind terminates session
929 - if hdr.Kind != protocol.KindRequest {
930 - return
931 - }
932 -
933 - if len(payload) <= int(^uint32(0)) {
934 - serverNotePayloadCapacity(&s.learnedRequestPayloadBytes, uint32(len(payload)))
935 - }
936 -
937 - if !s.methodSupported(hdr.Code) {
938 - respHdr := protocol.Header{
939 - Kind: protocol.KindResponse,
940 - Code: hdr.Code,
941 - MessageID: hdr.MessageID,
942 - TransportStatus: protocol.StatusUnsupported,
943 - ItemCount: 1,
944 - }
945 -
946 - if shm != nil {
947 - if len(msgBuf) < protocol.HeaderSize {
948 - msgBuf = make([]byte, protocol.HeaderSize)
949 - }
950 - msg := msgBuf[:protocol.HeaderSize]
951 - respHdr.Magic = protocol.MagicMsg
952 - respHdr.Version = protocol.Version
953 - respHdr.HeaderLen = protocol.HeaderLen
954 - respHdr.PayloadLen = 0
955 - respHdr.Encode(msg[:protocol.HeaderSize])
956 - if err := shm.ShmSend(msg); err != nil {
957 - return
958 - }
959 - } else if err := session.Send(&respHdr, nil); err != nil {
960 - return
961 - }
962 - continue
963 - }
964 -
965 - // Dispatch: single-item or batch
966 - responseLen := 0
967 - isBatch := (hdr.Flags&protocol.FlagBatch != 0) && hdr.ItemCount >= 1
968 - var dispatchErr error
969 -
970 - if !isBatch {
971 - var derr error
972 - responseLen, derr = s.dispatchSingle(hdr.Code, payload, respBuf)
973 - if derr != nil {
974 - dispatchErr = derr
975 - responseLen = 0
976 - } else if responseLen < 0 || responseLen > len(respBuf) {
977 - dispatchErr = protocol.ErrOverflow
978 - responseLen = 0
979 - }
980 - } else {
981 - var bb protocol.BatchBuilder
982 - bb.Reset(respBuf, hdr.ItemCount)
983 -
984 - for i := uint32(0); i < hdr.ItemCount && dispatchErr == nil; i++ {
985 - itemData, gerr := protocol.BatchItemGet(payload, hdr.ItemCount, i)
986 - if gerr != nil {
987 - dispatchErr = gerr
988 - break
989 - }
990 -
991 - itemResultLen, derr := s.dispatchSingle(hdr.Code, itemData, itemRespBuf)
992 - if derr != nil {
993 - dispatchErr = derr
994 - break
995 - }
996 - if itemResultLen < 0 || itemResultLen > len(itemRespBuf) {
997 - dispatchErr = protocol.ErrOverflow
998 - break
999 - }
1000 -
1001 - if aerr := bb.Add(itemRespBuf[:itemResultLen]); aerr != nil {
1002 - dispatchErr = aerr
1003 - break
1004 - }
1005 - }
1006 -
1007 - if dispatchErr == nil {
1008 - responseLen, _ = bb.Finish()
1009 - }
1010 - }
1011 -
1012 - // Build response header
1013 - respHdr := protocol.Header{
1014 - Kind: protocol.KindResponse,
1015 - Code: hdr.Code,
1016 - MessageID: hdr.MessageID,
1017 - }
1018 -
1019 - if dispatchErr == nil {
1020 - if responseLen <= int(^uint32(0)) {
1021 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, uint32(responseLen))
1022 - }
1023 - respHdr.TransportStatus = protocol.StatusOK
1024 - if isBatch {
1025 - respHdr.Flags = protocol.FlagBatch
1026 - respHdr.ItemCount = hdr.ItemCount
1027 - } else {
1028 - respHdr.ItemCount = 1
1029 - }
1030 - } else if errors.Is(dispatchErr, protocol.ErrOverflow) {
1031 - current := session.MaxResponsePayloadBytes
1032 - if current >= ^uint32(0)/2 {
1033 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, ^uint32(0))
1034 - } else {
1035 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, current*2)
1036 - }
1037 - respHdr.TransportStatus = protocol.StatusLimitExceeded
1038 - respHdr.ItemCount = 1
1039 - responseLen = 0
1040 - } else if errors.Is(dispatchErr, errHandlerFailed) {
1041 - respHdr.TransportStatus = protocol.StatusInternalError
1042 - respHdr.ItemCount = 1
1043 - responseLen = 0
1044 - } else {
1045 - respHdr.TransportStatus = protocol.StatusBadEnvelope
1046 - respHdr.ItemCount = 1
1047 - responseLen = 0
1048 - }
1049 -
1050 - // Send response via the active transport
1051 - if shm != nil {
1052 - msgLen := protocol.HeaderSize + responseLen
1053 - if len(msgBuf) < msgLen {
1054 - msgBuf = make([]byte, msgLen)
1055 - }
1056 - msg := msgBuf[:msgLen]
1057 -
1058 - respHdr.Magic = protocol.MagicMsg
1059 - respHdr.Version = protocol.Version
1060 - respHdr.HeaderLen = protocol.HeaderLen
1061 - respHdr.PayloadLen = uint32(responseLen)
1062 -
1063 - respHdr.Encode(msg[:protocol.HeaderSize])
1064 - if responseLen > 0 {
1065 - copy(msg[protocol.HeaderSize:], respBuf[:responseLen])
1066 - }
1067 -
1068 - if err := shm.ShmSend(msg); err != nil {
1069 - return
1070 - }
1071 - if respHdr.TransportStatus == protocol.StatusLimitExceeded {
1072 - return
1073 - }
1074 - } else {
1075 - if err := session.Send(&respHdr, respBuf[:responseLen]); err != nil {
1076 - return
1077 - }
1078 - if respHdr.TransportStatus == protocol.StatusLimitExceeded {
1079 - return
1080 - }
1081 - }
1082 - }
1083 -}
1084 -
1085 -// ---------------------------------------------------------------------------
1086 -// Internal: poll helper (raw syscall, pure Go, no cgo)
1087 -// ---------------------------------------------------------------------------
1088 -
1089 -// poll constants (not exported by Go's syscall package)
1090 -const (
1091 - _POLLIN = 0x0001
1092 - _POLLERR = 0x0008
1093 - _POLLHUP = 0x0010
1094 - _POLLNVAL = 0x0020
1095 -)
1096 -
1097 -// pollfd matches struct pollfd from <poll.h>.
1098 -type pollfd struct {
1099 - fd int32
1100 - events int16
1101 - revents int16
1102 -}
1103 -
1104 -// pollFd polls a file descriptor for readability with a timeout in ms.
1105 -// Returns: 1 = data ready, 0 = timeout, -1 = error/hangup.
1106 -func pollFd(fd int, timeoutMs int) int {
1107 - pfd := pollfd{
1108 - fd: int32(fd),
1109 - events: _POLLIN,
1110 - }
1111 -
1112 - r, _, errno := syscall.Syscall(
1113 - syscall.SYS_POLL,
1114 - uintptr(unsafe.Pointer(&pfd)),
1115 - 1,
1116 - uintptr(timeoutMs),
1117 - )
1118 -
1119 - n := int(r)
1120 - if n < 0 {
1121 - if errno == syscall.EINTR {
1122 - return 0
1123 - }
1124 - return -1
1125 - }
1126 -
1127 - if n == 0 {
1128 - return 0
1129 - }
1130 -
1131 - if pfd.revents&(_POLLERR|_POLLHUP|_POLLNVAL) != 0 {
1132 - return -1
1133 - }
1134 -
1135 - if pfd.revents&_POLLIN != 0 {
1136 - return 1
1137 - }
1138 -
1139 - return 0
1140 -}
1141 -
1142 -// Suppress unused import warnings.
1143 -var _ = binary.NativeEndian
src/go/pkg/netipc/service/raw/client_unix.go new
+215
@@ -0,0 +1,215 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "errors"
7 + "time"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
11 +)
12 +
13 +// Client is an internal L2 client context bound to one service kind.
14 +// It manages connection lifecycle and provides typed blocking calls with
15 +// at-least-once retry semantics.
16 +type Client struct {
17 + state ClientState
18 + runDir string
19 + serviceName string
20 + expectedMethodCode uint16
21 + config posix.ClientConfig
22 +
23 + // Connection (managed internally)
24 + session *posix.Session
25 + shm *posix.ShmContext
26 +
27 + // Reusable scratch buffers owned by the client for hot request paths.
28 + requestBuf []byte
29 + sendBuf []byte
30 + transportBuf []byte
31 +
32 + // Stats
33 + connectCount uint32
34 + reconnectCount uint32
35 + callCount uint32
36 + errorCount uint32
37 +}
38 +
39 +func newClient(runDir, serviceName string, config posix.ClientConfig, expectedMethodCode uint16) *Client {
40 + return &Client{
41 + state: StateDisconnected,
42 + runDir: runDir,
43 + serviceName: serviceName,
44 + expectedMethodCode: expectedMethodCode,
45 + config: config,
46 + }
47 +}
48 +
49 +func (c *Client) disconnect() {
50 + if c.shm != nil {
51 + c.shm.ShmClose()
52 + c.shm = nil
53 + }
54 + if c.session != nil {
55 + c.session.Close()
56 + c.session = nil
57 + }
58 +}
59 +
60 +func (c *Client) tryConnect() ClientState {
61 + session, err := posix.Connect(c.runDir, c.serviceName, &c.config)
62 + if err != nil {
63 + switch {
64 + case isConnectError(err):
65 + return StateNotFound
66 + case isAuthError(err):
67 + return StateAuthFailed
68 + case isIncompatibleError(err):
69 + return StateIncompatible
70 + default:
71 + return StateDisconnected
72 + }
73 + }
74 +
75 + // SHM upgrade if negotiated
76 + if session.SelectedProfile == protocol.ProfileSHMHybrid ||
77 + session.SelectedProfile == protocol.ProfileSHMFutex {
78 + // Retry attach: the server prepared SHM before handshake, but the
79 + // client may still race slightly with the peer exposing the region.
80 + deadline := time.Now().Add(clientShmAttachRetryTimeout)
81 + for {
82 + shm, serr := posix.ShmClientAttach(c.runDir, c.serviceName, session.SessionID)
83 + if serr == nil {
84 + c.shm = shm
85 + break
86 + }
87 + if !time.Now().Before(deadline) {
88 + break
89 + }
90 + time.Sleep(clientShmAttachRetryInterval)
91 + }
92 + if c.shm == nil {
93 + // SHM attach failed after negotiation. Close that session,
94 + // blacklist SHM for this client context, and retry baseline.
95 + session.Close()
96 + c.config.SupportedProfiles &^= posixShmProfiles
97 + c.config.PreferredProfiles &^= posixShmProfiles
98 + if c.config.SupportedProfiles == 0 {
99 + return StateDisconnected
100 + }
101 + return c.tryConnect()
102 + }
103 + }
104 +
105 + c.session = session
106 + return StateReady
107 +}
108 +
109 +func (c *Client) transportSend(hdr *protocol.Header, payload []byte) error {
110 + payloadLen, err := checkedLookupU32(len(payload))
111 + if err != nil {
112 + c.noteRequestCapacity(^uint32(0))
113 + return protocol.ErrOverflow
114 + }
115 +
116 + if c.shm != nil {
117 + if payloadLen > c.sessionMaxRequestPayloadBytes() {
118 + c.noteRequestCapacity(payloadLen)
119 + return protocol.ErrOverflow
120 + }
121 +
122 + msgLen, err := checkedLookupAdd(protocol.HeaderSize, len(payload))
123 + if err != nil {
124 + return protocol.ErrOverflow
125 + }
126 + msg := ensureClientScratch(&c.sendBuf, msgLen)
127 +
128 + hdr.Magic = protocol.MagicMsg
129 + hdr.Version = protocol.Version
130 + hdr.HeaderLen = protocol.HeaderLen
131 + hdr.PayloadLen = payloadLen
132 +
133 + hdr.Encode(msg[:protocol.HeaderSize])
134 + if len(payload) > 0 {
135 + copy(msg[protocol.HeaderSize:], payload)
136 + }
137 +
138 + if err := c.shm.ShmSend(msg[:msgLen]); err != nil {
139 + if errors.Is(err, posix.ErrShmMsgTooLarge) {
140 + c.noteRequestCapacity(payloadLen)
141 + return protocol.ErrOverflow
142 + }
143 + return protocol.ErrTruncated
144 + }
145 + return nil
146 + }
147 +
148 + // UDS path
149 + if c.session == nil {
150 + return protocol.ErrTruncated
151 + }
152 + if err := c.session.Send(hdr, payload); err != nil {
153 + if errors.Is(err, posix.ErrLimitExceeded) {
154 + c.noteRequestCapacity(payloadLen)
155 + return protocol.ErrOverflow
156 + }
157 + return protocol.ErrTruncated
158 + }
159 + return nil
160 +}
161 +
162 +func (c *Client) transportReceive() (protocol.Header, []byte, error) {
163 + scratch := ensureClientScratch(&c.transportBuf, c.maxReceiveMessageBytes())
164 +
165 + if c.shm != nil {
166 + mlen, err := c.shm.ShmReceive(scratch, 30000)
167 + if err != nil {
168 + return protocol.Header{}, nil, protocol.ErrTruncated
169 + }
170 + if mlen < protocol.HeaderSize {
171 + return protocol.Header{}, nil, protocol.ErrTruncated
172 + }
173 +
174 + hdr, err := protocol.DecodeHeader(scratch[:mlen])
175 + if err != nil {
176 + return protocol.Header{}, nil, err
177 + }
178 + return hdr, scratch[protocol.HeaderSize:mlen], nil
179 + }
180 +
181 + // UDS path: receive returns (Header, payload, error)
182 + if c.session == nil {
183 + return protocol.Header{}, nil, protocol.ErrTruncated
184 + }
185 +
186 + hdr, payload, err := c.session.Receive(scratch)
187 + if err != nil {
188 + return protocol.Header{}, nil, protocol.ErrTruncated
189 + }
190 + return hdr, payload, nil
191 +}
192 +
193 +func (c *Client) maxReceiveMessageBytes() int {
194 + maxPayload := c.config.MaxResponsePayloadBytes
195 + if c.session != nil && c.session.MaxResponsePayloadBytes > 0 {
196 + maxPayload = c.session.MaxResponsePayloadBytes
197 + }
198 + if maxPayload == 0 {
199 + maxPayload = cacheResponseBufSize
200 + }
201 + return protocol.HeaderSize + int(maxPayload)
202 +}
203 +
204 +// Error classification helpers
205 +func isConnectError(err error) bool {
206 + return errors.Is(err, posix.ErrConnect) || errors.Is(err, posix.ErrSocket)
207 +}
208 +
209 +func isAuthError(err error) bool {
210 + return errors.Is(err, posix.ErrAuthFailed)
211 +}
212 +
213 +func isIncompatibleError(err error) bool {
214 + return errors.Is(err, posix.ErrNoProfile) || errors.Is(err, posix.ErrIncompatible)
215 +}
src/go/pkg/netipc/service/raw/client_windows.go
-919
@@ -1,32 +1,15 @@
1 //go:build windows
2
3 -// Internal typed service client for Windows.
4 -//
5 -// Identical state machine and retry logic as the POSIX client.
6 -// Uses Named Pipe + Win SHM transports instead of UDS + POSIX SHM.
7 -//
8 -// Pure Go — no cgo. Works with CGO_ENABLED=0.
9 -
3 package raw
4
5 import (
13 - "encoding/binary"
6 "errors"
15 - "sync"
16 - "sync/atomic"
7 "time"
8
9 "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
11 )
12
23 -const clientShmAttachRetryInterval = 5 * time.Millisecond
24 -const clientShmAttachRetryTimeout = 5 * time.Second
25 -
26 -// ---------------------------------------------------------------------------
27 -// Client context
28 -// ---------------------------------------------------------------------------
29 -
13 // Client is an internal L2 client context bound to one service kind.
14 type Client struct {
15 state ClientState
@@ -58,437 +41,6 @@ func newClient(runDir, serviceName string, config windows.ClientConfig, expected
41 }
42 }
43
61 -// NewSnapshotClient creates a raw client bound to the cgroups-snapshot service kind.
62 -func NewSnapshotClient(runDir, serviceName string, config windows.ClientConfig) *Client {
63 - return newClient(runDir, serviceName, config, protocol.MethodCgroupsSnapshot)
64 -}
65 -
66 -// NewIncrementClient creates a raw client bound to the increment service kind.
67 -func NewIncrementClient(runDir, serviceName string, config windows.ClientConfig) *Client {
68 - return newClient(runDir, serviceName, config, protocol.MethodIncrement)
69 -}
70 -
71 -// NewStringReverseClient creates a raw client bound to the string-reverse service kind.
72 -func NewStringReverseClient(runDir, serviceName string, config windows.ClientConfig) *Client {
73 - return newClient(runDir, serviceName, config, protocol.MethodStringReverse)
74 -}
75 -
76 -func nextPowerOf2U32(n uint32) uint32 {
77 - if n < 16 {
78 - return 16
79 - }
80 - n--
81 - n |= n >> 1
82 - n |= n >> 2
83 - n |= n >> 4
84 - n |= n >> 8
85 - n |= n >> 16
86 - return n + 1
87 -}
88 -
89 -func (c *Client) validateMethod(methodCode uint16) error {
90 - if c.expectedMethodCode != methodCode {
91 - return protocol.ErrBadLayout
92 - }
93 - return nil
94 -}
95 -
96 -// Refresh attempts connect if DISCONNECTED/NOT_FOUND, reconnect if BROKEN.
97 -func (c *Client) Refresh() bool {
98 - oldState := c.state
99 -
100 - switch c.state {
101 - case StateDisconnected, StateNotFound:
102 - c.state = StateConnecting
103 - c.state = c.tryConnect()
104 - if c.state == StateReady {
105 - c.connectCount++
106 - }
107 -
108 - case StateBroken:
109 - c.disconnect()
110 - c.state = StateConnecting
111 - c.state = c.tryConnect()
112 - if c.state == StateReady {
113 - c.reconnectCount++
114 - }
115 -
116 - case StateReady, StateConnecting, StateAuthFailed, StateIncompatible:
117 - // No action needed
118 - }
119 -
120 - return c.state != oldState
121 -}
122 -
123 -// Ready returns true only if the client is in the READY state.
124 -func (c *Client) Ready() bool {
125 - return c.state == StateReady
126 -}
127 -
128 -// Status returns a diagnostic counters snapshot.
129 -func (c *Client) Status() ClientStatus {
130 - return ClientStatus{
131 - State: c.state,
132 - ConnectCount: c.connectCount,
133 - ReconnectCount: c.reconnectCount,
134 - CallCount: c.callCount,
135 - ErrorCount: c.errorCount,
136 - }
137 -}
138 -
139 -func (c *Client) sessionMaxRequestPayloadBytes() uint32 {
140 - if c.session != nil {
141 - return c.session.MaxRequestPayloadBytes
142 - }
143 - return c.config.MaxRequestPayloadBytes
144 -}
145 -
146 -func (c *Client) sessionMaxResponsePayloadBytes() uint32 {
147 - if c.session != nil {
148 - return c.session.MaxResponsePayloadBytes
149 - }
150 - return c.config.MaxResponsePayloadBytes
151 -}
152 -
153 -func (c *Client) noteRequestCapacity(payloadLen uint32) {
154 - grown := nextPowerOf2U32(payloadLen)
155 - if grown > protocol.MaxPayloadCap {
156 - grown = protocol.MaxPayloadCap
157 - }
158 - if grown > c.config.MaxRequestPayloadBytes {
159 - c.config.MaxRequestPayloadBytes = grown
160 - }
161 -}
162 -
163 -func (c *Client) noteResponseCapacity(payloadLen uint32) {
164 - grown := nextPowerOf2U32(payloadLen)
165 - if grown > protocol.MaxPayloadCap {
166 - grown = protocol.MaxPayloadCap
167 - }
168 - if grown > c.config.MaxResponsePayloadBytes {
169 - c.config.MaxResponsePayloadBytes = grown
170 - }
171 -}
172 -
173 -// callWithRetry manages reconnect-driven recovery for a typed call.
174 -// Ordinary failures retry once. Overflow-driven resize recovery may
175 -// reconnect more than once while negotiated capacities grow.
176 -func (c *Client) callWithRetry(attempt func() error) error {
177 - if c.state != StateReady {
178 - c.errorCount++
179 - return protocol.ErrBadLayout
180 - }
181 -
182 - for {
183 - prevReq := c.sessionMaxRequestPayloadBytes()
184 - prevResp := c.sessionMaxResponsePayloadBytes()
185 - prevCfgReq := c.config.MaxRequestPayloadBytes
186 - prevCfgResp := c.config.MaxResponsePayloadBytes
187 -
188 - firstErr := attempt()
189 - if firstErr == nil {
190 - c.callCount++
191 - return nil
192 - }
193 -
194 - if !errors.Is(firstErr, protocol.ErrOverflow) {
195 - c.disconnect()
196 - c.state = StateBroken
197 -
198 - c.state = c.tryConnect()
199 - if c.state != StateReady {
200 - c.errorCount++
201 - return firstErr
202 - }
203 - c.reconnectCount++
204 -
205 - retryErr := attempt()
206 - if retryErr == nil {
207 - c.callCount++
208 - return nil
209 - }
210 -
211 - c.disconnect()
212 - c.state = StateBroken
213 - c.errorCount++
214 - return retryErr
215 - }
216 -
217 - c.disconnect()
218 - c.state = StateBroken
219 -
220 - c.state = c.tryConnect()
221 - if c.state != StateReady {
222 - c.errorCount++
223 - return firstErr
224 - }
225 - c.reconnectCount++
226 -
227 - if c.sessionMaxRequestPayloadBytes() <= prevReq &&
228 - c.sessionMaxResponsePayloadBytes() <= prevResp &&
229 - c.config.MaxRequestPayloadBytes <= prevCfgReq &&
230 - c.config.MaxResponsePayloadBytes <= prevCfgResp {
231 - c.disconnect()
232 - c.state = StateBroken
233 - c.errorCount++
234 - return firstErr
235 - }
236 - }
237 -}
238 -
239 -// doRawCall sends a request and receives/validates the response envelope.
240 -// Returns the validated response header and a borrowed payload view.
241 -func (c *Client) doRawCall(methodCode uint16, reqPayload []byte) (protocol.Header, []byte, error) {
242 - hdr := protocol.Header{
243 - Kind: protocol.KindRequest,
244 - Code: methodCode,
245 - Flags: 0,
246 - ItemCount: 1,
247 - MessageID: uint64(c.callCount) + 1,
248 - TransportStatus: protocol.StatusOK,
249 - }
250 -
251 - if err := c.transportSend(&hdr, reqPayload); err != nil {
252 - return protocol.Header{}, nil, err
253 - }
254 -
255 - respHdr, payload, err := c.transportReceive()
256 - if err != nil {
257 - return protocol.Header{}, nil, err
258 - }
259 -
260 - if respHdr.Kind != protocol.KindResponse {
261 - return protocol.Header{}, nil, protocol.ErrBadKind
262 - }
263 - if respHdr.Code != methodCode {
264 - return protocol.Header{}, nil, protocol.ErrBadLayout
265 - }
266 - if respHdr.MessageID != hdr.MessageID {
267 - return protocol.Header{}, nil, protocol.ErrBadLayout
268 - }
269 - switch respHdr.TransportStatus {
270 - case protocol.StatusOK:
271 - case protocol.StatusLimitExceeded:
272 - if current := c.sessionMaxResponsePayloadBytes(); current > 0 {
273 - if current >= ^uint32(0)/2 {
274 - c.noteResponseCapacity(^uint32(0))
275 - } else {
276 - c.noteResponseCapacity(current * 2)
277 - }
278 - }
279 - return protocol.Header{}, nil, protocol.ErrOverflow
280 - default:
281 - return protocol.Header{}, nil, protocol.ErrBadLayout
282 - }
283 -
284 - return respHdr, payload, nil
285 -}
286 -
287 -// CallSnapshot performs a blocking typed cgroups snapshot call.
288 -func (c *Client) CallSnapshot() (*protocol.CgroupsResponseView, error) {
289 - if err := c.validateMethod(protocol.MethodCgroupsSnapshot); err != nil {
290 - return nil, err
291 - }
292 -
293 - var result *protocol.CgroupsResponseView
294 -
295 - err := c.callWithRetry(func() error {
296 - req := protocol.CgroupsRequest{LayoutVersion: 1, Flags: 0}
297 - var reqBuf [4]byte
298 - if req.Encode(reqBuf[:]) == 0 {
299 - return protocol.ErrTruncated
300 - }
301 -
302 - _, payload, rerr := c.doRawCall(protocol.MethodCgroupsSnapshot, reqBuf[:])
303 - if rerr != nil {
304 - return rerr
305 - }
306 -
307 - view, derr := protocol.DecodeCgroupsResponse(payload)
308 - if derr != nil {
309 - return derr
310 - }
311 - result = &view
312 - return nil
313 - })
314 - if err != nil {
315 - return nil, err
316 - }
317 - return result, nil
318 -}
319 -
320 -// CallIncrement performs a blocking INCREMENT call.
321 -// Sends requestValue, returns the server's response value.
322 -func (c *Client) CallIncrement(requestValue uint64) (uint64, error) {
323 - if err := c.validateMethod(protocol.MethodIncrement); err != nil {
324 - return 0, err
325 - }
326 -
327 - var result uint64
328 -
329 - err := c.callWithRetry(func() error {
330 - var reqBuf [protocol.IncrementPayloadSize]byte
331 - if protocol.IncrementEncode(requestValue, reqBuf[:]) == 0 {
332 - return protocol.ErrTruncated
333 - }
334 -
335 - _, payload, rerr := c.doRawCall(protocol.MethodIncrement, reqBuf[:])
336 - if rerr != nil {
337 - return rerr
338 - }
339 -
340 - val, derr := protocol.IncrementDecode(payload)
341 - if derr != nil {
342 - return derr
343 - }
344 - result = val
345 - return nil
346 - })
347 - return result, err
348 -}
349 -
350 -// CallStringReverse performs a blocking STRING_REVERSE call.
351 -// Sends requestStr, returns the server's reversed string view.
352 -func (c *Client) CallStringReverse(requestStr string) (*protocol.StringReverseView, error) {
353 - if err := c.validateMethod(protocol.MethodStringReverse); err != nil {
354 - return nil, err
355 - }
356 -
357 - var result *protocol.StringReverseView
358 -
359 - err := c.callWithRetry(func() error {
360 - reqBuf := ensureClientScratch(&c.requestBuf, protocol.StringReverseHdrSize+len(requestStr)+1)
361 - if protocol.StringReverseEncode(requestStr, reqBuf) == 0 {
362 - return protocol.ErrTruncated
363 - }
364 -
365 - _, payload, rerr := c.doRawCall(protocol.MethodStringReverse, reqBuf)
366 - if rerr != nil {
367 - return rerr
368 - }
369 -
370 - view, derr := protocol.StringReverseDecode(payload)
371 - if derr != nil {
372 - return derr
373 - }
374 - result = &view
375 - return nil
376 - })
377 - if err != nil {
378 - return nil, err
379 - }
380 - return result, nil
381 -}
382 -
383 -// CallIncrementBatch performs a blocking batch INCREMENT call.
384 -// Sends multiple values, returns the server's response values.
385 -func (c *Client) CallIncrementBatch(values []uint64) ([]uint64, error) {
386 - if err := c.validateMethod(protocol.MethodIncrement); err != nil {
387 - return nil, err
388 - }
389 -
390 - if len(values) == 0 {
391 - return nil, nil
392 - }
393 -
394 - var results []uint64
395 - itemCount := uint32(len(values))
396 -
397 - err := c.callWithRetry(func() error {
398 - // Build batch request payload
399 - batchBufSize := protocol.Align8(int(itemCount)*8) + int(itemCount)*protocol.IncrementPayloadSize + int(itemCount)*protocol.Alignment
400 - batchBuf := ensureClientScratch(&c.requestBuf, batchBufSize)
401 - bb := protocol.NewBatchBuilder(batchBuf, itemCount)
402 -
403 - for _, v := range values {
404 - var item [protocol.IncrementPayloadSize]byte
405 - if protocol.IncrementEncode(v, item[:]) == 0 {
406 - return protocol.ErrTruncated
407 - }
408 - if err := bb.Add(item[:]); err != nil {
409 - return err
410 - }
411 - }
412 -
413 - totalPayloadLen, _ := bb.Finish()
414 - reqPayload := batchBuf[:totalPayloadLen]
415 -
416 - // Build and send header with batch flags
417 - hdr := protocol.Header{
418 - Kind: protocol.KindRequest,
419 - Code: protocol.MethodIncrement,
420 - Flags: protocol.FlagBatch,
421 - ItemCount: itemCount,
422 - MessageID: uint64(c.callCount) + 1,
423 - TransportStatus: protocol.StatusOK,
424 - }
425 -
426 - if err := c.transportSend(&hdr, reqPayload); err != nil {
427 - return err
428 - }
429 -
430 - // Receive response
431 - respHdr, respPayload, err := c.transportReceive()
432 - if err != nil {
433 - return err
434 - }
435 -
436 - if respHdr.Kind != protocol.KindResponse {
437 - return protocol.ErrBadKind
438 - }
439 - if respHdr.Code != protocol.MethodIncrement {
440 - return protocol.ErrBadLayout
441 - }
442 - if respHdr.MessageID != hdr.MessageID {
443 - return protocol.ErrBadLayout
444 - }
445 - switch respHdr.TransportStatus {
446 - case protocol.StatusOK:
447 - case protocol.StatusLimitExceeded:
448 - if current := c.sessionMaxResponsePayloadBytes(); current > 0 {
449 - if current >= ^uint32(0)/2 {
450 - c.noteResponseCapacity(^uint32(0))
451 - } else {
452 - c.noteResponseCapacity(current * 2)
453 - }
454 - }
455 - return protocol.ErrOverflow
456 - default:
457 - return protocol.ErrBadLayout
458 - }
459 - if respHdr.Flags&protocol.FlagBatch == 0 || respHdr.ItemCount != itemCount {
460 - return protocol.ErrBadItemCount
461 - }
462 -
463 - // Extract each response item
464 - out := make([]uint64, itemCount)
465 - for i := uint32(0); i < itemCount; i++ {
466 - itemData, gerr := protocol.BatchItemGet(respPayload, itemCount, i)
467 - if gerr != nil {
468 - return gerr
469 - }
470 - val, derr := protocol.IncrementDecode(itemData)
471 - if derr != nil {
472 - return derr
473 - }
474 - out[i] = val
475 - }
476 - results = out
477 - return nil
478 - })
479 - return results, err
480 -}
481 -
482 -// Close tears down the connection and releases resources.
483 -func (c *Client) Close() {
484 - c.disconnect()
485 - c.state = StateDisconnected
486 -}
487 -
488 -// ------------------------------------------------------------------
489 -// Internal helpers
490 -// ------------------------------------------------------------------
491 -
44 func (c *Client) disconnect() {
45 if c.shm != nil {
46 c.shm.WinShmClose()
@@ -648,474 +200,3 @@ func isAuthError(err error) bool {
200 func isIncompatibleError(err error) bool {
201 return errors.Is(err, windows.ErrNoProfile) || errors.Is(err, windows.ErrIncompatible)
202 }
651 -
652 -// ---------------------------------------------------------------------------
653 -// Managed server
654 -// ---------------------------------------------------------------------------
655 -
656 -// Server is an internal managed server bound to one expected request kind.
657 -type Server struct {
658 - runDir string
659 - serviceName string
660 - config windows.ServerConfig
661 - expectedMethodCode uint16
662 - handler DispatchHandler
663 - running atomic.Bool
664 - learnedRequestPayloadBytes atomic.Uint32
665 - learnedResponsePayloadBytes atomic.Uint32
666 - nextSessionID atomic.Uint64
667 - workerCount int
668 - wg sync.WaitGroup
669 - listener *windows.Listener // stored so Stop() can close it
670 -}
671 -
672 -// NewServer creates a new managed server.
673 -func NewServer(
674 - runDir, serviceName string,
675 - config windows.ServerConfig,
676 - expectedMethodCode uint16,
677 - handler DispatchHandler,
678 -) *Server {
679 - return NewServerWithWorkers(runDir, serviceName, config, expectedMethodCode, handler, 8)
680 -}
681 -
682 -// NewServerWithWorkers creates a server with an explicit worker count limit.
683 -func NewServerWithWorkers(
684 - runDir, serviceName string,
685 - config windows.ServerConfig,
686 - expectedMethodCode uint16,
687 - handler DispatchHandler,
688 - workerCount int,
689 -) *Server {
690 - if workerCount < 1 {
691 - workerCount = 1
692 - }
693 - learnedRequest := config.MaxRequestPayloadBytes
694 - if learnedRequest == 0 {
695 - learnedRequest = protocol.MaxPayloadDefault
696 - }
697 - learnedResponse := config.MaxResponsePayloadBytes
698 - if learnedResponse == 0 {
699 - learnedResponse = protocol.MaxPayloadDefault
700 - }
701 - s := &Server{
702 - runDir: runDir,
703 - serviceName: serviceName,
704 - config: config,
705 - expectedMethodCode: expectedMethodCode,
706 - handler: handler,
707 - workerCount: workerCount,
708 - }
709 - s.learnedRequestPayloadBytes.Store(learnedRequest)
710 - s.learnedResponsePayloadBytes.Store(learnedResponse)
711 - // Session ids are 1-based; prepareAcceptConfig() allocates with Add(1).
712 - s.nextSessionID.Store(0)
713 - return s
714 -}
715 -
716 -func (s *Server) dispatchSingle(methodCode uint16, request []byte, responseBuf []byte) (int, error) {
717 - if methodCode != s.expectedMethodCode || s.handler == nil {
718 - return 0, errHandlerFailed
719 - }
720 -
721 - return s.handler(request, responseBuf)
722 -}
723 -
724 -func (s *Server) methodSupported(methodCode uint16) bool {
725 - return s.handler != nil && methodCode == s.expectedMethodCode
726 -}
727 -
728 -func serverNotePayloadCapacity(target *atomic.Uint32, payloadLen uint32) {
729 - grown := nextPowerOf2U32(payloadLen)
730 - for {
731 - current := target.Load()
732 - if grown <= current {
733 - return
734 - }
735 - if target.CompareAndSwap(current, grown) {
736 - return
737 - }
738 - }
739 -}
740 -
741 -type preparedWinShm struct {
742 - hybrid *windows.WinShmContext
743 - busywait *windows.WinShmContext
744 -}
745 -
746 -func (p *preparedWinShm) take(profile uint32) *windows.WinShmContext {
747 - if p == nil {
748 - return nil
749 - }
750 - switch profile {
751 - case windows.WinShmProfileHybrid:
752 - ctx := p.hybrid
753 - p.hybrid = nil
754 - return ctx
755 - case windows.WinShmProfileBusywait:
756 - ctx := p.busywait
757 - p.busywait = nil
758 - return ctx
759 - default:
760 - return nil
761 - }
762 -}
763 -
764 -func (p *preparedWinShm) destroyAll() {
765 - if p == nil {
766 - return
767 - }
768 - if p.hybrid != nil {
769 - p.hybrid.WinShmDestroy()
770 - p.hybrid = nil
771 - }
772 - if p.busywait != nil {
773 - p.busywait.WinShmDestroy()
774 - p.busywait = nil
775 - }
776 -}
777 -
778 -const winShmProfiles = windows.WinShmProfileHybrid | windows.WinShmProfileBusywait
779 -
780 -func (s *Server) prepareAcceptConfig() (uint64, windows.ServerConfig, *preparedWinShm, bool) {
781 - sessionID := s.nextSessionID.Add(1)
782 - cfg := s.config
783 - cfg.MaxRequestPayloadBytes = s.learnedRequestPayloadBytes.Load()
784 - cfg.MaxResponsePayloadBytes = s.learnedResponsePayloadBytes.Load()
785 -
786 - if cfg.SupportedProfiles&winShmProfiles == 0 {
787 - return sessionID, cfg, nil, true
788 - }
789 -
790 - prepared := &preparedWinShm{}
791 - for _, profile := range []uint32{windows.WinShmProfileHybrid, windows.WinShmProfileBusywait} {
792 - if cfg.SupportedProfiles&profile == 0 {
793 - continue
794 - }
795 - shm, err := windows.WinShmServerCreate(
796 - s.runDir, s.serviceName,
797 - cfg.AuthToken,
798 - sessionID,
799 - profile,
800 - cfg.MaxRequestPayloadBytes+uint32(protocol.HeaderSize),
801 - cfg.MaxResponsePayloadBytes+uint32(protocol.HeaderSize),
802 - )
803 - if err != nil {
804 - cfg.SupportedProfiles &^= profile
805 - cfg.PreferredProfiles &^= profile
806 - continue
807 - }
808 - if profile == windows.WinShmProfileHybrid {
809 - prepared.hybrid = shm
810 - } else {
811 - prepared.busywait = shm
812 - }
813 - }
814 -
815 - if cfg.SupportedProfiles == 0 {
816 - prepared.destroyAll()
817 - return sessionID, cfg, nil, false
818 - }
819 -
820 - if prepared.hybrid == nil && prepared.busywait == nil {
821 - return sessionID, cfg, nil, true
822 - }
823 -
824 - return sessionID, cfg, prepared, true
825 -}
826 -
827 -// Run starts the acceptor loop. Blocking.
828 -func (s *Server) Run() error {
829 - listener, err := windows.Listen(s.runDir, s.serviceName, s.config)
830 - if err != nil {
831 - return err
832 - }
833 - s.listener = listener
834 - defer func() {
835 - listener.Close()
836 - s.listener = nil
837 - }()
838 -
839 - s.running.Store(true)
840 - sem := make(chan struct{}, s.workerCount)
841 -
842 - for s.running.Load() {
843 - sessionID, acceptCfg, preparedShm, ok := s.prepareAcceptConfig()
844 - if !ok {
845 - time.Sleep(10 * time.Millisecond)
846 - continue
847 - }
848 -
849 - session, err := listener.AcceptWithConfig(sessionID, acceptCfg)
850 - if err != nil {
851 - if preparedShm != nil {
852 - preparedShm.destroyAll()
853 - }
854 - if !s.running.Load() {
855 - break
856 - }
857 - time.Sleep(10 * time.Millisecond)
858 - continue
859 - }
860 -
861 - select {
862 - case sem <- struct{}{}:
863 - default:
864 - if preparedShm != nil {
865 - preparedShm.destroyAll()
866 - }
867 - session.Close()
868 - continue
869 - }
870 -
871 - var shm *windows.WinShmContext
872 - if session.SelectedProfile == windows.WinShmProfileHybrid ||
873 - session.SelectedProfile == windows.WinShmProfileBusywait {
874 - shm = preparedShm.take(session.SelectedProfile)
875 - if shm == nil {
876 - if preparedShm != nil {
877 - preparedShm.destroyAll()
878 - }
879 - session.Close()
880 - <-sem
881 - continue
882 - }
883 - }
884 - if preparedShm != nil {
885 - preparedShm.destroyAll()
886 - }
887 -
888 - s.wg.Add(1)
889 - go func(sess *windows.Session, shmCtx *windows.WinShmContext) {
890 - defer func() {
891 - if r := recover(); r != nil {
892 - // Session handler panicked; log but don't crash the server
893 - }
894 - <-sem
895 - s.wg.Done()
896 - }()
897 - s.handleSession(sess, shmCtx)
898 - }(session, shm)
899 - }
900 -
901 - s.wg.Wait()
902 - return nil
903 -}
904 -
905 -// Stop signals the server to stop and unblocks Accept by closing the listener.
906 -func (s *Server) Stop() {
907 - s.running.Store(false)
908 - if s.listener != nil {
909 - s.listener.Close()
910 - }
911 -}
912 -
913 -func (s *Server) handleSession(session *windows.Session, shm *windows.WinShmContext) {
914 - recvBuf := make([]byte, protocol.HeaderSize+int(session.MaxRequestPayloadBytes))
915 - respBuf := make([]byte, int(session.MaxResponsePayloadBytes))
916 - itemRespBuf := make([]byte, int(session.MaxResponsePayloadBytes))
917 - msgBuf := make([]byte, int(session.MaxResponsePayloadBytes)+protocol.HeaderSize)
918 -
919 - defer func() {
920 - if shm != nil {
921 - shm.WinShmDestroy()
922 - }
923 - session.Close()
924 - }()
925 -
926 - for s.running.Load() {
927 - var hdr protocol.Header
928 - var payload []byte
929 -
930 - if shm != nil {
931 - mlen, err := shm.WinShmReceive(recvBuf, serverPollTimeoutMs)
932 - if err != nil {
933 - if err == windows.ErrWinShmTimeout {
934 - continue
935 - }
936 - return
937 - }
938 - if mlen < protocol.HeaderSize {
939 - return
940 - }
941 - h, err := protocol.DecodeHeader(recvBuf[:mlen])
942 - if err != nil {
943 - return
944 - }
945 - hdr = h
946 - payload = recvBuf[protocol.HeaderSize:mlen]
947 - } else {
948 - // Named Pipe path
949 - ready, waitErr := session.WaitReadable(serverPollTimeoutMs)
950 - if waitErr != nil {
951 - return
952 - }
953 - if !ready {
954 - continue
955 - }
956 - h, p, err := session.Receive(recvBuf)
957 - if err != nil {
958 - return
959 - }
960 - hdr = h
961 - payload = p
962 - }
963 -
964 - // Protocol violation: unexpected message kind terminates session
965 - if hdr.Kind != protocol.KindRequest {
966 - return
967 - }
968 -
969 - if len(payload) <= int(^uint32(0)) {
970 - serverNotePayloadCapacity(&s.learnedRequestPayloadBytes, uint32(len(payload)))
971 - }
972 -
973 - if !s.methodSupported(hdr.Code) {
974 - respHdr := protocol.Header{
975 - Kind: protocol.KindResponse,
976 - Code: hdr.Code,
977 - MessageID: hdr.MessageID,
978 - TransportStatus: protocol.StatusUnsupported,
979 - ItemCount: 1,
980 - }
981 -
982 - if shm != nil {
983 - if len(msgBuf) < protocol.HeaderSize {
984 - msgBuf = make([]byte, protocol.HeaderSize)
985 - }
986 - msg := msgBuf[:protocol.HeaderSize]
987 - respHdr.Magic = protocol.MagicMsg
988 - respHdr.Version = protocol.Version
989 - respHdr.HeaderLen = protocol.HeaderLen
990 - respHdr.PayloadLen = 0
991 - respHdr.Encode(msg[:protocol.HeaderSize])
992 - if err := shm.WinShmSend(msg); err != nil {
993 - return
994 - }
995 - } else if err := session.Send(&respHdr, nil); err != nil {
996 - return
997 - }
998 - continue
999 - }
1000 -
1001 - // Dispatch: single-item or batch
1002 - responseLen := 0
1003 - isBatch := (hdr.Flags&protocol.FlagBatch != 0) && hdr.ItemCount >= 1
1004 - var dispatchErr error
1005 -
1006 - if !isBatch {
1007 - var derr error
1008 - responseLen, derr = s.dispatchSingle(hdr.Code, payload, respBuf)
1009 - if derr != nil {
1010 - dispatchErr = derr
1011 - responseLen = 0
1012 - } else if responseLen < 0 || responseLen > len(respBuf) {
1013 - dispatchErr = protocol.ErrOverflow
1014 - responseLen = 0
1015 - }
1016 - } else {
1017 - var bb protocol.BatchBuilder
1018 - bb.Reset(respBuf, hdr.ItemCount)
1019 -
1020 - for i := uint32(0); i < hdr.ItemCount && dispatchErr == nil; i++ {
1021 - itemData, gerr := protocol.BatchItemGet(payload, hdr.ItemCount, i)
1022 - if gerr != nil {
1023 - dispatchErr = gerr
1024 - break
1025 - }
1026 -
1027 - itemResultLen, derr := s.dispatchSingle(hdr.Code, itemData, itemRespBuf)
1028 - if derr != nil {
1029 - dispatchErr = derr
1030 - break
1031 - }
1032 - if itemResultLen < 0 || itemResultLen > len(itemRespBuf) {
1033 - dispatchErr = protocol.ErrOverflow
1034 - break
1035 - }
1036 -
1037 - if aerr := bb.Add(itemRespBuf[:itemResultLen]); aerr != nil {
1038 - dispatchErr = aerr
1039 - break
1040 - }
1041 - }
1042 -
1043 - if dispatchErr == nil {
1044 - responseLen, _ = bb.Finish()
1045 - }
1046 - }
1047 -
1048 - // Build response header
1049 - respHdr := protocol.Header{
1050 - Kind: protocol.KindResponse,
1051 - Code: hdr.Code,
1052 - MessageID: hdr.MessageID,
1053 - }
1054 -
1055 - if dispatchErr == nil {
1056 - if responseLen <= int(^uint32(0)) {
1057 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, uint32(responseLen))
1058 - }
1059 - respHdr.TransportStatus = protocol.StatusOK
1060 - if isBatch {
1061 - respHdr.Flags = protocol.FlagBatch
1062 - respHdr.ItemCount = hdr.ItemCount
1063 - } else {
1064 - respHdr.ItemCount = 1
1065 - }
1066 - } else if errors.Is(dispatchErr, protocol.ErrOverflow) {
1067 - current := session.MaxResponsePayloadBytes
1068 - if current >= ^uint32(0)/2 {
1069 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, ^uint32(0))
1070 - } else {
1071 - serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, current*2)
1072 - }
1073 - respHdr.TransportStatus = protocol.StatusLimitExceeded
1074 - respHdr.ItemCount = 1
1075 - responseLen = 0
1076 - } else if errors.Is(dispatchErr, errHandlerFailed) {
1077 - respHdr.TransportStatus = protocol.StatusInternalError
1078 - respHdr.ItemCount = 1
1079 - responseLen = 0
1080 - } else {
1081 - respHdr.TransportStatus = protocol.StatusBadEnvelope
1082 - respHdr.ItemCount = 1
1083 - responseLen = 0
1084 - }
1085 -
1086 - if shm != nil {
1087 - msgLen := protocol.HeaderSize + responseLen
1088 - if len(msgBuf) < msgLen {
1089 - msgBuf = make([]byte, msgLen)
1090 - }
1091 - msg := msgBuf[:msgLen]
1092 -
1093 - respHdr.Magic = protocol.MagicMsg
1094 - respHdr.Version = protocol.Version
1095 - respHdr.HeaderLen = protocol.HeaderLen
1096 - respHdr.PayloadLen = uint32(responseLen)
1097 -
1098 - respHdr.Encode(msg[:protocol.HeaderSize])
1099 - if responseLen > 0 {
1100 - copy(msg[protocol.HeaderSize:], respBuf[:responseLen])
1101 - }
1102 -
1103 - if err := shm.WinShmSend(msg); err != nil {
1104 - return
1105 - }
1106 - if respHdr.TransportStatus == protocol.StatusLimitExceeded {
1107 - return
1108 - }
1109 - } else {
1110 - if err := session.Send(&respHdr, respBuf[:responseLen]); err != nil {
1111 - return
1112 - }
1113 - if respHdr.TransportStatus == protocol.StatusLimitExceeded {
1114 - return
1115 - }
1116 - }
1117 - }
1118 -}
1119 -
1120 -// Suppress unused import warnings.
1121 -var _ = binary.NativeEndian
src/go/pkg/netipc/service/raw/dispatch.go new
+9
@@ -0,0 +1,9 @@
1 +package raw
2 +
3 +import "errors"
4 +
5 +// DispatchHandler validates/decodes a single service kind request and writes
6 +// the matching response into responseBuf.
7 +type DispatchHandler func(request []byte, responseBuf []byte) (int, error)
8 +
9 +var errHandlerFailed = errors.New("dispatch handler failed")
src/go/pkg/netipc/service/raw/increment.go new
+179
@@ -0,0 +1,179 @@
1 +package raw
2 +
3 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
4 +
5 +// IncrementHandler serves a single INCREMENT service kind.
6 +type IncrementHandler func(uint64) (uint64, bool)
7 +
8 +// CallIncrement performs a blocking INCREMENT call.
9 +// Sends requestValue, returns the server's response value.
10 +func (c *Client) CallIncrement(requestValue uint64) (uint64, error) {
11 + if err := c.validateMethod(protocol.MethodIncrement); err != nil {
12 + return 0, err
13 + }
14 +
15 + var result uint64
16 +
17 + err := c.callWithRetry(func() error {
18 + var reqBuf [protocol.IncrementPayloadSize]byte
19 + if protocol.IncrementEncode(requestValue, reqBuf[:]) == 0 {
20 + return protocol.ErrTruncated
21 + }
22 +
23 + _, payload, rerr := c.doRawCall(protocol.MethodIncrement, reqBuf[:])
24 + if rerr != nil {
25 + return rerr
26 + }
27 +
28 + val, derr := protocol.IncrementDecode(payload)
29 + if derr != nil {
30 + return derr
31 + }
32 + result = val
33 + return nil
34 + })
35 + return result, err
36 +}
37 +
38 +// CallIncrementBatch performs a blocking batch INCREMENT call.
39 +// Sends multiple values, returns the server's response values.
40 +func (c *Client) CallIncrementBatch(values []uint64) ([]uint64, error) {
41 + if err := c.validateMethod(protocol.MethodIncrement); err != nil {
42 + return nil, err
43 + }
44 +
45 + if len(values) == 0 {
46 + return nil, nil
47 + }
48 +
49 + var results []uint64
50 + itemCount, err := checkedLookupU32(len(values))
51 + if err != nil {
52 + return nil, err
53 + }
54 +
55 + err = c.callWithRetry(func() error {
56 + dirBytes, err := checkedLookupMul(len(values), 8)
57 + if err != nil {
58 + return err
59 + }
60 + dirAligned, err := checkedLookupAlign8(dirBytes)
61 + if err != nil {
62 + return err
63 + }
64 + itemsBytes, err := checkedLookupMul(len(values), protocol.IncrementPayloadSize)
65 + if err != nil {
66 + return err
67 + }
68 + paddingBytes, err := checkedLookupMul(len(values), protocol.Alignment)
69 + if err != nil {
70 + return err
71 + }
72 + batchBufSize, err := checkedLookupAdd(dirAligned, itemsBytes)
73 + if err != nil {
74 + return err
75 + }
76 + batchBufSize, err = checkedLookupAdd(batchBufSize, paddingBytes)
77 + if err != nil {
78 + return err
79 + }
80 + batchBuf := ensureClientScratch(&c.requestBuf, batchBufSize)
81 + bb := protocol.NewBatchBuilder(batchBuf, itemCount)
82 +
83 + for _, v := range values {
84 + var item [protocol.IncrementPayloadSize]byte
85 + if protocol.IncrementEncode(v, item[:]) == 0 {
86 + return protocol.ErrTruncated
87 + }
88 + if err := bb.Add(item[:]); err != nil {
89 + return err
90 + }
91 + }
92 +
93 + totalPayloadLen, _ := bb.Finish()
94 + reqPayload := batchBuf[:totalPayloadLen]
95 +
96 + hdr := protocol.Header{
97 + Kind: protocol.KindRequest,
98 + Code: protocol.MethodIncrement,
99 + Flags: protocol.FlagBatch,
100 + ItemCount: itemCount,
101 + MessageID: uint64(c.callCount) + 1,
102 + TransportStatus: protocol.StatusOK,
103 + }
104 +
105 + if err := c.transportSend(&hdr, reqPayload); err != nil {
106 + return err
107 + }
108 +
109 + respHdr, respPayload, err := c.transportReceive()
110 + if err != nil {
111 + return err
112 + }
113 +
114 + if respHdr.Kind != protocol.KindResponse {
115 + return protocol.ErrBadKind
116 + }
117 + if respHdr.Code != protocol.MethodIncrement {
118 + return protocol.ErrBadLayout
119 + }
120 + if respHdr.MessageID != hdr.MessageID {
121 + return protocol.ErrBadLayout
122 + }
123 + switch respHdr.TransportStatus {
124 + case protocol.StatusOK:
125 + case protocol.StatusLimitExceeded:
126 + if current := c.sessionMaxResponsePayloadBytes(); current > 0 {
127 + if current >= ^uint32(0)/2 {
128 + c.noteResponseCapacity(^uint32(0))
129 + } else {
130 + c.noteResponseCapacity(current * 2)
131 + }
132 + }
133 + return protocol.ErrOverflow
134 + default:
135 + return protocol.ErrBadLayout
136 + }
137 + if respHdr.Flags&protocol.FlagBatch == 0 || respHdr.ItemCount != itemCount {
138 + return protocol.ErrBadItemCount
139 + }
140 +
141 + out := make([]uint64, itemCount)
142 + for i := range itemCount {
143 + itemData, gerr := protocol.BatchItemGet(respPayload, itemCount, i)
144 + if gerr != nil {
145 + return gerr
146 + }
147 + val, derr := protocol.IncrementDecode(itemData)
148 + if derr != nil {
149 + return derr
150 + }
151 + out[i] = val
152 + }
153 + results = out
154 + return nil
155 + })
156 + return results, err
157 +}
158 +
159 +// IncrementDispatch adapts a typed increment handler to the raw dispatch shape.
160 +func IncrementDispatch(handle IncrementHandler) DispatchHandler {
161 + if handle == nil {
162 + return nil
163 + }
164 + return func(request []byte, responseBuf []byte) (int, error) {
165 + value, err := protocol.IncrementDecode(request)
166 + if err != nil {
167 + return 0, err
168 + }
169 + result, ok := handle(value)
170 + if !ok {
171 + return 0, errHandlerFailed
172 + }
173 + n := protocol.IncrementEncode(result, responseBuf)
174 + if n == 0 {
175 + return 0, protocol.ErrOverflow
176 + }
177 + return n, nil
178 + }
179 +}
src/go/pkg/netipc/service/raw/increment_unix.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +// NewIncrementClient creates a raw client bound to the increment service kind.
11 +func NewIncrementClient(runDir, serviceName string, config posix.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodIncrement)
13 +}
src/go/pkg/netipc/service/raw/increment_windows.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +// NewIncrementClient creates a raw client bound to the increment service kind.
11 +func NewIncrementClient(runDir, serviceName string, config windows.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodIncrement)
13 +}
src/go/pkg/netipc/service/raw/lookup_common.go new
+55
@@ -0,0 +1,55 @@
1 +package raw
2 +
3 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
4 +
5 +func checkedLookupAdd(a, b int) (int, error) {
6 + if a < 0 || b < 0 {
7 + return 0, protocol.ErrOverflow
8 + }
9 + maxInt := int(^uint(0) >> 1)
10 + if a > maxInt-b {
11 + return 0, protocol.ErrOverflow
12 + }
13 + return a + b, nil
14 +}
15 +
16 +func checkedLookupMul(a, b int) (int, error) {
17 + if a < 0 || b < 0 {
18 + return 0, protocol.ErrOverflow
19 + }
20 + maxInt := int(^uint(0) >> 1)
21 + if a != 0 && b > maxInt/a {
22 + return 0, protocol.ErrOverflow
23 + }
24 + return a * b, nil
25 +}
26 +
27 +func checkedLookupAlign8(v int) (int, error) {
28 + if v < 0 {
29 + return 0, protocol.ErrOverflow
30 + }
31 + maxInt := int(^uint(0) >> 1)
32 + if v > maxInt-7 {
33 + return 0, protocol.ErrOverflow
34 + }
35 + return protocol.Align8(v), nil
36 +}
37 +
38 +func checkedLookupU32(value int) (uint32, error) {
39 + if value < 0 || uint64(value) > uint64(^uint32(0)) {
40 + return 0, protocol.ErrOverflow
41 + }
42 + return uint32(value), nil // #nosec G115 -- value is bounded by the uint32 maximum above.
43 +}
44 +
45 +func lookupMinRequired(hdrSize int, itemCount uint32) (int, error) {
46 + if hdrSize < 0 {
47 + return 0, protocol.ErrOverflow
48 + }
49 + dirSize64 := uint64(itemCount) * uint64(protocol.LookupDirEntrySize)
50 + min64 := uint64(hdrSize) + dirSize64
51 + if min64 > uint64(int(^uint(0)>>1)) {
52 + return 0, protocol.ErrOverflow
53 + }
54 + return int(min64), nil
55 +}
src/go/pkg/netipc/service/raw/lookup_unix_test.go new
+407
@@ -0,0 +1,407 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "fmt"
7 + "sync"
8 + "testing"
9 + "time"
10 +
11 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
12 +)
13 +
14 +func testCgroupsLookupHandler(req *protocol.CgroupsLookupRequestView, builder *protocol.CgroupsLookupBuilder) bool {
15 + for i := uint32(0); i < req.ItemCount; i++ {
16 + path, err := req.Item(i)
17 + if err != nil {
18 + return false
19 + }
20 + if path.String() == "/known" {
21 + if err := builder.Add(
22 + protocol.CgroupLookupKnown,
23 + protocol.OrchestratorK8s,
24 + path.Bytes(),
25 + []byte("pod-a"),
26 + []struct{ Key, Value []byte }{
27 + {Key: []byte("namespace"), Value: []byte("default")},
28 + },
29 + ); err != nil {
30 + return false
31 + }
32 + } else {
33 + if err := builder.Add(
34 + protocol.CgroupLookupUnknownRetryLater,
35 + 0,
36 + path.Bytes(),
37 + nil,
38 + nil,
39 + ); err != nil {
40 + return false
41 + }
42 + }
43 + }
44 + return true
45 +}
46 +
47 +func testAppsLookupHandler(req *protocol.AppsLookupRequestView, builder *protocol.AppsLookupBuilder) bool {
48 + for i := uint32(0); i < req.ItemCount; i++ {
49 + pid, err := req.Item(i)
50 + if err != nil {
51 + return false
52 + }
53 + switch pid {
54 + case 1234:
55 + if err := builder.Add(
56 + protocol.PidLookupKnown,
57 + protocol.AppsCgroupKnown,
58 + protocol.OrchestratorDocker,
59 + pid,
60 + 1,
61 + 1000,
62 + 42,
63 + []byte("nginx"),
64 + []byte("/docker/abc"),
65 + []byte("container-a"),
66 + []struct{ Key, Value []byte }{
67 + {Key: []byte("image"), Value: []byte("nginx:latest")},
68 + },
69 + ); err != nil {
70 + return false
71 + }
72 + case 0:
73 + if err := builder.Add(
74 + protocol.PidLookupKnown,
75 + protocol.AppsCgroupHostRoot,
76 + 0,
77 + pid,
78 + 0,
79 + 0,
80 + 0,
81 + []byte("swapper"),
82 + nil,
83 + nil,
84 + nil,
85 + ); err != nil {
86 + return false
87 + }
88 + default:
89 + if err := builder.Add(
90 + protocol.PidLookupUnknown,
91 + protocol.AppsCgroupKnown,
92 + 0,
93 + pid,
94 + 0,
95 + protocol.NipcUIDUnset,
96 + 0,
97 + nil,
98 + nil,
99 + nil,
100 + nil,
101 + ); err != nil {
102 + return false
103 + }
104 + }
105 + }
106 + return true
107 +}
108 +
109 +func startLookupTestServer(service string, method uint16, handler DispatchHandler) *testServer {
110 + return startLookupTestServerWithWorkers(service, method, handler, 8)
111 +}
112 +
113 +func startLookupTestServerWithWorkers(service string, method uint16, handler DispatchHandler, workers int) *testServer {
114 + ensureRunDir()
115 + cleanupAll(service)
116 +
117 + s := NewServerWithWorkers(testRunDir, service, testServerConfig(), method, handler, workers)
118 + doneCh := make(chan struct{})
119 +
120 + go func() {
121 + defer close(doneCh)
122 + s.Run()
123 + }()
124 +
125 + waitUnixServerReady(service)
126 + return &testServer{server: s, doneCh: doneCh}
127 +}
128 +
129 +func verifyCgroupsLookupView(t *testing.T, view *protocol.CgroupsLookupResponseView) {
130 + t.Helper()
131 + if err := checkCgroupsLookupView(view); err != nil {
132 + t.Fatal(err)
133 + }
134 +}
135 +
136 +func checkCgroupsLookupView(view *protocol.CgroupsLookupResponseView) error {
137 + if view.ItemCount != 2 {
138 + return fmt.Errorf("item count = %d, want 2", view.ItemCount)
139 + }
140 + item0, err := view.Item(0)
141 + if err != nil {
142 + return fmt.Errorf("item 0: %w", err)
143 + }
144 + if item0.Status != protocol.CgroupLookupKnown ||
145 + item0.Orchestrator != protocol.OrchestratorK8s ||
146 + item0.Path.String() != "/known" ||
147 + item0.Name.String() != "pod-a" ||
148 + item0.LabelCount != 1 {
149 + return fmt.Errorf("bad item 0: %+v", item0)
150 + }
151 + item1, err := view.Item(1)
152 + if err != nil {
153 + return fmt.Errorf("item 1: %w", err)
154 + }
155 + if item1.Status != protocol.CgroupLookupUnknownRetryLater || item1.Path.String() != "/missing" {
156 + return fmt.Errorf("bad item 1: %+v", item1)
157 + }
158 + return nil
159 +}
160 +
161 +func verifyAppsLookupView(t *testing.T, view *protocol.AppsLookupResponseView) {
162 + t.Helper()
163 + if err := checkAppsLookupView(view); err != nil {
164 + t.Fatal(err)
165 + }
166 +}
167 +
168 +func checkAppsLookupView(view *protocol.AppsLookupResponseView) error {
169 + if view.ItemCount != 3 {
170 + return fmt.Errorf("item count = %d, want 3", view.ItemCount)
171 + }
172 + item0, err := view.Item(0)
173 + if err != nil {
174 + return fmt.Errorf("item 0: %w", err)
175 + }
176 + if item0.Pid != 1234 ||
177 + item0.Status != protocol.PidLookupKnown ||
178 + item0.CgroupStatus != protocol.AppsCgroupKnown ||
179 + item0.Comm.String() != "nginx" ||
180 + item0.CgroupPath.String() != "/docker/abc" ||
181 + item0.LabelCount != 1 {
182 + return fmt.Errorf("bad item 0: %+v", item0)
183 + }
184 + item1, err := view.Item(1)
185 + if err != nil {
186 + return fmt.Errorf("item 1: %w", err)
187 + }
188 + if item1.Pid != 0 || item1.CgroupStatus != protocol.AppsCgroupHostRoot || item1.CgroupPath.Len() != 0 {
189 + return fmt.Errorf("bad item 1: %+v", item1)
190 + }
191 + item2, err := view.Item(2)
192 + if err != nil {
193 + return fmt.Errorf("item 2: %w", err)
194 + }
195 + if item2.Pid != 9999 || item2.Status != protocol.PidLookupUnknown || item2.Uid != protocol.NipcUIDUnset {
196 + return fmt.Errorf("bad item 2: %+v", item2)
197 + }
198 + return nil
199 +}
200 +
201 +func TestCgroupsLookupCall(t *testing.T) {
202 + svc := "go_svc_cgroups_lookup"
203 + ts := startLookupTestServer(svc, protocol.MethodCgroupsLookup, CgroupsLookupDispatch(testCgroupsLookupHandler))
204 + defer ts.stop()
205 +
206 + client := NewCgroupsLookupClient(testRunDir, svc, testClientConfig())
207 + client.Refresh()
208 + if !client.Ready() {
209 + t.Fatal("client not ready")
210 + }
211 +
212 + view, err := client.CallCgroupsLookup([][]byte{[]byte("/known"), []byte("/missing")})
213 + if err != nil {
214 + t.Fatalf("call failed: %v", err)
215 + }
216 + verifyCgroupsLookupView(t, view)
217 +}
218 +
219 +func TestAppsLookupCall(t *testing.T) {
220 + svc := "go_svc_apps_lookup"
221 + ts := startLookupTestServer(svc, protocol.MethodAppsLookup, AppsLookupDispatch(testAppsLookupHandler))
222 + defer ts.stop()
223 +
224 + client := NewAppsLookupClient(testRunDir, svc, testClientConfig())
225 + client.Refresh()
226 + if !client.Ready() {
227 + t.Fatal("client not ready")
228 + }
229 +
230 + view, err := client.CallAppsLookup([]uint32{1234, 0, 9999})
231 + if err != nil {
232 + t.Fatalf("call failed: %v", err)
233 + }
234 + verifyAppsLookupView(t, view)
235 +}
236 +
237 +func TestLookupHandlerFailures(t *testing.T) {
238 + t.Run("cgroups", func(t *testing.T) {
239 + svc := uniqueUnixService("go_svc_cgroups_lookup_fail")
240 + ts := startLookupTestServer(svc, protocol.MethodCgroupsLookup, CgroupsLookupDispatch(
241 + func(*protocol.CgroupsLookupRequestView, *protocol.CgroupsLookupBuilder) bool { return false },
242 + ))
243 + defer ts.stop()
244 +
245 + client := NewCgroupsLookupClient(testRunDir, svc, testClientConfig())
246 + defer client.Close()
247 + client.Refresh()
248 + if !client.Ready() {
249 + t.Fatal("client not ready")
250 + }
251 + if _, err := client.CallCgroupsLookup([][]byte{[]byte("/known")}); err == nil {
252 + t.Fatal("expected cgroups lookup handler failure")
253 + }
254 + })
255 +
256 + t.Run("apps", func(t *testing.T) {
257 + svc := uniqueUnixService("go_svc_apps_lookup_fail")
258 + ts := startLookupTestServer(svc, protocol.MethodAppsLookup, AppsLookupDispatch(
259 + func(*protocol.AppsLookupRequestView, *protocol.AppsLookupBuilder) bool { return false },
260 + ))
261 + defer ts.stop()
262 +
263 + client := NewAppsLookupClient(testRunDir, svc, testClientConfig())
264 + defer client.Close()
265 + client.Refresh()
266 + if !client.Ready() {
267 + t.Fatal("client not ready")
268 + }
269 + if _, err := client.CallAppsLookup([]uint32{1234}); err == nil {
270 + t.Fatal("expected apps lookup handler failure")
271 + }
272 + })
273 +}
274 +
275 +func TestCgroupsLookupRetryOnFailure(t *testing.T) {
276 + svc := uniqueUnixService("go_svc_cgroups_lookup_retry")
277 + ts1 := startLookupTestServer(svc, protocol.MethodCgroupsLookup, CgroupsLookupDispatch(testCgroupsLookupHandler))
278 +
279 + client := NewCgroupsLookupClient(testRunDir, svc, testClientConfig())
280 + defer client.Close()
281 + client.Refresh()
282 + if !client.Ready() {
283 + t.Fatal("client not ready")
284 + }
285 +
286 + view, err := client.CallCgroupsLookup([][]byte{[]byte("/known"), []byte("/missing")})
287 + if err != nil {
288 + t.Fatalf("first call failed: %v", err)
289 + }
290 + verifyCgroupsLookupView(t, view)
291 +
292 + ts1.stop()
293 + cleanupAll(svc)
294 + time.Sleep(50 * time.Millisecond)
295 +
296 + ts2 := startLookupTestServer(svc, protocol.MethodCgroupsLookup, CgroupsLookupDispatch(testCgroupsLookupHandler))
297 + defer ts2.stop()
298 +
299 + view, err = client.CallCgroupsLookup([][]byte{[]byte("/known"), []byte("/missing")})
300 + if err != nil {
301 + t.Fatalf("retry call failed: %v", err)
302 + }
303 + verifyCgroupsLookupView(t, view)
304 + if client.Status().ReconnectCount < 1 {
305 + t.Fatalf("expected reconnect_count >= 1, got %d", client.Status().ReconnectCount)
306 + }
307 +}
308 +
309 +func TestLookupConcurrentClients(t *testing.T) {
310 + t.Run("cgroups", func(t *testing.T) {
311 + svc := uniqueUnixService("go_svc_cgroups_lookup_concurrent")
312 + ts := startLookupTestServerWithWorkers(
313 + svc,
314 + protocol.MethodCgroupsLookup,
315 + CgroupsLookupDispatch(testCgroupsLookupHandler),
316 + 16,
317 + )
318 + defer ts.stop()
319 +
320 + const clients = 16
321 + const callsPerClient = 10
322 + var wg sync.WaitGroup
323 + errs := make(chan error, clients*callsPerClient)
324 + for range clients {
325 + wg.Go(func() {
326 + client := NewCgroupsLookupClient(testRunDir, svc, testClientConfig())
327 + defer client.Close()
328 + for range 200 {
329 + client.Refresh()
330 + if client.Ready() {
331 + break
332 + }
333 + time.Sleep(5 * time.Millisecond)
334 + }
335 + if !client.Ready() {
336 + errs <- fmt.Errorf("client not ready")
337 + return
338 + }
339 + for range callsPerClient {
340 + view, err := client.CallCgroupsLookup([][]byte{[]byte("/known"), []byte("/missing")})
341 + if err != nil {
342 + errs <- err
343 + return
344 + }
345 + if err := checkCgroupsLookupView(view); err != nil {
346 + errs <- err
347 + return
348 + }
349 + }
350 + })
351 + }
352 + wg.Wait()
353 + close(errs)
354 + for err := range errs {
355 + t.Fatalf("concurrent cgroups lookup failed: %v", err)
356 + }
357 + })
358 +
359 + t.Run("apps", func(t *testing.T) {
360 + svc := uniqueUnixService("go_svc_apps_lookup_concurrent")
361 + ts := startLookupTestServerWithWorkers(
362 + svc,
363 + protocol.MethodAppsLookup,
364 + AppsLookupDispatch(testAppsLookupHandler),
365 + 16,
366 + )
367 + defer ts.stop()
368 +
369 + const clients = 16
370 + const callsPerClient = 10
371 + var wg sync.WaitGroup
372 + errs := make(chan error, clients*callsPerClient)
373 + for range clients {
374 + wg.Go(func() {
375 + client := NewAppsLookupClient(testRunDir, svc, testClientConfig())
376 + defer client.Close()
377 + for range 200 {
378 + client.Refresh()
379 + if client.Ready() {
380 + break
381 + }
382 + time.Sleep(5 * time.Millisecond)
383 + }
384 + if !client.Ready() {
385 + errs <- fmt.Errorf("client not ready")
386 + return
387 + }
388 + for range callsPerClient {
389 + view, err := client.CallAppsLookup([]uint32{1234, 0, 9999})
390 + if err != nil {
391 + errs <- err
392 + return
393 + }
394 + if err := checkAppsLookupView(view); err != nil {
395 + errs <- err
396 + return
397 + }
398 + }
399 + })
400 + }
401 + wg.Wait()
402 + close(errs)
403 + for err := range errs {
404 + t.Fatalf("concurrent apps lookup failed: %v", err)
405 + }
406 + })
407 +}
src/go/pkg/netipc/service/raw/poll_unix.go new
+65
@@ -0,0 +1,65 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "syscall"
7 + "unsafe"
8 +)
9 +
10 +// poll constants (not exported by Go's syscall package)
11 +const (
12 + _POLLIN = 0x0001
13 + _POLLERR = 0x0008
14 + _POLLHUP = 0x0010
15 + _POLLNVAL = 0x0020
16 +)
17 +
18 +// pollfd matches struct pollfd from <poll.h>.
19 +type pollfd struct {
20 + fd int32
21 + events int16
22 + revents int16
23 +}
24 +
25 +// pollFd polls a file descriptor for readability with a timeout in ms.
26 +// Returns: 1 = data ready, 0 = timeout, -1 = error/hangup.
27 +func pollFd(fd int, timeoutMs int) int {
28 + const maxInt32 = 1<<31 - 1
29 + if fd < 0 || fd > maxInt32 {
30 + return -1
31 + }
32 + pfd := pollfd{
33 + fd: int32(fd), // #nosec G115 -- fd is checked against int32 range above.
34 + events: _POLLIN,
35 + }
36 +
37 + r, _, errno := syscall.Syscall(
38 + syscall.SYS_POLL,
39 + uintptr(unsafe.Pointer(&pfd)), // #nosec G103 -- raw poll syscall requires a pollfd pointer.
40 + 1,
41 + uintptr(timeoutMs),
42 + )
43 +
44 + n := int(r)
45 + if n < 0 {
46 + if errno == syscall.EINTR {
47 + return 0
48 + }
49 + return -1
50 + }
51 +
52 + if n == 0 {
53 + return 0
54 + }
55 +
56 + if pfd.revents&(_POLLERR|_POLLHUP|_POLLNVAL) != 0 {
57 + return -1
58 + }
59 +
60 + if pfd.revents&_POLLIN != 0 {
61 + return 1
62 + }
63 +
64 + return 0
65 +}
src/go/pkg/netipc/service/raw/server.go new
+285
@@ -0,0 +1,285 @@
1 +package raw
2 +
3 +import (
4 + "errors"
5 + "sync/atomic"
6 + "time"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
9 +)
10 +
11 +const defaultServerWorkerCount = 8
12 +
13 +func (s *Server) initCommon(
14 + runDir string,
15 + serviceName string,
16 + expectedMethodCode uint16,
17 + handler DispatchHandler,
18 + workerCount int,
19 + maxRequestPayloadBytes uint32,
20 + maxResponsePayloadBytes uint32,
21 +) {
22 + if workerCount < 1 {
23 + workerCount = 1
24 + }
25 + if maxRequestPayloadBytes == 0 {
26 + maxRequestPayloadBytes = protocol.MaxPayloadDefault
27 + }
28 + if maxResponsePayloadBytes == 0 {
29 + maxResponsePayloadBytes = protocol.MaxPayloadDefault
30 + }
31 +
32 + s.runDir = runDir
33 + s.serviceName = serviceName
34 + s.expectedMethodCode = expectedMethodCode
35 + s.handler = handler
36 + s.workerCount = workerCount
37 + s.learnedRequestPayloadBytes.Store(maxRequestPayloadBytes)
38 + s.learnedResponsePayloadBytes.Store(maxResponsePayloadBytes)
39 +}
40 +
41 +func (s *Server) retryAcceptAfter(cleanup func()) bool {
42 + if cleanup != nil {
43 + cleanup()
44 + }
45 + if !s.running.Load() {
46 + return false
47 + }
48 + time.Sleep(10 * time.Millisecond)
49 + return true
50 +}
51 +
52 +func (s *Server) acquireWorkerSlot(sem chan struct{}, cleanup func(), closeSession func()) bool {
53 + select {
54 + case sem <- struct{}{}:
55 + return true
56 + default:
57 + if cleanup != nil {
58 + cleanup()
59 + }
60 + closeSession()
61 + return false
62 + }
63 +}
64 +
65 +func (s *Server) startSessionWorker(sem chan struct{}, run func()) {
66 + s.wg.Add(1)
67 + go func() {
68 + defer func() {
69 + if recover() != nil {
70 + // Session handler panicked; keep the accept loop alive.
71 + }
72 + <-sem
73 + s.wg.Done()
74 + }()
75 + run()
76 + }()
77 +}
78 +
79 +func (s *Server) dispatchSingle(methodCode uint16, request []byte, responseBuf []byte) (int, error) {
80 + if methodCode != s.expectedMethodCode || s.handler == nil {
81 + return 0, errHandlerFailed
82 + }
83 +
84 + return s.handler(request, responseBuf)
85 +}
86 +
87 +func (s *Server) methodSupported(methodCode uint16) bool {
88 + return s.handler != nil && methodCode == s.expectedMethodCode
89 +}
90 +
91 +func serverNotePayloadCapacity(target *atomic.Uint32, payloadLen uint32) {
92 + grown := nextPowerOf2U32(payloadLen)
93 + for {
94 + current := target.Load()
95 + if grown <= current {
96 + return
97 + }
98 + if target.CompareAndSwap(current, grown) {
99 + return
100 + }
101 + }
102 +}
103 +
104 +type serverReceiveAction uint8
105 +
106 +const (
107 + serverReceiveOK serverReceiveAction = iota
108 + serverReceiveContinue
109 + serverReceiveStop
110 +)
111 +
112 +type serverSessionOps struct {
113 + maxRequestPayloadBytes uint32
114 + maxResponsePayloadBytes uint32
115 + receive func([]byte) (protocol.Header, []byte, serverReceiveAction)
116 + send func(*protocol.Header, []byte, *[]byte) error
117 + close func()
118 +}
119 +
120 +func (s *Server) handleServerSession(ops serverSessionOps) {
121 + recvBuf := make([]byte, protocol.HeaderSize+int(ops.maxRequestPayloadBytes))
122 + respBuf := make([]byte, int(ops.maxResponsePayloadBytes))
123 + itemRespBuf := make([]byte, int(ops.maxResponsePayloadBytes))
124 + msgBuf := make([]byte, int(ops.maxResponsePayloadBytes)+protocol.HeaderSize)
125 +
126 + defer ops.close()
127 +
128 + for s.running.Load() {
129 + hdr, payload, action := ops.receive(recvBuf)
130 + switch action {
131 + case serverReceiveContinue:
132 + continue
133 + case serverReceiveStop:
134 + return
135 + }
136 +
137 + if hdr.Kind != protocol.KindRequest {
138 + return
139 + }
140 +
141 + respHdr, responseLen, closeAfterSend := s.handleServerRequest(
142 + hdr, payload, respBuf, itemRespBuf, ops.maxResponsePayloadBytes)
143 + if err := ops.send(&respHdr, respBuf[:responseLen], &msgBuf); err != nil {
144 + return
145 + }
146 + if closeAfterSend {
147 + return
148 + }
149 + }
150 +}
151 +
152 +func (s *Server) handleServerRequest(
153 + hdr protocol.Header,
154 + payload []byte,
155 + respBuf []byte,
156 + itemRespBuf []byte,
157 + maxResponsePayloadBytes uint32,
158 +) (protocol.Header, int, bool) {
159 + if payloadLen, err := checkedLookupU32(len(payload)); err == nil {
160 + serverNotePayloadCapacity(&s.learnedRequestPayloadBytes, payloadLen)
161 + }
162 +
163 + if !s.methodSupported(hdr.Code) {
164 + return serverUnsupportedResponseHeader(hdr), 0, false
165 + }
166 +
167 + responseLen, isBatch, dispatchErr := s.dispatchServerResponse(hdr, payload, respBuf, itemRespBuf)
168 + respHdr := serverResponseHeader(hdr)
169 +
170 + if dispatchErr == nil {
171 + if responseLen32, err := checkedLookupU32(responseLen); err == nil {
172 + serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, responseLen32)
173 + }
174 + respHdr.TransportStatus = protocol.StatusOK
175 + if isBatch {
176 + respHdr.Flags = protocol.FlagBatch
177 + respHdr.ItemCount = hdr.ItemCount
178 + } else {
179 + respHdr.ItemCount = 1
180 + }
181 + return respHdr, responseLen, false
182 + }
183 +
184 + respHdr.ItemCount = 1
185 + responseLen = 0
186 + switch {
187 + case errors.Is(dispatchErr, protocol.ErrOverflow):
188 + if maxResponsePayloadBytes >= ^uint32(0)/2 {
189 + serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, ^uint32(0))
190 + } else {
191 + serverNotePayloadCapacity(&s.learnedResponsePayloadBytes, maxResponsePayloadBytes*2)
192 + }
193 + respHdr.TransportStatus = protocol.StatusLimitExceeded
194 + return respHdr, responseLen, true
195 + case errors.Is(dispatchErr, errHandlerFailed):
196 + respHdr.TransportStatus = protocol.StatusInternalError
197 + default:
198 + respHdr.TransportStatus = protocol.StatusBadEnvelope
199 + }
200 +
201 + return respHdr, responseLen, false
202 +}
203 +
204 +func (s *Server) dispatchServerResponse(
205 + hdr protocol.Header,
206 + payload []byte,
207 + respBuf []byte,
208 + itemRespBuf []byte,
209 +) (int, bool, error) {
210 + isBatch := (hdr.Flags&protocol.FlagBatch != 0) && hdr.ItemCount >= 1
211 + if !isBatch {
212 + responseLen, err := s.dispatchSingle(hdr.Code, payload, respBuf)
213 + if err != nil {
214 + return 0, false, err
215 + }
216 + if responseLen < 0 || responseLen > len(respBuf) {
217 + return 0, false, protocol.ErrOverflow
218 + }
219 + return responseLen, false, nil
220 + }
221 +
222 + var bb protocol.BatchBuilder
223 + bb.Reset(respBuf, hdr.ItemCount)
224 + for i := uint32(0); i < hdr.ItemCount; i++ {
225 + itemData, err := protocol.BatchItemGet(payload, hdr.ItemCount, i)
226 + if err != nil {
227 + return 0, true, err
228 + }
229 +
230 + itemResultLen, err := s.dispatchSingle(hdr.Code, itemData, itemRespBuf)
231 + if err != nil {
232 + return 0, true, err
233 + }
234 + if itemResultLen < 0 || itemResultLen > len(itemRespBuf) {
235 + return 0, true, protocol.ErrOverflow
236 + }
237 + if err := bb.Add(itemRespBuf[:itemResultLen]); err != nil {
238 + return 0, true, err
239 + }
240 + }
241 +
242 + responseLen, _ := bb.Finish()
243 + return responseLen, true, nil
244 +}
245 +
246 +func serverResponseHeader(hdr protocol.Header) protocol.Header {
247 + return protocol.Header{
248 + Kind: protocol.KindResponse,
249 + Code: hdr.Code,
250 + MessageID: hdr.MessageID,
251 + }
252 +}
253 +
254 +func serverUnsupportedResponseHeader(hdr protocol.Header) protocol.Header {
255 + respHdr := serverResponseHeader(hdr)
256 + respHdr.TransportStatus = protocol.StatusUnsupported
257 + respHdr.ItemCount = 1
258 + return respHdr
259 +}
260 +
261 +func serverEncodeSharedResponse(
262 + respHdr *protocol.Header,
263 + payload []byte,
264 + msgBuf *[]byte,
265 +) ([]byte, error) {
266 + payloadLen, err := checkedLookupU32(len(payload))
267 + if err != nil {
268 + return nil, err
269 + }
270 + msgLen := protocol.HeaderSize + len(payload)
271 + if len(*msgBuf) < msgLen {
272 + *msgBuf = make([]byte, msgLen)
273 + }
274 + msg := (*msgBuf)[:msgLen]
275 +
276 + respHdr.Magic = protocol.MagicMsg
277 + respHdr.Version = protocol.Version
278 + respHdr.HeaderLen = protocol.HeaderLen
279 + respHdr.PayloadLen = payloadLen
280 + respHdr.Encode(msg[:protocol.HeaderSize])
281 + if len(payload) > 0 {
282 + copy(msg[protocol.HeaderSize:], payload)
283 + }
284 + return msg, nil
285 +}
src/go/pkg/netipc/service/raw/server_unix.go new
+227
@@ -0,0 +1,227 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "sync"
7 + "sync/atomic"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
11 +)
12 +
13 +// Server is an internal managed server bound to one expected request kind.
14 +// Supports multiple concurrent client sessions up to workerCount.
15 +type Server struct {
16 + runDir string
17 + serviceName string
18 + config posix.ServerConfig
19 + expectedMethodCode uint16
20 + handler DispatchHandler
21 + running atomic.Bool
22 + learnedRequestPayloadBytes atomic.Uint32
23 + learnedResponsePayloadBytes atomic.Uint32
24 + nextSessionID atomic.Uint64
25 + workerCount int
26 + wg sync.WaitGroup
27 +}
28 +
29 +// NewServer creates a new managed server. workerCount limits the
30 +// maximum number of concurrent client sessions (default 1 if <= 0).
31 +func NewServer(
32 + runDir, serviceName string,
33 + config posix.ServerConfig,
34 + expectedMethodCode uint16,
35 + handler DispatchHandler,
36 +) *Server {
37 + return NewServerWithWorkers(runDir, serviceName, config, expectedMethodCode, handler, defaultServerWorkerCount)
38 +}
39 +
40 +// NewServerWithWorkers creates a server with an explicit worker count limit.
41 +func NewServerWithWorkers(
42 + runDir, serviceName string,
43 + config posix.ServerConfig,
44 + expectedMethodCode uint16,
45 + handler DispatchHandler,
46 + workerCount int,
47 +) *Server {
48 + s := &Server{config: config}
49 + s.initCommon(
50 + runDir,
51 + serviceName,
52 + expectedMethodCode,
53 + handler,
54 + workerCount,
55 + config.MaxRequestPayloadBytes,
56 + config.MaxResponsePayloadBytes,
57 + )
58 + return s
59 +}
60 +
61 +const posixShmProfiles = protocol.ProfileSHMHybrid | protocol.ProfileSHMFutex
62 +
63 +func (s *Server) prepareAcceptConfig() (uint64, posix.ServerConfig, *posix.ShmContext, bool) {
64 + sessionID := s.nextSessionID.Add(1)
65 + cfg := s.config
66 + cfg.MaxRequestPayloadBytes = s.learnedRequestPayloadBytes.Load()
67 + cfg.MaxResponsePayloadBytes = s.learnedResponsePayloadBytes.Load()
68 +
69 + if cfg.SupportedProfiles&posixShmProfiles == 0 {
70 + return sessionID, cfg, nil, true
71 + }
72 +
73 + shm, err := posix.ShmServerCreate(
74 + s.runDir, s.serviceName, sessionID,
75 + cfg.MaxRequestPayloadBytes+uint32(protocol.HeaderSize),
76 + cfg.MaxResponsePayloadBytes+uint32(protocol.HeaderSize),
77 + )
78 + if err == nil {
79 + return sessionID, cfg, shm, true
80 + }
81 +
82 + cfg.SupportedProfiles &^= posixShmProfiles
83 + cfg.PreferredProfiles &^= posixShmProfiles
84 + if cfg.SupportedProfiles == 0 {
85 + return sessionID, cfg, nil, false
86 + }
87 +
88 + return sessionID, cfg, nil, true
89 +}
90 +
91 +// Run starts the acceptor loop. Blocking. Accepts clients, spawns a
92 +// goroutine per session (up to workerCount concurrently).
93 +// Returns when Stop() is called or on fatal error.
94 +func (s *Server) Run() error {
95 + posix.ShmCleanupStale(s.runDir, s.serviceName)
96 +
97 + listener, err := posix.Listen(s.runDir, s.serviceName, s.config)
98 + if err != nil {
99 + return err
100 + }
101 + defer listener.Close()
102 +
103 + s.running.Store(true)
104 +
105 + /* Semaphore channel limits concurrent sessions */
106 + sem := make(chan struct{}, s.workerCount)
107 +
108 + for s.running.Load() {
109 + // Poll the listener fd before blocking on accept
110 + ready := pollFd(listener.Fd(), serverPollTimeoutMs)
111 + if ready < 0 {
112 + break
113 + }
114 + if ready == 0 {
115 + continue
116 + }
117 +
118 + sessionID, acceptCfg, precreatedShm, ok := s.prepareAcceptConfig()
119 + if !ok {
120 + s.retryAcceptAfter(nil)
121 + continue
122 + }
123 +
124 + session, err := listener.AcceptWithConfig(sessionID, acceptCfg)
125 + if err != nil {
126 + if !s.retryAcceptAfter(func() {
127 + if precreatedShm != nil {
128 + precreatedShm.ShmDestroy()
129 + }
130 + }) {
131 + break
132 + }
133 + continue
134 + }
135 +
136 + if !s.acquireWorkerSlot(sem, func() {
137 + if precreatedShm != nil {
138 + precreatedShm.ShmDestroy()
139 + }
140 + }, session.Close) {
141 + continue
142 + }
143 +
144 + var shm *posix.ShmContext
145 + if session.SelectedProfile == protocol.ProfileSHMHybrid ||
146 + session.SelectedProfile == protocol.ProfileSHMFutex {
147 + if precreatedShm == nil {
148 + session.Close()
149 + <-sem
150 + continue
151 + }
152 + shm = precreatedShm
153 + } else if precreatedShm != nil {
154 + precreatedShm.ShmDestroy()
155 + }
156 +
157 + s.startSessionWorker(sem, func() {
158 + s.handleSession(session, shm)
159 + })
160 + }
161 +
162 + // Wait for all active session goroutines to finish
163 + s.wg.Wait()
164 +
165 + return nil
166 +}
167 +
168 +// Stop signals the server to stop.
169 +func (s *Server) Stop() {
170 + s.running.Store(false)
171 +}
172 +
173 +func (s *Server) handleSession(session *posix.Session, shm *posix.ShmContext) {
174 + s.handleServerSession(serverSessionOps{
175 + maxRequestPayloadBytes: session.MaxRequestPayloadBytes,
176 + maxResponsePayloadBytes: session.MaxResponsePayloadBytes,
177 + receive: func(recvBuf []byte) (protocol.Header, []byte, serverReceiveAction) {
178 + if shm != nil {
179 + mlen, err := shm.ShmReceive(recvBuf, serverPollTimeoutMs)
180 + if err != nil {
181 + if err == posix.ErrShmTimeout {
182 + return protocol.Header{}, nil, serverReceiveContinue
183 + }
184 + return protocol.Header{}, nil, serverReceiveStop
185 + }
186 + if mlen < protocol.HeaderSize {
187 + return protocol.Header{}, nil, serverReceiveStop
188 + }
189 + hdr, err := protocol.DecodeHeader(recvBuf[:mlen])
190 + if err != nil {
191 + return protocol.Header{}, nil, serverReceiveStop
192 + }
193 + return hdr, recvBuf[protocol.HeaderSize:mlen], serverReceiveOK
194 + }
195 +
196 + ready := pollFd(session.Fd(), serverPollTimeoutMs)
197 + if ready < 0 {
198 + return protocol.Header{}, nil, serverReceiveStop
199 + }
200 + if ready == 0 {
201 + return protocol.Header{}, nil, serverReceiveContinue
202 + }
203 +
204 + hdr, payload, err := session.Receive(recvBuf)
205 + if err != nil {
206 + return protocol.Header{}, nil, serverReceiveStop
207 + }
208 + return hdr, payload, serverReceiveOK
209 + },
210 + send: func(respHdr *protocol.Header, payload []byte, msgBuf *[]byte) error {
211 + if shm == nil {
212 + return session.Send(respHdr, payload)
213 + }
214 + msg, err := serverEncodeSharedResponse(respHdr, payload, msgBuf)
215 + if err != nil {
216 + return err
217 + }
218 + return shm.ShmSend(msg)
219 + },
220 + close: func() {
221 + if shm != nil {
222 + shm.ShmDestroy()
223 + }
224 + session.Close()
225 + },
226 + })
227 +}
src/go/pkg/netipc/service/raw/server_windows.go new
+276
@@ -0,0 +1,276 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "sync"
7 + "sync/atomic"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
11 +)
12 +
13 +// Server is an internal managed server bound to one expected request kind.
14 +type Server struct {
15 + runDir string
16 + serviceName string
17 + config windows.ServerConfig
18 + expectedMethodCode uint16
19 + handler DispatchHandler
20 + running atomic.Bool
21 + learnedRequestPayloadBytes atomic.Uint32
22 + learnedResponsePayloadBytes atomic.Uint32
23 + nextSessionID atomic.Uint64
24 + workerCount int
25 + wg sync.WaitGroup
26 + listener *windows.Listener // stored so Stop() can close it
27 +}
28 +
29 +// NewServer creates a new managed server.
30 +func NewServer(
31 + runDir, serviceName string,
32 + config windows.ServerConfig,
33 + expectedMethodCode uint16,
34 + handler DispatchHandler,
35 +) *Server {
36 + return NewServerWithWorkers(runDir, serviceName, config, expectedMethodCode, handler, defaultServerWorkerCount)
37 +}
38 +
39 +// NewServerWithWorkers creates a server with an explicit worker count limit.
40 +func NewServerWithWorkers(
41 + runDir, serviceName string,
42 + config windows.ServerConfig,
43 + expectedMethodCode uint16,
44 + handler DispatchHandler,
45 + workerCount int,
46 +) *Server {
47 + s := &Server{config: config}
48 + s.initCommon(
49 + runDir,
50 + serviceName,
51 + expectedMethodCode,
52 + handler,
53 + workerCount,
54 + config.MaxRequestPayloadBytes,
55 + config.MaxResponsePayloadBytes,
56 + )
57 + return s
58 +}
59 +
60 +type preparedWinShm struct {
61 + hybrid *windows.WinShmContext
62 + busywait *windows.WinShmContext
63 +}
64 +
65 +func (p *preparedWinShm) take(profile uint32) *windows.WinShmContext {
66 + if p == nil {
67 + return nil
68 + }
69 + switch profile {
70 + case windows.WinShmProfileHybrid:
71 + ctx := p.hybrid
72 + p.hybrid = nil
73 + return ctx
74 + case windows.WinShmProfileBusywait:
75 + ctx := p.busywait
76 + p.busywait = nil
77 + return ctx
78 + default:
79 + return nil
80 + }
81 +}
82 +
83 +func (p *preparedWinShm) destroyAll() {
84 + if p == nil {
85 + return
86 + }
87 + if p.hybrid != nil {
88 + p.hybrid.WinShmDestroy()
89 + p.hybrid = nil
90 + }
91 + if p.busywait != nil {
92 + p.busywait.WinShmDestroy()
93 + p.busywait = nil
94 + }
95 +}
96 +
97 +const winShmProfiles = windows.WinShmProfileHybrid | windows.WinShmProfileBusywait
98 +
99 +func (s *Server) prepareAcceptConfig() (uint64, windows.ServerConfig, *preparedWinShm, bool) {
100 + sessionID := s.nextSessionID.Add(1)
101 + cfg := s.config
102 + cfg.MaxRequestPayloadBytes = s.learnedRequestPayloadBytes.Load()
103 + cfg.MaxResponsePayloadBytes = s.learnedResponsePayloadBytes.Load()
104 +
105 + if cfg.SupportedProfiles&winShmProfiles == 0 {
106 + return sessionID, cfg, nil, true
107 + }
108 +
109 + prepared := &preparedWinShm{}
110 + for _, profile := range []uint32{windows.WinShmProfileHybrid, windows.WinShmProfileBusywait} {
111 + if cfg.SupportedProfiles&profile == 0 {
112 + continue
113 + }
114 + shm, err := windows.WinShmServerCreate(
115 + s.runDir, s.serviceName,
116 + cfg.AuthToken,
117 + sessionID,
118 + profile,
119 + cfg.MaxRequestPayloadBytes+uint32(protocol.HeaderSize),
120 + cfg.MaxResponsePayloadBytes+uint32(protocol.HeaderSize),
121 + )
122 + if err != nil {
123 + cfg.SupportedProfiles &^= profile
124 + cfg.PreferredProfiles &^= profile
125 + continue
126 + }
127 + if profile == windows.WinShmProfileHybrid {
128 + prepared.hybrid = shm
129 + } else {
130 + prepared.busywait = shm
131 + }
132 + }
133 +
134 + if cfg.SupportedProfiles == 0 {
135 + prepared.destroyAll()
136 + return sessionID, cfg, nil, false
137 + }
138 +
139 + if prepared.hybrid == nil && prepared.busywait == nil {
140 + return sessionID, cfg, nil, true
141 + }
142 +
143 + return sessionID, cfg, prepared, true
144 +}
145 +
146 +// Run starts the acceptor loop. Blocking.
147 +func (s *Server) Run() error {
148 + listener, err := windows.Listen(s.runDir, s.serviceName, s.config)
149 + if err != nil {
150 + return err
151 + }
152 + s.listener = listener
153 + defer func() {
154 + listener.Close()
155 + s.listener = nil
156 + }()
157 +
158 + s.running.Store(true)
159 + sem := make(chan struct{}, s.workerCount)
160 +
161 + for s.running.Load() {
162 + sessionID, acceptCfg, preparedShm, ok := s.prepareAcceptConfig()
163 + if !ok {
164 + s.retryAcceptAfter(nil)
165 + continue
166 + }
167 +
168 + session, err := listener.AcceptWithConfig(sessionID, acceptCfg)
169 + if err != nil {
170 + if !s.retryAcceptAfter(func() {
171 + if preparedShm != nil {
172 + preparedShm.destroyAll()
173 + }
174 + }) {
175 + break
176 + }
177 + continue
178 + }
179 +
180 + if !s.acquireWorkerSlot(sem, func() {
181 + if preparedShm != nil {
182 + preparedShm.destroyAll()
183 + }
184 + }, session.Close) {
185 + continue
186 + }
187 +
188 + var shm *windows.WinShmContext
189 + if session.SelectedProfile == windows.WinShmProfileHybrid ||
190 + session.SelectedProfile == windows.WinShmProfileBusywait {
191 + shm = preparedShm.take(session.SelectedProfile)
192 + if shm == nil {
193 + if preparedShm != nil {
194 + preparedShm.destroyAll()
195 + }
196 + session.Close()
197 + <-sem
198 + continue
199 + }
200 + }
201 + if preparedShm != nil {
202 + preparedShm.destroyAll()
203 + }
204 +
205 + s.startSessionWorker(sem, func() {
206 + s.handleSession(session, shm)
207 + })
208 + }
209 +
210 + s.wg.Wait()
211 + return nil
212 +}
213 +
214 +// Stop signals the server to stop and unblocks Accept by closing the listener.
215 +func (s *Server) Stop() {
216 + s.running.Store(false)
217 + if s.listener != nil {
218 + s.listener.Close()
219 + }
220 +}
221 +
222 +func (s *Server) handleSession(session *windows.Session, shm *windows.WinShmContext) {
223 + s.handleServerSession(serverSessionOps{
224 + maxRequestPayloadBytes: session.MaxRequestPayloadBytes,
225 + maxResponsePayloadBytes: session.MaxResponsePayloadBytes,
226 + receive: func(recvBuf []byte) (protocol.Header, []byte, serverReceiveAction) {
227 + if shm != nil {
228 + mlen, err := shm.WinShmReceive(recvBuf, serverPollTimeoutMs)
229 + if err != nil {
230 + if err == windows.ErrWinShmTimeout {
231 + return protocol.Header{}, nil, serverReceiveContinue
232 + }
233 + return protocol.Header{}, nil, serverReceiveStop
234 + }
235 + if mlen < protocol.HeaderSize {
236 + return protocol.Header{}, nil, serverReceiveStop
237 + }
238 + hdr, err := protocol.DecodeHeader(recvBuf[:mlen])
239 + if err != nil {
240 + return protocol.Header{}, nil, serverReceiveStop
241 + }
242 + return hdr, recvBuf[protocol.HeaderSize:mlen], serverReceiveOK
243 + }
244 +
245 + ready, waitErr := session.WaitReadable(serverPollTimeoutMs)
246 + if waitErr != nil {
247 + return protocol.Header{}, nil, serverReceiveStop
248 + }
249 + if !ready {
250 + return protocol.Header{}, nil, serverReceiveContinue
251 + }
252 +
253 + hdr, payload, err := session.Receive(recvBuf)
254 + if err != nil {
255 + return protocol.Header{}, nil, serverReceiveStop
256 + }
257 + return hdr, payload, serverReceiveOK
258 + },
259 + send: func(respHdr *protocol.Header, payload []byte, msgBuf *[]byte) error {
260 + if shm == nil {
261 + return session.Send(respHdr, payload)
262 + }
263 + msg, err := serverEncodeSharedResponse(respHdr, payload, msgBuf)
264 + if err != nil {
265 + return err
266 + }
267 + return shm.WinShmSend(msg)
268 + },
269 + close: func() {
270 + if shm != nil {
271 + shm.WinShmDestroy()
272 + }
273 + session.Close()
274 + },
275 + })
276 +}
src/go/pkg/netipc/service/raw/string_reverse.go new
+61
@@ -0,0 +1,61 @@
1 +package raw
2 +
3 +import "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
4 +
5 +// StringReverseHandler serves a single STRING_REVERSE service kind.
6 +type StringReverseHandler func(string) (string, bool)
7 +
8 +// CallStringReverse performs a blocking STRING_REVERSE call.
9 +// The returned view is valid until the next typed call on this client.
10 +func (c *Client) CallStringReverse(requestStr string) (*protocol.StringReverseView, error) {
11 + if err := c.validateMethod(protocol.MethodStringReverse); err != nil {
12 + return nil, err
13 + }
14 +
15 + var result *protocol.StringReverseView
16 +
17 + err := c.callWithRetry(func() error {
18 + reqBuf := ensureClientScratch(&c.requestBuf, protocol.StringReverseHdrSize+len(requestStr)+1)
19 + if protocol.StringReverseEncode(requestStr, reqBuf) == 0 {
20 + return protocol.ErrTruncated
21 + }
22 +
23 + _, payload, rerr := c.doRawCall(protocol.MethodStringReverse, reqBuf)
24 + if rerr != nil {
25 + return rerr
26 + }
27 +
28 + view, derr := protocol.StringReverseDecode(payload)
29 + if derr != nil {
30 + return derr
31 + }
32 + result = &view
33 + return nil
34 + })
35 + if err != nil {
36 + return nil, err
37 + }
38 + return result, nil
39 +}
40 +
41 +// StringReverseDispatch adapts a typed string-reverse handler to the raw dispatch shape.
42 +func StringReverseDispatch(handle StringReverseHandler) DispatchHandler {
43 + if handle == nil {
44 + return nil
45 + }
46 + return func(request []byte, responseBuf []byte) (int, error) {
47 + view, err := protocol.StringReverseDecode(request)
48 + if err != nil {
49 + return 0, err
50 + }
51 + result, ok := handle(view.Str)
52 + if !ok {
53 + return 0, errHandlerFailed
54 + }
55 + n := protocol.StringReverseEncode(result, responseBuf)
56 + if n == 0 {
57 + return 0, protocol.ErrOverflow
58 + }
59 + return n, nil
60 + }
61 +}
src/go/pkg/netipc/service/raw/string_reverse_unix.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build unix
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/posix"
8 +)
9 +
10 +// NewStringReverseClient creates a raw client bound to the string-reverse service kind.
11 +func NewStringReverseClient(runDir, serviceName string, config posix.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodStringReverse)
13 +}
src/go/pkg/netipc/service/raw/string_reverse_windows.go new
+13
@@ -0,0 +1,13 @@
1 +//go:build windows
2 +
3 +package raw
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 + windows "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/windows"
8 +)
9 +
10 +// NewStringReverseClient creates a raw client bound to the string-reverse service kind.
11 +func NewStringReverseClient(runDir, serviceName string, config windows.ClientConfig) *Client {
12 + return newClient(runDir, serviceName, config, protocol.MethodStringReverse)
13 +}
src/go/pkg/netipc/service/raw/types.go
-132
@@ -8,12 +8,6 @@
8 // Pure Go — no cgo. Works with CGO_ENABLED=0.
9 package raw
10
11 -import (
12 - "errors"
13 -
14 - "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
15 -)
16 -
11 // Poll/receive timeout for server loops (ms). Controls shutdown detection latency.
12 const serverPollTimeoutMs = 100
13
@@ -42,129 +36,3 @@ type ClientStatus struct {
36 CallCount uint32
37 ErrorCount uint32
38 }
45 -
46 -// IncrementHandler serves a single INCREMENT service kind.
47 -type IncrementHandler func(uint64) (uint64, bool)
48 -
49 -// StringReverseHandler serves a single STRING_REVERSE service kind.
50 -type StringReverseHandler func(string) (string, bool)
51 -
52 -// SnapshotHandler serves a single CGROUPS_SNAPSHOT service kind.
53 -type SnapshotHandler func(*protocol.CgroupsRequest, *protocol.CgroupsBuilder) bool
54 -
55 -var errHandlerFailed = errors.New("dispatch handler failed")
56 -
57 -// DispatchHandler validates/decodes a single service kind request and writes
58 -// the matching response into responseBuf.
59 -type DispatchHandler func(request []byte, responseBuf []byte) (int, error)
60 -
61 -// IncrementDispatch adapts a typed increment handler to the raw dispatch shape.
62 -func IncrementDispatch(handle IncrementHandler) DispatchHandler {
63 - if handle == nil {
64 - return nil
65 - }
66 - return func(request []byte, responseBuf []byte) (int, error) {
67 - value, err := protocol.IncrementDecode(request)
68 - if err != nil {
69 - return 0, err
70 - }
71 - result, ok := handle(value)
72 - if !ok {
73 - return 0, errHandlerFailed
74 - }
75 - n := protocol.IncrementEncode(result, responseBuf)
76 - if n == 0 {
77 - return 0, protocol.ErrOverflow
78 - }
79 - return n, nil
80 - }
81 -}
82 -
83 -// StringReverseDispatch adapts a typed string-reverse handler to the raw dispatch shape.
84 -func StringReverseDispatch(handle StringReverseHandler) DispatchHandler {
85 - if handle == nil {
86 - return nil
87 - }
88 - return func(request []byte, responseBuf []byte) (int, error) {
89 - view, err := protocol.StringReverseDecode(request)
90 - if err != nil {
91 - return 0, err
92 - }
93 - result, ok := handle(view.Str)
94 - if !ok {
95 - return 0, errHandlerFailed
96 - }
97 - n := protocol.StringReverseEncode(result, responseBuf)
98 - if n == 0 {
99 - return 0, protocol.ErrOverflow
100 - }
101 - return n, nil
102 - }
103 -}
104 -
105 -// SnapshotMaxItems returns the item budget for a single snapshot service kind.
106 -func SnapshotMaxItems(responseBufSize int, override uint32) uint32 {
107 - if override != 0 {
108 - return override
109 - }
110 - return protocol.EstimateCgroupsMaxItems(responseBufSize)
111 -}
112 -
113 -// SnapshotDispatch adapts a typed snapshot handler to the raw dispatch shape.
114 -func SnapshotDispatch(handle SnapshotHandler, maxItems uint32) DispatchHandler {
115 - if handle == nil {
116 - return nil
117 - }
118 - return func(request []byte, responseBuf []byte) (int, error) {
119 - req, err := protocol.DecodeCgroupsRequest(request)
120 - if err != nil {
121 - return 0, err
122 - }
123 - itemBudget := SnapshotMaxItems(len(responseBuf), maxItems)
124 - if itemBudget == 0 {
125 - return 0, protocol.ErrOverflow
126 - }
127 - minRequired, ok := protocol.CgroupsBuilderMinBytes(itemBudget)
128 - if !ok || len(responseBuf) < minRequired {
129 - return 0, protocol.ErrOverflow
130 - }
131 - builder := protocol.NewCgroupsBuilder(responseBuf, itemBudget, 0, 0)
132 - if !handle(&req, builder) {
133 - return 0, errHandlerFailed
134 - }
135 - n := builder.Finish()
136 - if n == 0 {
137 - return 0, protocol.ErrOverflow
138 - }
139 - return n, nil
140 - }
141 -}
142 -
143 -// ---------------------------------------------------------------------------
144 -// L3 cache types (shared across platforms)
145 -// ---------------------------------------------------------------------------
146 -
147 -// Default response buffer size for L3 cache refresh.
148 -const cacheResponseBufSize = 65536
149 -
150 -// CacheItem is an owned copy of a single cgroup item.
151 -// Built from ephemeral L2 views during cache construction.
152 -type CacheItem struct {
153 - Hash uint32
154 - Options uint32
155 - Enabled uint32
156 - Name string // owned copy
157 - Path string // owned copy
158 -}
159 -
160 -// CacheStatus is a diagnostic snapshot for the L3 cache.
161 -type CacheStatus struct {
162 - Populated bool
163 - ItemCount uint32
164 - SystemdEnabled uint32
165 - Generation uint64
166 - RefreshSuccessCount uint32
167 - RefreshFailureCount uint32
168 - ConnectionState ClientState // underlying L2 client state
169 - LastRefreshTs int64 // monotonic timestamp (ms) of last successful refresh, 0 if never
170 -}
src/go/pkg/netipc/transport/internal/framing/handshake.go new
+312
@@ -0,0 +1,312 @@
1 +package framing
2 +
3 +import (
4 + "encoding/binary"
5 + "errors"
6 +
7 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
8 +)
9 +
10 +const (
11 + HelloPayloadSize = 44
12 + HelloAckPayloadSize = 48
13 +)
14 +
15 +type HelloConfig struct {
16 + PacketSize uint32
17 + SupportedProfiles uint32
18 + PreferredProfiles uint32
19 + MaxRequestPayloadBytes uint32
20 + MaxRequestBatchItems uint32
21 + MaxResponsePayloadBytes uint32
22 + MaxResponseBatchItems uint32
23 + AuthToken uint64
24 +}
25 +
26 +type ServerHelloConfig struct {
27 + PacketSize uint32
28 + MaxResponsePayloadBytes uint32
29 + SupportedProfiles uint32
30 + PreferredProfiles uint32
31 + AuthToken uint64
32 +}
33 +
34 +type ClientHandshakeConfig struct {
35 + Hello HelloConfig
36 +
37 + Send func([]byte) error
38 + Recv func([]byte) (int, error)
39 + StatusError func(uint16) error
40 +
41 + ErrSend func(string) error
42 + ErrRecv func(string) error
43 + ErrProtocol func(string) error
44 + ErrIncompatible func(string) error
45 +}
46 +
47 +type ServerHandshakeConfig struct {
48 + ServerHelloConfig
49 + SessionID uint64
50 +
51 + Recv func([]byte) (int, error)
52 + SendAck func(uint16, protocol.HelloAck) error
53 + StatusError func(uint16) error
54 +
55 + ErrRecv func(string) error
56 + ErrSend func(string) error
57 + ErrProtocol func(string) error
58 + ErrIncompatible func(string) error
59 +}
60 +
61 +func HeaderVersionIncompatible(buf []byte, expectedCode uint16) bool {
62 + if len(buf) < protocol.HeaderSize {
63 + return false
64 + }
65 +
66 + return binary.NativeEndian.Uint32(buf[0:4]) == protocol.MagicMsg &&
67 + binary.NativeEndian.Uint16(buf[4:6]) != protocol.Version &&
68 + binary.NativeEndian.Uint16(buf[6:8]) == protocol.HeaderLen &&
69 + binary.NativeEndian.Uint16(buf[8:10]) == protocol.KindControl &&
70 + binary.NativeEndian.Uint16(buf[12:14]) == expectedCode
71 +}
72 +
73 +func HelloLayoutIncompatible(buf []byte) bool {
74 + return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
75 +}
76 +
77 +func HelloAckLayoutIncompatible(buf []byte) bool {
78 + return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
79 +}
80 +
81 +func BuildHelloPacket(config HelloConfig) [protocol.HeaderSize + HelloPayloadSize]byte {
82 + if config.SupportedProfiles == 0 {
83 + config.SupportedProfiles = protocol.ProfileBaseline
84 + }
85 +
86 + hello := protocol.Hello{
87 + LayoutVersion: 1,
88 + Flags: 0,
89 + SupportedProfiles: config.SupportedProfiles,
90 + PreferredProfiles: config.PreferredProfiles,
91 + MaxRequestPayloadBytes: config.MaxRequestPayloadBytes,
92 + MaxRequestBatchItems: config.MaxRequestBatchItems,
93 + MaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
94 + MaxResponseBatchItems: config.MaxResponseBatchItems,
95 + AuthToken: config.AuthToken,
96 + PacketSize: config.PacketSize,
97 + }
98 +
99 + var payload [HelloPayloadSize]byte
100 + hello.Encode(payload[:])
101 +
102 + hdr := protocol.Header{
103 + Magic: protocol.MagicMsg,
104 + Version: protocol.Version,
105 + HeaderLen: protocol.HeaderLen,
106 + Kind: protocol.KindControl,
107 + Flags: 0,
108 + Code: protocol.CodeHello,
109 + TransportStatus: protocol.StatusOK,
110 + PayloadLen: HelloPayloadSize,
111 + ItemCount: 1,
112 + MessageID: 0,
113 + }
114 +
115 + var pkt [protocol.HeaderSize + HelloPayloadSize]byte
116 + hdr.Encode(pkt[:protocol.HeaderSize])
117 + copy(pkt[protocol.HeaderSize:], payload[:])
118 + return pkt
119 +}
120 +
121 +func DecodeControlHeader(buf []byte, expectedCode uint16) (protocol.Header, error) {
122 + hdr, err := protocol.DecodeHeader(buf)
123 + if err != nil {
124 + return protocol.Header{}, err
125 + }
126 + if hdr.Kind != protocol.KindControl || hdr.Code != expectedCode {
127 + return protocol.Header{}, protocol.ErrBadKind
128 + }
129 + return hdr, nil
130 +}
131 +
132 +func DecodeHelloPayload(buf []byte) (protocol.Hello, error) {
133 + return protocol.DecodeHello(buf)
134 +}
135 +
136 +func DecodeHelloAckPayload(buf []byte) (protocol.HelloAck, error) {
137 + return protocol.DecodeHelloAck(buf)
138 +}
139 +
140 +func ClientHandshake(config ClientHandshakeConfig) (protocol.HelloAck, error) {
141 + pkt := BuildHelloPacket(config.Hello)
142 + if err := config.Send(pkt[:]); err != nil {
143 + return protocol.HelloAck{}, config.ErrSend("hello send: " + err.Error())
144 + }
145 +
146 + var ackBuf [128]byte
147 + n, err := config.Recv(ackBuf[:])
148 + if err != nil {
149 + return protocol.HelloAck{}, config.ErrRecv("hello_ack recv: " + err.Error())
150 + }
151 +
152 + ackHdr, err := DecodeControlHeader(ackBuf[:n], protocol.CodeHelloAck)
153 + if err != nil {
154 + if errors.Is(err, protocol.ErrBadVersion) {
155 + return protocol.HelloAck{}, config.ErrIncompatible("ack header version mismatch")
156 + }
157 + if errors.Is(err, protocol.ErrBadKind) {
158 + return protocol.HelloAck{}, config.ErrProtocol("expected HELLO_ACK")
159 + }
160 + return protocol.HelloAck{}, config.ErrProtocol("ack header: " + err.Error())
161 + }
162 +
163 + if err := config.StatusError(ackHdr.TransportStatus); err != nil {
164 + return protocol.HelloAck{}, err
165 + }
166 +
167 + if n < protocol.HeaderSize+HelloAckPayloadSize {
168 + return protocol.HelloAck{}, config.ErrProtocol("ack payload truncated")
169 + }
170 + ack, err := DecodeHelloAckPayload(ackBuf[protocol.HeaderSize:n])
171 + if err != nil {
172 + if errors.Is(err, protocol.ErrBadLayout) &&
173 + HelloAckLayoutIncompatible(ackBuf[protocol.HeaderSize:n]) {
174 + return protocol.HelloAck{}, config.ErrIncompatible("ack payload layout version mismatch")
175 + }
176 + return protocol.HelloAck{}, config.ErrProtocol("ack payload: " + err.Error())
177 + }
178 +
179 + return ack, nil
180 +}
181 +
182 +func NegotiateHello(hello protocol.Hello, config ServerHelloConfig) (protocol.HelloAck, uint16, bool) {
183 + if config.SupportedProfiles == 0 {
184 + config.SupportedProfiles = protocol.ProfileBaseline
185 + }
186 +
187 + intersection := hello.SupportedProfiles & config.SupportedProfiles
188 + if intersection == 0 {
189 + return protocol.HelloAck{}, protocol.StatusUnsupported, false
190 + }
191 + if hello.AuthToken != config.AuthToken {
192 + return protocol.HelloAck{}, protocol.StatusAuthFailed, false
193 + }
194 + // Level 1 keeps request payload limits client-proposed: the server rejects
195 + // values over the wire cap and echoes accepted values unchanged.
196 + if hello.MaxRequestPayloadBytes > protocol.MaxPayloadCap {
197 + return protocol.HelloAck{}, protocol.StatusLimitExceeded, false
198 + }
199 +
200 + preferredIntersection := intersection & hello.PreferredProfiles & config.PreferredProfiles
201 + selected := highestBit(intersection)
202 + if preferredIntersection != 0 {
203 + selected = highestBit(preferredIntersection)
204 + }
205 +
206 + agreedPkt := minU32(hello.PacketSize, config.PacketSize)
207 + if agreedPkt <= protocol.HeaderSize {
208 + return protocol.HelloAck{}, protocol.StatusIncompatible, false
209 + }
210 +
211 + ack := protocol.HelloAck{
212 + LayoutVersion: 1,
213 + Flags: 0,
214 + ServerSupportedProfiles: config.SupportedProfiles,
215 + IntersectionProfiles: intersection,
216 + SelectedProfile: selected,
217 + AgreedMaxRequestPayloadBytes: hello.MaxRequestPayloadBytes,
218 + AgreedMaxRequestBatchItems: hello.MaxRequestBatchItems,
219 + AgreedMaxResponsePayloadBytes: config.MaxResponsePayloadBytes,
220 + AgreedMaxResponseBatchItems: hello.MaxRequestBatchItems,
221 + AgreedPacketSize: agreedPkt,
222 + }
223 + return ack, protocol.StatusOK, true
224 +}
225 +
226 +func ServerHandshake(config ServerHandshakeConfig) (protocol.HelloAck, error) {
227 + var buf [128]byte
228 + n, err := config.Recv(buf[:])
229 + if err != nil {
230 + return protocol.HelloAck{}, config.ErrRecv("hello recv: " + err.Error())
231 + }
232 +
233 + _, err = DecodeControlHeader(buf[:n], protocol.CodeHello)
234 + if err != nil {
235 + if errors.Is(err, protocol.ErrBadVersion) &&
236 + HeaderVersionIncompatible(buf[:n], protocol.CodeHello) {
237 + sendRejection(config.SendAck, protocol.StatusIncompatible)
238 + return protocol.HelloAck{}, config.ErrIncompatible("hello header version mismatch")
239 + }
240 + if errors.Is(err, protocol.ErrBadKind) {
241 + return protocol.HelloAck{}, config.ErrProtocol("expected HELLO")
242 + }
243 + return protocol.HelloAck{}, config.ErrProtocol("hello header: " + err.Error())
244 + }
245 +
246 + hello, err := DecodeHelloPayload(buf[protocol.HeaderSize:n])
247 + if err != nil {
248 + if errors.Is(err, protocol.ErrBadLayout) &&
249 + HelloLayoutIncompatible(buf[protocol.HeaderSize:n]) {
250 + sendRejection(config.SendAck, protocol.StatusIncompatible)
251 + return protocol.HelloAck{}, config.ErrIncompatible("hello payload layout version mismatch")
252 + }
253 + return protocol.HelloAck{}, config.ErrProtocol("hello payload: " + err.Error())
254 + }
255 +
256 + ack, status, ok := NegotiateHello(hello, config.ServerHelloConfig)
257 + if !ok {
258 + sendRejection(config.SendAck, status)
259 + return protocol.HelloAck{}, config.StatusError(status)
260 + }
261 + ack.SessionID = config.SessionID
262 +
263 + if err := config.SendAck(protocol.StatusOK, ack); err != nil {
264 + return protocol.HelloAck{}, config.ErrSend("hello_ack send: " + err.Error())
265 + }
266 +
267 + return ack, nil
268 +}
269 +
270 +func BuildHelloAckPacket(status uint16, ack protocol.HelloAck) [protocol.HeaderSize + HelloAckPayloadSize]byte {
271 + var payload [HelloAckPayloadSize]byte
272 + ack.Encode(payload[:])
273 +
274 + hdr := protocol.Header{
275 + Magic: protocol.MagicMsg,
276 + Version: protocol.Version,
277 + HeaderLen: protocol.HeaderLen,
278 + Kind: protocol.KindControl,
279 + Code: protocol.CodeHelloAck,
280 + TransportStatus: status,
281 + PayloadLen: HelloAckPayloadSize,
282 + ItemCount: 1,
283 + }
284 +
285 + var pkt [protocol.HeaderSize + HelloAckPayloadSize]byte
286 + hdr.Encode(pkt[:protocol.HeaderSize])
287 + copy(pkt[protocol.HeaderSize:], payload[:])
288 + return pkt
289 +}
290 +
291 +func sendRejection(sendAck func(uint16, protocol.HelloAck) error, status uint16) {
292 + _ = sendAck(status, protocol.HelloAck{LayoutVersion: 1})
293 +}
294 +
295 +func highestBit(mask uint32) uint32 {
296 + if mask == 0 {
297 + return 0
298 + }
299 + var b uint32 = 1
300 + for mask>>1 != 0 {
301 + mask >>= 1
302 + b <<= 1
303 + }
304 + return b
305 +}
306 +
307 +func minU32(a, b uint32) uint32 {
308 + if a < b {
309 + return a
310 + }
311 + return b
312 +}
src/go/pkg/netipc/transport/internal/framing/receive.go new
+300
@@ -0,0 +1,300 @@
1 +package framing
2 +
3 +import (
4 + "fmt"
5 +
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 +)
8 +
9 +// Receiver assembles one framed NetIPC message over a transport-specific
10 +// packet receive function.
11 +type Receiver struct {
12 + PacketSize uint32
13 +
14 + MaxPayload uint32
15 + MaxBatchItems uint32
16 +
17 + TrackResponses bool
18 + InflightIDs map[uint64]struct{}
19 +
20 + RecvBuf *[]byte
21 + PacketBuf *[]byte
22 +
23 + Recv func([]byte) (int, error)
24 + EnsurePacketScratch func(*[]byte, int) []byte
25 + OnRecvError func(error)
26 +
27 + ErrLimitExceeded func(string) error
28 + ErrProtocol func(string) error
29 + ErrChunk func(string) error
30 + ErrUnknownMsgID func(string) error
31 + ErrRecv func(string) error
32 +}
33 +
34 +type SessionReceiveConfig struct {
35 + RoleServer bool
36 +
37 + PacketSize uint32
38 + MaxRequestPayloadBytes uint32
39 + MaxRequestBatchItems uint32
40 + MaxResponsePayloadBytes uint32
41 + MaxResponseBatchItems uint32
42 +
43 + InflightIDs map[uint64]struct{}
44 + RecvBuf *[]byte
45 + PacketBuf *[]byte
46 +
47 + Recv func([]byte) (int, error)
48 + EnsurePacketScratch func(*[]byte, int) []byte
49 + IsRecvDisconnect func(error) bool
50 + FailAllInflight func()
51 +
52 + ErrLimitExceeded func(string) error
53 + ErrProtocol func(string) error
54 + ErrChunk func(string) error
55 + ErrUnknownMsgID func(string) error
56 + ErrRecv func(string) error
57 +}
58 +
59 +func SessionReceive(config SessionReceiveConfig, buf []byte) (protocol.Header, []byte, error) {
60 + maxPayload := config.MaxResponsePayloadBytes
61 + maxBatch := config.MaxResponseBatchItems
62 + if config.RoleServer {
63 + maxPayload = config.MaxRequestPayloadBytes
64 + maxBatch = config.MaxRequestBatchItems
65 + }
66 +
67 + return Receiver{
68 + PacketSize: config.PacketSize,
69 + MaxPayload: maxPayload,
70 + MaxBatchItems: maxBatch,
71 + TrackResponses: !config.RoleServer,
72 + InflightIDs: config.InflightIDs,
73 + RecvBuf: config.RecvBuf,
74 + PacketBuf: config.PacketBuf,
75 + Recv: config.Recv,
76 + EnsurePacketScratch: config.EnsurePacketScratch,
77 + OnRecvError: func(err error) {
78 + if config.IsRecvDisconnect(err) {
79 + config.FailAllInflight()
80 + }
81 + },
82 + ErrLimitExceeded: config.ErrLimitExceeded,
83 + ErrProtocol: config.ErrProtocol,
84 + ErrChunk: config.ErrChunk,
85 + ErrUnknownMsgID: config.ErrUnknownMsgID,
86 + ErrRecv: config.ErrRecv,
87 + }.Receive(buf)
88 +}
89 +
90 +// Receive reads and validates one complete logical message.
91 +func (r Receiver) Receive(buf []byte) (protocol.Header, []byte, error) {
92 + n, err := r.Recv(buf)
93 + if err != nil {
94 + r.noteRecvError(err)
95 + return protocol.Header{}, nil, err
96 + }
97 +
98 + if n < protocol.HeaderSize {
99 + return protocol.Header{}, nil, r.ErrProtocol("packet too short for header")
100 + }
101 +
102 + hdr, err := protocol.DecodeHeader(buf[:n])
103 + if err != nil {
104 + return protocol.Header{}, nil, r.ErrProtocol("header decode: " + err.Error())
105 + }
106 +
107 + if err := r.validateInboundLimits(hdr); err != nil {
108 + return protocol.Header{}, nil, err
109 + }
110 + if err := r.trackInboundResponse(hdr); err != nil {
111 + return protocol.Header{}, nil, err
112 + }
113 +
114 + totalMsg, err := r.totalMessageLen(hdr)
115 + if err != nil {
116 + return protocol.Header{}, nil, err
117 + }
118 + payloadLen := int(hdr.PayloadLen)
119 + if n > totalMsg {
120 + return protocol.Header{}, nil, r.ErrProtocol("packet exceeds declared payload_len")
121 + }
122 + if n == totalMsg {
123 + payload := buf[protocol.HeaderSize : protocol.HeaderSize+payloadLen]
124 + if err := r.validateBatchPayload(hdr, payload); err != nil {
125 + return protocol.Header{}, nil, err
126 + }
127 + return hdr, payload, nil
128 + }
129 +
130 + return r.receiveChunked(buf, n, hdr, totalMsg)
131 +}
132 +
133 +func (r Receiver) totalMessageLen(hdr protocol.Header) (int, error) {
134 + maxInt := uint64(int(^uint(0) >> 1))
135 + totalMsg := uint64(protocol.HeaderSize) + uint64(hdr.PayloadLen)
136 + if totalMsg > maxInt {
137 + return 0, r.ErrLimitExceeded("total message length exceeds platform limit")
138 + }
139 + return int(totalMsg), nil
140 +}
141 +
142 +func (r Receiver) noteRecvError(err error) {
143 + if r.OnRecvError != nil {
144 + r.OnRecvError(err)
145 + }
146 +}
147 +
148 +func (r Receiver) validateInboundLimits(hdr protocol.Header) error {
149 + if hdr.PayloadLen > r.MaxPayload {
150 + return r.ErrLimitExceeded(
151 + fmt.Sprintf("payload_len %d exceeds negotiated max %d", hdr.PayloadLen, r.MaxPayload))
152 + }
153 + if hdr.ItemCount > r.MaxBatchItems {
154 + return r.ErrLimitExceeded(
155 + fmt.Sprintf("item_count %d exceeds negotiated max %d", hdr.ItemCount, r.MaxBatchItems))
156 + }
157 + return nil
158 +}
159 +
160 +func (r Receiver) trackInboundResponse(hdr protocol.Header) error {
161 + if !r.TrackResponses || hdr.Kind != protocol.KindResponse {
162 + return nil
163 + }
164 + if r.InflightIDs == nil {
165 + return r.ErrUnknownMsgID(fmt.Sprintf("message_id %d", hdr.MessageID))
166 + }
167 + if _, exists := r.InflightIDs[hdr.MessageID]; !exists {
168 + return r.ErrUnknownMsgID(fmt.Sprintf("message_id %d", hdr.MessageID))
169 + }
170 + delete(r.InflightIDs, hdr.MessageID)
171 + return nil
172 +}
173 +
174 +func (r Receiver) validateBatchPayload(hdr protocol.Header, payload []byte) error {
175 + if hdr.Flags&protocol.FlagBatch == 0 || hdr.ItemCount <= 1 {
176 + return nil
177 + }
178 +
179 + dirBytes64 := uint64(hdr.ItemCount) * 8
180 + if dirBytes64 > uint64(int(^uint(0)>>1)) {
181 + return r.ErrProtocol("batch dir exceeds platform limit")
182 + }
183 + dirBytes := int(dirBytes64)
184 + dirAligned := protocol.Align8(dirBytes)
185 + if len(payload) < dirAligned {
186 + return r.ErrProtocol("batch dir exceeds payload")
187 + }
188 + packedAreaLen := uint32(len(payload) - dirAligned)
189 + if err := protocol.BatchDirValidate(payload[:dirBytes], hdr.ItemCount, packedAreaLen); err != nil {
190 + return r.ErrProtocol("batch dir: " + err.Error())
191 + }
192 + return nil
193 +}
194 +
195 +func (r Receiver) receiveChunked(
196 + buf []byte,
197 + firstPacketLen int,
198 + hdr protocol.Header,
199 + totalMsg int,
200 +) (protocol.Header, []byte, error) {
201 + firstPayloadBytes := firstPacketLen - protocol.HeaderSize
202 + needed := int(hdr.PayloadLen)
203 + if len(*r.RecvBuf) < needed {
204 + *r.RecvBuf = make([]byte, needed)
205 + }
206 +
207 + copy((*r.RecvBuf)[:firstPayloadBytes],
208 + buf[protocol.HeaderSize:protocol.HeaderSize+firstPayloadBytes])
209 +
210 + assembled := firstPayloadBytes
211 + chunkPayloadBudget := int(r.PacketSize) - protocol.HeaderSize
212 + expectedChunkCount := expectedReceiveChunkCount(
213 + int(hdr.PayloadLen), firstPayloadBytes, chunkPayloadBudget)
214 +
215 + pktBuf := r.EnsurePacketScratch(r.PacketBuf, int(r.PacketSize))
216 + for ci := uint32(1); assembled < int(hdr.PayloadLen); ci++ {
217 + err := r.receiveOneChunk(pktBuf, hdr, ci, expectedChunkCount,
218 + totalMsg, &assembled)
219 + if err != nil {
220 + return protocol.Header{}, nil, err
221 + }
222 + }
223 +
224 + payload := (*r.RecvBuf)[:int(hdr.PayloadLen)]
225 + if err := r.validateBatchPayload(hdr, payload); err != nil {
226 + return protocol.Header{}, nil, err
227 + }
228 + return hdr, payload, nil
229 +}
230 +
231 +func expectedReceiveChunkCount(payloadLen, firstPayloadBytes, chunkPayloadBudget int) uint32 {
232 + remainingAfterFirst := payloadLen - firstPayloadBytes
233 + expectedContinuations := uint32(0)
234 + if remainingAfterFirst > 0 && chunkPayloadBudget > 0 {
235 + expectedContinuations = uint32(1 + ((remainingAfterFirst - 1) / chunkPayloadBudget))
236 + }
237 + return 1 + expectedContinuations
238 +}
239 +
240 +func (r Receiver) receiveOneChunk(
241 + pktBuf []byte,
242 + hdr protocol.Header,
243 + chunkIndex uint32,
244 + expectedChunkCount uint32,
245 + totalMsg int,
246 + assembled *int,
247 +) error {
248 + cn, err := r.Recv(pktBuf)
249 + if err != nil {
250 + r.noteRecvError(err)
251 + return r.ErrRecv("continuation recv: " + err.Error())
252 + }
253 + if cn < protocol.HeaderSize {
254 + return r.ErrChunk("continuation too short")
255 + }
256 +
257 + chk, err := protocol.DecodeChunkHeader(pktBuf[:cn])
258 + if err != nil {
259 + return r.ErrChunk("chunk header: " + err.Error())
260 + }
261 + if err := r.validateReceiveChunk(chk, hdr, chunkIndex, expectedChunkCount, totalMsg); err != nil {
262 + return err
263 + }
264 +
265 + chunkData := cn - protocol.HeaderSize
266 + if chunkData != int(chk.ChunkPayloadLen) {
267 + return r.ErrChunk("chunk_payload_len mismatch")
268 + }
269 + if *assembled+chunkData > int(hdr.PayloadLen) {
270 + return r.ErrChunk("chunk exceeds payload_len")
271 + }
272 +
273 + copy((*r.RecvBuf)[*assembled:*assembled+chunkData],
274 + pktBuf[protocol.HeaderSize:protocol.HeaderSize+chunkData])
275 + *assembled += chunkData
276 + return nil
277 +}
278 +
279 +func (r Receiver) validateReceiveChunk(
280 + chk protocol.ChunkHeader,
281 + hdr protocol.Header,
282 + chunkIndex uint32,
283 + expectedChunkCount uint32,
284 + totalMsg int,
285 +) error {
286 + if chk.MessageID != hdr.MessageID {
287 + return r.ErrChunk("message_id mismatch")
288 + }
289 + if chk.ChunkIndex != chunkIndex {
290 + return r.ErrChunk(fmt.Sprintf(
291 + "chunk_index mismatch: expected %d, got %d", chunkIndex, chk.ChunkIndex))
292 + }
293 + if chk.ChunkCount != expectedChunkCount {
294 + return r.ErrChunk("chunk_count mismatch")
295 + }
296 + if chk.TotalMessageLen != uint32(totalMsg) {
297 + return r.ErrChunk("total_message_len mismatch")
298 + }
299 + return nil
300 +}
src/go/pkg/netipc/transport/internal/framing/receive_test.go new
+73
@@ -0,0 +1,73 @@
1 +package framing
2 +
3 +import (
4 + "errors"
5 + "strings"
6 + "testing"
7 +
8 + "github.com/netdata/netdata/go/plugins/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/go/pkg/netipc/transport/internal/framing/send.go new
+177
@@ -0,0 +1,177 @@
1 +package framing
2 +
3 +import (
4 + "fmt"
5 +
6 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
7 +)
8 +
9 +// HeaderPayloadLen validates header + payload before lengths are encoded into
10 +// the protocol's uint32 wire fields.
11 +func HeaderPayloadLen(payloadLen int) (int, uint32, bool) {
12 + if payloadLen < 0 {
13 + return 0, 0, false
14 + }
15 + totalMsg := uint64(protocol.HeaderSize) + uint64(payloadLen)
16 + if totalMsg > uint64(^uint32(0)) || totalMsg > uint64(int(^uint(0)>>1)) {
17 + return 0, 0, false
18 + }
19 + return int(totalMsg), uint32(payloadLen), true
20 +}
21 +
22 +// FillMessageHeader applies the common NetIPC message header fields.
23 +func FillMessageHeader(hdr *protocol.Header, payloadLen uint32) {
24 + hdr.Magic = protocol.MagicMsg
25 + hdr.Version = protocol.Version
26 + hdr.HeaderLen = protocol.HeaderLen
27 + hdr.PayloadLen = payloadLen
28 +}
29 +
30 +// Sender chunks one logical NetIPC message over a transport-specific packet
31 +// writer.
32 +type Sender struct {
33 + PacketSize uint32
34 +
35 + SendFirstPacket func(*protocol.Header, []byte, int) error
36 + SendChunk func(protocol.ChunkHeader, []byte) error
37 + ErrBadParam func(string) error
38 +}
39 +
40 +type SessionSendConfig struct {
41 + RoleClient bool
42 + PacketSize uint32
43 +
44 + InflightIDs *map[uint64]struct{}
45 + FailAllInflight func()
46 + IsSendDisconnect func(error) bool
47 +
48 + SendFirstPacket func(*protocol.Header, []byte, int) error
49 + SendChunk func(protocol.ChunkHeader, []byte) error
50 +
51 + ErrLimitExceeded func(string) error
52 + ErrDuplicateMsgID func(string) error
53 + ErrBadParam func(string) error
54 +}
55 +
56 +func SessionSend(config SessionSendConfig, hdr *protocol.Header, payload []byte) error {
57 + totalMsg, payloadLen, ok := HeaderPayloadLen(len(payload))
58 + if !ok {
59 + return config.ErrLimitExceeded("total message length exceeds protocol limit")
60 + }
61 +
62 + tracked, err := trackOutboundRequest(config, hdr)
63 + if err != nil {
64 + return err
65 + }
66 +
67 + FillMessageHeader(hdr, payloadLen)
68 + sendErr := Sender{
69 + PacketSize: config.PacketSize,
70 + SendFirstPacket: config.SendFirstPacket,
71 + SendChunk: config.SendChunk,
72 + ErrBadParam: config.ErrBadParam,
73 + }.Send(hdr, payload, totalMsg)
74 +
75 + if sendErr != nil && tracked {
76 + if config.IsSendDisconnect(sendErr) {
77 + config.FailAllInflight()
78 + } else {
79 + delete(*config.InflightIDs, hdr.MessageID)
80 + }
81 + }
82 +
83 + return sendErr
84 +}
85 +
86 +func trackOutboundRequest(config SessionSendConfig, hdr *protocol.Header) (bool, error) {
87 + if !config.RoleClient || hdr.Kind != protocol.KindRequest {
88 + return false, nil
89 + }
90 +
91 + if *config.InflightIDs == nil {
92 + *config.InflightIDs = make(map[uint64]struct{})
93 + }
94 + if _, exists := (*config.InflightIDs)[hdr.MessageID]; exists {
95 + return false, config.ErrDuplicateMsgID(fmt.Sprintf("message_id %d", hdr.MessageID))
96 + }
97 + (*config.InflightIDs)[hdr.MessageID] = struct{}{}
98 + return true, nil
99 +}
100 +
101 +// Send writes one complete logical message.
102 +func (s Sender) Send(hdr *protocol.Header, payload []byte, totalMsg int) error {
103 + packetSize, err := s.packetSize()
104 + if err != nil {
105 + return err
106 + }
107 + if totalMsg <= packetSize {
108 + return s.SendFirstPacket(hdr, payload, totalMsg)
109 + }
110 +
111 + chunkPayloadBudget := packetSize - protocol.HeaderSize
112 + if chunkPayloadBudget <= 0 {
113 + return s.ErrBadParam("packet_size too small")
114 + }
115 +
116 + firstChunkPayload := minInt(len(payload), chunkPayloadBudget)
117 + remainingAfterFirst := len(payload) - firstChunkPayload
118 + continuationChunks := uint32(0)
119 + if remainingAfterFirst > 0 {
120 + continuationChunks = uint32(1 + ((remainingAfterFirst - 1) / chunkPayloadBudget))
121 + }
122 + chunkCount := 1 + continuationChunks
123 +
124 + if err := s.SendFirstPacket(hdr, payload[:firstChunkPayload],
125 + protocol.HeaderSize+firstChunkPayload); err != nil {
126 + return err
127 + }
128 +
129 + offset := firstChunkPayload
130 + for ci := uint32(1); ci < chunkCount; ci++ {
131 + remaining := len(payload) - offset
132 + thisChunk := minInt(remaining, chunkPayloadBudget)
133 + chunkLen, ok := checkedU32(thisChunk)
134 + if !ok {
135 + return s.ErrBadParam("chunk payload length exceeds protocol limit")
136 + }
137 +
138 + chk := protocol.ChunkHeader{
139 + Magic: protocol.MagicChunk,
140 + Version: protocol.Version,
141 + Flags: 0,
142 + MessageID: hdr.MessageID,
143 + TotalMessageLen: uint32(totalMsg),
144 + ChunkIndex: ci,
145 + ChunkCount: chunkCount,
146 + ChunkPayloadLen: chunkLen,
147 + }
148 + if err := s.SendChunk(chk, payload[offset:offset+thisChunk]); err != nil {
149 + return err
150 + }
151 +
152 + offset += thisChunk
153 + }
154 +
155 + return nil
156 +}
157 +
158 +func (s Sender) packetSize() (int, error) {
159 + if uint64(s.PacketSize) > uint64(int(^uint(0)>>1)) {
160 + return 0, s.ErrBadParam("packet_size exceeds platform limit")
161 + }
162 + return int(s.PacketSize), nil
163 +}
164 +
165 +func minInt(a, b int) int {
166 + if a < b {
167 + return a
168 + }
169 + return b
170 +}
171 +
172 +func checkedU32(value int) (uint32, bool) {
173 + if value < 0 || uint64(value) > uint64(^uint32(0)) {
174 + return 0, false
175 + }
176 + return uint32(value), true
177 +}
src/go/pkg/netipc/transport/posix/shm_linux.go
+81 -62
@@ -156,15 +156,15 @@ func ShmServerCreate(runDir, serviceName string, sessionID uint64, reqCapacity,
156 regionSize := int(respOff + respCap)
157
158 // Try O_EXCL create first (fast path, no stale check needed).
159 - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
159 + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) // #nosec G304 -- path is built from runDir, validated service name, and hex session ID.
160 if err != nil && os.IsExist(err) {
161 // File exists — do stale recovery and retry.
162 - stale := checkShmStale(path)
162 + stale := checkShmStale(path, runDirAllowsStaleUnlink(runDir))
163 if stale == shmStaleLive {
164 return nil, fmt.Errorf("%w: live server owns SHM region", ErrShmOpen)
165 }
166 // Stale file was unlinked, retry create
167 - f, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
167 + f, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) // #nosec G304 -- path is built from runDir, validated service name, and hex session ID.
168 }
169 if err != nil {
170 return nil, fmt.Errorf("%w: %v", ErrShmOpen, err)
@@ -172,16 +172,16 @@ func ShmServerCreate(runDir, serviceName string, sessionID uint64, reqCapacity,
172 fd := int(f.Fd())
173
174 if err := syscall.Ftruncate(fd, int64(regionSize)); err != nil {
175 - f.Close()
176 - os.Remove(path)
175 + _ = f.Close()
176 + _ = os.Remove(path)
177 return nil, fmt.Errorf("%w: %v", ErrShmTruncate, err)
178 }
179
180 data, err := syscall.Mmap(fd, 0, regionSize,
181 syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
182 if err != nil {
183 - f.Close()
184 - os.Remove(path)
183 + _ = f.Close()
184 + _ = os.Remove(path)
185 return nil, fmt.Errorf("%w: %v", ErrShmMmap, err)
186 }
187
@@ -206,7 +206,7 @@ func ShmServerCreate(runDir, serviceName string, sessionID uint64, reqCapacity,
206 binary.NativeEndian.PutUint32(data[shmHeaderRespCapOff:shmHeaderRespCapOff+4], respCap)
207
208 // Release fence: ensure header writes are visible before clients
209 - atomic.StoreUint32((*uint32)(unsafe.Pointer(&data[shmHeaderReqSignalOff])), 0)
209 + atomic.StoreUint32((*uint32)(unsafe.Pointer(&data[shmHeaderReqSignalOff])), 0) // #nosec G103 -- mmap header fields require raw aligned atomic access.
210
211 // Close the os.File but keep the fd open (Mmap holds a reference).
212 // Actually, we need to keep the fd ourselves for the context.
@@ -216,12 +216,12 @@ func ShmServerCreate(runDir, serviceName string, sessionID uint64, reqCapacity,
216 // The safe way: dup the fd, then close the file.
217 newFd, err := syscall.Dup(fd)
218 if err != nil {
219 - syscall.Munmap(data)
220 - f.Close()
221 - os.Remove(path)
219 + _ = syscall.Munmap(data)
220 + _ = f.Close()
221 + _ = os.Remove(path)
222 return nil, fmt.Errorf("%w: dup: %v", ErrShmOpen, err)
223 }
224 - f.Close() // closes original fd
224 + _ = f.Close() // closes original fd
225
226 return &ShmContext{
227 role: ShmRoleServer,
@@ -242,15 +242,15 @@ func ShmServerCreate(runDir, serviceName string, sessionID uint64, reqCapacity,
242 // ShmDestroy destroys a server SHM region (munmap, close, unlink).
243 func (c *ShmContext) ShmDestroy() {
244 if c.data != nil {
245 - syscall.Munmap(c.data)
245 + _ = syscall.Munmap(c.data)
246 c.data = nil
247 }
248 if c.fd >= 0 {
249 - syscall.Close(c.fd)
249 + _ = syscall.Close(c.fd)
250 c.fd = -1
251 }
252 if c.path != "" {
253 - os.Remove(c.path)
253 + _ = os.Remove(c.path)
254 c.path = ""
255 }
256 }
@@ -266,7 +266,7 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
266 return nil, err
267 }
268
269 - f, err := os.OpenFile(path, os.O_RDWR, 0)
269 + f, err := os.OpenFile(path, os.O_RDWR, 0) // #nosec G304 -- path is built from runDir, validated service name, and hex session ID.
270 if err != nil {
271 return nil, fmt.Errorf("%w: %v", ErrShmOpen, err)
272 }
@@ -274,45 +274,45 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
274
275 info, err := f.Stat()
276 if err != nil {
277 - f.Close()
277 + _ = f.Close()
278 return nil, fmt.Errorf("%w: stat: %v", ErrShmOpen, err)
279 }
280
281 fileSize := int(info.Size())
282 if fileSize < int(shmHeaderLen) {
283 - f.Close()
283 + _ = f.Close()
284 return nil, ErrShmNotReady
285 }
286
287 data, err := syscall.Mmap(fd, 0, fileSize,
288 syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
289 if err != nil {
290 - f.Close()
290 + _ = f.Close()
291 return nil, fmt.Errorf("%w: %v", ErrShmMmap, err)
292 }
293
294 // Acquire fence
295 - atomic.LoadUint32((*uint32)(unsafe.Pointer(&data[shmHeaderReqSignalOff])))
295 + atomic.LoadUint32((*uint32)(unsafe.Pointer(&data[shmHeaderReqSignalOff]))) // #nosec G103 -- mmap header fields require raw aligned atomic access.
296
297 // Validate header
298 magic := binary.NativeEndian.Uint32(data[shmHeaderMagicOff : shmHeaderMagicOff+4])
299 if magic != shmRegionMagic {
300 - syscall.Munmap(data)
301 - f.Close()
300 + _ = syscall.Munmap(data)
301 + _ = f.Close()
302 return nil, ErrShmBadMagic
303 }
304
305 version := binary.NativeEndian.Uint16(data[shmHeaderVersionOff : shmHeaderVersionOff+2])
306 if version != shmRegionVersion {
307 - syscall.Munmap(data)
308 - f.Close()
307 + _ = syscall.Munmap(data)
308 + _ = f.Close()
309 return nil, ErrShmBadVersion
310 }
311
312 hdrLen := binary.NativeEndian.Uint16(data[shmHeaderHeaderLenOff : shmHeaderHeaderLenOff+2])
313 if hdrLen != uint16(shmHeaderLen) {
314 - syscall.Munmap(data)
315 - f.Close()
314 + _ = syscall.Munmap(data)
315 + _ = f.Close()
316 return nil, ErrShmBadHeader
317 }
318
@@ -323,8 +323,8 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
323
324 headerEnd := shmAlign64(uint32(shmHeaderLen))
325 if reqOff < headerEnd || reqCap == 0 || respOff < headerEnd || respCap == 0 {
326 - syscall.Munmap(data)
327 - f.Close()
326 + _ = syscall.Munmap(data)
327 + _ = f.Close()
328 return nil, ErrShmNotReady
329 }
330
@@ -333,8 +333,8 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
333 respOff%shmRegionAlignment != 0 ||
334 respCap%shmRegionAlignment != 0 ||
335 respOff < shmAlign64(reqOff+reqCap) {
336 - syscall.Munmap(data)
337 - f.Close()
336 + _ = syscall.Munmap(data)
337 + _ = f.Close()
338 return nil, ErrShmBadSize
339 }
340
@@ -343,22 +343,22 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
343 respEnd := int(respOff) + int(respCap)
344 needed := max(respEnd, reqEnd)
345 if fileSize < needed {
346 - syscall.Munmap(data)
347 - f.Close()
346 + _ = syscall.Munmap(data)
347 + _ = f.Close()
348 return nil, ErrShmBadSize
349 }
350
351 // Read current sequence numbers
352 curReqSeq, err := atomicLoadU64(data, shmHeaderReqSeqOff)
353 if err != nil {
354 - syscall.Munmap(data)
355 - f.Close()
354 + _ = syscall.Munmap(data)
355 + _ = f.Close()
356 return nil, fmt.Errorf("%w: load req_seq: %v", ErrShmBadParam, err)
357 }
358 curRespSeq, err := atomicLoadU64(data, shmHeaderRespSeqOff)
359 if err != nil {
360 - syscall.Munmap(data)
361 - f.Close()
360 + _ = syscall.Munmap(data)
361 + _ = f.Close()
362 return nil, fmt.Errorf("%w: load resp_seq: %v", ErrShmBadParam, err)
363 }
364 ownerGen := binary.NativeEndian.Uint32(data[shmHeaderOwnerGenOff : shmHeaderOwnerGenOff+4])
@@ -366,11 +366,11 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
366 // Dup fd and close file
367 newFd, err := syscall.Dup(fd)
368 if err != nil {
369 - syscall.Munmap(data)
370 - f.Close()
369 + _ = syscall.Munmap(data)
370 + _ = f.Close()
371 return nil, fmt.Errorf("%w: dup: %v", ErrShmOpen, err)
372 }
373 - f.Close()
373 + _ = f.Close()
374
375 return &ShmContext{
376 role: ShmRoleClient,
@@ -391,11 +391,11 @@ func ShmClientAttach(runDir, serviceName string, sessionID uint64) (*ShmContext,
391 // ShmClose closes a client SHM context (no unlink).
392 func (c *ShmContext) ShmClose() {
393 if c.data != nil {
394 - syscall.Munmap(c.data)
394 + _ = syscall.Munmap(c.data)
395 c.data = nil
396 }
397 if c.fd >= 0 {
398 - syscall.Close(c.fd)
398 + _ = syscall.Close(c.fd)
399 c.fd = -1
400 }
401 }
@@ -527,7 +527,7 @@ func (c *ShmContext) ShmReceive(buf []byte, timeoutMs uint32) (int, error) {
527 var deadlineNs uint64
528 if timeoutMs > 0 {
529 var nowTs syscall.Timespec
530 - syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 1 /* CLOCK_MONOTONIC */, uintptr(unsafe.Pointer(&nowTs)), 0)
530 + syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 1 /* CLOCK_MONOTONIC */, uintptr(unsafe.Pointer(&nowTs)), 0) // #nosec G103 -- syscall requires passing a Timespec pointer.
531 deadlineNs = uint64(nowTs.Sec)*1_000_000_000 + uint64(nowTs.Nsec) +
532 uint64(timeoutMs)*1_000_000
533 }
@@ -550,7 +550,7 @@ func (c *ShmContext) ShmReceive(buf []byte, timeoutMs uint32) (int, error) {
550 var ts *syscall.Timespec
551 if deadlineNs > 0 {
552 var nowTs syscall.Timespec
553 - syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 1 /* CLOCK_MONOTONIC */, uintptr(unsafe.Pointer(&nowTs)), 0)
553 + syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 1 /* CLOCK_MONOTONIC */, uintptr(unsafe.Pointer(&nowTs)), 0) // #nosec G103 -- syscall requires passing a Timespec pointer.
554 nowVal := uint64(nowTs.Sec)*1_000_000_000 + uint64(nowTs.Nsec)
555 if nowVal >= deadlineNs {
556 return 0, ErrShmTimeout
@@ -654,7 +654,7 @@ func atomicLoadU64(data []byte, off int) (uint64, error) {
654 if off < 0 || off+8 > len(data) {
655 return 0, errShmOutOfBounds
656 }
657 - ptr := (*uint64)(unsafe.Pointer(&data[off]))
657 + ptr := (*uint64)(unsafe.Pointer(&data[off])) // #nosec G103 -- mmap header fields require raw aligned atomic access.
658 return atomic.LoadUint64(ptr), nil
659 }
660
@@ -662,7 +662,7 @@ func atomicLoadU32(data []byte, off int) (uint32, error) {
662 if off < 0 || off+4 > len(data) {
663 return 0, errShmOutOfBounds
664 }
665 - ptr := (*uint32)(unsafe.Pointer(&data[off]))
665 + ptr := (*uint32)(unsafe.Pointer(&data[off])) // #nosec G103 -- mmap header fields require raw aligned atomic access.
666 return atomic.LoadUint32(ptr), nil
667 }
668
@@ -670,7 +670,7 @@ func atomicStoreU32(data []byte, off int, val uint32) error {
670 if off < 0 || off+4 > len(data) {
671 return errShmOutOfBounds
672 }
673 - ptr := (*uint32)(unsafe.Pointer(&data[off]))
673 + ptr := (*uint32)(unsafe.Pointer(&data[off])) // #nosec G103 -- mmap header fields require raw aligned atomic access.
674 atomic.StoreUint32(ptr, val)
675 return nil
676 }
@@ -679,7 +679,7 @@ func atomicAddU64(data []byte, off int, val uint64) error {
679 if off < 0 || off+8 > len(data) {
680 return errShmOutOfBounds
681 }
682 - ptr := (*uint64)(unsafe.Pointer(&data[off]))
682 + ptr := (*uint64)(unsafe.Pointer(&data[off])) // #nosec G103 -- mmap header fields require raw aligned atomic access.
683 atomic.AddUint64(ptr, val)
684 return nil
685 }
@@ -688,7 +688,7 @@ func atomicAddU32(data []byte, off int, val uint32) error {
688 if off < 0 || off+4 > len(data) {
689 return errShmOutOfBounds
690 }
691 - ptr := (*uint32)(unsafe.Pointer(&data[off]))
691 + ptr := (*uint32)(unsafe.Pointer(&data[off])) // #nosec G103 -- mmap header fields require raw aligned atomic access.
692 atomic.AddUint32(ptr, val)
693 return nil
694 }
@@ -697,7 +697,7 @@ func futexWakeCall(data []byte, off int, count int) int {
697 if off < 0 || off+4 > len(data) {
698 return -1
699 }
700 - addr := unsafe.Pointer(&data[off])
700 + addr := unsafe.Pointer(&data[off]) // #nosec G103 -- futex needs the address of the mmap signal word.
701 r1, _, _ := syscall.Syscall6(
702 syscall.SYS_FUTEX,
703 uintptr(addr),
@@ -712,10 +712,10 @@ func futexWaitCall(data []byte, off int, expected uint32, ts *syscall.Timespec)
712 if off < 0 || off+4 > len(data) {
713 return -1
714 }
715 - addr := unsafe.Pointer(&data[off])
715 + addr := unsafe.Pointer(&data[off]) // #nosec G103 -- futex needs the address of the mmap signal word.
716 var tsPtr uintptr
717 if ts != nil {
718 - tsPtr = uintptr(unsafe.Pointer(ts))
718 + tsPtr = uintptr(unsafe.Pointer(ts)) // #nosec G103 -- futex syscall requires an optional Timespec pointer.
719 }
720 r1, _, errno := syscall.Syscall6(
721 syscall.SYS_FUTEX,
@@ -742,6 +742,7 @@ func ShmCleanupStale(runDir, serviceName string) {
742 if err != nil {
743 return
744 }
745 + allowStaleUnlink := runDirAllowsStaleUnlink(runDir)
746 prefix := serviceName + "-"
747 suffix := ".ipcshm"
748 for _, e := range entries {
@@ -756,7 +757,7 @@ func ShmCleanupStale(runDir, serviceName string) {
757 continue
758 }
759 path := filepath.Join(runDir, name)
759 - result := checkShmStale(path)
760 + result := checkShmStale(path, allowStaleUnlink)
761 _ = result // checkShmStale already unlinks stale/invalid files
762 }
763 }
@@ -774,47 +775,65 @@ const (
775 shmStaleInvalid
776 )
777
777 -func checkShmStale(path string) shmStaleResult {
778 +func removeStalePath(path string, allowStaleUnlink bool) bool {
779 + if !allowStaleUnlink {
780 + return false
781 + }
782 + err := os.Remove(path)
783 + return err == nil || os.IsNotExist(err)
784 +}
785 +
786 +func checkShmStale(path string, allowStaleUnlink bool) shmStaleResult {
787 info, err := os.Stat(path)
788 if err != nil {
789 return shmStaleNotExist
790 }
791
792 if info.Size() < int64(shmHeaderLen) {
784 - os.Remove(path)
793 + if !removeStalePath(path, allowStaleUnlink) {
794 + return shmStaleLive
795 + }
796 return shmStaleInvalid
797 }
798
788 - f, err := os.Open(path)
799 + f, err := os.Open(path) // #nosec G304 -- path comes from the validated SHM cleanup scan or buildShmPath.
800 if err != nil {
790 - os.Remove(path)
801 + if !removeStalePath(path, allowStaleUnlink) {
802 + return shmStaleLive
803 + }
804 return shmStaleInvalid
805 }
806
807 data, err := syscall.Mmap(int(f.Fd()), 0, int(shmHeaderLen),
808 syscall.PROT_READ, syscall.MAP_SHARED)
796 - f.Close()
809 + _ = f.Close()
810 if err != nil {
798 - os.Remove(path)
811 + if !removeStalePath(path, allowStaleUnlink) {
812 + return shmStaleLive
813 + }
814 return shmStaleInvalid
815 }
816
817 magic := binary.NativeEndian.Uint32(data[shmHeaderMagicOff : shmHeaderMagicOff+4])
818 if magic != shmRegionMagic {
804 - syscall.Munmap(data)
805 - os.Remove(path)
819 + _ = syscall.Munmap(data)
820 + if !removeStalePath(path, allowStaleUnlink) {
821 + return shmStaleLive
822 + }
823 return shmStaleInvalid
824 }
825
826 ownerPid := int(int32(binary.NativeEndian.Uint32(data[shmHeaderOwnerPidOff : shmHeaderOwnerPidOff+4])))
827 ownerGen := binary.NativeEndian.Uint32(data[shmHeaderOwnerGenOff : shmHeaderOwnerGenOff+4])
811 - syscall.Munmap(data)
828 + _ = syscall.Munmap(data)
829
830 if pidAlive(ownerPid) && ownerGen != 0 {
831 return shmStaleLive
832 }
833
834 // Dead owner or zero generation (PID reuse / legacy) — stale
818 - os.Remove(path)
835 + if !removeStalePath(path, allowStaleUnlink) {
836 + return shmStaleLive
837 + }
838 return shmStaleRecovered
839 }
src/go/pkg/netipc/transport/posix/shm_more_edge_test.go
+13 -9
@@ -48,7 +48,11 @@ func writeRawShmRegionFile(t *testing.T, runDir, service string, sessionID uint6
48 if err != nil {
49 t.Fatalf("open %s: %v", path, err)
50 }
51 - defer f.Close()
51 + defer func() {
52 + if err := f.Close(); err != nil {
53 + t.Fatalf("close %s: %v", path, err)
54 + }
55 + }()
56
57 if err := f.Truncate(int64(size)); err != nil {
58 t.Fatalf("truncate %s: %v", path, err)
@@ -343,12 +347,12 @@ func TestCheckShmStaleVariants(t *testing.T) {
347 validSize := int(respOff + respCap)
348
349 missingPath := filepath.Join(runDir, "missing.ipcshm")
346 - if got := checkShmStale(missingPath); got != shmStaleNotExist {
350 + if got := checkShmStale(missingPath, true); got != shmStaleNotExist {
351 t.Fatalf("missing path result = %v, want %v", got, shmStaleNotExist)
352 }
353
354 tinyPath := writeRawShmRegionFile(t, runDir, "go_shm_check_tiny", 1, 8, nil)
351 - if got := checkShmStale(tinyPath); got != shmStaleInvalid {
355 + if got := checkShmStale(tinyPath, true); got != shmStaleInvalid {
356 t.Fatalf("tiny file result = %v, want %v", got, shmStaleInvalid)
357 }
358 if _, err := os.Stat(tinyPath); !errors.Is(err, os.ErrNotExist) {
@@ -359,14 +363,14 @@ func TestCheckShmStaleVariants(t *testing.T) {
363 fillShmHeader(data, int32(os.Getpid()), 1, reqOff, reqCap, respOff, respCap)
364 binary.NativeEndian.PutUint32(data[shmHeaderMagicOff:shmHeaderMagicOff+4], 0)
365 })
362 - if got := checkShmStale(badMagicPath); got != shmStaleInvalid {
366 + if got := checkShmStale(badMagicPath, true); got != shmStaleInvalid {
367 t.Fatalf("bad magic result = %v, want %v", got, shmStaleInvalid)
368 }
369
370 livePath := writeRawShmRegionFile(t, runDir, "go_shm_check_live", 3, validSize, func(data []byte) {
371 fillShmHeader(data, int32(os.Getpid()), 7, reqOff, reqCap, respOff, respCap)
372 })
369 - if got := checkShmStale(livePath); got != shmStaleLive {
373 + if got := checkShmStale(livePath, true); got != shmStaleLive {
374 t.Fatalf("live path result = %v, want %v", got, shmStaleLive)
375 }
376 if _, err := os.Stat(livePath); err != nil {
@@ -376,7 +380,7 @@ func TestCheckShmStaleVariants(t *testing.T) {
380 legacyPath := writeRawShmRegionFile(t, runDir, "go_shm_check_legacy", 4, validSize, func(data []byte) {
381 fillShmHeader(data, int32(os.Getpid()), 0, reqOff, reqCap, respOff, respCap)
382 })
379 - if got := checkShmStale(legacyPath); got != shmStaleRecovered {
383 + if got := checkShmStale(legacyPath, true); got != shmStaleRecovered {
384 t.Fatalf("legacy path result = %v, want %v", got, shmStaleRecovered)
385 }
386 if _, err := os.Stat(legacyPath); !errors.Is(err, os.ErrNotExist) {
@@ -389,7 +393,7 @@ func TestCheckShmStaleVariants(t *testing.T) {
393 if err := os.Chmod(unreadablePath, 0); err != nil {
394 t.Fatalf("chmod unreadable stale file: %v", err)
395 }
392 - if got := checkShmStale(unreadablePath); got != shmStaleInvalid {
396 + if got := checkShmStale(unreadablePath, true); got != shmStaleInvalid {
397 t.Fatalf("unreadable path result = %v, want %v", got, shmStaleInvalid)
398 }
399 if _, err := os.Stat(unreadablePath); !errors.Is(err, os.ErrNotExist) {
@@ -403,8 +407,8 @@ func TestCheckShmStaleVariants(t *testing.T) {
407 if err := os.MkdirAll(filepath.Join(dirPath, "keep"), 0700); err != nil {
408 t.Fatalf("mkdir stale dir: %v", err)
409 }
406 - if got := checkShmStale(dirPath); got != shmStaleInvalid {
407 - t.Fatalf("directory path result = %v, want %v", got, shmStaleInvalid)
410 + if got := checkShmStale(dirPath, true); got != shmStaleLive {
411 + t.Fatalf("directory path result = %v, want %v", got, shmStaleLive)
412 }
413 if info, err := os.Stat(dirPath); err != nil || !info.IsDir() {
414 t.Fatalf("non-empty stale directory should remain, stat err = %v", err)
src/go/pkg/netipc/transport/posix/uds.go
+3 -882
@@ -10,17 +10,11 @@
10 package posix
11
12 import (
13 - "encoding/binary"
13 "errors"
14 "fmt"
16 - "net"
17 - "os"
15 "path/filepath"
19 - "sync/atomic"
16 "syscall"
17 "unsafe"
22 -
23 - "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
18 )
19
20 // ---------------------------------------------------------------------------
@@ -68,529 +62,6 @@ func wrapErr(sentinel error, detail string) error {
62 return fmt.Errorf("%w: %s", sentinel, detail)
63 }
64
71 -func headerVersionIncompatible(buf []byte, expectedCode uint16) bool {
72 - if len(buf) < protocol.HeaderSize {
73 - return false
74 - }
75 -
76 - return binary.NativeEndian.Uint32(buf[0:4]) == protocol.MagicMsg &&
77 - binary.NativeEndian.Uint16(buf[4:6]) != protocol.Version &&
78 - binary.NativeEndian.Uint16(buf[6:8]) == protocol.HeaderLen &&
79 - binary.NativeEndian.Uint16(buf[8:10]) == protocol.KindControl &&
80 - binary.NativeEndian.Uint16(buf[12:14]) == expectedCode
81 -}
82 -
83 -func helloLayoutIncompatible(buf []byte) bool {
84 - return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
85 -}
86 -
87 -func helloAckLayoutIncompatible(buf []byte) bool {
88 - return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
89 -}
90 -
91 -// ---------------------------------------------------------------------------
92 -// Role
93 -// ---------------------------------------------------------------------------
94 -
95 -// Role distinguishes client vs server sessions.
96 -type Role int
97 -
98 -const (
99 - RoleClient Role = 1
100 - RoleServer Role = 2
101 -)
102 -
103 -// ---------------------------------------------------------------------------
104 -// Configuration
105 -// ---------------------------------------------------------------------------
106 -
107 -// ClientConfig configures a client connection.
108 -type ClientConfig struct {
109 - SupportedProfiles uint32
110 - PreferredProfiles uint32
111 - MaxRequestPayloadBytes uint32 // 0 = use default (1024)
112 - MaxRequestBatchItems uint32 // 0 = use default (1)
113 - MaxResponsePayloadBytes uint32
114 - MaxResponseBatchItems uint32
115 - AuthToken uint64
116 - PacketSize uint32 // 0 = auto-detect from SO_SNDBUF
117 -}
118 -
119 -// ServerConfig configures a listener and its accepted sessions.
120 -type ServerConfig struct {
121 - SupportedProfiles uint32
122 - PreferredProfiles uint32
123 - MaxRequestPayloadBytes uint32
124 - MaxRequestBatchItems uint32
125 - MaxResponsePayloadBytes uint32
126 - MaxResponseBatchItems uint32
127 - AuthToken uint64
128 - PacketSize uint32 // 0 = auto-detect from SO_SNDBUF
129 - Backlog int // 0 = default (16)
130 -}
131 -
132 -// ---------------------------------------------------------------------------
133 -// Session
134 -// ---------------------------------------------------------------------------
135 -
136 -// Session is a connected UDS SEQPACKET session (client or server side).
137 -type Session struct {
138 - fd int
139 - role Role
140 -
141 - // Negotiated limits
142 - MaxRequestPayloadBytes uint32
143 - MaxRequestBatchItems uint32
144 - MaxResponsePayloadBytes uint32
145 - MaxResponseBatchItems uint32
146 - PacketSize uint32
147 - SelectedProfile uint32
148 - SessionID uint64
149 -
150 - // Internal receive buffer for chunked reassembly
151 - recvBuf []byte
152 -
153 - // Reusable packet scratch buffer for receive chunk assembly.
154 - pktBuf []byte
155 -
156 - // In-flight message_id set (client-side only)
157 - inflightIDs map[uint64]struct{}
158 -}
159 -
160 -func (s *Session) failAllInflight() {
161 - if s.role != RoleClient || len(s.inflightIDs) == 0 {
162 - return
163 - }
164 - clear(s.inflightIDs)
165 -}
166 -
167 -// Fd returns the raw file descriptor for poll/epoll integration.
168 -func (s *Session) Fd() int {
169 - return s.fd
170 -}
171 -
172 -// Role returns the session role.
173 -func (s *Session) Role() Role {
174 - return s.role
175 -}
176 -
177 -// Close closes the session and releases resources.
178 -func (s *Session) Close() {
179 - if s.fd >= 0 {
180 - syscall.Close(s.fd)
181 - s.fd = -1
182 - }
183 - s.recvBuf = nil
184 - s.pktBuf = nil
185 - s.failAllInflight()
186 -}
187 -
188 -// Connect establishes a session to a server at {runDir}/{serviceName}.sock.
189 -// Performs the full handshake. Blocks until connected + handshake done.
190 -func Connect(runDir, serviceName string, config *ClientConfig) (*Session, error) {
191 - path, err := buildSocketPath(runDir, serviceName)
192 - if err != nil {
193 - return nil, err
194 - }
195 -
196 - fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
197 - if err != nil {
198 - return nil, wrapErr(ErrSocket, err.Error())
199 - }
200 -
201 - session, herr := connectAndHandshake(fd, path, config)
202 - if herr != nil {
203 - syscall.Close(fd)
204 - return nil, herr
205 - }
206 - return session, nil
207 -}
208 -
209 -// Send sends one logical message. The caller fills Kind, Code, Flags,
210 -// ItemCount, MessageID in hdr; this function sets Magic/Version/
211 -// HeaderLen/PayloadLen. If the total message exceeds PacketSize,
212 -// it is chunked transparently.
213 -func (s *Session) Send(hdr *protocol.Header, payload []byte) error {
214 - if s.fd < 0 {
215 - return wrapErr(ErrBadParam, "session closed")
216 - }
217 -
218 - // Client-side: track in-flight message_ids for requests
219 - if s.role == RoleClient && hdr.Kind == protocol.KindRequest {
220 - if s.inflightIDs == nil {
221 - s.inflightIDs = make(map[uint64]struct{})
222 - }
223 - if _, exists := s.inflightIDs[hdr.MessageID]; exists {
224 - return wrapErr(ErrDuplicateMsgID, fmt.Sprintf("message_id %d", hdr.MessageID))
225 - }
226 - s.inflightIDs[hdr.MessageID] = struct{}{}
227 - }
228 -
229 - // Fill envelope fields
230 - hdr.Magic = protocol.MagicMsg
231 - hdr.Version = protocol.Version
232 - hdr.HeaderLen = protocol.HeaderLen
233 - hdr.PayloadLen = uint32(len(payload))
234 -
235 - tracked := s.role == RoleClient && hdr.Kind == protocol.KindRequest
236 -
237 - sendErr := s.sendInner(hdr, payload)
238 -
239 - // Rollback: remove message_id from in-flight set on send failure
240 - if sendErr != nil && tracked {
241 - if errors.Is(sendErr, ErrSend) {
242 - s.failAllInflight()
243 - } else {
244 - delete(s.inflightIDs, hdr.MessageID)
245 - }
246 - }
247 -
248 - return sendErr
249 -}
250 -
251 -// sendInner performs the actual send logic, separated so the caller can
252 -// rollback the in-flight set on failure.
253 -func (s *Session) sendInner(hdr *protocol.Header, payload []byte) error {
254 - totalMsg := protocol.HeaderSize + len(payload)
255 -
256 - // Single packet?
257 - if totalMsg <= int(s.PacketSize) {
258 - var hdrBuf [protocol.HeaderSize]byte
259 - hdr.Encode(hdrBuf[:])
260 - return rawSendIov(s.fd, hdrBuf[:], payload)
261 - }
262 -
263 - // Chunked send
264 - chunkPayloadBudget := int(s.PacketSize) - protocol.HeaderSize
265 - if chunkPayloadBudget <= 0 {
266 - return wrapErr(ErrBadParam, "packet_size too small")
267 - }
268 -
269 - firstChunkPayload := min(len(payload), chunkPayloadBudget)
270 -
271 - remainingAfterFirst := len(payload) - firstChunkPayload
272 - continuationChunks := uint32(0)
273 - if remainingAfterFirst > 0 {
274 - continuationChunks = uint32((remainingAfterFirst + chunkPayloadBudget - 1) / chunkPayloadBudget)
275 - }
276 - chunkCount := 1 + continuationChunks
277 -
278 - // Send first chunk: outer header + first part of payload
279 - var hdrBuf [protocol.HeaderSize]byte
280 - hdr.Encode(hdrBuf[:])
281 - if err := rawSendIov(s.fd, hdrBuf[:], payload[:firstChunkPayload]); err != nil {
282 - return err
283 - }
284 -
285 - // Send continuation chunks
286 - offset := firstChunkPayload
287 - for ci := uint32(1); ci < chunkCount; ci++ {
288 - remaining := len(payload) - offset
289 - thisChunk := min(remaining, chunkPayloadBudget)
290 -
291 - chk := protocol.ChunkHeader{
292 - Magic: protocol.MagicChunk,
293 - Version: protocol.Version,
294 - Flags: 0,
295 - MessageID: hdr.MessageID,
296 - TotalMessageLen: uint32(totalMsg),
297 - ChunkIndex: ci,
298 - ChunkCount: chunkCount,
299 - ChunkPayloadLen: uint32(thisChunk),
300 - }
301 -
302 - var chkBuf [protocol.HeaderSize]byte
303 - chk.Encode(chkBuf[:])
304 - if err := rawSendIov(s.fd, chkBuf[:], payload[offset:offset+thisChunk]); err != nil {
305 - return err
306 - }
307 -
308 - offset += thisChunk
309 - }
310 -
311 - return nil
312 -}
313 -
314 -// Receive reads one logical message. Blocks until a complete message
315 -// arrives. buf is a caller-provided scratch buffer for the first packet.
316 -// On success, returns the header and a payload view valid until the next
317 -// Receive call on this session.
318 -func (s *Session) Receive(buf []byte) (protocol.Header, []byte, error) {
319 - if s.fd < 0 {
320 - return protocol.Header{}, nil, wrapErr(ErrBadParam, "session closed")
321 - }
322 -
323 - // Read first packet
324 - n, err := rawRecv(s.fd, buf)
325 - if err != nil {
326 - if errors.Is(err, ErrRecv) {
327 - s.failAllInflight()
328 - }
329 - return protocol.Header{}, nil, err
330 - }
331 -
332 - if n < protocol.HeaderSize {
333 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "packet too short for header")
334 - }
335 -
336 - hdr, err := protocol.DecodeHeader(buf[:n])
337 - if err != nil {
338 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "header decode: "+err.Error())
339 - }
340 -
341 - // Validate payload_len against negotiated directional limit.
342 - // Server receives requests; client receives responses.
343 - var maxPayload uint32
344 - if s.role == RoleServer {
345 - maxPayload = s.MaxRequestPayloadBytes
346 - } else {
347 - maxPayload = s.MaxResponsePayloadBytes
348 - }
349 - if hdr.PayloadLen > maxPayload {
350 - return protocol.Header{}, nil, wrapErr(ErrLimitExceeded,
351 - fmt.Sprintf("payload_len %d exceeds negotiated max %d", hdr.PayloadLen, maxPayload))
352 - }
353 -
354 - // Validate item_count against negotiated directional batch limit.
355 - var maxBatch uint32
356 - if s.role == RoleServer {
357 - maxBatch = s.MaxRequestBatchItems
358 - } else {
359 - maxBatch = s.MaxResponseBatchItems
360 - }
361 - if hdr.ItemCount > maxBatch {
362 - return protocol.Header{}, nil, wrapErr(ErrLimitExceeded,
363 - fmt.Sprintf("item_count %d exceeds negotiated max %d", hdr.ItemCount, maxBatch))
364 - }
365 -
366 - // Client-side: validate response message_id is in-flight
367 - if s.role == RoleClient && hdr.Kind == protocol.KindResponse {
368 - if s.inflightIDs == nil {
369 - return protocol.Header{}, nil, wrapErr(ErrUnknownMsgID,
370 - fmt.Sprintf("message_id %d", hdr.MessageID))
371 - }
372 - if _, exists := s.inflightIDs[hdr.MessageID]; !exists {
373 - return protocol.Header{}, nil, wrapErr(ErrUnknownMsgID,
374 - fmt.Sprintf("message_id %d", hdr.MessageID))
375 - }
376 - delete(s.inflightIDs, hdr.MessageID)
377 - }
378 -
379 - totalMsg := protocol.HeaderSize + int(hdr.PayloadLen)
380 -
381 - // Non-chunked: entire message in one packet
382 - if n >= totalMsg {
383 - payload := buf[protocol.HeaderSize : protocol.HeaderSize+int(hdr.PayloadLen)]
384 -
385 - // Validate batch directory
386 - if hdr.Flags&protocol.FlagBatch != 0 && hdr.ItemCount > 1 {
387 - dirBytes := int(hdr.ItemCount) * 8
388 - dirAligned := protocol.Align8(dirBytes)
389 - if len(payload) < dirAligned {
390 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir exceeds payload")
391 - }
392 - packedAreaLen := uint32(len(payload) - dirAligned)
393 - if err := protocol.BatchDirValidate(payload[:dirBytes], hdr.ItemCount, packedAreaLen); err != nil {
394 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir: "+err.Error())
395 - }
396 - }
397 -
398 - return hdr, payload, nil
399 - }
400 -
401 - // Chunked: first packet has partial payload
402 - firstPayloadBytes := n - protocol.HeaderSize
403 -
404 - // Ensure recv buffer is large enough
405 - needed := int(hdr.PayloadLen)
406 - if len(s.recvBuf) < needed {
407 - s.recvBuf = make([]byte, needed)
408 - }
409 -
410 - // Copy first chunk's payload
411 - copy(s.recvBuf[:firstPayloadBytes], buf[protocol.HeaderSize:protocol.HeaderSize+firstPayloadBytes])
412 -
413 - assembled := firstPayloadBytes
414 - chunkPayloadBudget := int(s.PacketSize) - protocol.HeaderSize
415 -
416 - // Expected chunk count
417 - remainingAfterFirst := int(hdr.PayloadLen) - firstPayloadBytes
418 - expectedContinuations := uint32(0)
419 - if remainingAfterFirst > 0 && chunkPayloadBudget > 0 {
420 - expectedContinuations = uint32((remainingAfterFirst + chunkPayloadBudget - 1) / chunkPayloadBudget)
421 - }
422 - expectedChunkCount := 1 + expectedContinuations
423 -
424 - // Temporary buffer for continuation packets
425 - pktBuf := ensureScratchBuf(&s.pktBuf, int(s.PacketSize))
426 -
427 - ci := uint32(1)
428 - for assembled < int(hdr.PayloadLen) {
429 - cn, err := rawRecv(s.fd, pktBuf)
430 - if err != nil {
431 - if errors.Is(err, ErrRecv) {
432 - s.failAllInflight()
433 - }
434 - return protocol.Header{}, nil, wrapErr(ErrRecv, "continuation recv: "+err.Error())
435 - }
436 -
437 - if cn < protocol.HeaderSize {
438 - return protocol.Header{}, nil, wrapErr(ErrChunk, "continuation too short")
439 - }
440 -
441 - chk, err := protocol.DecodeChunkHeader(pktBuf[:cn])
442 - if err != nil {
443 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk header: "+err.Error())
444 - }
445 -
446 - // Validate chunk header
447 - if chk.MessageID != hdr.MessageID {
448 - return protocol.Header{}, nil, wrapErr(ErrChunk, "message_id mismatch")
449 - }
450 - if chk.ChunkIndex != ci {
451 - return protocol.Header{}, nil, wrapErr(ErrChunk, fmt.Sprintf(
452 - "chunk_index mismatch: expected %d, got %d", ci, chk.ChunkIndex))
453 - }
454 - if chk.ChunkCount != expectedChunkCount {
455 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk_count mismatch")
456 - }
457 - if chk.TotalMessageLen != uint32(totalMsg) {
458 - return protocol.Header{}, nil, wrapErr(ErrChunk, "total_message_len mismatch")
459 - }
460 -
461 - chunkData := cn - protocol.HeaderSize
462 - if chunkData != int(chk.ChunkPayloadLen) {
463 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk_payload_len mismatch")
464 - }
465 - if assembled+chunkData > int(hdr.PayloadLen) {
466 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk exceeds payload_len")
467 - }
468 -
469 - copy(s.recvBuf[assembled:assembled+chunkData], pktBuf[protocol.HeaderSize:protocol.HeaderSize+chunkData])
470 - assembled += chunkData
471 - ci++
472 - }
473 -
474 - payload := s.recvBuf[:hdr.PayloadLen]
475 -
476 - // Validate batch directory
477 - if hdr.Flags&protocol.FlagBatch != 0 && hdr.ItemCount > 1 {
478 - dirBytes := int(hdr.ItemCount) * 8
479 - dirAligned := protocol.Align8(dirBytes)
480 - if len(payload) < dirAligned {
481 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir exceeds payload")
482 - }
483 - packedAreaLen := uint32(len(payload) - dirAligned)
484 - if err := protocol.BatchDirValidate(payload[:dirBytes], hdr.ItemCount, packedAreaLen); err != nil {
485 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir: "+err.Error())
486 - }
487 - }
488 -
489 - return hdr, payload, nil
490 -}
491 -
492 -// ---------------------------------------------------------------------------
493 -// Listener
494 -// ---------------------------------------------------------------------------
495 -
496 -// Listener is a listening UDS SEQPACKET endpoint.
497 -type Listener struct {
498 - fd int
499 - config ServerConfig
500 - path string
501 - nextSessionID atomic.Uint64
502 -}
503 -
504 -// Listen creates a listener on {runDir}/{serviceName}.sock.
505 -// Performs stale endpoint recovery.
506 -func Listen(runDir, serviceName string, config ServerConfig) (*Listener, error) {
507 - path, err := buildSocketPath(runDir, serviceName)
508 - if err != nil {
509 - return nil, err
510 - }
511 -
512 - // Stale recovery
513 - stale := checkAndRecoverStale(path)
514 - if stale == staleLiveServer {
515 - return nil, ErrAddrInUse
516 - }
517 -
518 - fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
519 - if err != nil {
520 - return nil, wrapErr(ErrSocket, err.Error())
521 - }
522 -
523 - // Bind
524 - sa := &syscall.SockaddrUnix{Name: path}
525 - if err := syscall.Bind(fd, sa); err != nil {
526 - syscall.Close(fd)
527 - return nil, wrapErr(ErrSocket, "bind: "+err.Error())
528 - }
529 -
530 - backlog := config.Backlog
531 - if backlog <= 0 {
532 - backlog = defaultBacklog
533 - }
534 -
535 - if err := syscall.Listen(fd, backlog); err != nil {
536 - syscall.Close(fd)
537 - os.Remove(path)
538 - return nil, wrapErr(ErrSocket, "listen: "+err.Error())
539 - }
540 -
541 - return &Listener{
542 - fd: fd,
543 - config: config,
544 - path: path,
545 - }, nil
546 -}
547 -
548 -// Fd returns the raw file descriptor for poll/epoll integration.
549 -func (l *Listener) Fd() int {
550 - return l.fd
551 -}
552 -
553 -// SetPayloadLimits updates the payload limits used for future handshakes.
554 -func (l *Listener) SetPayloadLimits(maxRequestPayloadBytes, maxResponsePayloadBytes uint32) {
555 - l.config.MaxRequestPayloadBytes = maxRequestPayloadBytes
556 - l.config.MaxResponsePayloadBytes = maxResponsePayloadBytes
557 -}
558 -
559 -// Accept accepts one client connection. Performs the full handshake.
560 -// Blocks until a client connects and the handshake completes.
561 -func (l *Listener) Accept() (*Session, error) {
562 - sessionID := l.nextSessionID.Add(1)
563 - return l.AcceptWithConfig(sessionID, l.config)
564 -}
565 -
566 -// AcceptWithConfig accepts one client connection using a caller-provided
567 -// per-session server config and session ID.
568 -func (l *Listener) AcceptWithConfig(sessionID uint64, config ServerConfig) (*Session, error) {
569 - nfd, _, err := syscall.Accept(l.fd)
570 - if err != nil {
571 - return nil, wrapErr(ErrAccept, err.Error())
572 - }
573 -
574 - session, herr := serverHandshake(nfd, &config, sessionID)
575 - if herr != nil {
576 - syscall.Close(nfd)
577 - return nil, herr
578 - }
579 - return session, nil
580 -}
581 -
582 -// Close closes the listener, stops accepting, and unlinks the socket file.
583 -func (l *Listener) Close() {
584 - if l.fd >= 0 {
585 - syscall.Close(l.fd)
586 - l.fd = -1
587 - }
588 - if l.path != "" {
589 - os.Remove(l.path)
590 - l.path = ""
591 - }
592 -}
593 -
65 // ---------------------------------------------------------------------------
66 // Internal helpers
67 // ---------------------------------------------------------------------------
@@ -664,13 +135,6 @@ func minU32(a, b uint32) uint32 {
135 return b
136 }
137
667 -func maxU32(a, b uint32) uint32 {
668 - if a > b {
669 - return a
670 - }
671 - return b
672 -}
673 -
138 // ---------------------------------------------------------------------------
139 // Low-level I/O
140 // ---------------------------------------------------------------------------
@@ -678,12 +142,12 @@ func maxU32(a, b uint32) uint32 {
142 // rawSendIov sends header + payload as one SEQPACKET message using sendmsg.
143 func rawSendIov(fd int, hdr []byte, payload []byte) error {
144 var iov [2]syscall.Iovec
681 - iov[0].Base = unsafe.SliceData(hdr)
145 + iov[0].Base = unsafe.SliceData(hdr) // #nosec G103 -- sendmsg iovec needs the backing byte-slice pointer.
146 iov[0].SetLen(len(hdr))
147
148 iovlen := uint64(1)
149 if len(payload) > 0 {
686 - iov[1].Base = unsafe.SliceData(payload)
150 + iov[1].Base = unsafe.SliceData(payload) // #nosec G103 -- sendmsg iovec needs the backing byte-slice pointer.
151 iov[1].SetLen(len(payload))
152 iovlen = 2
153 }
@@ -696,7 +160,7 @@ func rawSendIov(fd int, hdr []byte, payload []byte) error {
160 n, _, errno := syscall.Syscall(
161 syscall.SYS_SENDMSG,
162 uintptr(fd),
699 - uintptr(unsafe.Pointer(&msg)),
163 + uintptr(unsafe.Pointer(&msg)), // #nosec G103 -- raw sendmsg syscall requires a Msghdr pointer.
164 uintptr(syscall.MSG_NOSIGNAL),
165 )
166 if errno != 0 {
@@ -728,346 +192,3 @@ func rawRecv(fd int, buf []byte) (int, error) {
192 }
193 return n, nil
194 }
731 -
732 -// ---------------------------------------------------------------------------
733 -// Stale endpoint recovery
734 -// ---------------------------------------------------------------------------
735 -
736 -type staleResult int
737 -
738 -const (
739 - staleNotExist staleResult = 0
740 - staleRecovered staleResult = 1
741 - staleLiveServer staleResult = 2
742 -)
743 -
744 -func checkAndRecoverStale(path string) staleResult {
745 - _, err := os.Stat(path)
746 - if err != nil {
747 - return staleNotExist
748 - }
749 -
750 - // Try connecting to see if a live server is there.
751 - // We use net.Dial instead of raw syscalls for the probe — it handles
752 - // all the sockaddr setup and is fine for a one-shot connectivity test.
753 - conn, err := net.Dial("unixpacket", path)
754 - if err == nil {
755 - // Connected => live server
756 - conn.Close()
757 - return staleLiveServer
758 - }
759 -
760 - // Only unlink on connection-refused (stale socket).
761 - // Other errors (EACCES, etc.) should not remove the file.
762 - if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ENOENT) {
763 - os.Remove(path)
764 - return staleRecovered
765 - }
766 - // Can't determine ownership — treat as live to prevent overwriting
767 - return staleLiveServer
768 -}
769 -
770 -// ---------------------------------------------------------------------------
771 -// Handshake: client side
772 -// ---------------------------------------------------------------------------
773 -
774 -func connectAndHandshake(fd int, path string, config *ClientConfig) (*Session, error) {
775 - // Connect
776 - sa := &syscall.SockaddrUnix{Name: path}
777 - if err := syscall.Connect(fd, sa); err != nil {
778 - return nil, wrapErr(ErrConnect, err.Error())
779 - }
780 -
781 - pktSize := config.PacketSize
782 - if pktSize == 0 {
783 - pktSize = detectPacketSize(fd)
784 - }
785 -
786 - supported := config.SupportedProfiles
787 - if supported == 0 {
788 - supported = protocol.ProfileBaseline
789 - }
790 -
791 - // Build HELLO
792 - hello := protocol.Hello{
793 - LayoutVersion: 1,
794 - Flags: 0,
795 - SupportedProfiles: supported,
796 - PreferredProfiles: config.PreferredProfiles,
797 - MaxRequestPayloadBytes: applyDefault(config.MaxRequestPayloadBytes, protocol.MaxPayloadDefault),
798 - MaxRequestBatchItems: applyDefault(config.MaxRequestBatchItems, defaultBatchItems),
799 - MaxResponsePayloadBytes: applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault),
800 - MaxResponseBatchItems: applyDefault(config.MaxResponseBatchItems, defaultBatchItems),
801 - AuthToken: config.AuthToken,
802 - PacketSize: pktSize,
803 - }
804 -
805 - var helloBuf [helloPayloadSize]byte
806 - hello.Encode(helloBuf[:])
807 -
808 - // Build outer CONTROL header
809 - hdr := protocol.Header{
810 - Magic: protocol.MagicMsg,
811 - Version: protocol.Version,
812 - HeaderLen: protocol.HeaderLen,
813 - Kind: protocol.KindControl,
814 - Flags: 0,
815 - Code: protocol.CodeHello,
816 - TransportStatus: protocol.StatusOK,
817 - PayloadLen: helloPayloadSize,
818 - ItemCount: 1,
819 - MessageID: 0,
820 - }
821 -
822 - var pkt [protocol.HeaderSize + helloPayloadSize]byte
823 - hdr.Encode(pkt[:protocol.HeaderSize])
824 - copy(pkt[protocol.HeaderSize:], helloBuf[:])
825 -
826 - // Send HELLO
827 - n, err := syscall.SendmsgN(fd, pkt[:], nil, nil, 0)
828 - if err != nil {
829 - return nil, wrapErr(ErrSend, "hello send: "+err.Error())
830 - }
831 - if n != len(pkt) {
832 - return nil, wrapErr(ErrSend, "hello short write")
833 - }
834 -
835 - // Receive HELLO_ACK
836 - var ackBuf [128]byte
837 - an, _, _, _, err := syscall.Recvmsg(fd, ackBuf[:], nil, 0)
838 - if err != nil {
839 - return nil, wrapErr(ErrRecv, "hello_ack recv: "+err.Error())
840 - }
841 - if an == 0 {
842 - return nil, wrapErr(ErrRecv, "peer disconnected during handshake")
843 - }
844 -
845 - // Decode outer header
846 - ackHdr, err := protocol.DecodeHeader(ackBuf[:an])
847 - if err != nil {
848 - if errors.Is(err, protocol.ErrBadVersion) {
849 - return nil, wrapErr(ErrIncompatible, "ack header version mismatch")
850 - }
851 - return nil, wrapErr(ErrProtocol, "ack header: "+err.Error())
852 - }
853 -
854 - if ackHdr.Kind != protocol.KindControl || ackHdr.Code != protocol.CodeHelloAck {
855 - return nil, wrapErr(ErrProtocol, "expected HELLO_ACK")
856 - }
857 -
858 - // Check transport_status for rejection
859 - if ackHdr.TransportStatus == protocol.StatusAuthFailed {
860 - return nil, ErrAuthFailed
861 - }
862 - if ackHdr.TransportStatus == protocol.StatusUnsupported {
863 - return nil, ErrNoProfile
864 - }
865 - if ackHdr.TransportStatus == protocol.StatusIncompatible {
866 - return nil, ErrIncompatible
867 - }
868 - if ackHdr.TransportStatus == protocol.StatusLimitExceeded {
869 - return nil, ErrLimitExceeded
870 - }
871 - if ackHdr.TransportStatus != protocol.StatusOK {
872 - return nil, wrapErr(ErrHandshake, fmt.Sprintf("transport_status=%d", ackHdr.TransportStatus))
873 - }
874 -
875 - // Decode hello-ack payload
876 - if an < protocol.HeaderSize+helloAckPayloadSize {
877 - return nil, wrapErr(ErrProtocol, "ack payload truncated")
878 - }
879 - ack, err := protocol.DecodeHelloAck(ackBuf[protocol.HeaderSize:an])
880 - if err != nil {
881 - if errors.Is(err, protocol.ErrBadLayout) &&
882 - helloAckLayoutIncompatible(ackBuf[protocol.HeaderSize:an]) {
883 - return nil, wrapErr(ErrIncompatible, "ack payload layout version mismatch")
884 - }
885 - return nil, wrapErr(ErrProtocol, "ack payload: "+err.Error())
886 - }
887 -
888 - return &Session{
889 - fd: fd,
890 - role: RoleClient,
891 - MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
892 - MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
893 - MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
894 - MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
895 - PacketSize: ack.AgreedPacketSize,
896 - SelectedProfile: ack.SelectedProfile,
897 - SessionID: ack.SessionID,
898 - inflightIDs: make(map[uint64]struct{}),
899 - }, nil
900 -}
901 -
902 -// ---------------------------------------------------------------------------
903 -// Handshake: server side
904 -// ---------------------------------------------------------------------------
905 -
906 -func serverHandshake(fd int, config *ServerConfig, sessionID uint64) (*Session, error) {
907 - serverPktSize := config.PacketSize
908 - if serverPktSize == 0 {
909 - serverPktSize = detectPacketSize(fd)
910 - }
911 -
912 - sRespPay := applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault)
913 - sProfiles := config.SupportedProfiles
914 - if sProfiles == 0 {
915 - sProfiles = protocol.ProfileBaseline
916 - }
917 - sPreferred := config.PreferredProfiles
918 -
919 - // Helper: send rejection ACK
920 - sendRejection := func(status uint16) {
921 - ack := protocol.HelloAck{LayoutVersion: 1}
922 - var ackPayBuf [helloAckPayloadSize]byte
923 - ack.Encode(ackPayBuf[:])
924 -
925 - ackHdr := protocol.Header{
926 - Magic: protocol.MagicMsg,
927 - Version: protocol.Version,
928 - HeaderLen: protocol.HeaderLen,
929 - Kind: protocol.KindControl,
930 - Code: protocol.CodeHelloAck,
931 - TransportStatus: status,
932 - PayloadLen: helloAckPayloadSize,
933 - ItemCount: 1,
934 - }
935 -
936 - var pkt [protocol.HeaderSize + helloAckPayloadSize]byte
937 - ackHdr.Encode(pkt[:protocol.HeaderSize])
938 - copy(pkt[protocol.HeaderSize:], ackPayBuf[:])
939 - // Best effort send
940 - syscall.SendmsgN(fd, pkt[:], nil, nil, 0) //nolint:errcheck
941 - }
942 -
943 - // Receive HELLO
944 - var buf [128]byte
945 - n, _, _, _, err := syscall.Recvmsg(fd, buf[:], nil, 0)
946 - if err != nil {
947 - return nil, wrapErr(ErrRecv, "hello recv: "+err.Error())
948 - }
949 - if n == 0 {
950 - return nil, wrapErr(ErrRecv, "peer disconnected during handshake")
951 - }
952 -
953 - hdr, err := protocol.DecodeHeader(buf[:n])
954 - if err != nil {
955 - if errors.Is(err, protocol.ErrBadVersion) &&
956 - headerVersionIncompatible(buf[:n], protocol.CodeHello) {
957 - sendRejection(protocol.StatusIncompatible)
958 - return nil, ErrIncompatible
959 - }
960 - return nil, wrapErr(ErrProtocol, "hello header: "+err.Error())
961 - }
962 -
963 - if hdr.Kind != protocol.KindControl || hdr.Code != protocol.CodeHello {
964 - return nil, wrapErr(ErrProtocol, "expected HELLO")
965 - }
966 -
967 - hello, err := protocol.DecodeHello(buf[protocol.HeaderSize:n])
968 - if err != nil {
969 - if errors.Is(err, protocol.ErrBadLayout) &&
970 - helloLayoutIncompatible(buf[protocol.HeaderSize:n]) {
971 - sendRejection(protocol.StatusIncompatible)
972 - return nil, ErrIncompatible
973 - }
974 - return nil, wrapErr(ErrProtocol, "hello payload: "+err.Error())
975 - }
976 -
977 - // Compute intersection
978 - intersection := hello.SupportedProfiles & sProfiles
979 -
980 - // Check intersection
981 - if intersection == 0 {
982 - sendRejection(protocol.StatusUnsupported)
983 - return nil, ErrNoProfile
984 - }
985 -
986 - // Check auth
987 - if hello.AuthToken != config.AuthToken {
988 - sendRejection(protocol.StatusAuthFailed)
989 - return nil, ErrAuthFailed
990 - }
991 -
992 - // Select profile: prefer preferred_intersection, then intersection
993 - preferredIntersection := intersection & hello.PreferredProfiles & sPreferred
994 - var selected uint32
995 - if preferredIntersection != 0 {
996 - selected = highestBit(preferredIntersection)
997 - } else {
998 - selected = highestBit(intersection)
999 - }
1000 -
1001 - if hello.MaxRequestPayloadBytes > protocol.MaxPayloadCap {
1002 - sendRejection(protocol.StatusLimitExceeded)
1003 - return nil, ErrLimitExceeded
1004 - }
1005 -
1006 - // Negotiate limits:
1007 - // - request payload and batch size are client-proposed and echoed
1008 - // - response payload is server-authoritative
1009 - // - response batch size is symmetric with request batch size
1010 - agreedReqPay := hello.MaxRequestPayloadBytes
1011 - agreedReqBat := hello.MaxRequestBatchItems
1012 - agreedRespPay := sRespPay
1013 - agreedRespBat := agreedReqBat
1014 - agreedPkt := minU32(hello.PacketSize, serverPktSize)
1015 - if agreedPkt <= protocol.HeaderSize {
1016 - sendRejection(protocol.StatusIncompatible)
1017 - return nil, ErrIncompatible
1018 - }
1019 -
1020 - // Send HELLO_ACK (success)
1021 - ack := protocol.HelloAck{
1022 - LayoutVersion: 1,
1023 - Flags: 0,
1024 - ServerSupportedProfiles: sProfiles,
1025 - IntersectionProfiles: intersection,
1026 - SelectedProfile: selected,
1027 - AgreedMaxRequestPayloadBytes: agreedReqPay,
1028 - AgreedMaxRequestBatchItems: agreedReqBat,
1029 - AgreedMaxResponsePayloadBytes: agreedRespPay,
1030 - AgreedMaxResponseBatchItems: agreedRespBat,
1031 - AgreedPacketSize: agreedPkt,
1032 - SessionID: sessionID,
1033 - }
1034 -
1035 - var ackPayBuf [helloAckPayloadSize]byte
1036 - ack.Encode(ackPayBuf[:])
1037 -
1038 - ackHdr := protocol.Header{
1039 - Magic: protocol.MagicMsg,
1040 - Version: protocol.Version,
1041 - HeaderLen: protocol.HeaderLen,
1042 - Kind: protocol.KindControl,
1043 - Code: protocol.CodeHelloAck,
1044 - TransportStatus: protocol.StatusOK,
1045 - PayloadLen: helloAckPayloadSize,
1046 - ItemCount: 1,
1047 - }
1048 -
1049 - var pkt [protocol.HeaderSize + helloAckPayloadSize]byte
1050 - ackHdr.Encode(pkt[:protocol.HeaderSize])
1051 - copy(pkt[protocol.HeaderSize:], ackPayBuf[:])
1052 -
1053 - sn, err := syscall.SendmsgN(fd, pkt[:], nil, nil, 0)
1054 - if err != nil {
1055 - return nil, wrapErr(ErrSend, "hello_ack send: "+err.Error())
1056 - }
1057 - if sn != len(pkt) {
1058 - return nil, wrapErr(ErrSend, "hello_ack short write")
1059 - }
1060 -
1061 - return &Session{
1062 - fd: fd,
1063 - role: RoleServer,
1064 - MaxRequestPayloadBytes: agreedReqPay,
1065 - MaxRequestBatchItems: agreedReqBat,
1066 - MaxResponsePayloadBytes: agreedRespPay,
1067 - MaxResponseBatchItems: agreedRespBat,
1068 - PacketSize: agreedPkt,
1069 - SelectedProfile: selected,
1070 - SessionID: sessionID,
1071 - inflightIDs: make(map[uint64]struct{}),
1072 - }, nil
1073 -}
src/go/pkg/netipc/transport/posix/uds_handshake.go new
+156
@@ -0,0 +1,156 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "fmt"
7 + "syscall"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
11 +)
12 +
13 +func headerVersionIncompatible(buf []byte, expectedCode uint16) bool {
14 + return framing.HeaderVersionIncompatible(buf, expectedCode)
15 +}
16 +
17 +func helloLayoutIncompatible(buf []byte) bool {
18 + return framing.HelloLayoutIncompatible(buf)
19 +}
20 +
21 +func helloAckLayoutIncompatible(buf []byte) bool {
22 + return framing.HelloAckLayoutIncompatible(buf)
23 +}
24 +
25 +func connectAndHandshake(fd int, path string, config *ClientConfig) (*Session, error) {
26 + sa := &syscall.SockaddrUnix{Name: path}
27 + if err := syscall.Connect(fd, sa); err != nil {
28 + return nil, wrapErr(ErrConnect, err.Error())
29 + }
30 +
31 + pktSize := config.PacketSize
32 + if pktSize == 0 {
33 + pktSize = detectPacketSize(fd)
34 + }
35 +
36 + ack, err := framing.ClientHandshake(framing.ClientHandshakeConfig{
37 + Hello: framing.HelloConfig{
38 + SupportedProfiles: config.SupportedProfiles,
39 + PreferredProfiles: config.PreferredProfiles,
40 + MaxRequestPayloadBytes: applyDefault(config.MaxRequestPayloadBytes, protocol.MaxPayloadDefault),
41 + MaxRequestBatchItems: applyDefault(config.MaxRequestBatchItems, defaultBatchItems),
42 + MaxResponsePayloadBytes: applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault),
43 + MaxResponseBatchItems: applyDefault(config.MaxResponseBatchItems, defaultBatchItems),
44 + AuthToken: config.AuthToken,
45 + PacketSize: pktSize,
46 + },
47 + Send: func(pkt []byte) error {
48 + n, err := syscall.SendmsgN(fd, pkt, nil, nil, 0)
49 + if err != nil {
50 + return err
51 + }
52 + if n != len(pkt) {
53 + return fmt.Errorf("short write")
54 + }
55 + return nil
56 + },
57 + Recv: func(dst []byte) (int, error) { return rawRecv(fd, dst) },
58 + StatusError: helloAckStatusError,
59 + ErrSend: func(msg string) error { return wrapErr(ErrSend, msg) },
60 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
61 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
62 + ErrIncompatible: func(msg string) error { return wrapErr(ErrIncompatible, msg) },
63 + })
64 + if err != nil {
65 + return nil, err
66 + }
67 +
68 + return &Session{
69 + fd: fd,
70 + role: RoleClient,
71 + MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
72 + MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
73 + MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
74 + MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
75 + PacketSize: ack.AgreedPacketSize,
76 + SelectedProfile: ack.SelectedProfile,
77 + SessionID: ack.SessionID,
78 + inflightIDs: make(map[uint64]struct{}),
79 + }, nil
80 +}
81 +
82 +func helloAckStatusError(status uint16) error {
83 + switch status {
84 + case protocol.StatusOK:
85 + return nil
86 + case protocol.StatusAuthFailed:
87 + return ErrAuthFailed
88 + case protocol.StatusUnsupported:
89 + return ErrNoProfile
90 + case protocol.StatusIncompatible:
91 + return ErrIncompatible
92 + case protocol.StatusLimitExceeded:
93 + return ErrLimitExceeded
94 + default:
95 + return wrapErr(ErrHandshake, fmt.Sprintf("transport_status=%d", status))
96 + }
97 +}
98 +
99 +func serverHandshake(fd int, config *ServerConfig, sessionID uint64) (*Session, error) {
100 + serverPktSize := config.PacketSize
101 + if serverPktSize == 0 {
102 + serverPktSize = detectPacketSize(fd)
103 + }
104 +
105 + sRespPay := applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault)
106 + ack, err := framing.ServerHandshake(framing.ServerHandshakeConfig{
107 + ServerHelloConfig: framing.ServerHelloConfig{
108 + PacketSize: serverPktSize,
109 + MaxResponsePayloadBytes: sRespPay,
110 + SupportedProfiles: config.SupportedProfiles,
111 + PreferredProfiles: config.PreferredProfiles,
112 + AuthToken: config.AuthToken,
113 + },
114 + SessionID: sessionID,
115 + Recv: func(dst []byte) (int, error) { return rawRecv(fd, dst) },
116 + SendAck: func(status uint16, ack protocol.HelloAck) error { return sendHelloAck(fd, status, ack) },
117 + StatusError: helloAckStatusError,
118 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
119 + ErrSend: func(msg string) error { return wrapErr(ErrSend, msg) },
120 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
121 + ErrIncompatible: func(msg string) error { return wrapErr(ErrIncompatible, msg) },
122 + })
123 + if err != nil {
124 + return nil, err
125 + }
126 +
127 + return &Session{
128 + fd: fd,
129 + role: RoleServer,
130 + MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
131 + MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
132 + MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
133 + MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
134 + PacketSize: ack.AgreedPacketSize,
135 + SelectedProfile: ack.SelectedProfile,
136 + SessionID: sessionID,
137 + inflightIDs: make(map[uint64]struct{}),
138 + }, nil
139 +}
140 +
141 +func sendRejection(fd int, status uint16) {
142 + _ = sendHelloAck(fd, status, protocol.HelloAck{LayoutVersion: 1})
143 +}
144 +
145 +func sendHelloAck(fd int, status uint16, ack protocol.HelloAck) error {
146 + pkt := framing.BuildHelloAckPacket(status, ack)
147 +
148 + sn, err := syscall.SendmsgN(fd, pkt[:], nil, nil, 0)
149 + if err != nil {
150 + return err
151 + }
152 + if sn != len(pkt) {
153 + return wrapErr(ErrSend, "hello_ack short write")
154 + }
155 + return nil
156 +}
src/go/pkg/netipc/transport/posix/uds_listener.go new
+105
@@ -0,0 +1,105 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "os"
7 + "sync/atomic"
8 + "syscall"
9 +)
10 +
11 +// Listener is a listening UDS SEQPACKET endpoint.
12 +type Listener struct {
13 + fd int
14 + config ServerConfig
15 + path string
16 + nextSessionID atomic.Uint64
17 +}
18 +
19 +// Listen creates a listener on {runDir}/{serviceName}.sock.
20 +// Performs stale endpoint recovery.
21 +func Listen(runDir, serviceName string, config ServerConfig) (*Listener, error) {
22 + path, err := buildSocketPath(runDir, serviceName)
23 + if err != nil {
24 + return nil, err
25 + }
26 +
27 + stale := checkAndRecoverStale(path, runDirAllowsStaleUnlink(runDir))
28 + if stale == staleLiveServer {
29 + return nil, ErrAddrInUse
30 + }
31 +
32 + fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
33 + if err != nil {
34 + return nil, wrapErr(ErrSocket, err.Error())
35 + }
36 +
37 + sa := &syscall.SockaddrUnix{Name: path}
38 + if err := syscall.Bind(fd, sa); err != nil {
39 + _ = syscall.Close(fd)
40 + return nil, wrapErr(ErrSocket, "bind: "+err.Error())
41 + }
42 +
43 + backlog := config.Backlog
44 + if backlog <= 0 {
45 + backlog = defaultBacklog
46 + }
47 +
48 + if err := syscall.Listen(fd, backlog); err != nil {
49 + _ = syscall.Close(fd)
50 + _ = os.Remove(path)
51 + return nil, wrapErr(ErrSocket, "listen: "+err.Error())
52 + }
53 +
54 + return &Listener{
55 + fd: fd,
56 + config: config,
57 + path: path,
58 + }, nil
59 +}
60 +
61 +// Fd returns the raw file descriptor for poll/epoll integration.
62 +func (l *Listener) Fd() int {
63 + return l.fd
64 +}
65 +
66 +// SetPayloadLimits updates the payload limits used for future handshakes.
67 +func (l *Listener) SetPayloadLimits(maxRequestPayloadBytes, maxResponsePayloadBytes uint32) {
68 + l.config.MaxRequestPayloadBytes = maxRequestPayloadBytes
69 + l.config.MaxResponsePayloadBytes = maxResponsePayloadBytes
70 +}
71 +
72 +// Accept accepts one client connection. Performs the full handshake.
73 +// Blocks until a client connects and the handshake completes.
74 +func (l *Listener) Accept() (*Session, error) {
75 + sessionID := l.nextSessionID.Add(1)
76 + return l.AcceptWithConfig(sessionID, l.config)
77 +}
78 +
79 +// AcceptWithConfig accepts one client connection using a caller-provided
80 +// per-session server config and session ID.
81 +func (l *Listener) AcceptWithConfig(sessionID uint64, config ServerConfig) (*Session, error) {
82 + nfd, _, err := syscall.Accept(l.fd)
83 + if err != nil {
84 + return nil, wrapErr(ErrAccept, err.Error())
85 + }
86 +
87 + session, herr := serverHandshake(nfd, &config, sessionID)
88 + if herr != nil {
89 + _ = syscall.Close(nfd)
90 + return nil, herr
91 + }
92 + return session, nil
93 +}
94 +
95 +// Close closes the listener, stops accepting, and unlinks the socket file.
96 +func (l *Listener) Close() {
97 + if l.fd >= 0 {
98 + _ = syscall.Close(l.fd)
99 + l.fd = -1
100 + }
101 + if l.path != "" {
102 + _ = os.Remove(l.path)
103 + l.path = ""
104 + }
105 +}
src/go/pkg/netipc/transport/posix/uds_receive.go new
+41
@@ -0,0 +1,41 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "errors"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
10 +)
11 +
12 +// Receive reads one logical message. Blocks until a complete message
13 +// arrives. buf is a caller-provided scratch buffer for the first packet.
14 +// On success, returns the header and a payload view valid until the next
15 +// Receive call on this session.
16 +func (s *Session) Receive(buf []byte) (protocol.Header, []byte, error) {
17 + if s.fd < 0 {
18 + return protocol.Header{}, nil, wrapErr(ErrBadParam, "session closed")
19 + }
20 +
21 + return framing.SessionReceive(framing.SessionReceiveConfig{
22 + RoleServer: s.role == RoleServer,
23 + PacketSize: s.PacketSize,
24 + MaxRequestPayloadBytes: s.MaxRequestPayloadBytes,
25 + MaxRequestBatchItems: s.MaxRequestBatchItems,
26 + MaxResponsePayloadBytes: s.MaxResponsePayloadBytes,
27 + MaxResponseBatchItems: s.MaxResponseBatchItems,
28 + InflightIDs: s.inflightIDs,
29 + RecvBuf: &s.recvBuf,
30 + PacketBuf: &s.pktBuf,
31 + Recv: func(dst []byte) (int, error) { return rawRecv(s.fd, dst) },
32 + EnsurePacketScratch: ensureScratchBuf,
33 + IsRecvDisconnect: func(err error) bool { return errors.Is(err, ErrRecv) },
34 + FailAllInflight: s.failAllInflight,
35 + ErrLimitExceeded: func(msg string) error { return wrapErr(ErrLimitExceeded, msg) },
36 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
37 + ErrChunk: func(msg string) error { return wrapErr(ErrChunk, msg) },
38 + ErrUnknownMsgID: func(msg string) error { return wrapErr(ErrUnknownMsgID, msg) },
39 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
40 + }, buf)
41 +}
src/go/pkg/netipc/transport/posix/uds_send.go new
+41
@@ -0,0 +1,41 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "errors"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
10 +)
11 +
12 +// Send sends one logical message. The caller fills Kind, Code, Flags,
13 +// ItemCount, MessageID in hdr; this function sets Magic/Version/
14 +// HeaderLen/PayloadLen. If the total message exceeds PacketSize,
15 +// it is chunked transparently.
16 +func (s *Session) Send(hdr *protocol.Header, payload []byte) error {
17 + if s.fd < 0 {
18 + return wrapErr(ErrBadParam, "session closed")
19 + }
20 +
21 + return framing.SessionSend(framing.SessionSendConfig{
22 + RoleClient: s.role == RoleClient,
23 + PacketSize: s.PacketSize,
24 + InflightIDs: &s.inflightIDs,
25 + FailAllInflight: s.failAllInflight,
26 + IsSendDisconnect: func(err error) bool { return errors.Is(err, ErrSend) },
27 + SendFirstPacket: func(packetHdr *protocol.Header, packetPayload []byte, _ int) error {
28 + var hdrBuf [protocol.HeaderSize]byte
29 + packetHdr.Encode(hdrBuf[:])
30 + return rawSendIov(s.fd, hdrBuf[:], packetPayload)
31 + },
32 + SendChunk: func(chk protocol.ChunkHeader, chunkPayload []byte) error {
33 + var chkBuf [protocol.HeaderSize]byte
34 + chk.Encode(chkBuf[:])
35 + return rawSendIov(s.fd, chkBuf[:], chunkPayload)
36 + },
37 + ErrLimitExceeded: func(msg string) error { return wrapErr(ErrLimitExceeded, msg) },
38 + ErrDuplicateMsgID: func(msg string) error { return wrapErr(ErrDuplicateMsgID, msg) },
39 + ErrBadParam: func(msg string) error { return wrapErr(ErrBadParam, msg) },
40 + }, hdr, payload)
41 +}
src/go/pkg/netipc/transport/posix/uds_send_test.go new
+32
@@ -0,0 +1,32 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
10 +)
11 +
12 +func TestHeaderPayloadLenBounds(t *testing.T) {
13 + if _, _, ok := framing.HeaderPayloadLen(-1); ok {
14 + t.Fatal("negative payload length should fail")
15 + }
16 +
17 + maxInt := int(^uint(0) >> 1)
18 + if _, _, ok := framing.HeaderPayloadLen(maxInt); ok {
19 + t.Fatal("payload length that overflows int total length should fail")
20 + }
21 +
22 + protocolLimit := uint64(^uint32(0)) - uint64(protocol.HeaderSize)
23 + if protocolLimit <= uint64(maxInt) {
24 + got, _, ok := framing.HeaderPayloadLen(int(protocolLimit))
25 + if !ok {
26 + t.Fatal("protocol-limit payload should pass")
27 + }
28 + if uint64(got) != uint64(^uint32(0)) {
29 + t.Fatalf("total length = %d, want %d", got, uint64(^uint32(0)))
30 + }
31 + }
32 +}
src/go/pkg/netipc/transport/posix/uds_session.go new
+106
@@ -0,0 +1,106 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import "syscall"
6 +
7 +// Role distinguishes client vs server sessions.
8 +type Role int
9 +
10 +const (
11 + RoleClient Role = 1
12 + RoleServer Role = 2
13 +)
14 +
15 +// ClientConfig configures a client connection.
16 +type ClientConfig struct {
17 + SupportedProfiles uint32
18 + PreferredProfiles uint32
19 + MaxRequestPayloadBytes uint32 // 0 = use default (1024)
20 + MaxRequestBatchItems uint32 // 0 = use default (1)
21 + MaxResponsePayloadBytes uint32
22 + MaxResponseBatchItems uint32
23 + AuthToken uint64
24 + PacketSize uint32 // 0 = auto-detect from SO_SNDBUF
25 +}
26 +
27 +// ServerConfig configures a listener and its accepted sessions.
28 +type ServerConfig struct {
29 + SupportedProfiles uint32
30 + PreferredProfiles uint32
31 + MaxRequestPayloadBytes uint32
32 + MaxRequestBatchItems uint32
33 + MaxResponsePayloadBytes uint32
34 + MaxResponseBatchItems uint32
35 + AuthToken uint64
36 + PacketSize uint32 // 0 = auto-detect from SO_SNDBUF
37 + Backlog int // 0 = default (16)
38 +}
39 +
40 +// Session is a connected UDS SEQPACKET session (client or server side).
41 +type Session struct {
42 + fd int
43 + role Role
44 +
45 + MaxRequestPayloadBytes uint32
46 + MaxRequestBatchItems uint32
47 + MaxResponsePayloadBytes uint32
48 + MaxResponseBatchItems uint32
49 + PacketSize uint32
50 + SelectedProfile uint32
51 + SessionID uint64
52 +
53 + recvBuf []byte
54 + pktBuf []byte
55 +
56 + inflightIDs map[uint64]struct{}
57 +}
58 +
59 +func (s *Session) failAllInflight() {
60 + if s.role != RoleClient || len(s.inflightIDs) == 0 {
61 + return
62 + }
63 + clear(s.inflightIDs)
64 +}
65 +
66 +// Fd returns the raw file descriptor for poll/epoll integration.
67 +func (s *Session) Fd() int {
68 + return s.fd
69 +}
70 +
71 +// Role returns the session role.
72 +func (s *Session) Role() Role {
73 + return s.role
74 +}
75 +
76 +// Close closes the session and releases resources.
77 +func (s *Session) Close() {
78 + if s.fd >= 0 {
79 + _ = syscall.Close(s.fd)
80 + s.fd = -1
81 + }
82 + s.recvBuf = nil
83 + s.pktBuf = nil
84 + s.failAllInflight()
85 +}
86 +
87 +// Connect establishes a session to a server at {runDir}/{serviceName}.sock.
88 +// Performs the full handshake. Blocks until connected + handshake done.
89 +func Connect(runDir, serviceName string, config *ClientConfig) (*Session, error) {
90 + path, err := buildSocketPath(runDir, serviceName)
91 + if err != nil {
92 + return nil, err
93 + }
94 +
95 + fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
96 + if err != nil {
97 + return nil, wrapErr(ErrSocket, err.Error())
98 + }
99 +
100 + session, herr := connectAndHandshake(fd, path, config)
101 + if herr != nil {
102 + _ = syscall.Close(fd)
103 + return nil, herr
104 + }
105 + return session, nil
106 +}
src/go/pkg/netipc/transport/posix/uds_stale.go new
+86
@@ -0,0 +1,86 @@
1 +//go:build unix
2 +
3 +package posix
4 +
5 +import (
6 + "errors"
7 + "net"
8 + "os"
9 + "syscall"
10 + "time"
11 +)
12 +
13 +type staleResult int
14 +
15 +const (
16 + staleNotExist staleResult = 0
17 + staleRecovered staleResult = 1
18 + staleLiveServer staleResult = 2
19 +)
20 +
21 +const (
22 + staleDialAttempts = 3
23 + staleDialRetryDelay = 50 * time.Millisecond
24 + staleDialTimeout = 1 * time.Second
25 +)
26 +
27 +func runDirAllowsStaleUnlink(runDir string) bool {
28 + euid := uint32(os.Geteuid())
29 + info, err := os.Stat(runDir)
30 + if err != nil || !info.IsDir() {
31 + return false
32 + }
33 + st, ok := info.Sys().(*syscall.Stat_t)
34 + if !ok || st.Uid != euid {
35 + return false
36 + }
37 + return info.Mode().Perm()&0022 == 0
38 +}
39 +
40 +func dialStaleCandidate(path string) error {
41 + var err error
42 + for attempt := range staleDialAttempts {
43 + var conn net.Conn
44 + conn, err = net.DialTimeout("unixpacket", path, staleDialTimeout)
45 + if err == nil {
46 + _ = conn.Close()
47 + return nil
48 + }
49 + if !errors.Is(err, syscall.ECONNREFUSED) {
50 + return err
51 + }
52 + if attempt+1 < staleDialAttempts {
53 + time.Sleep(staleDialRetryDelay)
54 + }
55 + }
56 + return err
57 +}
58 +
59 +func checkAndRecoverStale(path string, allowStaleUnlink bool) staleResult {
60 + _, err := os.Stat(path)
61 + if err != nil {
62 + return staleNotExist
63 + }
64 +
65 + err = dialStaleCandidate(path)
66 + if err == nil {
67 + return staleLiveServer
68 + }
69 +
70 + if errors.Is(err, syscall.ENOENT) {
71 + return staleNotExist
72 + }
73 + if errors.Is(err, syscall.ECONNREFUSED) {
74 + if !allowStaleUnlink {
75 + return staleLiveServer
76 + }
77 + if removeErr := os.Remove(path); removeErr != nil {
78 + if os.IsNotExist(removeErr) {
79 + return staleNotExist
80 + }
81 + return staleLiveServer
82 + }
83 + return staleRecovered
84 + }
85 + return staleLiveServer
86 +}
src/go/pkg/netipc/transport/posix/uds_test.go
+1 -1
@@ -467,7 +467,7 @@ func TestChunking(t *testing.T) {
467 }
468
469 // Client receives
470 - rHdr, rPayload, err = client.Receive(recvBuf)
470 + _, rPayload, err = client.Receive(recvBuf)
471 if err != nil {
472 t.Fatalf("client Receive (chunked): %v", err)
473 }
src/go/pkg/netipc/transport/windows/pipe.go
-939
@@ -10,17 +10,11 @@
10 package windows
11
12 import (
13 - "encoding/binary"
13 "errors"
14 "fmt"
16 - "sync"
17 - "sync/atomic"
15 "syscall"
19 - "time"
16 "unicode/utf16"
17 "unsafe"
22 -
23 - "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
18 )
19
20 // ---------------------------------------------------------------------------
@@ -89,26 +83,6 @@ func wrapErr(sentinel error, detail string) error {
83 return fmt.Errorf("%w: %s", sentinel, detail)
84 }
85
92 -func headerVersionIncompatible(buf []byte, expectedCode uint16) bool {
93 - if len(buf) < protocol.HeaderSize {
94 - return false
95 - }
96 -
97 - return binary.NativeEndian.Uint32(buf[0:4]) == protocol.MagicMsg &&
98 - binary.NativeEndian.Uint16(buf[4:6]) != protocol.Version &&
99 - binary.NativeEndian.Uint16(buf[6:8]) == protocol.HeaderLen &&
100 - binary.NativeEndian.Uint16(buf[8:10]) == protocol.KindControl &&
101 - binary.NativeEndian.Uint16(buf[12:14]) == expectedCode
102 -}
103 -
104 -func helloLayoutIncompatible(buf []byte) bool {
105 - return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
106 -}
107 -
108 -func helloAckLayoutIncompatible(buf []byte) bool {
109 - return len(buf) >= 2 && binary.NativeEndian.Uint16(buf[0:2]) != 1
110 -}
111 -
86 // ---------------------------------------------------------------------------
87 // Win32 syscall imports (pure Go, no cgo)
88 // ---------------------------------------------------------------------------
@@ -297,46 +271,6 @@ func isDisconnectError(err error) bool {
271 errno == _ERROR_PIPE_NOT_CONNECTED
272 }
273
300 -// ---------------------------------------------------------------------------
301 -// Role
302 -// ---------------------------------------------------------------------------
303 -
304 -// Role distinguishes client vs server sessions.
305 -type Role int
306 -
307 -const (
308 - RoleClient Role = 1
309 - RoleServer Role = 2
310 -)
311 -
312 -// ---------------------------------------------------------------------------
313 -// Configuration
314 -// ---------------------------------------------------------------------------
315 -
316 -// ClientConfig configures a client connection.
317 -type ClientConfig struct {
318 - SupportedProfiles uint32
319 - PreferredProfiles uint32
320 - MaxRequestPayloadBytes uint32 // 0 = use default (1024)
321 - MaxRequestBatchItems uint32 // 0 = use default (1)
322 - MaxResponsePayloadBytes uint32
323 - MaxResponseBatchItems uint32
324 - AuthToken uint64
325 - PacketSize uint32 // 0 = use default (65536)
326 -}
327 -
328 -// ServerConfig configures a listener and its accepted sessions.
329 -type ServerConfig struct {
330 - SupportedProfiles uint32
331 - PreferredProfiles uint32
332 - MaxRequestPayloadBytes uint32
333 - MaxRequestBatchItems uint32
334 - MaxResponsePayloadBytes uint32
335 - MaxResponseBatchItems uint32
336 - AuthToken uint64
337 - PacketSize uint32 // 0 = use default (65536)
338 -}
339 -
274 // ---------------------------------------------------------------------------
275 // Low-level I/O
276 // ---------------------------------------------------------------------------
@@ -386,882 +320,9 @@ func rawRecv(handle syscall.Handle, buf []byte) (int, error) {
320 return int(read), nil
321 }
322
389 -// ---------------------------------------------------------------------------
390 -// Session
391 -// ---------------------------------------------------------------------------
392 -
393 -// Session is a connected Named Pipe session (client or server side).
394 -type Session struct {
395 - handle syscall.Handle
396 - role Role
397 -
398 - // Negotiated limits
399 - MaxRequestPayloadBytes uint32
400 - MaxRequestBatchItems uint32
401 - MaxResponsePayloadBytes uint32
402 - MaxResponseBatchItems uint32
403 - PacketSize uint32
404 - SelectedProfile uint32
405 - SessionID uint64
406 -
407 - // Internal receive buffer for chunked reassembly
408 - recvBuf []byte
409 -
410 - // Reusable packet scratch buffers for send/receive chunk assembly.
411 - sendBuf []byte
412 - pktBuf []byte
413 -
414 - // In-flight message_id set (client-side only)
415 - inflightIDs map[uint64]struct{}
416 -}
417 -
418 -func (s *Session) failAllInflight() {
419 - if s.role != RoleClient || len(s.inflightIDs) == 0 {
420 - return
421 - }
422 - clear(s.inflightIDs)
423 -}
424 -
425 -// Handle returns the raw HANDLE for WaitForSingleObject integration.
426 -func (s *Session) Handle() syscall.Handle {
427 - return s.handle
428 -}
429 -
430 -// Role returns the session role.
431 -func (s *Session) GetRole() Role {
432 - return s.role
433 -}
434 -
435 -// WaitReadable waits until bytes are available to read or the timeout expires.
436 -func (s *Session) WaitReadable(timeoutMs uint32) (bool, error) {
437 - if s.handle == syscall.InvalidHandle {
438 - return false, wrapErr(ErrBadParam, "session closed")
439 - }
440 -
441 - deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
442 - yielded := false
443 - for {
444 - available, err := peekNamedPipeAvailable(s.handle)
445 - if err != nil {
446 - if isDisconnectError(err) {
447 - s.failAllInflight()
448 - return false, ErrDisconnected
449 - }
450 - return false, wrapErr(ErrRecv, err.Error())
451 - }
452 - if available > 0 {
453 - return true, nil
454 - }
455 - if !time.Now().Before(deadline) {
456 - return false, nil
457 - }
458 - if !yielded {
459 - yielded = true
460 - for i := 0; i < 256; i++ {
461 - procSwitchToThread.Call()
462 - available, err = peekNamedPipeAvailable(s.handle)
463 - if err != nil {
464 - if isDisconnectError(err) {
465 - s.failAllInflight()
466 - return false, ErrDisconnected
467 - }
468 - return false, wrapErr(ErrRecv, err.Error())
469 - }
470 - if available > 0 {
471 - return true, nil
472 - }
473 - if !time.Now().Before(deadline) {
474 - return false, nil
475 - }
476 - }
477 - continue
478 - }
479 - time.Sleep(time.Millisecond)
480 - }
481 -}
482 -
483 -// Close closes the session and releases resources.
484 -func (s *Session) Close() {
485 - if s.handle != syscall.InvalidHandle {
486 - if s.role == RoleServer {
487 - // Flush before server-side disconnect so the client can
488 - // consume any final response bytes already written.
489 - flushFileBuffers(s.handle)
490 - disconnectNamedPipe(s.handle)
491 - }
492 - syscall.CloseHandle(s.handle)
493 - s.handle = syscall.InvalidHandle
494 - }
495 - s.recvBuf = nil
496 - s.sendBuf = nil
497 - s.pktBuf = nil
498 - s.failAllInflight()
499 -}
500 -
501 -// Connect establishes a session to a server pipe derived from runDir + serviceName.
502 -func Connect(runDir, serviceName string, config *ClientConfig) (*Session, error) {
503 - pipeName, err := BuildPipeName(runDir, serviceName)
504 - if err != nil {
505 - return nil, err
506 - }
507 -
508 - handle, err := syscall.CreateFile(
509 - &pipeName[0],
510 - _GENERIC_READ|_GENERIC_WRITE,
511 - 0,
512 - nil,
513 - _OPEN_EXISTING,
514 - 0,
515 - 0,
516 - )
517 - if err != nil {
518 - return nil, wrapErr(ErrConnect, err.Error())
519 - }
520 -
521 - // Set read mode to message mode
522 - mode := uint32(_PIPE_READMODE_MESSAGE)
523 - if err := setNamedPipeHandleState(handle, &mode); err != nil {
524 - syscall.CloseHandle(handle)
525 - return nil, wrapErr(ErrConnect, "SetNamedPipeHandleState: "+err.Error())
526 - }
527 -
528 - session, herr := clientHandshake(handle, config)
529 - if herr != nil {
530 - syscall.CloseHandle(handle)
531 - return nil, herr
532 - }
533 - return session, nil
534 -}
535 -
536 -// Send sends one logical message. Fills magic/version/header_len/payload_len.
537 -func (s *Session) Send(hdr *protocol.Header, payload []byte) error {
538 - if s.handle == syscall.InvalidHandle {
539 - return wrapErr(ErrBadParam, "session closed")
540 - }
541 -
542 - // Client-side: track in-flight message_ids
543 - if s.role == RoleClient && hdr.Kind == protocol.KindRequest {
544 - if s.inflightIDs == nil {
545 - s.inflightIDs = make(map[uint64]struct{})
546 - }
547 - if _, exists := s.inflightIDs[hdr.MessageID]; exists {
548 - return wrapErr(ErrDuplicateMsgID, fmt.Sprintf("message_id %d", hdr.MessageID))
549 - }
550 - s.inflightIDs[hdr.MessageID] = struct{}{}
551 - }
552 -
553 - // Fill envelope
554 - hdr.Magic = protocol.MagicMsg
555 - hdr.Version = protocol.Version
556 - hdr.HeaderLen = protocol.HeaderLen
557 - hdr.PayloadLen = uint32(len(payload))
558 -
559 - tracked := s.role == RoleClient && hdr.Kind == protocol.KindRequest
560 -
561 - sendErr := s.sendInner(hdr, payload)
562 -
563 - if sendErr != nil && tracked {
564 - if errors.Is(sendErr, ErrDisconnected) {
565 - s.failAllInflight()
566 - } else {
567 - delete(s.inflightIDs, hdr.MessageID)
568 - }
569 - }
570 -
571 - return sendErr
572 -}
573 -
574 -func (s *Session) sendInner(hdr *protocol.Header, payload []byte) error {
575 - totalMsg := protocol.HeaderSize + len(payload)
576 -
577 - // Single packet?
578 - if totalMsg <= int(s.PacketSize) {
579 - msg := ensurePipeScratchBuf(&s.sendBuf, totalMsg)
580 - hdr.Encode(msg[:protocol.HeaderSize])
581 - copy(msg[protocol.HeaderSize:], payload)
582 - return rawSendMsg(s.handle, msg[:totalMsg])
583 - }
584 -
585 - // Chunked send
586 - chunkPayloadBudget := int(s.PacketSize) - protocol.HeaderSize
587 - if chunkPayloadBudget <= 0 {
588 - return wrapErr(ErrBadParam, "packet_size too small")
589 - }
590 -
591 - firstChunkPayload := len(payload)
592 - if firstChunkPayload > chunkPayloadBudget {
593 - firstChunkPayload = chunkPayloadBudget
594 - }
595 -
596 - remainingAfterFirst := len(payload) - firstChunkPayload
597 - continuationChunks := uint32(0)
598 - if remainingAfterFirst > 0 {
599 - continuationChunks = uint32((remainingAfterFirst + chunkPayloadBudget - 1) / chunkPayloadBudget)
600 - }
601 - chunkCount := 1 + continuationChunks
602 -
603 - // First chunk
604 - pktBuf := ensurePipeScratchBuf(&s.sendBuf, int(s.PacketSize))
605 - hdr.Encode(pktBuf[:protocol.HeaderSize])
606 - copy(pktBuf[protocol.HeaderSize:], payload[:firstChunkPayload])
607 - if err := rawSendMsg(s.handle, pktBuf[:protocol.HeaderSize+firstChunkPayload]); err != nil {
608 - return err
609 - }
610 -
611 - // Continuation chunks
612 - offset := firstChunkPayload
613 - for ci := uint32(1); ci < chunkCount; ci++ {
614 - remaining := len(payload) - offset
615 - thisChunk := remaining
616 - if thisChunk > chunkPayloadBudget {
617 - thisChunk = chunkPayloadBudget
618 - }
619 -
620 - chk := protocol.ChunkHeader{
621 - Magic: protocol.MagicChunk,
622 - Version: protocol.Version,
623 - Flags: 0,
624 - MessageID: hdr.MessageID,
625 - TotalMessageLen: uint32(totalMsg),
626 - ChunkIndex: ci,
627 - ChunkCount: chunkCount,
628 - ChunkPayloadLen: uint32(thisChunk),
629 - }
630 -
631 - chk.Encode(pktBuf[:protocol.HeaderSize])
632 - copy(pktBuf[protocol.HeaderSize:], payload[offset:offset+thisChunk])
633 - if err := rawSendMsg(s.handle, pktBuf[:protocol.HeaderSize+thisChunk]); err != nil {
634 - return err
635 - }
636 -
637 - offset += thisChunk
638 - }
639 -
640 - return nil
641 -}
642 -
643 -// Receive reads one logical message. buf is a scratch buffer.
644 -func (s *Session) Receive(buf []byte) (protocol.Header, []byte, error) {
645 - if s.handle == syscall.InvalidHandle {
646 - return protocol.Header{}, nil, wrapErr(ErrBadParam, "session closed")
647 - }
648 -
649 - n, err := rawRecv(s.handle, buf)
650 - if err != nil {
651 - if errors.Is(err, ErrDisconnected) {
652 - s.failAllInflight()
653 - }
654 - return protocol.Header{}, nil, err
655 - }
656 -
657 - if n < protocol.HeaderSize {
658 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "packet too short for header")
659 - }
660 -
661 - hdr, err := protocol.DecodeHeader(buf[:n])
662 - if err != nil {
663 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "header decode: "+err.Error())
664 - }
665 -
666 - // Validate payload_len
667 - var maxPayload uint32
668 - if s.role == RoleServer {
669 - maxPayload = s.MaxRequestPayloadBytes
670 - } else {
671 - maxPayload = s.MaxResponsePayloadBytes
672 - }
673 - if hdr.PayloadLen > maxPayload {
674 - return protocol.Header{}, nil, wrapErr(ErrLimitExceeded,
675 - fmt.Sprintf("payload_len %d exceeds negotiated max %d", hdr.PayloadLen, maxPayload))
676 - }
677 -
678 - // Validate item_count
679 - var maxBatch uint32
680 - if s.role == RoleServer {
681 - maxBatch = s.MaxRequestBatchItems
682 - } else {
683 - maxBatch = s.MaxResponseBatchItems
684 - }
685 - if hdr.ItemCount > maxBatch {
686 - return protocol.Header{}, nil, wrapErr(ErrLimitExceeded,
687 - fmt.Sprintf("item_count %d exceeds negotiated max %d", hdr.ItemCount, maxBatch))
688 - }
689 -
690 - // Client-side: validate response message_id
691 - if s.role == RoleClient && hdr.Kind == protocol.KindResponse {
692 - if s.inflightIDs == nil {
693 - return protocol.Header{}, nil, wrapErr(ErrUnknownMsgID,
694 - fmt.Sprintf("message_id %d", hdr.MessageID))
695 - }
696 - if _, exists := s.inflightIDs[hdr.MessageID]; !exists {
697 - return protocol.Header{}, nil, wrapErr(ErrUnknownMsgID,
698 - fmt.Sprintf("message_id %d", hdr.MessageID))
699 - }
700 - delete(s.inflightIDs, hdr.MessageID)
701 - }
702 -
703 - totalMsg := protocol.HeaderSize + int(hdr.PayloadLen)
704 -
705 - // Non-chunked
706 - if n >= totalMsg {
707 - payload := buf[protocol.HeaderSize : protocol.HeaderSize+int(hdr.PayloadLen)]
708 -
709 - // Validate batch directory
710 - if hdr.Flags&protocol.FlagBatch != 0 && hdr.ItemCount > 1 {
711 - dirBytes := int(hdr.ItemCount) * 8
712 - dirAligned := protocol.Align8(dirBytes)
713 - if len(payload) < dirAligned {
714 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir exceeds payload")
715 - }
716 - packedAreaLen := uint32(len(payload) - dirAligned)
717 - if err := protocol.BatchDirValidate(payload[:dirBytes], hdr.ItemCount, packedAreaLen); err != nil {
718 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir: "+err.Error())
719 - }
720 - }
721 -
722 - return hdr, payload, nil
723 - }
724 -
725 - // Chunked
726 - firstPayloadBytes := n - protocol.HeaderSize
727 - needed := int(hdr.PayloadLen)
728 - if len(s.recvBuf) < needed {
729 - s.recvBuf = make([]byte, needed)
730 - }
731 -
732 - copy(s.recvBuf[:firstPayloadBytes], buf[protocol.HeaderSize:protocol.HeaderSize+firstPayloadBytes])
733 -
734 - assembled := firstPayloadBytes
735 - chunkPayloadBudget := int(s.PacketSize) - protocol.HeaderSize
736 -
737 - remainingAfterFirst := int(hdr.PayloadLen) - firstPayloadBytes
738 - expectedContinuations := uint32(0)
739 - if remainingAfterFirst > 0 && chunkPayloadBudget > 0 {
740 - expectedContinuations = uint32((remainingAfterFirst + chunkPayloadBudget - 1) / chunkPayloadBudget)
741 - }
742 - expectedChunkCount := 1 + expectedContinuations
743 -
744 - pktBuf := ensurePipeScratchBuf(&s.pktBuf, int(s.PacketSize))
745 -
746 - ci := uint32(1)
747 - for assembled < int(hdr.PayloadLen) {
748 - cn, err := rawRecv(s.handle, pktBuf)
749 - if err != nil {
750 - if errors.Is(err, ErrDisconnected) {
751 - s.failAllInflight()
752 - }
753 - return protocol.Header{}, nil, wrapErr(ErrRecv, "continuation recv: "+err.Error())
754 - }
755 -
756 - if cn < protocol.HeaderSize {
757 - return protocol.Header{}, nil, wrapErr(ErrChunk, "continuation too short")
758 - }
759 -
760 - chk, err := protocol.DecodeChunkHeader(pktBuf[:cn])
761 - if err != nil {
762 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk header: "+err.Error())
763 - }
764 -
765 - if chk.MessageID != hdr.MessageID {
766 - return protocol.Header{}, nil, wrapErr(ErrChunk, "message_id mismatch")
767 - }
768 - if chk.ChunkIndex != ci {
769 - return protocol.Header{}, nil, wrapErr(ErrChunk, fmt.Sprintf(
770 - "chunk_index mismatch: expected %d, got %d", ci, chk.ChunkIndex))
771 - }
772 - if chk.ChunkCount != expectedChunkCount {
773 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk_count mismatch")
774 - }
775 - if chk.TotalMessageLen != uint32(totalMsg) {
776 - return protocol.Header{}, nil, wrapErr(ErrChunk, "total_message_len mismatch")
777 - }
778 -
779 - chunkData := cn - protocol.HeaderSize
780 - if chunkData != int(chk.ChunkPayloadLen) {
781 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk_payload_len mismatch")
782 - }
783 - if assembled+chunkData > int(hdr.PayloadLen) {
784 - return protocol.Header{}, nil, wrapErr(ErrChunk, "chunk exceeds payload_len")
785 - }
786 -
787 - copy(s.recvBuf[assembled:assembled+chunkData], pktBuf[protocol.HeaderSize:protocol.HeaderSize+chunkData])
788 - assembled += chunkData
789 - ci++
790 - }
791 -
792 - payload := s.recvBuf[:hdr.PayloadLen]
793 -
794 - // Validate batch directory
795 - if hdr.Flags&protocol.FlagBatch != 0 && hdr.ItemCount > 1 {
796 - dirBytes := int(hdr.ItemCount) * 8
797 - dirAligned := protocol.Align8(dirBytes)
798 - if len(payload) < dirAligned {
799 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir exceeds payload")
800 - }
801 - packedAreaLen := uint32(len(payload) - dirAligned)
802 - if err := protocol.BatchDirValidate(payload[:dirBytes], hdr.ItemCount, packedAreaLen); err != nil {
803 - return protocol.Header{}, nil, wrapErr(ErrProtocol, "batch dir: "+err.Error())
804 - }
805 - }
806 -
807 - return hdr, payload, nil
808 -}
809 -
323 func ensurePipeScratchBuf(buf *[]byte, needed int) []byte {
324 if len(*buf) < needed {
325 *buf = make([]byte, needed)
326 }
327 return (*buf)[:needed]
328 }
816 -
817 -// ---------------------------------------------------------------------------
818 -// Listener
819 -// ---------------------------------------------------------------------------
820 -
821 -// Listener is a listening Named Pipe endpoint.
822 -type Listener struct {
823 - mu sync.Mutex
824 - handle syscall.Handle
825 - config ServerConfig
826 - pipeName []uint16
827 - nextSessionID atomic.Uint64
828 - closing bool
829 - accepting bool
830 -}
831 -
832 -// Listen creates a listener on a Named Pipe derived from runDir + serviceName.
833 -func Listen(runDir, serviceName string, config ServerConfig) (*Listener, error) {
834 - pipeName, err := BuildPipeName(runDir, serviceName)
835 - if err != nil {
836 - return nil, err
837 - }
838 -
839 - bufSize := pipeBufferSize(config.PacketSize)
840 -
841 - // Create first instance with FILE_FLAG_FIRST_PIPE_INSTANCE
842 - handle, err := createPipeInstance(pipeName, bufSize, true)
843 - if err != nil {
844 - return nil, err
845 - }
846 -
847 - return &Listener{
848 - handle: handle,
849 - config: config,
850 - pipeName: pipeName,
851 - }, nil
852 -}
853 -
854 -// Handle returns the raw HANDLE.
855 -func (l *Listener) Handle() syscall.Handle {
856 - l.mu.Lock()
857 - defer l.mu.Unlock()
858 - return l.handle
859 -}
860 -
861 -// SetPayloadLimits updates the payload limits used for future handshakes.
862 -func (l *Listener) SetPayloadLimits(maxRequestPayloadBytes, maxResponsePayloadBytes uint32) {
863 - l.mu.Lock()
864 - defer l.mu.Unlock()
865 - l.config.MaxRequestPayloadBytes = maxRequestPayloadBytes
866 - l.config.MaxResponsePayloadBytes = maxResponsePayloadBytes
867 -}
868 -
869 -// Accept accepts one client connection. Performs the full handshake.
870 -func (l *Listener) Accept() (*Session, error) {
871 - sessionID := l.nextSessionID.Add(1)
872 - return l.AcceptWithConfig(sessionID, l.config)
873 -}
874 -
875 -// AcceptWithConfig accepts one client connection using a caller-provided
876 -// per-session server config and session ID.
877 -func (l *Listener) AcceptWithConfig(sessionID uint64, config ServerConfig) (*Session, error) {
878 - l.mu.Lock()
879 - if l.handle == syscall.InvalidHandle {
880 - l.mu.Unlock()
881 - return nil, wrapErr(ErrAccept, "listener closed")
882 - }
883 - sessionHandle := l.handle
884 - l.accepting = true
885 - l.mu.Unlock()
886 -
887 - err := connectNamedPipe(sessionHandle)
888 - if err != nil {
889 - // ERROR_PIPE_CONNECTED is fine — client connected between
890 - // CreateNamedPipe and ConnectNamedPipe
891 - if errno, ok := err.(syscall.Errno); !ok || errno != _ERROR_PIPE_CONNECTED {
892 - l.mu.Lock()
893 - l.accepting = false
894 - l.mu.Unlock()
895 - return nil, wrapErr(ErrAccept, err.Error())
896 - }
897 - }
898 -
899 - l.mu.Lock()
900 - l.accepting = false
901 - if l.closing {
902 - if l.handle == sessionHandle {
903 - l.handle = syscall.InvalidHandle
904 - }
905 - l.mu.Unlock()
906 - disconnectNamedPipe(sessionHandle)
907 - syscall.CloseHandle(sessionHandle)
908 - return nil, wrapErr(ErrAccept, "listener closed")
909 - }
910 -
911 - // Create new pipe instance for next client
912 - bufSize := pipeBufferSize(l.config.PacketSize)
913 - next, perr := createPipeInstance(l.pipeName, bufSize, false)
914 - if perr != nil {
915 - if l.handle == sessionHandle {
916 - l.handle = syscall.InvalidHandle
917 - }
918 - l.mu.Unlock()
919 - disconnectNamedPipe(sessionHandle)
920 - syscall.CloseHandle(sessionHandle)
921 - return nil, perr
922 - }
923 - l.handle = next
924 - l.mu.Unlock()
925 -
926 - // Handshake
927 - session, herr := serverHandshake(sessionHandle, &config, sessionID)
928 - if herr != nil {
929 - disconnectNamedPipe(sessionHandle)
930 - syscall.CloseHandle(sessionHandle)
931 - return nil, herr
932 - }
933 - return session, nil
934 -}
935 -
936 -// Close closes the listener.
937 -func (l *Listener) Close() {
938 - l.mu.Lock()
939 - handle := l.handle
940 - if handle == syscall.InvalidHandle {
941 - l.mu.Unlock()
942 - return
943 - }
944 - l.closing = true
945 - accepting := l.accepting
946 - if !accepting {
947 - l.handle = syscall.InvalidHandle
948 - }
949 - pipeName := l.pipeName
950 - l.mu.Unlock()
951 -
952 - if accepting && len(pipeName) > 0 && pipeName[0] != 0 {
953 - // A loopback connect reliably wakes a blocking ConnectNamedPipe()
954 - // so Accept() can observe shutdown and close the live listener handle
955 - // from the owning goroutine.
956 - wake, err := syscall.CreateFile(
957 - &pipeName[0],
958 - _GENERIC_READ|_GENERIC_WRITE,
959 - 0,
960 - nil,
961 - _OPEN_EXISTING,
962 - 0,
963 - 0,
964 - )
965 - if err == nil && wake != syscall.InvalidHandle && wake != 0 {
966 - syscall.CloseHandle(wake)
967 - }
968 - return
969 - }
970 -
971 - syscall.CloseHandle(handle)
972 -}
973 -
974 -// ---------------------------------------------------------------------------
975 -// Pipe instance creation
976 -// ---------------------------------------------------------------------------
977 -
978 -func createPipeInstance(pipeName []uint16, bufSize uint32, firstInstance bool) (syscall.Handle, error) {
979 - openMode := uint32(_PIPE_ACCESS_DUPLEX)
980 - if firstInstance {
981 - openMode |= _FILE_FLAG_FIRST_PIPE_INSTANCE
982 - }
983 -
984 - handle, err := createNamedPipe(
985 - &pipeName[0],
986 - openMode,
987 - _PIPE_TYPE_MESSAGE|_PIPE_READMODE_MESSAGE|_PIPE_WAIT,
988 - _PIPE_UNLIMITED_INSTANCES,
989 - bufSize,
990 - bufSize,
991 - 0,
992 - )
993 - if err != nil {
994 - errno, ok := err.(syscall.Errno)
995 - if ok && (errno == _ERROR_ACCESS_DENIED || errno == _ERROR_PIPE_BUSY) {
996 - return syscall.InvalidHandle, ErrAddrInUse
997 - }
998 - return syscall.InvalidHandle, wrapErr(ErrCreatePipe, err.Error())
999 - }
1000 - return handle, nil
1001 -}
1002 -
1003 -// ---------------------------------------------------------------------------
1004 -// Client handshake
1005 -// ---------------------------------------------------------------------------
1006 -
1007 -func clientHandshake(handle syscall.Handle, config *ClientConfig) (*Session, error) {
1008 - pktSize := applyDefault(config.PacketSize, defaultPacketSize)
1009 -
1010 - supported := config.SupportedProfiles
1011 - if supported == 0 {
1012 - supported = protocol.ProfileBaseline
1013 - }
1014 -
1015 - hello := protocol.Hello{
1016 - LayoutVersion: 1,
1017 - Flags: 0,
1018 - SupportedProfiles: supported,
1019 - PreferredProfiles: config.PreferredProfiles,
1020 - MaxRequestPayloadBytes: applyDefault(config.MaxRequestPayloadBytes, protocol.MaxPayloadDefault),
1021 - MaxRequestBatchItems: applyDefault(config.MaxRequestBatchItems, defaultBatchItems),
1022 - MaxResponsePayloadBytes: applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault),
1023 - MaxResponseBatchItems: applyDefault(config.MaxResponseBatchItems, defaultBatchItems),
1024 - AuthToken: config.AuthToken,
1025 - PacketSize: pktSize,
1026 - }
1027 -
1028 - var helloBuf [helloPayloadSize]byte
1029 - hello.Encode(helloBuf[:])
1030 -
1031 - hdr := protocol.Header{
1032 - Magic: protocol.MagicMsg,
1033 - Version: protocol.Version,
1034 - HeaderLen: protocol.HeaderLen,
1035 - Kind: protocol.KindControl,
1036 - Flags: 0,
1037 - Code: protocol.CodeHello,
1038 - TransportStatus: protocol.StatusOK,
1039 - PayloadLen: helloPayloadSize,
1040 - ItemCount: 1,
1041 - MessageID: 0,
1042 - }
1043 -
1044 - var pkt [protocol.HeaderSize + helloPayloadSize]byte
1045 - hdr.Encode(pkt[:protocol.HeaderSize])
1046 - copy(pkt[protocol.HeaderSize:], helloBuf[:])
1047 -
1048 - // Send HELLO
1049 - if err := rawWrite(handle, pkt[:]); err != nil {
1050 - return nil, wrapErr(ErrSend, "hello send: "+err.Error())
1051 - }
1052 -
1053 - // Receive HELLO_ACK
1054 - var ackBuf [128]byte
1055 - an, err := rawRecv(handle, ackBuf[:])
1056 - if err != nil {
1057 - return nil, wrapErr(ErrRecv, "hello_ack recv: "+err.Error())
1058 - }
1059 -
1060 - ackHdr, err := protocol.DecodeHeader(ackBuf[:an])
1061 - if err != nil {
1062 - if errors.Is(err, protocol.ErrBadVersion) {
1063 - return nil, wrapErr(ErrIncompatible, "ack header version mismatch")
1064 - }
1065 - return nil, wrapErr(ErrProtocol, "ack header: "+err.Error())
1066 - }
1067 -
1068 - if ackHdr.Kind != protocol.KindControl || ackHdr.Code != protocol.CodeHelloAck {
1069 - return nil, wrapErr(ErrProtocol, "expected HELLO_ACK")
1070 - }
1071 -
1072 - if ackHdr.TransportStatus == protocol.StatusAuthFailed {
1073 - return nil, ErrAuthFailed
1074 - }
1075 - if ackHdr.TransportStatus == protocol.StatusUnsupported {
1076 - return nil, ErrNoProfile
1077 - }
1078 - if ackHdr.TransportStatus == protocol.StatusIncompatible {
1079 - return nil, ErrIncompatible
1080 - }
1081 - if ackHdr.TransportStatus == protocol.StatusLimitExceeded {
1082 - return nil, ErrLimitExceeded
1083 - }
1084 - if ackHdr.TransportStatus != protocol.StatusOK {
1085 - return nil, wrapErr(ErrHandshake, fmt.Sprintf("transport_status=%d", ackHdr.TransportStatus))
1086 - }
1087 -
1088 - if an < protocol.HeaderSize+helloAckPayloadSize {
1089 - return nil, wrapErr(ErrProtocol, "ack payload truncated")
1090 - }
1091 - ack, err := protocol.DecodeHelloAck(ackBuf[protocol.HeaderSize:an])
1092 - if err != nil {
1093 - if errors.Is(err, protocol.ErrBadLayout) &&
1094 - helloAckLayoutIncompatible(ackBuf[protocol.HeaderSize:an]) {
1095 - return nil, wrapErr(ErrIncompatible, "ack payload layout version mismatch")
1096 - }
1097 - return nil, wrapErr(ErrProtocol, "ack payload: "+err.Error())
1098 - }
1099 -
1100 - return &Session{
1101 - handle: handle,
1102 - role: RoleClient,
1103 - MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
1104 - MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
1105 - MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
1106 - MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
1107 - PacketSize: ack.AgreedPacketSize,
1108 - SelectedProfile: ack.SelectedProfile,
1109 - SessionID: ack.SessionID,
1110 - inflightIDs: make(map[uint64]struct{}),
1111 - }, nil
1112 -}
1113 -
1114 -// ---------------------------------------------------------------------------
1115 -// Server handshake
1116 -// ---------------------------------------------------------------------------
1117 -
1118 -func serverHandshake(handle syscall.Handle, config *ServerConfig, sessionID uint64) (*Session, error) {
1119 - serverPktSize := applyDefault(config.PacketSize, defaultPacketSize)
1120 - sRespPay := applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault)
1121 - sProfiles := config.SupportedProfiles
1122 - if sProfiles == 0 {
1123 - sProfiles = protocol.ProfileBaseline
1124 - }
1125 - sPreferred := config.PreferredProfiles
1126 -
1127 - // Helper: send rejection
1128 - sendRejection := func(status uint16) {
1129 - ack := protocol.HelloAck{LayoutVersion: 1}
1130 - var ackPayBuf [helloAckPayloadSize]byte
1131 - ack.Encode(ackPayBuf[:])
1132 -
1133 - ackHdr := protocol.Header{
1134 - Magic: protocol.MagicMsg,
1135 - Version: protocol.Version,
1136 - HeaderLen: protocol.HeaderLen,
1137 - Kind: protocol.KindControl,
1138 - Code: protocol.CodeHelloAck,
1139 - TransportStatus: status,
1140 - PayloadLen: helloAckPayloadSize,
1141 - ItemCount: 1,
1142 - }
1143 -
1144 - var pkt [protocol.HeaderSize + helloAckPayloadSize]byte
1145 - ackHdr.Encode(pkt[:protocol.HeaderSize])
1146 - copy(pkt[protocol.HeaderSize:], ackPayBuf[:])
1147 - rawWrite(handle, pkt[:]) //nolint:errcheck
1148 - }
1149 -
1150 - // Receive HELLO
1151 - var buf [128]byte
1152 - n, err := rawRecv(handle, buf[:])
1153 - if err != nil {
1154 - return nil, wrapErr(ErrRecv, "hello recv: "+err.Error())
1155 - }
1156 -
1157 - hdr, err := protocol.DecodeHeader(buf[:n])
1158 - if err != nil {
1159 - if errors.Is(err, protocol.ErrBadVersion) &&
1160 - headerVersionIncompatible(buf[:n], protocol.CodeHello) {
1161 - sendRejection(protocol.StatusIncompatible)
1162 - return nil, ErrIncompatible
1163 - }
1164 - return nil, wrapErr(ErrProtocol, "hello header: "+err.Error())
1165 - }
1166 -
1167 - if hdr.Kind != protocol.KindControl || hdr.Code != protocol.CodeHello {
1168 - return nil, wrapErr(ErrProtocol, "expected HELLO")
1169 - }
1170 -
1171 - hello, err := protocol.DecodeHello(buf[protocol.HeaderSize:n])
1172 - if err != nil {
1173 - if errors.Is(err, protocol.ErrBadLayout) &&
1174 - helloLayoutIncompatible(buf[protocol.HeaderSize:n]) {
1175 - sendRejection(protocol.StatusIncompatible)
1176 - return nil, ErrIncompatible
1177 - }
1178 - return nil, wrapErr(ErrProtocol, "hello payload: "+err.Error())
1179 - }
1180 -
1181 - intersection := hello.SupportedProfiles & sProfiles
1182 -
1183 - if intersection == 0 {
1184 - sendRejection(protocol.StatusUnsupported)
1185 - return nil, ErrNoProfile
1186 - }
1187 -
1188 - if hello.AuthToken != config.AuthToken {
1189 - sendRejection(protocol.StatusAuthFailed)
1190 - return nil, ErrAuthFailed
1191 - }
1192 -
1193 - // Select profile
1194 - preferredIntersection := intersection & hello.PreferredProfiles & sPreferred
1195 - var selected uint32
1196 - if preferredIntersection != 0 {
1197 - selected = highestBit(preferredIntersection)
1198 - } else {
1199 - selected = highestBit(intersection)
1200 - }
1201 -
1202 - if hello.MaxRequestPayloadBytes > protocol.MaxPayloadCap {
1203 - sendRejection(protocol.StatusLimitExceeded)
1204 - return nil, ErrLimitExceeded
1205 - }
1206 -
1207 - // Negotiate limits
1208 - agreedReqPay := hello.MaxRequestPayloadBytes
1209 - agreedReqBat := hello.MaxRequestBatchItems
1210 - agreedRespPay := sRespPay
1211 - agreedRespBat := agreedReqBat
1212 - agreedPkt := minU32(hello.PacketSize, serverPktSize)
1213 - if agreedPkt <= protocol.HeaderSize {
1214 - sendRejection(protocol.StatusIncompatible)
1215 - return nil, ErrIncompatible
1216 - }
1217 -
1218 - // Send HELLO_ACK
1219 - ack := protocol.HelloAck{
1220 - LayoutVersion: 1,
1221 - Flags: 0,
1222 - ServerSupportedProfiles: sProfiles,
1223 - IntersectionProfiles: intersection,
1224 - SelectedProfile: selected,
1225 - AgreedMaxRequestPayloadBytes: agreedReqPay,
1226 - AgreedMaxRequestBatchItems: agreedReqBat,
1227 - AgreedMaxResponsePayloadBytes: agreedRespPay,
1228 - AgreedMaxResponseBatchItems: agreedRespBat,
1229 - AgreedPacketSize: agreedPkt,
1230 - SessionID: sessionID,
1231 - }
1232 -
1233 - var ackPayBuf [helloAckPayloadSize]byte
1234 - ack.Encode(ackPayBuf[:])
1235 -
1236 - ackHdr := protocol.Header{
1237 - Magic: protocol.MagicMsg,
1238 - Version: protocol.Version,
1239 - HeaderLen: protocol.HeaderLen,
1240 - Kind: protocol.KindControl,
1241 - Code: protocol.CodeHelloAck,
1242 - TransportStatus: protocol.StatusOK,
1243 - PayloadLen: helloAckPayloadSize,
1244 - ItemCount: 1,
1245 - }
1246 -
1247 - var pkt [protocol.HeaderSize + helloAckPayloadSize]byte
1248 - ackHdr.Encode(pkt[:protocol.HeaderSize])
1249 - copy(pkt[protocol.HeaderSize:], ackPayBuf[:])
1250 -
1251 - if err := rawWrite(handle, pkt[:]); err != nil {
1252 - return nil, wrapErr(ErrSend, "hello_ack send: "+err.Error())
1253 - }
1254 -
1255 - return &Session{
1256 - handle: handle,
1257 - role: RoleServer,
1258 - MaxRequestPayloadBytes: agreedReqPay,
1259 - MaxRequestBatchItems: agreedReqBat,
1260 - MaxResponsePayloadBytes: agreedRespPay,
1261 - MaxResponseBatchItems: agreedRespBat,
1262 - PacketSize: agreedPkt,
1263 - SelectedProfile: selected,
1264 - SessionID: sessionID,
1265 - inflightIDs: make(map[uint64]struct{}),
1266 - }, nil
1267 -}
src/go/pkg/netipc/transport/windows/pipe_handshake.go new
+128
@@ -0,0 +1,128 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "fmt"
7 + "syscall"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
11 +)
12 +
13 +func headerVersionIncompatible(buf []byte, expectedCode uint16) bool {
14 + return framing.HeaderVersionIncompatible(buf, expectedCode)
15 +}
16 +
17 +func helloLayoutIncompatible(buf []byte) bool {
18 + return framing.HelloLayoutIncompatible(buf)
19 +}
20 +
21 +func helloAckLayoutIncompatible(buf []byte) bool {
22 + return framing.HelloAckLayoutIncompatible(buf)
23 +}
24 +
25 +func clientHandshake(handle syscall.Handle, config *ClientConfig) (*Session, error) {
26 + pktSize := applyDefault(config.PacketSize, defaultPacketSize)
27 +
28 + ack, err := framing.ClientHandshake(framing.ClientHandshakeConfig{
29 + Hello: framing.HelloConfig{
30 + SupportedProfiles: config.SupportedProfiles,
31 + PreferredProfiles: config.PreferredProfiles,
32 + MaxRequestPayloadBytes: applyDefault(config.MaxRequestPayloadBytes, protocol.MaxPayloadDefault),
33 + MaxRequestBatchItems: applyDefault(config.MaxRequestBatchItems, defaultBatchItems),
34 + MaxResponsePayloadBytes: applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault),
35 + MaxResponseBatchItems: applyDefault(config.MaxResponseBatchItems, defaultBatchItems),
36 + AuthToken: config.AuthToken,
37 + PacketSize: pktSize,
38 + },
39 + Send: func(pkt []byte) error { return rawWrite(handle, pkt) },
40 + Recv: func(dst []byte) (int, error) { return rawRecv(handle, dst) },
41 + StatusError: helloAckStatusError,
42 + ErrSend: func(msg string) error { return wrapErr(ErrSend, msg) },
43 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
44 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
45 + ErrIncompatible: func(msg string) error { return wrapErr(ErrIncompatible, msg) },
46 + })
47 + if err != nil {
48 + return nil, err
49 + }
50 +
51 + return &Session{
52 + handle: handle,
53 + role: RoleClient,
54 + MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
55 + MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
56 + MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
57 + MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
58 + PacketSize: ack.AgreedPacketSize,
59 + SelectedProfile: ack.SelectedProfile,
60 + SessionID: ack.SessionID,
61 + inflightIDs: make(map[uint64]struct{}),
62 + }, nil
63 +}
64 +
65 +func helloAckStatusError(status uint16) error {
66 + switch status {
67 + case protocol.StatusOK:
68 + return nil
69 + case protocol.StatusAuthFailed:
70 + return ErrAuthFailed
71 + case protocol.StatusUnsupported:
72 + return ErrNoProfile
73 + case protocol.StatusIncompatible:
74 + return ErrIncompatible
75 + case protocol.StatusLimitExceeded:
76 + return ErrLimitExceeded
77 + default:
78 + return wrapErr(ErrHandshake, fmt.Sprintf("transport_status=%d", status))
79 + }
80 +}
81 +
82 +func serverHandshake(handle syscall.Handle, config *ServerConfig, sessionID uint64) (*Session, error) {
83 + serverPktSize := applyDefault(config.PacketSize, defaultPacketSize)
84 + sRespPay := applyDefault(config.MaxResponsePayloadBytes, protocol.MaxPayloadDefault)
85 +
86 + ack, err := framing.ServerHandshake(framing.ServerHandshakeConfig{
87 + ServerHelloConfig: framing.ServerHelloConfig{
88 + PacketSize: serverPktSize,
89 + MaxResponsePayloadBytes: sRespPay,
90 + SupportedProfiles: config.SupportedProfiles,
91 + PreferredProfiles: config.PreferredProfiles,
92 + AuthToken: config.AuthToken,
93 + },
94 + SessionID: sessionID,
95 + Recv: func(dst []byte) (int, error) { return rawRecv(handle, dst) },
96 + SendAck: func(status uint16, ack protocol.HelloAck) error { return sendHelloAck(handle, status, ack) },
97 + StatusError: helloAckStatusError,
98 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
99 + ErrSend: func(msg string) error { return wrapErr(ErrSend, msg) },
100 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
101 + ErrIncompatible: func(msg string) error { return wrapErr(ErrIncompatible, msg) },
102 + })
103 + if err != nil {
104 + return nil, err
105 + }
106 +
107 + return &Session{
108 + handle: handle,
109 + role: RoleServer,
110 + MaxRequestPayloadBytes: ack.AgreedMaxRequestPayloadBytes,
111 + MaxRequestBatchItems: ack.AgreedMaxRequestBatchItems,
112 + MaxResponsePayloadBytes: ack.AgreedMaxResponsePayloadBytes,
113 + MaxResponseBatchItems: ack.AgreedMaxResponseBatchItems,
114 + PacketSize: ack.AgreedPacketSize,
115 + SelectedProfile: ack.SelectedProfile,
116 + SessionID: sessionID,
117 + inflightIDs: make(map[uint64]struct{}),
118 + }, nil
119 +}
120 +
121 +func sendRejection(handle syscall.Handle, status uint16) {
122 + _ = sendHelloAck(handle, status, protocol.HelloAck{LayoutVersion: 1})
123 +}
124 +
125 +func sendHelloAck(handle syscall.Handle, status uint16, ack protocol.HelloAck) error {
126 + pkt := framing.BuildHelloAckPacket(status, ack)
127 + return rawWrite(handle, pkt[:])
128 +}
src/go/pkg/netipc/transport/windows/pipe_integration_test.go
+53 -2
@@ -139,6 +139,57 @@ func TestPipeSingleClientPingPong(t *testing.T) {
139 }
140 }
141
142 +func TestPipeWaitReadable(t *testing.T) {
143 + client, server := sessionPair(t, defaultServerConfig(), defaultClientConfig())
144 +
145 + ready, err := server.WaitReadable(1)
146 + if err != nil {
147 + t.Fatalf("WaitReadable before send returned error: %v", err)
148 + }
149 + if ready {
150 + t.Fatal("WaitReadable before send returned ready")
151 + }
152 +
153 + payload := []byte("wake")
154 + hdr := protocol.Header{
155 + Kind: protocol.KindRequest,
156 + Code: protocol.MethodIncrement,
157 + ItemCount: 1,
158 + MessageID: 77,
159 + }
160 + if err := client.Send(&hdr, payload); err != nil {
161 + t.Fatalf("client Send: %v", err)
162 + }
163 +
164 + ready, err = server.WaitReadable(1000)
165 + if err != nil {
166 + t.Fatalf("WaitReadable after send returned error: %v", err)
167 + }
168 + if !ready {
169 + t.Fatal("WaitReadable after send returned not ready")
170 + }
171 +
172 + rHdr, rPayload, err := server.Receive(make([]byte, 4096))
173 + if err != nil {
174 + t.Fatalf("server Receive: %v", err)
175 + }
176 + if rHdr.MessageID != hdr.MessageID || !bytes.Equal(rPayload, payload) {
177 + t.Fatalf("unexpected WaitReadable receive: hdr=%+v payload=%q", rHdr, rPayload)
178 + }
179 +}
180 +
181 +func TestPipeListenerSetPayloadLimits(t *testing.T) {
182 + service := uniquePipeService(t)
183 + listener := startListener(t, testPipeRunDir, service, defaultServerConfig())
184 + defer listener.Close()
185 +
186 + listener.SetPayloadLimits(1234, 5678)
187 + if listener.config.MaxRequestPayloadBytes != 1234 ||
188 + listener.config.MaxResponsePayloadBytes != 5678 {
189 + t.Fatalf("listener payload limits not updated: %+v", listener.config)
190 + }
191 +}
192 +
193 func TestPipeMultiClient(t *testing.T) {
194 service := uniquePipeService(t)
195 listener := startListener(t, testPipeRunDir, service, defaultServerConfig())
@@ -1022,8 +1073,8 @@ func TestPipeHandleAndRole(t *testing.T) {
1073 if client.Handle() == syscall.InvalidHandle || server.Handle() == syscall.InvalidHandle {
1074 t.Fatal("session handles should be valid")
1075 }
1025 - if client.GetRole() != RoleClient || server.GetRole() != RoleServer {
1026 - t.Fatalf("unexpected roles client=%d server=%d", client.GetRole(), server.GetRole())
1076 + if client.Role() != RoleClient || server.Role() != RoleServer {
1077 + t.Fatalf("unexpected roles client=%d server=%d", client.Role(), server.Role())
1078 }
1079 }
1080
src/go/pkg/netipc/transport/windows/pipe_listener.go new
+186
@@ -0,0 +1,186 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "sync"
7 + "sync/atomic"
8 + "syscall"
9 +)
10 +
11 +// Listener is a listening Named Pipe endpoint.
12 +type Listener struct {
13 + mu sync.Mutex
14 + handle syscall.Handle
15 + config ServerConfig
16 + pipeName []uint16
17 + nextSessionID atomic.Uint64
18 + closing bool
19 + accepting bool
20 +}
21 +
22 +// Listen creates a listener on a Named Pipe derived from runDir + serviceName.
23 +func Listen(runDir, serviceName string, config ServerConfig) (*Listener, error) {
24 + pipeName, err := BuildPipeName(runDir, serviceName)
25 + if err != nil {
26 + return nil, err
27 + }
28 +
29 + bufSize := pipeBufferSize(config.PacketSize)
30 + handle, err := createPipeInstance(pipeName, bufSize, true)
31 + if err != nil {
32 + return nil, err
33 + }
34 +
35 + return &Listener{
36 + handle: handle,
37 + config: config,
38 + pipeName: pipeName,
39 + }, nil
40 +}
41 +
42 +// Handle returns the raw HANDLE.
43 +func (l *Listener) Handle() syscall.Handle {
44 + l.mu.Lock()
45 + defer l.mu.Unlock()
46 + return l.handle
47 +}
48 +
49 +// SetPayloadLimits updates the payload limits used for future handshakes.
50 +func (l *Listener) SetPayloadLimits(maxRequestPayloadBytes, maxResponsePayloadBytes uint32) {
51 + l.mu.Lock()
52 + defer l.mu.Unlock()
53 + l.config.MaxRequestPayloadBytes = maxRequestPayloadBytes
54 + l.config.MaxResponsePayloadBytes = maxResponsePayloadBytes
55 +}
56 +
57 +// Accept accepts one client connection. Performs the full handshake.
58 +func (l *Listener) Accept() (*Session, error) {
59 + sessionID := l.nextSessionID.Add(1)
60 + l.mu.Lock()
61 + config := l.config
62 + l.mu.Unlock()
63 + return l.AcceptWithConfig(sessionID, config)
64 +}
65 +
66 +// AcceptWithConfig accepts one client connection using a caller-provided
67 +// per-session server config and session ID.
68 +func (l *Listener) AcceptWithConfig(sessionID uint64, config ServerConfig) (*Session, error) {
69 + l.mu.Lock()
70 + if l.handle == syscall.InvalidHandle {
71 + l.mu.Unlock()
72 + return nil, wrapErr(ErrAccept, "listener closed")
73 + }
74 + sessionHandle := l.handle
75 + l.accepting = true
76 + l.mu.Unlock()
77 +
78 + err := connectNamedPipe(sessionHandle)
79 + if err != nil {
80 + if errno, ok := err.(syscall.Errno); !ok || errno != _ERROR_PIPE_CONNECTED {
81 + l.mu.Lock()
82 + l.accepting = false
83 + l.mu.Unlock()
84 + return nil, wrapErr(ErrAccept, err.Error())
85 + }
86 + }
87 +
88 + l.mu.Lock()
89 + l.accepting = false
90 + if l.closing {
91 + if l.handle == sessionHandle {
92 + l.handle = syscall.InvalidHandle
93 + }
94 + l.mu.Unlock()
95 + disconnectNamedPipe(sessionHandle)
96 + syscall.CloseHandle(sessionHandle)
97 + return nil, wrapErr(ErrAccept, "listener closed")
98 + }
99 +
100 + bufSize := pipeBufferSize(l.config.PacketSize)
101 + next, perr := createPipeInstance(l.pipeName, bufSize, false)
102 + if perr != nil {
103 + if l.handle == sessionHandle {
104 + l.handle = syscall.InvalidHandle
105 + }
106 + l.mu.Unlock()
107 + disconnectNamedPipe(sessionHandle)
108 + syscall.CloseHandle(sessionHandle)
109 + return nil, perr
110 + }
111 + l.handle = next
112 + l.mu.Unlock()
113 +
114 + session, herr := serverHandshake(sessionHandle, &config, sessionID)
115 + if herr != nil {
116 + disconnectNamedPipe(sessionHandle)
117 + syscall.CloseHandle(sessionHandle)
118 + return nil, herr
119 + }
120 + return session, nil
121 +}
122 +
123 +// Close closes the listener.
124 +func (l *Listener) Close() {
125 + l.mu.Lock()
126 + handle := l.handle
127 + if handle == syscall.InvalidHandle {
128 + l.mu.Unlock()
129 + return
130 + }
131 + l.closing = true
132 + accepting := l.accepting
133 + if !accepting {
134 + l.handle = syscall.InvalidHandle
135 + }
136 + pipeName := l.pipeName
137 + l.mu.Unlock()
138 +
139 + if accepting && len(pipeName) > 0 && pipeName[0] != 0 {
140 + wake, err := syscall.CreateFile(
141 + &pipeName[0],
142 + _GENERIC_READ|_GENERIC_WRITE,
143 + 0,
144 + nil,
145 + _OPEN_EXISTING,
146 + 0,
147 + 0,
148 + )
149 + if err == nil && wake != syscall.InvalidHandle && wake != 0 {
150 + syscall.CloseHandle(wake)
151 + return
152 + }
153 + l.mu.Lock()
154 + if l.handle == handle {
155 + l.handle = syscall.InvalidHandle
156 + }
157 + l.mu.Unlock()
158 + }
159 +
160 + syscall.CloseHandle(handle)
161 +}
162 +
163 +func createPipeInstance(pipeName []uint16, bufSize uint32, firstInstance bool) (syscall.Handle, error) {
164 + openMode := uint32(_PIPE_ACCESS_DUPLEX)
165 + if firstInstance {
166 + openMode |= _FILE_FLAG_FIRST_PIPE_INSTANCE
167 + }
168 +
169 + handle, err := createNamedPipe(
170 + &pipeName[0],
171 + openMode,
172 + _PIPE_TYPE_MESSAGE|_PIPE_READMODE_MESSAGE|_PIPE_WAIT,
173 + _PIPE_UNLIMITED_INSTANCES,
174 + bufSize,
175 + bufSize,
176 + 0,
177 + )
178 + if err != nil {
179 + errno, ok := err.(syscall.Errno)
180 + if ok && (errno == _ERROR_ACCESS_DENIED || errno == _ERROR_PIPE_BUSY) {
181 + return syscall.InvalidHandle, ErrAddrInUse
182 + }
183 + return syscall.InvalidHandle, wrapErr(ErrCreatePipe, err.Error())
184 + }
185 + return handle, nil
186 +}
src/go/pkg/netipc/transport/windows/pipe_receive.go new
+39
@@ -0,0 +1,39 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "errors"
7 + "syscall"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
11 +)
12 +
13 +// Receive reads one logical message. buf is a scratch buffer.
14 +func (s *Session) Receive(buf []byte) (protocol.Header, []byte, error) {
15 + if s.handle == syscall.InvalidHandle {
16 + return protocol.Header{}, nil, wrapErr(ErrBadParam, "session closed")
17 + }
18 +
19 + return framing.SessionReceive(framing.SessionReceiveConfig{
20 + RoleServer: s.role == RoleServer,
21 + PacketSize: s.PacketSize,
22 + MaxRequestPayloadBytes: s.MaxRequestPayloadBytes,
23 + MaxRequestBatchItems: s.MaxRequestBatchItems,
24 + MaxResponsePayloadBytes: s.MaxResponsePayloadBytes,
25 + MaxResponseBatchItems: s.MaxResponseBatchItems,
26 + InflightIDs: s.inflightIDs,
27 + RecvBuf: &s.recvBuf,
28 + PacketBuf: &s.pktBuf,
29 + Recv: func(dst []byte) (int, error) { return rawRecv(s.handle, dst) },
30 + EnsurePacketScratch: ensurePipeScratchBuf,
31 + IsRecvDisconnect: func(err error) bool { return errors.Is(err, ErrDisconnected) },
32 + FailAllInflight: s.failAllInflight,
33 + ErrLimitExceeded: func(msg string) error { return wrapErr(ErrLimitExceeded, msg) },
34 + ErrProtocol: func(msg string) error { return wrapErr(ErrProtocol, msg) },
35 + ErrChunk: func(msg string) error { return wrapErr(ErrChunk, msg) },
36 + ErrUnknownMsgID: func(msg string) error { return wrapErr(ErrUnknownMsgID, msg) },
37 + ErrRecv: func(msg string) error { return wrapErr(ErrRecv, msg) },
38 + }, buf)
39 +}
src/go/pkg/netipc/transport/windows/pipe_send.go new
+42
@@ -0,0 +1,42 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "errors"
7 + "syscall"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
10 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
11 +)
12 +
13 +// Send sends one logical message. Fills magic/version/header_len/payload_len.
14 +func (s *Session) Send(hdr *protocol.Header, payload []byte) error {
15 + if s.handle == syscall.InvalidHandle {
16 + return wrapErr(ErrBadParam, "session closed")
17 + }
18 +
19 + return framing.SessionSend(framing.SessionSendConfig{
20 + RoleClient: s.role == RoleClient,
21 + PacketSize: s.PacketSize,
22 + InflightIDs: &s.inflightIDs,
23 + FailAllInflight: s.failAllInflight,
24 + IsSendDisconnect: func(err error) bool { return errors.Is(err, ErrDisconnected) },
25 + SendFirstPacket: func(packetHdr *protocol.Header, packetPayload []byte, packetLen int) error {
26 + msg := ensurePipeScratchBuf(&s.sendBuf, packetLen)
27 + packetHdr.Encode(msg[:protocol.HeaderSize])
28 + copy(msg[protocol.HeaderSize:], packetPayload)
29 + return rawSendMsg(s.handle, msg[:packetLen])
30 + },
31 + SendChunk: func(chk protocol.ChunkHeader, chunkPayload []byte) error {
32 + pktLen := protocol.HeaderSize + len(chunkPayload)
33 + msg := ensurePipeScratchBuf(&s.sendBuf, pktLen)
34 + chk.Encode(msg[:protocol.HeaderSize])
35 + copy(msg[protocol.HeaderSize:], chunkPayload)
36 + return rawSendMsg(s.handle, msg[:pktLen])
37 + },
38 + ErrLimitExceeded: func(msg string) error { return wrapErr(ErrLimitExceeded, msg) },
39 + ErrDuplicateMsgID: func(msg string) error { return wrapErr(ErrDuplicateMsgID, msg) },
40 + ErrBadParam: func(msg string) error { return wrapErr(ErrBadParam, msg) },
41 + }, hdr, payload)
42 +}
src/go/pkg/netipc/transport/windows/pipe_send_test.go new
+32
@@ -0,0 +1,32 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/netipc/protocol"
9 + "github.com/netdata/netdata/go/plugins/pkg/netipc/transport/internal/framing"
10 +)
11 +
12 +func TestHeaderPayloadLenBounds(t *testing.T) {
13 + if _, _, ok := framing.HeaderPayloadLen(-1); ok {
14 + t.Fatal("negative payload length should fail")
15 + }
16 +
17 + maxInt := int(^uint(0) >> 1)
18 + if _, _, ok := framing.HeaderPayloadLen(maxInt); ok {
19 + t.Fatal("payload length that overflows int total length should fail")
20 + }
21 +
22 + protocolLimit := uint64(^uint32(0)) - uint64(protocol.HeaderSize)
23 + if protocolLimit <= uint64(maxInt) {
24 + got, _, ok := framing.HeaderPayloadLen(int(protocolLimit))
25 + if !ok {
26 + t.Fatal("protocol-limit payload should pass")
27 + }
28 + if uint64(got) != uint64(^uint32(0)) {
29 + t.Fatalf("total length = %d, want %d", got, uint64(^uint32(0)))
30 + }
31 + }
32 +}
src/go/pkg/netipc/transport/windows/pipe_session.go new
+184
@@ -0,0 +1,184 @@
1 +//go:build windows
2 +
3 +package windows
4 +
5 +import (
6 + "syscall"
7 + "time"
8 +)
9 +
10 +// Role distinguishes client vs server sessions.
11 +type Role int
12 +
13 +const (
14 + RoleClient Role = 1
15 + RoleServer Role = 2
16 +)
17 +
18 +// spinWaitIterations limits cooperative polling before falling back to sleep.
19 +const spinWaitIterations = 256
20 +
21 +// ClientConfig configures a client connection.
22 +type ClientConfig struct {
23 + SupportedProfiles uint32
24 + PreferredProfiles uint32
25 + MaxRequestPayloadBytes uint32 // 0 = use default (1024)
26 + MaxRequestBatchItems uint32 // 0 = use default (1)
27 + MaxResponsePayloadBytes uint32
28 + MaxResponseBatchItems uint32
29 + AuthToken uint64
30 + PacketSize uint32 // 0 = use default (65536)
31 +}
32 +
33 +// ServerConfig configures a listener and its accepted sessions.
34 +type ServerConfig struct {
35 + SupportedProfiles uint32
36 + PreferredProfiles uint32
37 + MaxRequestPayloadBytes uint32
38 + MaxRequestBatchItems uint32
39 + MaxResponsePayloadBytes uint32
40 + MaxResponseBatchItems uint32
41 + AuthToken uint64
42 + PacketSize uint32 // 0 = use default (65536)
43 +}
44 +
45 +// Session is a connected Named Pipe session (client or server side).
46 +type Session struct {
47 + handle syscall.Handle
48 + role Role
49 +
50 + MaxRequestPayloadBytes uint32
51 + MaxRequestBatchItems uint32
52 + MaxResponsePayloadBytes uint32
53 + MaxResponseBatchItems uint32
54 + PacketSize uint32
55 + SelectedProfile uint32
56 + SessionID uint64
57 +
58 + recvBuf []byte
59 + sendBuf []byte
60 + pktBuf []byte
61 +
62 + inflightIDs map[uint64]struct{}
63 +}
64 +
65 +func (s *Session) failAllInflight() {
66 + if s.role != RoleClient || len(s.inflightIDs) == 0 {
67 + return
68 + }
69 + clear(s.inflightIDs)
70 +}
71 +
72 +// Handle returns the raw HANDLE for WaitForSingleObject integration.
73 +func (s *Session) Handle() syscall.Handle {
74 + return s.handle
75 +}
76 +
77 +// Role returns the session role.
78 +func (s *Session) Role() Role {
79 + return s.role
80 +}
81 +
82 +// GetRole returns the session role.
83 +// Deprecated: use Role.
84 +func (s *Session) GetRole() Role {
85 + return s.Role()
86 +}
87 +
88 +// WaitReadable waits until bytes are available to read or the timeout expires.
89 +func (s *Session) WaitReadable(timeoutMs uint32) (bool, error) {
90 + if s.handle == syscall.InvalidHandle {
91 + return false, wrapErr(ErrBadParam, "session closed")
92 + }
93 +
94 + deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
95 + yielded := false
96 + for {
97 + available, err := peekNamedPipeAvailable(s.handle)
98 + if err != nil {
99 + if isDisconnectError(err) {
100 + s.failAllInflight()
101 + return false, ErrDisconnected
102 + }
103 + return false, wrapErr(ErrRecv, err.Error())
104 + }
105 + if available > 0 {
106 + return true, nil
107 + }
108 + if !time.Now().Before(deadline) {
109 + return false, nil
110 + }
111 + if !yielded {
112 + yielded = true
113 + for i := 0; i < spinWaitIterations; i++ {
114 + procSwitchToThread.Call()
115 + available, err = peekNamedPipeAvailable(s.handle)
116 + if err != nil {
117 + if isDisconnectError(err) {
118 + s.failAllInflight()
119 + return false, ErrDisconnected
120 + }
121 + return false, wrapErr(ErrRecv, err.Error())
122 + }
123 + if available > 0 {
124 + return true, nil
125 + }
126 + if !time.Now().Before(deadline) {
127 + return false, nil
128 + }
129 + }
130 + continue
131 + }
132 + time.Sleep(time.Millisecond)
133 + }
134 +}
135 +
136 +// Close closes the session and releases resources.
137 +func (s *Session) Close() {
138 + if s.handle != syscall.InvalidHandle {
139 + if s.role == RoleServer {
140 + flushFileBuffers(s.handle)
141 + disconnectNamedPipe(s.handle)
142 + }
143 + syscall.CloseHandle(s.handle)
144 + s.handle = syscall.InvalidHandle
145 + }
146 + s.recvBuf = nil
147 + s.sendBuf = nil
148 + s.pktBuf = nil
149 + s.failAllInflight()
150 +}
151 +
152 +// Connect establishes a session to a server pipe derived from runDir + serviceName.
153 +func Connect(runDir, serviceName string, config *ClientConfig) (*Session, error) {
154 + pipeName, err := BuildPipeName(runDir, serviceName)
155 + if err != nil {
156 + return nil, err
157 + }
158 +
159 + handle, err := syscall.CreateFile(
160 + &pipeName[0],
161 + _GENERIC_READ|_GENERIC_WRITE,
162 + 0,
163 + nil,
164 + _OPEN_EXISTING,
165 + 0,
166 + 0,
167 + )
168 + if err != nil {
169 + return nil, wrapErr(ErrConnect, err.Error())
170 + }
171 +
172 + mode := uint32(_PIPE_READMODE_MESSAGE)
173 + if err := setNamedPipeHandleState(handle, &mode); err != nil {
174 + syscall.CloseHandle(handle)
175 + return nil, wrapErr(ErrConnect, "SetNamedPipeHandleState: "+err.Error())
176 + }
177 +
178 + session, herr := clientHandshake(handle, config)
179 + if herr != nil {
180 + syscall.CloseHandle(handle)
181 + return nil, herr
182 + }
183 + return session, nil
184 +}
src/go/pkg/netipc/transport/windows/pipe_test.go
+26
@@ -30,6 +30,18 @@ func TestMinU32(t *testing.T) {
30 }
31 }
32
33 +func TestMaxU32(t *testing.T) {
34 + if got := maxU32(9, 1); got != 9 {
35 + t.Fatalf("maxU32(9, 1) = %d, want 9", got)
36 + }
37 + if got := maxU32(1, 9); got != 9 {
38 + t.Fatalf("maxU32(1, 9) = %d, want 9", got)
39 + }
40 + if got := maxU32(5, 5); got != 5 {
41 + t.Fatalf("maxU32(5, 5) = %d, want 5", got)
42 + }
43 +}
44 +
45 func TestHighestBit(t *testing.T) {
46 cases := []struct {
47 mask uint32
@@ -218,3 +230,17 @@ func TestSetNamedPipeHandleStateInvalidHandle(t *testing.T) {
230 t.Fatal("setNamedPipeHandleState on invalid handle should fail")
231 }
232 }
233 +
234 +func TestWaitReadableClosedSession(t *testing.T) {
235 + session := &Session{handle: syscall.InvalidHandle}
236 + ready, err := session.WaitReadable(1)
237 + if err == nil {
238 + t.Fatal("WaitReadable on closed session should fail")
239 + }
240 + if ready {
241 + t.Fatal("WaitReadable on closed session returned ready")
242 + }
243 + if !errors.Is(err, ErrBadParam) {
244 + t.Fatalf("WaitReadable on closed session = %v, want ErrBadParam", err)
245 + }
246 +}
src/go/pkg/netipc/transport/windows/shm.go
+5 -1
@@ -160,7 +160,11 @@ type WinShmContext struct {
160 }
161
162 // Role returns the context role.
163 -func (c *WinShmContext) GetRole() WinShmRole { return c.role }
163 +func (c *WinShmContext) Role() WinShmRole { return c.role }
164 +
165 +// GetRole returns the context role.
166 +// Deprecated: use Role.
167 +func (c *WinShmContext) GetRole() WinShmRole { return c.Role() }
168
169 // ---------------------------------------------------------------------------
170 // Server API
src/go/pkg/netipc/transport/windows/shm_test.go
+4 -4
@@ -111,11 +111,11 @@ func TestWinShmCreateAttachAndCloseValidation(t *testing.T) {
111 }
112 defer client.WinShmClose()
113
114 - if server.GetRole() != WinShmRoleServer {
115 - t.Fatalf("server role = %d, want %d", server.GetRole(), WinShmRoleServer)
114 + if server.Role() != WinShmRoleServer {
115 + t.Fatalf("server role = %d, want %d", server.Role(), WinShmRoleServer)
116 }
117 - if client.GetRole() != WinShmRoleClient {
118 - t.Fatalf("client role = %d, want %d", client.GetRole(), WinShmRoleClient)
117 + if client.Role() != WinShmRoleClient {
118 + t.Fatalf("client role = %d, want %d", client.Role(), WinShmRoleClient)
119 }
120 }
121
src/libnetdata/netipc/include/netipc/netipc_protocol.h
+257
@@ -56,6 +56,8 @@ extern "C" {
56 #define NIPC_METHOD_INCREMENT 1u
57 #define NIPC_METHOD_CGROUPS_SNAPSHOT 2u
58 #define NIPC_METHOD_STRING_REVERSE 3u
59 +#define NIPC_METHOD_CGROUPS_LOOKUP 4u
60 +#define NIPC_METHOD_APPS_LOOKUP 5u
61
62 /* Profile bits */
63 #define NIPC_PROFILE_BASELINE 0x01u
@@ -73,6 +75,9 @@ extern "C" {
75 /* Alignment for batch items and cgroups items */
76 #define NIPC_ALIGNMENT 8u
77
78 +/* Common lookup sentinel values */
79 +#define NIPC_UID_UNSET 0xFFFFFFFFu
80 +
81 /* ------------------------------------------------------------------ */
82 /* Error codes */
83 /* ------------------------------------------------------------------ */
@@ -94,6 +99,39 @@ typedef enum {
99 NIPC_ERR_NOT_READY, /* client not connected / service unavailable */
100 } nipc_error_t;
101
102 +/* ------------------------------------------------------------------ */
103 +/* Shared cgroups/apps lookup enums */
104 +/* ------------------------------------------------------------------ */
105 +
106 +typedef enum {
107 + NIPC_ORCHESTRATOR_UNKNOWN = 0,
108 + NIPC_ORCHESTRATOR_SYSTEMD = 1,
109 + NIPC_ORCHESTRATOR_DOCKER = 2,
110 + NIPC_ORCHESTRATOR_K8S = 3,
111 + NIPC_ORCHESTRATOR_KVM = 4,
112 + NIPC_ORCHESTRATOR_LXC = 5,
113 + NIPC_ORCHESTRATOR_PODMAN = 6,
114 + NIPC_ORCHESTRATOR_NSPAWN = 7,
115 +} nipc_orchestrator_t;
116 +
117 +typedef enum {
118 + NIPC_CGROUP_LOOKUP_KNOWN = 0,
119 + NIPC_CGROUP_LOOKUP_UNKNOWN_RETRY_LATER = 1,
120 + NIPC_CGROUP_LOOKUP_UNKNOWN_PERMANENT = 2,
121 +} nipc_cgroup_lookup_status_t;
122 +
123 +typedef enum {
124 + NIPC_PID_LOOKUP_KNOWN = 0,
125 + NIPC_PID_LOOKUP_UNKNOWN = 1,
126 +} nipc_pid_lookup_status_t;
127 +
128 +typedef enum {
129 + NIPC_APPS_CGROUP_KNOWN = 0,
130 + NIPC_APPS_CGROUP_UNKNOWN_RETRY_LATER = 1,
131 + NIPC_APPS_CGROUP_UNKNOWN_PERMANENT = 2,
132 + NIPC_APPS_CGROUP_HOST_ROOT = 3,
133 +} nipc_apps_cgroup_status_t;
134 +
135 /* ------------------------------------------------------------------ */
136 /* Outer message header (32 bytes) */
137 /* ------------------------------------------------------------------ */
@@ -306,6 +344,11 @@ typedef struct {
344 uint32_t len; /* length excluding the NUL */
345 } nipc_str_view_t;
346
347 +typedef struct {
348 + nipc_str_view_t key;
349 + nipc_str_view_t value;
350 +} nipc_lookup_label_view_t;
351 +
352 /*
353 * Per-item view -- ephemeral, borrows the payload buffer.
354 * Valid only while the payload buffer is alive.
@@ -408,6 +451,200 @@ nipc_error_t nipc_cgroups_builder_add(nipc_cgroups_builder_t *b,
451 */
452 size_t nipc_cgroups_builder_finish(nipc_cgroups_builder_t *b);
453
454 +/* ------------------------------------------------------------------ */
455 +/* Cgroups/apps lookup codecs */
456 +/* ------------------------------------------------------------------ */
457 +
458 +#define NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE 16u
459 +#define NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE 16u
460 +#define NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE 28u
461 +#define NIPC_APPS_LOOKUP_REQ_HDR_SIZE 16u
462 +#define NIPC_APPS_LOOKUP_RESP_HDR_SIZE 16u
463 +#define NIPC_APPS_LOOKUP_ITEM_HDR_SIZE 60u
464 +#define NIPC_LOOKUP_DIR_ENTRY_SIZE 8u
465 +#define NIPC_LOOKUP_LABEL_ENTRY_SIZE 16u
466 +#define NIPC_APPS_LOOKUP_KEY_SIZE 8u
467 +
468 +typedef struct {
469 + uint32_t offset;
470 + uint32_t length;
471 +} nipc_lookup_dir_entry_t;
472 +
473 +typedef struct {
474 + uint32_t key_offset;
475 + uint32_t key_length;
476 + uint32_t value_offset;
477 + uint32_t value_length;
478 +} nipc_lookup_label_entry_t;
479 +
480 +typedef struct {
481 + const uint8_t *_payload;
482 + size_t _payload_len;
483 + uint32_t item_count;
484 +} nipc_cgroups_lookup_req_view_t;
485 +
486 +typedef struct {
487 + nipc_str_view_t path;
488 +} nipc_cgroups_lookup_req_item_t;
489 +
490 +typedef struct {
491 + uint16_t layout_version;
492 + uint16_t flags;
493 + uint32_t item_count;
494 + uint64_t generation;
495 + const uint8_t *_payload;
496 + size_t _payload_len;
497 +} nipc_cgroups_lookup_resp_view_t;
498 +
499 +typedef struct {
500 + uint16_t status;
501 + uint16_t orchestrator;
502 + nipc_str_view_t path;
503 + nipc_str_view_t name;
504 + uint16_t label_count;
505 + const uint8_t *_item;
506 + uint32_t _item_len;
507 + uint32_t _label_table_offset;
508 +} nipc_cgroups_lookup_item_view_t;
509 +
510 +typedef struct {
511 + uint8_t *buf;
512 + size_t buf_len;
513 + uint64_t generation;
514 + uint32_t item_count;
515 + uint32_t max_items;
516 + nipc_error_t error;
517 + size_t data_offset;
518 +} nipc_cgroups_lookup_builder_t;
519 +
520 +typedef struct {
521 + const uint8_t *_payload;
522 + size_t _payload_len;
523 + uint32_t item_count;
524 +} nipc_apps_lookup_req_view_t;
525 +
526 +typedef struct {
527 + uint32_t pid;
528 +} nipc_apps_lookup_req_item_t;
529 +
530 +typedef struct {
531 + uint16_t layout_version;
532 + uint16_t flags;
533 + uint32_t item_count;
534 + uint64_t generation;
535 + const uint8_t *_payload;
536 + size_t _payload_len;
537 +} nipc_apps_lookup_resp_view_t;
538 +
539 +typedef struct {
540 + uint16_t status;
541 + uint16_t orchestrator;
542 + uint16_t cgroup_status;
543 + uint32_t pid;
544 + uint32_t ppid;
545 + uint32_t uid;
546 + uint64_t starttime;
547 + nipc_str_view_t comm;
548 + nipc_str_view_t cgroup_path;
549 + nipc_str_view_t cgroup_name;
550 + uint16_t label_count;
551 + const uint8_t *_item;
552 + uint32_t _item_len;
553 + uint32_t _label_table_offset;
554 +} nipc_apps_lookup_item_view_t;
555 +
556 +typedef struct {
557 + uint8_t *buf;
558 + size_t buf_len;
559 + uint64_t generation;
560 + uint32_t item_count;
561 + uint32_t max_items;
562 + nipc_error_t error;
563 + size_t data_offset;
564 +} nipc_apps_lookup_builder_t;
565 +
566 +size_t nipc_cgroups_lookup_req_encode(const nipc_str_view_t *paths,
567 + uint32_t item_count,
568 + void *buf, size_t buf_len);
569 +nipc_error_t nipc_cgroups_lookup_req_decode(const void *buf, size_t buf_len,
570 + nipc_cgroups_lookup_req_view_t *out);
571 +nipc_error_t nipc_cgroups_lookup_req_item(
572 + const nipc_cgroups_lookup_req_view_t *view,
573 + uint32_t index,
574 + nipc_cgroups_lookup_req_item_t *out);
575 +
576 +nipc_error_t nipc_cgroups_lookup_resp_decode(const void *buf, size_t buf_len,
577 + nipc_cgroups_lookup_resp_view_t *out);
578 +nipc_error_t nipc_cgroups_lookup_resp_item(
579 + const nipc_cgroups_lookup_resp_view_t *view,
580 + uint32_t index,
581 + nipc_cgroups_lookup_item_view_t *out);
582 +nipc_error_t nipc_cgroups_lookup_item_label(
583 + const nipc_cgroups_lookup_item_view_t *item,
584 + uint32_t index,
585 + nipc_lookup_label_view_t *out);
586 +
587 +void nipc_cgroups_lookup_builder_init(nipc_cgroups_lookup_builder_t *b,
588 + void *buf, size_t buf_len,
589 + uint32_t max_items,
590 + uint64_t generation);
591 +void nipc_cgroups_lookup_builder_set_generation(nipc_cgroups_lookup_builder_t *b,
592 + uint64_t generation);
593 +uint32_t nipc_cgroups_lookup_builder_estimate_max_items(size_t buf_len);
594 +nipc_error_t nipc_cgroups_lookup_builder_add(
595 + nipc_cgroups_lookup_builder_t *b,
596 + uint16_t status,
597 + uint16_t orchestrator,
598 + const char *path, uint32_t path_len,
599 + const char *name, uint32_t name_len,
600 + const nipc_lookup_label_view_t *labels,
601 + uint16_t label_count);
602 +size_t nipc_cgroups_lookup_builder_finish(nipc_cgroups_lookup_builder_t *b);
603 +
604 +size_t nipc_apps_lookup_req_encode(const uint32_t *pids,
605 + uint32_t item_count,
606 + void *buf, size_t buf_len);
607 +nipc_error_t nipc_apps_lookup_req_decode(const void *buf, size_t buf_len,
608 + nipc_apps_lookup_req_view_t *out);
609 +nipc_error_t nipc_apps_lookup_req_item(
610 + const nipc_apps_lookup_req_view_t *view,
611 + uint32_t index,
612 + nipc_apps_lookup_req_item_t *out);
613 +
614 +nipc_error_t nipc_apps_lookup_resp_decode(const void *buf, size_t buf_len,
615 + nipc_apps_lookup_resp_view_t *out);
616 +nipc_error_t nipc_apps_lookup_resp_item(
617 + const nipc_apps_lookup_resp_view_t *view,
618 + uint32_t index,
619 + nipc_apps_lookup_item_view_t *out);
620 +nipc_error_t nipc_apps_lookup_item_label(
621 + const nipc_apps_lookup_item_view_t *item,
622 + uint32_t index,
623 + nipc_lookup_label_view_t *out);
624 +
625 +void nipc_apps_lookup_builder_init(nipc_apps_lookup_builder_t *b,
626 + void *buf, size_t buf_len,
627 + uint32_t max_items,
628 + uint64_t generation);
629 +void nipc_apps_lookup_builder_set_generation(nipc_apps_lookup_builder_t *b,
630 + uint64_t generation);
631 +uint32_t nipc_apps_lookup_builder_estimate_max_items(size_t buf_len);
632 +nipc_error_t nipc_apps_lookup_builder_add(
633 + nipc_apps_lookup_builder_t *b,
634 + uint16_t status,
635 + uint16_t cgroup_status,
636 + uint16_t orchestrator,
637 + uint32_t pid,
638 + uint32_t ppid,
639 + uint32_t uid,
640 + uint64_t starttime,
641 + const char *comm, uint32_t comm_len,
642 + const char *cgroup_path, uint32_t cgroup_path_len,
643 + const char *cgroup_name, uint32_t cgroup_name_len,
644 + const nipc_lookup_label_view_t *labels,
645 + uint16_t label_count);
646 +size_t nipc_apps_lookup_builder_finish(nipc_apps_lookup_builder_t *b);
647 +
648 /* ------------------------------------------------------------------ */
649 /* INCREMENT codec (8 bytes) */
650 /* ------------------------------------------------------------------ */
@@ -502,6 +739,26 @@ nipc_error_t nipc_dispatch_cgroups_snapshot(
739 uint32_t max_items,
740 nipc_cgroups_handler_fn handler, void *user);
741
742 +typedef bool (*nipc_cgroups_lookup_handler_fn)(
743 + void *user,
744 + const nipc_cgroups_lookup_req_view_t *request,
745 + nipc_cgroups_lookup_builder_t *builder);
746 +
747 +nipc_error_t nipc_dispatch_cgroups_lookup(
748 + const uint8_t *req, size_t req_len,
749 + uint8_t *resp, size_t resp_size, size_t *resp_len,
750 + nipc_cgroups_lookup_handler_fn handler, void *user);
751 +
752 +typedef bool (*nipc_apps_lookup_handler_fn)(
753 + void *user,
754 + const nipc_apps_lookup_req_view_t *request,
755 + nipc_apps_lookup_builder_t *builder);
756 +
757 +nipc_error_t nipc_dispatch_apps_lookup(
758 + const uint8_t *req, size_t req_len,
759 + uint8_t *resp, size_t resp_size, size_t *resp_len,
760 + nipc_apps_lookup_handler_fn handler, void *user);
761 +
762 /* ------------------------------------------------------------------ */
763 /* Utility: 8-byte alignment */
764 /* ------------------------------------------------------------------ */
src/libnetdata/netipc/include/netipc/netipc_service.h
+45 -1
@@ -211,6 +211,18 @@ nipc_error_t nipc_client_call_cgroups_snapshot(
211 nipc_client_ctx_t *ctx,
212 nipc_cgroups_resp_view_t *view_out);
213
214 +nipc_error_t nipc_client_call_cgroups_lookup(
215 + nipc_client_ctx_t *ctx,
216 + const nipc_str_view_t *paths,
217 + uint32_t path_count,
218 + nipc_cgroups_lookup_resp_view_t *view_out);
219 +
220 +nipc_error_t nipc_client_call_apps_lookup(
221 + nipc_client_ctx_t *ctx,
222 + const uint32_t *pids,
223 + uint32_t pid_count,
224 + nipc_apps_lookup_resp_view_t *view_out);
225 +
226 /* ------------------------------------------------------------------ */
227 /* Managed server */
228 /* ------------------------------------------------------------------ */
@@ -235,6 +247,22 @@ typedef struct {
247 void *user;
248 } nipc_cgroups_service_handler_t;
249
250 +typedef struct {
251 + nipc_cgroups_lookup_handler_fn handle;
252 + void *user;
253 +} nipc_cgroups_lookup_service_handler_t;
254 +
255 +typedef struct {
256 + nipc_apps_lookup_handler_fn handle;
257 + void *user;
258 +} nipc_apps_lookup_service_handler_t;
259 +
260 +typedef union {
261 + nipc_cgroups_service_handler_t cgroups_snapshot;
262 + nipc_cgroups_lookup_service_handler_t cgroups_lookup;
263 + nipc_apps_lookup_service_handler_t apps_lookup;
264 +} nipc_server_typed_handler_t;
265 +
266 typedef struct nipc_managed_server nipc_managed_server_t;
267
268 /* Per-session context for multi-client server */
@@ -269,7 +297,7 @@ struct nipc_managed_server {
297 /* Callback */
298 nipc_server_handler_fn handler;
299 void *handler_user;
272 - nipc_cgroups_service_handler_t service_handler;
300 + nipc_server_typed_handler_t typed_handler;
301 uint16_t expected_method_code;
302 uint32_t learned_request_payload_bytes;
303 uint32_t learned_response_payload_bytes;
@@ -318,6 +346,22 @@ nipc_error_t nipc_server_init_typed(nipc_managed_server_t *server,
346 int worker_count,
347 const nipc_cgroups_service_handler_t *service_handler);
348
349 +nipc_error_t nipc_server_init_cgroups_lookup(
350 + nipc_managed_server_t *server,
351 + const char *run_dir,
352 + const char *service_name,
353 + const nipc_server_config_t *config,
354 + int worker_count,
355 + const nipc_cgroups_lookup_service_handler_t *service_handler);
356 +
357 +nipc_error_t nipc_server_init_apps_lookup(
358 + nipc_managed_server_t *server,
359 + const char *run_dir,
360 + const char *service_name,
361 + const nipc_server_config_t *config,
362 + int worker_count,
363 + const nipc_apps_lookup_service_handler_t *service_handler);
364 +
365 #ifdef NIPC_INTERNAL_TESTING
366 /*
367 * Internal compatibility entrypoint for repo tests and benchmarks that
src/libnetdata/netipc/src/protocol/netipc_protocol.c
+1 -505
@@ -6,18 +6,7 @@
6 * No endianness conversion — both peers share host byte order.
7 */
8
9 -#include "netipc/netipc_protocol.h"
10 -#include <stddef.h>
11 -#include <string.h>
12 -
13 -/*
14 - * Safe multiplication check: returns true if count * entry_size would
15 - * overflow size_t. Portable across 32-bit and 64-bit without triggering
16 - * -Wtype-limits.
17 - */
18 -static inline bool mul_would_overflow(size_t count, size_t entry_size) {
19 - return entry_size != 0 && count > SIZE_MAX / entry_size;
20 -}
9 +#include "netipc_protocol_internal.h"
10
11 /* ------------------------------------------------------------------ */
12 /* Compile-time layout assertions */
@@ -87,36 +76,6 @@ _Static_assert(offsetof(nipc_hello_ack_t, agreed_packet_size) == 32, "");
76 _Static_assert(offsetof(nipc_hello_ack_t, _reserved) == 36, "");
77 _Static_assert(offsetof(nipc_hello_ack_t, session_id) == 40, "");
78
90 -/* Cgroups snapshot response header (24 bytes) */
91 -_Static_assert(sizeof(nipc_cgroups_resp_header_t) == 24,
92 - "nipc_cgroups_resp_header_t must be 24 bytes");
93 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, layout_version) == 0, "");
94 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, flags) == 2, "");
95 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, item_count) == 4, "");
96 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, systemd_enabled) == 8, "");
97 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, reserved) == 12, "");
98 -_Static_assert(offsetof(nipc_cgroups_resp_header_t, generation) == 16, "");
99 -
100 -/* Cgroups item wire header (internal, 32 bytes) */
101 -typedef struct {
102 - uint16_t layout_version;
103 - uint16_t flags;
104 - uint32_t hash;
105 - uint32_t options;
106 - uint32_t enabled;
107 - uint32_t name_offset;
108 - uint32_t name_length;
109 - uint32_t path_offset;
110 - uint32_t path_length;
111 -} nipc_cgroups_item_wire_t;
112 -
113 -_Static_assert(sizeof(nipc_cgroups_item_wire_t) == 32,
114 - "nipc_cgroups_item_wire_t must be 32 bytes");
115 -
116 -/* Cgroups request (4 bytes) */
117 -_Static_assert(sizeof(nipc_cgroups_req_t) == 4,
118 - "nipc_cgroups_req_t must be 4 bytes");
119 -
79 /* ------------------------------------------------------------------ */
80 /* Outer message header (32 bytes) */
81 /* ------------------------------------------------------------------ */
@@ -403,466 +362,3 @@ nipc_error_t nipc_hello_ack_decode(const void *buf, size_t buf_len,
362
363 return NIPC_OK;
364 }
406 -
407 -/* ------------------------------------------------------------------ */
408 -/* Cgroups snapshot request (4 bytes) */
409 -/* ------------------------------------------------------------------ */
410 -
411 -size_t nipc_cgroups_req_encode(const nipc_cgroups_req_t *r,
412 - void *buf, size_t buf_len) {
413 - if (buf_len < sizeof(nipc_cgroups_req_t))
414 - return 0;
415 -
416 - memcpy(buf, r, sizeof(nipc_cgroups_req_t));
417 - return sizeof(nipc_cgroups_req_t);
418 -}
419 -
420 -nipc_error_t nipc_cgroups_req_decode(const void *buf, size_t buf_len,
421 - nipc_cgroups_req_t *out) {
422 - if (buf_len < sizeof(nipc_cgroups_req_t))
423 - return NIPC_ERR_TRUNCATED;
424 -
425 - memcpy(out, buf, sizeof(nipc_cgroups_req_t));
426 -
427 - if (out->layout_version != 1)
428 - return NIPC_ERR_BAD_LAYOUT;
429 - if (out->flags != 0)
430 - return NIPC_ERR_BAD_LAYOUT;
431 -
432 - return NIPC_OK;
433 -}
434 -
435 -/* ------------------------------------------------------------------ */
436 -/* Cgroups snapshot response decode */
437 -/* ------------------------------------------------------------------ */
438 -
439 -nipc_error_t nipc_cgroups_resp_decode(const void *buf, size_t buf_len,
440 - nipc_cgroups_resp_view_t *out) {
441 - if (buf_len < NIPC_CGROUPS_RESP_HDR_SIZE)
442 - return NIPC_ERR_TRUNCATED;
443 -
444 - nipc_cgroups_resp_header_t hdr;
445 - memcpy(&hdr, buf, sizeof(hdr));
446 -
447 - if (hdr.layout_version != 1)
448 - return NIPC_ERR_BAD_LAYOUT;
449 - if (hdr.flags != 0)
450 - return NIPC_ERR_BAD_LAYOUT;
451 - if (hdr.reserved != 0)
452 - return NIPC_ERR_BAD_LAYOUT;
453 -
454 - out->layout_version = hdr.layout_version;
455 - out->flags = hdr.flags;
456 - out->item_count = hdr.item_count;
457 - out->systemd_enabled = hdr.systemd_enabled;
458 - out->generation = hdr.generation;
459 -
460 - /* Validate directory fits (with overflow check) */
461 - if (mul_would_overflow((size_t)out->item_count, NIPC_CGROUPS_DIR_ENTRY_SIZE))
462 - return NIPC_ERR_BAD_ITEM_COUNT;
463 - size_t dir_size = (size_t)out->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
464 - size_t dir_end = NIPC_CGROUPS_RESP_HDR_SIZE + dir_size;
465 - if (dir_end > buf_len)
466 - return NIPC_ERR_TRUNCATED;
467 -
468 - size_t packed_area_len = buf_len - dir_end;
469 -
470 - /* Validate each directory entry */
471 - const uint8_t *dir = (const uint8_t *)buf + NIPC_CGROUPS_RESP_HDR_SIZE;
472 - for (uint32_t i = 0; i < out->item_count; i++) {
473 - nipc_batch_entry_t entry;
474 - memcpy(&entry, dir + i * sizeof(entry), sizeof(entry));
475 -
476 - if (entry.offset % NIPC_ALIGNMENT != 0)
477 - return NIPC_ERR_BAD_ALIGNMENT;
478 - if ((uint64_t)entry.offset + entry.length > packed_area_len)
479 - return NIPC_ERR_OUT_OF_BOUNDS;
480 - if (entry.length < NIPC_CGROUPS_ITEM_HDR_SIZE)
481 - return NIPC_ERR_TRUNCATED;
482 - }
483 -
484 - out->_payload = (const uint8_t *)buf;
485 - out->_payload_len = buf_len;
486 - return NIPC_OK;
487 -}
488 -
489 -nipc_error_t nipc_cgroups_resp_item(const nipc_cgroups_resp_view_t *view,
490 - uint32_t index,
491 - nipc_cgroups_item_view_t *out) {
492 - if (index >= view->item_count)
493 - return NIPC_ERR_OUT_OF_BOUNDS;
494 -
495 - /* Overflow already checked in nipc_cgroups_resp_decode, but
496 - * guard defensively since this is a public API. */
497 - if (mul_would_overflow((size_t)view->item_count, NIPC_CGROUPS_DIR_ENTRY_SIZE))
498 - return NIPC_ERR_BAD_ITEM_COUNT;
499 -
500 - size_t dir_start = NIPC_CGROUPS_RESP_HDR_SIZE;
501 - size_t dir_size = (size_t)view->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
502 - size_t packed_area_start = dir_start + dir_size;
503 -
504 - /* Read directory entry */
505 - nipc_batch_entry_t dir_entry;
506 - memcpy(&dir_entry,
507 - view->_payload + dir_start + index * sizeof(dir_entry),
508 - sizeof(dir_entry));
509 -
510 - const uint8_t *item = view->_payload + packed_area_start + dir_entry.offset;
511 - uint32_t item_len = dir_entry.length;
512 -
513 - /* Read the 32-byte item wire header in one copy */
514 - nipc_cgroups_item_wire_t wire;
515 - memcpy(&wire, item, sizeof(wire));
516 -
517 - if (wire.layout_version != 1)
518 - return NIPC_ERR_BAD_LAYOUT;
519 - if (wire.flags != 0)
520 - return NIPC_ERR_BAD_LAYOUT;
521 -
522 - /* Validate name string */
523 - if (wire.name_offset < NIPC_CGROUPS_ITEM_HDR_SIZE)
524 - return NIPC_ERR_OUT_OF_BOUNDS;
525 - if ((uint64_t)wire.name_offset + wire.name_length + 1 > item_len)
526 - return NIPC_ERR_OUT_OF_BOUNDS;
527 - if (item[wire.name_offset + wire.name_length] != '\0')
528 - return NIPC_ERR_MISSING_NUL;
529 -
530 - /* Validate path string */
531 - if (wire.path_offset < NIPC_CGROUPS_ITEM_HDR_SIZE)
532 - return NIPC_ERR_OUT_OF_BOUNDS;
533 - if ((uint64_t)wire.path_offset + wire.path_length + 1 > item_len)
534 - return NIPC_ERR_OUT_OF_BOUNDS;
535 - if (item[wire.path_offset + wire.path_length] != '\0')
536 - return NIPC_ERR_MISSING_NUL;
537 -
538 - /* Reject overlapping name and path regions (including NUL) */
539 - {
540 - uint64_t name_start = wire.name_offset;
541 - uint64_t name_end = name_start + wire.name_length + 1;
542 - uint64_t path_start = wire.path_offset;
543 - uint64_t path_end = path_start + wire.path_length + 1;
544 - if (name_start < path_end && path_start < name_end)
545 - return NIPC_ERR_BAD_LAYOUT;
546 - }
547 -
548 - out->layout_version = wire.layout_version;
549 - out->flags = wire.flags;
550 - out->hash = wire.hash;
551 - out->options = wire.options;
552 - out->enabled = wire.enabled;
553 - out->name.ptr = (const char *)(item + wire.name_offset);
554 - out->name.len = wire.name_length;
555 - out->path.ptr = (const char *)(item + wire.path_offset);
556 - out->path.len = wire.path_length;
557 -
558 - return NIPC_OK;
559 -}
560 -
561 -/* ------------------------------------------------------------------ */
562 -/* Cgroups snapshot response builder */
563 -/* */
564 -/* Layout during building (max_items directory slots reserved): */
565 -/* [24-byte header space] [max_items*8 directory] [packed items] */
566 -/* */
567 -/* Layout after finish (compacted to actual item_count): */
568 -/* [24-byte header] [item_count*8 directory] [packed items] */
569 -/* */
570 -/* If item_count < max_items, finish() shifts packed data left and */
571 -/* adjusts directory offsets accordingly. */
572 -/* ------------------------------------------------------------------ */
573 -
574 -void nipc_cgroups_builder_init(nipc_cgroups_builder_t *b,
575 - void *buf, size_t buf_len,
576 - uint32_t max_items,
577 - uint32_t systemd_enabled,
578 - uint64_t generation) {
579 - b->buf = (uint8_t *)buf;
580 - b->buf_len = buf_len;
581 - b->systemd_enabled = systemd_enabled;
582 - b->generation = generation;
583 - b->item_count = 0;
584 - b->max_items = max_items;
585 - b->error = NIPC_OK;
586 -
587 - /* Packed item data starts after reserved directory */
588 - b->data_offset = NIPC_CGROUPS_RESP_HDR_SIZE +
589 - (size_t)max_items * NIPC_CGROUPS_DIR_ENTRY_SIZE;
590 -}
591 -
592 -void nipc_cgroups_builder_set_header(nipc_cgroups_builder_t *b,
593 - uint32_t systemd_enabled,
594 - uint64_t generation) {
595 - b->systemd_enabled = systemd_enabled;
596 - b->generation = generation;
597 -}
598 -
599 -uint32_t nipc_cgroups_builder_estimate_max_items(size_t buf_len) {
600 - if (buf_len <= NIPC_CGROUPS_RESP_HDR_SIZE)
601 - return 0;
602 -
603 - size_t min_aligned_item = nipc_align8(NIPC_CGROUPS_ITEM_HDR_SIZE + 2u);
604 - return (uint32_t)((buf_len - NIPC_CGROUPS_RESP_HDR_SIZE) /
605 - (NIPC_CGROUPS_DIR_ENTRY_SIZE + min_aligned_item));
606 -}
607 -
608 -nipc_error_t nipc_cgroups_builder_add(nipc_cgroups_builder_t *b,
609 - uint32_t hash,
610 - uint32_t options,
611 - uint32_t enabled,
612 - const char *name, uint32_t name_len,
613 - const char *path, uint32_t path_len) {
614 - if (b->item_count >= b->max_items) {
615 - b->error = NIPC_ERR_OVERFLOW;
616 - return NIPC_ERR_OVERFLOW;
617 - }
618 -
619 - /* Align item start to 8 bytes */
620 - size_t item_start = nipc_align8(b->data_offset);
621 -
622 - /* Item payload: 32-byte header + name + NUL + path + NUL */
623 - size_t item_size = NIPC_CGROUPS_ITEM_HDR_SIZE +
624 - (size_t)name_len + 1 +
625 - (size_t)path_len + 1;
626 -
627 - if (item_start + item_size > b->buf_len) {
628 - b->error = NIPC_ERR_OVERFLOW;
629 - return NIPC_ERR_OVERFLOW;
630 - }
631 -
632 - /* Zero alignment padding */
633 - if (item_start > b->data_offset)
634 - memset(b->buf + b->data_offset, 0, item_start - b->data_offset);
635 -
636 - uint8_t *item = b->buf + item_start;
637 -
638 - /* Write item header as a single struct copy */
639 - nipc_cgroups_item_wire_t wire = {
640 - .layout_version = 1,
641 - .flags = 0,
642 - .hash = hash,
643 - .options = options,
644 - .enabled = enabled,
645 - .name_offset = NIPC_CGROUPS_ITEM_HDR_SIZE,
646 - .name_length = name_len,
647 - .path_offset = NIPC_CGROUPS_ITEM_HDR_SIZE + name_len + 1,
648 - .path_length = path_len,
649 - };
650 - memcpy(item, &wire, sizeof(wire));
651 -
652 - /* Write strings with NUL terminators */
653 - memcpy(item + wire.name_offset, name, name_len);
654 - item[wire.name_offset + name_len] = '\0';
655 - memcpy(item + wire.path_offset, path, path_len);
656 - item[wire.path_offset + path_len] = '\0';
657 -
658 - /* Write directory entry (absolute offset stored temporarily) */
659 - nipc_batch_entry_t dir_entry = {
660 - .offset = (uint32_t)item_start,
661 - .length = (uint32_t)item_size,
662 - };
663 - size_t dir_pos = NIPC_CGROUPS_RESP_HDR_SIZE +
664 - (size_t)b->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
665 - memcpy(b->buf + dir_pos, &dir_entry, sizeof(dir_entry));
666 -
667 - b->data_offset = item_start + item_size;
668 - b->item_count++;
669 - return NIPC_OK;
670 -}
671 -
672 -size_t nipc_cgroups_builder_finish(nipc_cgroups_builder_t *b) {
673 - uint8_t *p = b->buf;
674 -
675 - nipc_cgroups_resp_header_t hdr = {
676 - .layout_version = 1,
677 - .flags = 0,
678 - .item_count = b->item_count,
679 - .systemd_enabled = b->systemd_enabled,
680 - .reserved = 0,
681 - .generation = b->generation,
682 - };
683 -
684 - if (b->item_count == 0) {
685 - memcpy(p, &hdr, sizeof(hdr));
686 - return NIPC_CGROUPS_RESP_HDR_SIZE;
687 - }
688 -
689 - /* Where the decoder expects packed data to start */
690 - size_t final_packed_start = NIPC_CGROUPS_RESP_HDR_SIZE +
691 - (size_t)b->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
692 -
693 - /* Read the first directory entry to find where packed data actually begins */
694 - nipc_batch_entry_t first_entry;
695 - memcpy(&first_entry, p + NIPC_CGROUPS_RESP_HDR_SIZE, sizeof(first_entry));
696 - uint32_t first_item_abs = first_entry.offset;
697 -
698 - /* Guard against underflow if builder state is inconsistent */
699 - if (b->data_offset < first_item_abs) {
700 - hdr.item_count = 0;
701 - memcpy(p, &hdr, sizeof(hdr));
702 - return NIPC_CGROUPS_RESP_HDR_SIZE;
703 - }
704 -
705 - size_t packed_data_len = b->data_offset - first_item_abs;
706 -
707 - if (final_packed_start < first_item_abs) {
708 - memmove(p + final_packed_start, p + first_item_abs, packed_data_len);
709 - }
710 -
711 - /* Convert directory entries from absolute offsets to relative offsets */
712 - size_t dir_base = NIPC_CGROUPS_RESP_HDR_SIZE;
713 - for (uint32_t i = 0; i < b->item_count; i++) {
714 - size_t entry_pos = dir_base + (size_t)i * NIPC_CGROUPS_DIR_ENTRY_SIZE;
715 - nipc_batch_entry_t entry;
716 - memcpy(&entry, p + entry_pos, sizeof(entry));
717 - if (entry.offset < first_item_abs)
718 - continue; /* skip corrupted entry */
719 - entry.offset -= first_item_abs;
720 - memcpy(p + entry_pos, &entry, sizeof(entry));
721 - }
722 -
723 - /* Write snapshot header */
724 - memcpy(p, &hdr, sizeof(hdr));
725 -
726 - return final_packed_start + packed_data_len;
727 -}
728 -
729 -/* ------------------------------------------------------------------ */
730 -/* INCREMENT codec */
731 -/* ------------------------------------------------------------------ */
732 -
733 -size_t nipc_increment_encode(uint64_t value, void *buf, size_t buf_len) {
734 - if (buf_len < NIPC_INCREMENT_PAYLOAD_SIZE)
735 - return 0;
736 - memcpy(buf, &value, 8);
737 - return NIPC_INCREMENT_PAYLOAD_SIZE;
738 -}
739 -
740 -nipc_error_t nipc_increment_decode(const void *buf, size_t buf_len,
741 - uint64_t *value_out) {
742 - if (buf_len < NIPC_INCREMENT_PAYLOAD_SIZE)
743 - return NIPC_ERR_TRUNCATED;
744 - memcpy(value_out, buf, 8);
745 - return NIPC_OK;
746 -}
747 -
748 -/* ------------------------------------------------------------------ */
749 -/* STRING_REVERSE codec */
750 -/* ------------------------------------------------------------------ */
751 -
752 -size_t nipc_string_reverse_encode(const char *str, uint32_t str_len,
753 - void *buf, size_t buf_len) {
754 - /* Guard against size_t overflow only where uint32_t can exceed size_t. */
755 -#if SIZE_MAX <= UINT32_MAX
756 - if ((size_t)str_len > SIZE_MAX - (size_t)NIPC_STRING_REVERSE_HDR_SIZE - 1u)
757 - return 0;
758 -#endif
759 -
760 - size_t total = NIPC_STRING_REVERSE_HDR_SIZE + str_len + 1;
761 - if (buf_len < total)
762 - return 0;
763 -
764 - uint8_t *p = (uint8_t *)buf;
765 - uint32_t offset = NIPC_STRING_REVERSE_HDR_SIZE;
766 - memcpy(p + 0, &offset, 4);
767 - memcpy(p + 4, &str_len, 4);
768 - if (str_len > 0)
769 - memcpy(p + offset, str, str_len);
770 - p[offset + str_len] = '\0';
771 - return total;
772 -}
773 -
774 -nipc_error_t nipc_string_reverse_decode(const void *buf, size_t buf_len,
775 - nipc_string_reverse_view_t *view_out) {
776 - if (buf_len < NIPC_STRING_REVERSE_HDR_SIZE)
777 - return NIPC_ERR_TRUNCATED;
778 -
779 - const uint8_t *p = (const uint8_t *)buf;
780 - uint32_t str_offset, str_length;
781 - memcpy(&str_offset, p + 0, 4);
782 - memcpy(&str_length, p + 4, 4);
783 -
784 - if ((uint64_t)str_offset + str_length + 1 > buf_len)
785 - return NIPC_ERR_OUT_OF_BOUNDS;
786 -
787 - if (p[str_offset + str_length] != '\0')
788 - return NIPC_ERR_MISSING_NUL;
789 -
790 - view_out->str = (const char *)(p + str_offset);
791 - view_out->str_len = str_length;
792 - return NIPC_OK;
793 -}
794 -
795 -/* ------------------------------------------------------------------ */
796 -/* Server-side typed dispatch helpers */
797 -/* ------------------------------------------------------------------ */
798 -
799 -bool nipc_dispatch_increment(
800 - const uint8_t *req, size_t req_len,
801 - uint8_t *resp, size_t resp_size, size_t *resp_len,
802 - nipc_increment_handler_fn handler, void *user)
803 -{
804 - uint64_t value;
805 - if (nipc_increment_decode(req, req_len, &value) != NIPC_OK)
806 - return false;
807 -
808 - uint64_t result;
809 - if (!handler(user, value, &result))
810 - return false;
811 -
812 - *resp_len = nipc_increment_encode(result, resp, resp_size);
813 - return *resp_len > 0;
814 -}
815 -
816 -bool nipc_dispatch_string_reverse(
817 - const uint8_t *req, size_t req_len,
818 - uint8_t *resp, size_t resp_size, size_t *resp_len,
819 - nipc_string_reverse_handler_fn handler, void *user)
820 -{
821 - nipc_string_reverse_view_t view;
822 - if (nipc_string_reverse_decode(req, req_len, &view) != NIPC_OK)
823 - return false;
824 -
825 - /* Provide a scratch buffer for the handler's response string.
826 - * The handler writes the response string into it; we encode after. */
827 - uint32_t capacity = (resp_size > NIPC_STRING_REVERSE_HDR_SIZE + 1)
828 - ? (uint32_t)(resp_size - NIPC_STRING_REVERSE_HDR_SIZE - 1)
829 - : 0;
830 - char *scratch = (char *)(resp + NIPC_STRING_REVERSE_HDR_SIZE);
831 -
832 - uint32_t response_str_len = 0;
833 - if (!handler(user, view.str, view.str_len,
834 - scratch, capacity, &response_str_len))
835 - return false;
836 -
837 - /* Encode from the scratch area (already at the right offset) */
838 - *resp_len = nipc_string_reverse_encode(scratch, response_str_len,
839 - resp, resp_size);
840 - return *resp_len > 0;
841 -}
842 -
843 -nipc_error_t nipc_dispatch_cgroups_snapshot(
844 - const uint8_t *req, size_t req_len,
845 - uint8_t *resp, size_t resp_size, size_t *resp_len,
846 - uint32_t max_items,
847 - nipc_cgroups_handler_fn handler, void *user)
848 -{
849 - nipc_cgroups_req_t request;
850 - nipc_error_t err = nipc_cgroups_req_decode(req, req_len, &request);
851 - if (err != NIPC_OK)
852 - return err;
853 -
854 - nipc_cgroups_builder_t builder;
855 - nipc_cgroups_builder_init(&builder, resp, resp_size, max_items, 0, 0);
856 -
857 - if (!handler(user, &request, &builder)) {
858 - if (builder.error != NIPC_OK)
859 - return builder.error;
860 - return NIPC_ERR_HANDLER_FAILED;
861 - }
862 -
863 - if (builder.error != NIPC_OK)
864 - return builder.error;
865 -
866 - *resp_len = nipc_cgroups_builder_finish(&builder);
867 - return (*resp_len > 0) ? NIPC_OK : NIPC_ERR_OVERFLOW;
868 -}
src/libnetdata/netipc/src/protocol/netipc_protocol_apps_lookup.c new
+547
@@ -0,0 +1,547 @@
1 +#include "netipc_protocol_apps_lookup_internal.h"
2 +
3 +_Static_assert(sizeof(nipc_apps_lookup_key_wire_t) == NIPC_APPS_LOOKUP_KEY_SIZE,
4 + "apps lookup key must be 8 bytes");
5 +_Static_assert(offsetof(nipc_apps_lookup_key_wire_t, pid) == 0, "");
6 +_Static_assert(offsetof(nipc_apps_lookup_key_wire_t, reserved) == 4, "");
7 +
8 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, layout_version) == 0, "");
9 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, status) == 2, "");
10 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, orchestrator) == 4, "");
11 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, cgroup_status) == 6, "");
12 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, pid) == 8, "");
13 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, ppid) == 12, "");
14 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, uid) == 16, "");
15 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, reserved0) == 20, "");
16 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, starttime) == 24, "");
17 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, comm_offset) == 32, "");
18 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, comm_length) == 36, "");
19 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, cgroup_path_offset) == 40, "");
20 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, cgroup_path_length) == 44, "");
21 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, cgroup_name_offset) == 48, "");
22 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, cgroup_name_length) == 52, "");
23 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, label_count) == 56, "");
24 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, reserved1) == 58, "");
25 +_Static_assert(offsetof(nipc_apps_lookup_item_wire_t, reserved1) +
26 + sizeof(((nipc_apps_lookup_item_wire_t *)0)->reserved1) ==
27 + NIPC_APPS_LOOKUP_ITEM_HDR_SIZE,
28 + "apps lookup fixed wire header must end at byte 60");
29 +_Static_assert(sizeof(nipc_apps_lookup_item_wire_t) >= NIPC_APPS_LOOKUP_ITEM_HDR_SIZE,
30 + "apps lookup C struct must cover the fixed wire header");
31 +
32 +static nipc_error_t apps_lookup_validate_domains(uint16_t status,
33 + uint16_t cgroup_status,
34 + uint64_t comm_len) {
35 + if (status != NIPC_PID_LOOKUP_KNOWN && status != NIPC_PID_LOOKUP_UNKNOWN)
36 + return NIPC_ERR_BAD_LAYOUT;
37 + if (cgroup_status != NIPC_APPS_CGROUP_KNOWN &&
38 + cgroup_status != NIPC_APPS_CGROUP_UNKNOWN_RETRY_LATER &&
39 + cgroup_status != NIPC_APPS_CGROUP_UNKNOWN_PERMANENT &&
40 + cgroup_status != NIPC_APPS_CGROUP_HOST_ROOT)
41 + return NIPC_ERR_BAD_LAYOUT;
42 + if (comm_len > 15)
43 + return NIPC_ERR_BAD_LAYOUT;
44 + return NIPC_OK;
45 +}
46 +
47 +static nipc_error_t
48 +apps_lookup_validate_unknown(uint16_t orchestrator, uint16_t cgroup_status,
49 + uint32_t ppid, uint32_t uid, uint64_t starttime,
50 + uint64_t comm_len, uint64_t cgroup_path_len,
51 + uint64_t cgroup_name_len, uint64_t label_count) {
52 + if (orchestrator != 0 || cgroup_status != 0 || ppid != 0 ||
53 + uid != NIPC_UID_UNSET || starttime != 0 || comm_len != 0 ||
54 + cgroup_path_len != 0 || cgroup_name_len != 0 || label_count != 0)
55 + return NIPC_ERR_BAD_LAYOUT;
56 + return NIPC_OK;
57 +}
58 +
59 +static nipc_error_t
60 +apps_lookup_validate_known(uint16_t cgroup_status, uint16_t orchestrator,
61 + uint64_t comm_len, uint64_t cgroup_path_len,
62 + uint64_t cgroup_name_len, uint64_t label_count) {
63 + if (comm_len == 0)
64 + return NIPC_ERR_BAD_LAYOUT;
65 +
66 + switch (cgroup_status) {
67 + case NIPC_APPS_CGROUP_KNOWN:
68 + if (cgroup_path_len == 0)
69 + return NIPC_ERR_BAD_LAYOUT;
70 + break;
71 + case NIPC_APPS_CGROUP_UNKNOWN_RETRY_LATER:
72 + if (orchestrator != 0 || cgroup_name_len != 0 || label_count != 0)
73 + return NIPC_ERR_BAD_LAYOUT;
74 + break;
75 + case NIPC_APPS_CGROUP_UNKNOWN_PERMANENT:
76 + if (cgroup_path_len == 0 || orchestrator != 0 || cgroup_name_len != 0 ||
77 + label_count != 0)
78 + return NIPC_ERR_BAD_LAYOUT;
79 + break;
80 + case NIPC_APPS_CGROUP_HOST_ROOT:
81 + if (orchestrator != 0 || cgroup_path_len != 0 || cgroup_name_len != 0 ||
82 + label_count != 0)
83 + return NIPC_ERR_BAD_LAYOUT;
84 + break;
85 + default:
86 + return NIPC_ERR_BAD_LAYOUT;
87 + }
88 + return NIPC_OK;
89 +}
90 +
91 +static nipc_error_t apps_lookup_validate_semantics(
92 + uint16_t status, uint16_t cgroup_status, uint16_t orchestrator,
93 + uint32_t ppid, uint32_t uid, uint64_t starttime, uint64_t comm_len,
94 + uint64_t cgroup_path_len, uint64_t cgroup_name_len, uint64_t label_count) {
95 + nipc_error_t err =
96 + apps_lookup_validate_domains(status, cgroup_status, comm_len);
97 + if (err != NIPC_OK)
98 + return err;
99 + if (status == NIPC_PID_LOOKUP_UNKNOWN)
100 + return apps_lookup_validate_unknown(orchestrator, cgroup_status, ppid, uid,
101 + starttime, comm_len, cgroup_path_len,
102 + cgroup_name_len, label_count);
103 + return apps_lookup_validate_known(cgroup_status, orchestrator, comm_len,
104 + cgroup_path_len, cgroup_name_len,
105 + label_count);
106 +}
107 +
108 +/* ------------------------------------------------------------------ */
109 +/* Apps lookup request */
110 +/* ------------------------------------------------------------------ */
111 +
112 +size_t nipc_apps_lookup_req_encode(const uint32_t *pids, uint32_t item_count,
113 + void *buf, size_t buf_len) {
114 + if (mul_would_overflow((size_t)item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
115 + return 0;
116 + if (mul_would_overflow((size_t)item_count, NIPC_APPS_LOOKUP_KEY_SIZE))
117 + return 0;
118 +
119 + size_t dir_size = (size_t)item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
120 + size_t key_size = (size_t)item_count * NIPC_APPS_LOOKUP_KEY_SIZE;
121 +#if SIZE_MAX <= UINT32_MAX
122 + if (dir_size > SIZE_MAX - NIPC_APPS_LOOKUP_REQ_HDR_SIZE)
123 + return 0;
124 +#endif
125 + size_t packed_start = NIPC_APPS_LOOKUP_REQ_HDR_SIZE + dir_size;
126 +#if SIZE_MAX <= UINT32_MAX
127 + if (key_size > SIZE_MAX - packed_start)
128 + return 0;
129 +#endif
130 + if (buf_len < packed_start + key_size)
131 + return 0;
132 + if (item_count > 0 && !pids)
133 + return 0;
134 +
135 + uint8_t *p = (uint8_t *)buf;
136 + for (uint32_t i = 0; i < item_count; i++) {
137 + uint64_t key_offset = (uint64_t)i * NIPC_APPS_LOOKUP_KEY_SIZE;
138 + if (key_offset > UINT32_MAX)
139 + return 0;
140 + nipc_lookup_dir_entry_t entry = {
141 + .offset = (uint32_t)key_offset,
142 + .length = NIPC_APPS_LOOKUP_KEY_SIZE,
143 + };
144 + nipc_apps_lookup_key_wire_t key = {
145 + .pid = pids[i],
146 + .reserved = 0,
147 + };
148 + memcpy(p + NIPC_APPS_LOOKUP_REQ_HDR_SIZE +
149 + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE,
150 + &entry, sizeof(entry));
151 + memcpy(p + packed_start + (size_t)i * NIPC_APPS_LOOKUP_KEY_SIZE, &key,
152 + sizeof(key));
153 + }
154 +
155 + nipc_lookup_req_header_wire_t hdr = {
156 + .layout_version = 1,
157 + .flags = 0,
158 + .item_count = item_count,
159 + .reserved0 = 0,
160 + .reserved1 = 0,
161 + };
162 + memcpy(p, &hdr, sizeof(hdr));
163 + return packed_start + key_size;
164 +}
165 +
166 +nipc_error_t nipc_apps_lookup_req_decode(const void *buf, size_t buf_len,
167 + nipc_apps_lookup_req_view_t *out) {
168 + if (buf_len < NIPC_APPS_LOOKUP_REQ_HDR_SIZE)
169 + return NIPC_ERR_TRUNCATED;
170 +
171 + nipc_lookup_req_header_wire_t hdr;
172 + memcpy(&hdr, buf, sizeof(hdr));
173 + if (hdr.layout_version != 1 || hdr.flags != 0 || hdr.reserved0 != 0 ||
174 + hdr.reserved1 != 0)
175 + return NIPC_ERR_BAD_LAYOUT;
176 +
177 + if (mul_would_overflow((size_t)hdr.item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE) ||
178 + mul_would_overflow((size_t)hdr.item_count, NIPC_APPS_LOOKUP_KEY_SIZE))
179 + return NIPC_ERR_BAD_ITEM_COUNT;
180 +
181 + size_t dir_size = (size_t)hdr.item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
182 + size_t dir_end = NIPC_APPS_LOOKUP_REQ_HDR_SIZE + dir_size;
183 + if (dir_end > buf_len)
184 + return NIPC_ERR_TRUNCATED;
185 + size_t packed_area_len = buf_len - dir_end;
186 + if (packed_area_len > UINT32_MAX)
187 + return NIPC_ERR_BAD_ITEM_COUNT;
188 +
189 + const uint8_t *p = (const uint8_t *)buf;
190 + const uint8_t *dir = p + NIPC_APPS_LOOKUP_REQ_HDR_SIZE;
191 + nipc_error_t err = nipc_lookup_validate_ordered_dir(
192 + dir, hdr.item_count, (uint32_t)packed_area_len, 0, true,
193 + NIPC_APPS_LOOKUP_KEY_SIZE);
194 + if (err != NIPC_OK)
195 + return err;
196 +
197 + const uint8_t *packed = p + dir_end;
198 + for (uint32_t i = 0; i < hdr.item_count; i++) {
199 + nipc_lookup_dir_entry_t entry;
200 + memcpy(&entry, dir + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE, sizeof(entry));
201 + nipc_apps_lookup_key_wire_t key;
202 + memcpy(&key, packed + entry.offset, sizeof(key));
203 + if (key.reserved != 0)
204 + return NIPC_ERR_BAD_LAYOUT;
205 + }
206 +
207 + out->item_count = hdr.item_count;
208 + out->_payload = p;
209 + out->_payload_len = buf_len;
210 + return NIPC_OK;
211 +}
212 +
213 +nipc_error_t nipc_apps_lookup_req_item(const nipc_apps_lookup_req_view_t *view,
214 + uint32_t index,
215 + nipc_apps_lookup_req_item_t *out) {
216 + if (index >= view->item_count)
217 + return NIPC_ERR_OUT_OF_BOUNDS;
218 +
219 + /* Decode validates this, but item accessors are public and may be
220 + * called with manually constructed views. */
221 + if (mul_would_overflow((size_t)view->item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
222 + return NIPC_ERR_BAD_ITEM_COUNT;
223 +
224 + size_t dir_size = (size_t)view->item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
225 + size_t dir_end = NIPC_APPS_LOOKUP_REQ_HDR_SIZE + dir_size;
226 + const uint8_t *dir = view->_payload + NIPC_APPS_LOOKUP_REQ_HDR_SIZE;
227 + const uint8_t *packed = view->_payload + dir_end;
228 +
229 + nipc_lookup_dir_entry_t entry;
230 + memcpy(&entry, dir + (size_t)index * NIPC_LOOKUP_DIR_ENTRY_SIZE,
231 + sizeof(entry));
232 + nipc_apps_lookup_key_wire_t key;
233 + memcpy(&key, packed + entry.offset, sizeof(key));
234 + out->pid = key.pid;
235 + return NIPC_OK;
236 +}
237 +
238 +/* ------------------------------------------------------------------ */
239 +/* Apps lookup response */
240 +/* ------------------------------------------------------------------ */
241 +
242 +static nipc_error_t
243 +apps_lookup_decode_item_bytes(const uint8_t *item, uint32_t item_len,
244 + nipc_apps_lookup_item_view_t *out) {
245 + if (item_len < NIPC_APPS_LOOKUP_ITEM_HDR_SIZE)
246 + return NIPC_ERR_TRUNCATED;
247 +
248 + nipc_apps_lookup_item_wire_t wire;
249 + memcpy(&wire, item, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE);
250 +
251 + if (wire.layout_version != 1 || wire.reserved0 != 0 || wire.reserved1 != 0)
252 + return NIPC_ERR_BAD_LAYOUT;
253 + nipc_error_t err = apps_lookup_validate_semantics(
254 + wire.status, wire.cgroup_status, wire.orchestrator, wire.ppid, wire.uid,
255 + wire.starttime, wire.comm_length, wire.cgroup_path_length,
256 + wire.cgroup_name_length, wire.label_count);
257 + if (err != NIPC_OK)
258 + return err;
259 +
260 + nipc_str_view_t comm, cgroup_path, cgroup_name;
261 + uint64_t comm_end, path_end, name_end;
262 + err = nipc_lookup_string_view(item, item_len, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE,
263 + wire.comm_offset, wire.comm_length, &comm,
264 + &comm_end);
265 + if (err != NIPC_OK)
266 + return err;
267 + err = nipc_lookup_string_view(
268 + item, item_len, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, wire.cgroup_path_offset,
269 + wire.cgroup_path_length, &cgroup_path, &path_end);
270 + if (err != NIPC_OK)
271 + return err;
272 + err = nipc_lookup_string_view(
273 + item, item_len, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, wire.cgroup_name_offset,
274 + wire.cgroup_name_length, &cgroup_name, &name_end);
275 + if (err != NIPC_OK)
276 + return err;
277 +
278 + if (nipc_lookup_ranges_overlap_u64(wire.comm_offset, comm_end,
279 + wire.cgroup_path_offset, path_end) ||
280 + nipc_lookup_ranges_overlap_u64(wire.comm_offset, comm_end,
281 + wire.cgroup_name_offset, name_end) ||
282 + nipc_lookup_ranges_overlap_u64(wire.cgroup_path_offset, path_end,
283 + wire.cgroup_name_offset, name_end))
284 + return NIPC_ERR_BAD_LAYOUT;
285 +
286 + uint64_t fixed_end = comm_end;
287 + if (path_end > fixed_end)
288 + fixed_end = path_end;
289 + if (name_end > fixed_end)
290 + fixed_end = name_end;
291 + uint32_t label_table_offset = 0;
292 + err = nipc_lookup_validate_labels(
293 + item, item_len, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, wire.label_count,
294 + fixed_end, &label_table_offset);
295 + if (err != NIPC_OK)
296 + return err;
297 +
298 + if (out) {
299 + out->status = wire.status;
300 + out->orchestrator = wire.orchestrator;
301 + out->cgroup_status = wire.cgroup_status;
302 + out->pid = wire.pid;
303 + out->ppid = wire.ppid;
304 + out->uid = wire.uid;
305 + out->starttime = wire.starttime;
306 + out->comm = comm;
307 + out->cgroup_path = cgroup_path;
308 + out->cgroup_name = cgroup_name;
309 + out->label_count = wire.label_count;
310 + out->_item = item;
311 + out->_item_len = item_len;
312 + out->_label_table_offset = label_table_offset;
313 + }
314 + return NIPC_OK;
315 +}
316 +
317 +nipc_error_t nipc_apps_lookup_resp_decode(const void *buf, size_t buf_len,
318 + nipc_apps_lookup_resp_view_t *out) {
319 + if (buf_len < NIPC_APPS_LOOKUP_RESP_HDR_SIZE)
320 + return NIPC_ERR_TRUNCATED;
321 +
322 + nipc_lookup_resp_header_wire_t hdr;
323 + memcpy(&hdr, buf, sizeof(hdr));
324 + if (hdr.layout_version != 1 || hdr.flags != 0)
325 + return NIPC_ERR_BAD_LAYOUT;
326 +
327 + if (mul_would_overflow((size_t)hdr.item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
328 + return NIPC_ERR_BAD_ITEM_COUNT;
329 + size_t dir_size = (size_t)hdr.item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
330 + size_t dir_end = NIPC_APPS_LOOKUP_RESP_HDR_SIZE + dir_size;
331 + if (dir_end > buf_len)
332 + return NIPC_ERR_TRUNCATED;
333 + size_t packed_area_len = buf_len - dir_end;
334 + if (packed_area_len > UINT32_MAX)
335 + return NIPC_ERR_BAD_ITEM_COUNT;
336 +
337 + const uint8_t *p = (const uint8_t *)buf;
338 + const uint8_t *dir = p + NIPC_APPS_LOOKUP_RESP_HDR_SIZE;
339 + nipc_error_t err = nipc_lookup_validate_ordered_dir(
340 + dir, hdr.item_count, (uint32_t)packed_area_len,
341 + NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, false, 0);
342 + if (err != NIPC_OK)
343 + return err;
344 +
345 + const uint8_t *packed = p + dir_end;
346 + for (uint32_t i = 0; i < hdr.item_count; i++) {
347 + nipc_lookup_dir_entry_t entry;
348 + memcpy(&entry, dir + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE, sizeof(entry));
349 + err = apps_lookup_decode_item_bytes(packed + entry.offset, entry.length,
350 + NULL);
351 + if (err != NIPC_OK)
352 + return err;
353 + }
354 +
355 + out->layout_version = hdr.layout_version;
356 + out->flags = hdr.flags;
357 + out->item_count = hdr.item_count;
358 + out->generation = hdr.generation;
359 + out->_payload = p;
360 + out->_payload_len = buf_len;
361 + return NIPC_OK;
362 +}
363 +
364 +nipc_error_t
365 +nipc_apps_lookup_resp_item(const nipc_apps_lookup_resp_view_t *view,
366 + uint32_t index, nipc_apps_lookup_item_view_t *out) {
367 + if (index >= view->item_count)
368 + return NIPC_ERR_OUT_OF_BOUNDS;
369 +
370 + /* Decode validates this, but item accessors are public and may be
371 + * called with manually constructed views. */
372 + if (mul_would_overflow((size_t)view->item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
373 + return NIPC_ERR_BAD_ITEM_COUNT;
374 +
375 + size_t dir_size = (size_t)view->item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
376 + size_t dir_end = NIPC_APPS_LOOKUP_RESP_HDR_SIZE + dir_size;
377 + const uint8_t *dir = view->_payload + NIPC_APPS_LOOKUP_RESP_HDR_SIZE;
378 + const uint8_t *packed = view->_payload + dir_end;
379 + nipc_lookup_dir_entry_t entry;
380 + memcpy(&entry, dir + (size_t)index * NIPC_LOOKUP_DIR_ENTRY_SIZE,
381 + sizeof(entry));
382 + return apps_lookup_decode_item_bytes(packed + entry.offset, entry.length,
383 + out);
384 +}
385 +
386 +nipc_error_t
387 +nipc_apps_lookup_item_label(const nipc_apps_lookup_item_view_t *item,
388 + uint32_t index, nipc_lookup_label_view_t *out) {
389 + return nipc_lookup_label_at(item->_item, item->_item_len,
390 + NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, item->label_count,
391 + item->_label_table_offset, index, out);
392 +}
393 +
394 +void nipc_apps_lookup_builder_init(nipc_apps_lookup_builder_t *b, void *buf,
395 + size_t buf_len, uint32_t max_items,
396 + uint64_t generation) {
397 + b->buf = (uint8_t *)buf;
398 + b->buf_len = buf_len;
399 + b->generation = generation;
400 + b->item_count = 0;
401 + b->max_items = max_items;
402 + b->error = NIPC_OK;
403 + if (mul_would_overflow((size_t)max_items, NIPC_LOOKUP_DIR_ENTRY_SIZE)) {
404 + b->data_offset = SIZE_MAX;
405 + } else {
406 + size_t dir_size = (size_t)max_items * NIPC_LOOKUP_DIR_ENTRY_SIZE;
407 +#if SIZE_MAX <= UINT32_MAX
408 + if (dir_size > SIZE_MAX - NIPC_APPS_LOOKUP_RESP_HDR_SIZE) {
409 + b->data_offset = SIZE_MAX;
410 + } else
411 +#endif
412 + {
413 + b->data_offset = NIPC_APPS_LOOKUP_RESP_HDR_SIZE + dir_size;
414 + }
415 + }
416 +}
417 +
418 +void nipc_apps_lookup_builder_set_generation(nipc_apps_lookup_builder_t *b,
419 + uint64_t generation) {
420 + b->generation = generation;
421 +}
422 +
423 +uint32_t nipc_apps_lookup_builder_estimate_max_items(size_t buf_len) {
424 + if (buf_len <= NIPC_APPS_LOOKUP_RESP_HDR_SIZE)
425 + return 0;
426 + size_t min_item = nipc_align8(NIPC_APPS_LOOKUP_ITEM_HDR_SIZE + 3u);
427 + return (uint32_t)((buf_len - NIPC_APPS_LOOKUP_RESP_HDR_SIZE) /
428 + (NIPC_LOOKUP_DIR_ENTRY_SIZE + min_item));
429 +}
430 +
431 +nipc_error_t nipc_apps_lookup_builder_add(
432 + nipc_apps_lookup_builder_t *b, uint16_t status, uint16_t cgroup_status,
433 + uint16_t orchestrator, uint32_t pid, uint32_t ppid, uint32_t uid,
434 + uint64_t starttime, const char *comm, uint32_t comm_len,
435 + const char *cgroup_path, uint32_t cgroup_path_len, const char *cgroup_name,
436 + uint32_t cgroup_name_len, const nipc_lookup_label_view_t *labels,
437 + uint16_t label_count) {
438 + if (b->item_count >= b->max_items) {
439 + b->error = NIPC_ERR_OVERFLOW;
440 + return b->error;
441 + }
442 + b->error = apps_lookup_validate_semantics(
443 + status, cgroup_status, orchestrator, ppid, uid, starttime, comm_len,
444 + cgroup_path_len, cgroup_name_len, label_count);
445 + if (b->error != NIPC_OK)
446 + return b->error;
447 +
448 + nipc_lookup_builder_string_t strings[] = {
449 + {
450 + .ptr = comm,
451 + .len = comm_len,
452 + .require_non_empty = status == NIPC_PID_LOOKUP_KNOWN,
453 + },
454 + {.ptr = cgroup_path, .len = cgroup_path_len, .require_non_empty = false},
455 + {.ptr = cgroup_name, .len = cgroup_name_len, .require_non_empty = false},
456 + };
457 + b->error = nipc_lookup_builder_validate_strings(strings, 3);
458 + if (b->error != NIPC_OK)
459 + return b->error;
460 +
461 + nipc_lookup_builder_item_layout_t layout = {0};
462 + b->error = nipc_lookup_builder_layout_item(
463 + b->data_offset, b->buf_len, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE, strings, 3,
464 + labels, label_count, &layout);
465 + if (b->error != NIPC_OK)
466 + return b->error;
467 +
468 + size_t item_start = (size_t)layout.item_start;
469 + size_t item_size = (size_t)layout.item_size;
470 +
471 + if (item_start > b->data_offset)
472 + memset(b->buf + b->data_offset, 0, item_start - b->data_offset);
473 +
474 + uint8_t *item = b->buf + item_start;
475 + nipc_apps_lookup_item_wire_t wire = {
476 + .layout_version = 1,
477 + .status = status,
478 + .orchestrator = orchestrator,
479 + .cgroup_status = cgroup_status,
480 + .pid = pid,
481 + .ppid = ppid,
482 + .uid = uid,
483 + .reserved0 = 0,
484 + .starttime = starttime,
485 + .comm_offset = (uint32_t)strings[0].offset,
486 + .comm_length = comm_len,
487 + .cgroup_path_offset = (uint32_t)strings[1].offset,
488 + .cgroup_path_length = cgroup_path_len,
489 + .cgroup_name_offset = (uint32_t)strings[2].offset,
490 + .cgroup_name_length = cgroup_name_len,
491 + .label_count = label_count,
492 + .reserved1 = 0,
493 + };
494 + memcpy(item, &wire, NIPC_APPS_LOOKUP_ITEM_HDR_SIZE);
495 + nipc_lookup_builder_write_strings(item, strings, 3);
496 +
497 + if (label_count > 0) {
498 + size_t fixed_end = (size_t)layout.fixed_end;
499 + size_t table_start = (size_t)layout.table_start;
500 + if (table_start > fixed_end)
501 + memset(item + fixed_end, 0, table_start - fixed_end);
502 + nipc_lookup_write_labels(item, table_start, (size_t)layout.table_bytes,
503 + labels, label_count);
504 + }
505 +
506 + nipc_lookup_builder_write_dir_entry(b->buf, NIPC_APPS_LOOKUP_RESP_HDR_SIZE,
507 + b->item_count, item_start, item_size);
508 +
509 + b->data_offset = item_start + item_size;
510 + b->item_count++;
511 + return NIPC_OK;
512 +}
513 +
514 +size_t nipc_apps_lookup_builder_finish(nipc_apps_lookup_builder_t *b) {
515 + return nipc_lookup_finish_common(
516 + b->buf, b->buf_len, b->item_count, b->data_offset,
517 + NIPC_APPS_LOOKUP_RESP_HDR_SIZE, b->generation);
518 +}
519 +
520 +nipc_error_t nipc_dispatch_apps_lookup(
521 + const uint8_t *req, size_t req_len,
522 + uint8_t *resp, size_t resp_size, size_t *resp_len,
523 + nipc_apps_lookup_handler_fn handler, void *user)
524 +{
525 + nipc_apps_lookup_req_view_t request;
526 + nipc_error_t err = nipc_apps_lookup_req_decode(req, req_len, &request);
527 + if (err != NIPC_OK)
528 + return err;
529 +
530 + nipc_apps_lookup_builder_t builder;
531 + nipc_apps_lookup_builder_init(&builder, resp, resp_size,
532 + request.item_count, 0);
533 +
534 + if (!handler(user, &request, &builder)) {
535 + if (builder.error != NIPC_OK)
536 + return builder.error;
537 + return NIPC_ERR_HANDLER_FAILED;
538 + }
539 +
540 + if (builder.error != NIPC_OK)
541 + return builder.error;
542 + if (builder.item_count != request.item_count)
543 + return NIPC_ERR_BAD_ITEM_COUNT;
544 +
545 + *resp_len = nipc_apps_lookup_builder_finish(&builder);
546 + return (*resp_len > 0) ? NIPC_OK : NIPC_ERR_OVERFLOW;
547 +}
src/libnetdata/netipc/src/protocol/netipc_protocol_apps_lookup_internal.h new
+31
@@ -0,0 +1,31 @@
1 +#ifndef NETIPC_PROTOCOL_APPS_LOOKUP_INTERNAL_H
2 +#define NETIPC_PROTOCOL_APPS_LOOKUP_INTERNAL_H
3 +
4 +#include "netipc_protocol_lookup_common.h"
5 +
6 +typedef struct {
7 + uint32_t pid;
8 + uint32_t reserved;
9 +} nipc_apps_lookup_key_wire_t;
10 +
11 +typedef struct {
12 + uint16_t layout_version;
13 + uint16_t status;
14 + uint16_t orchestrator;
15 + uint16_t cgroup_status;
16 + uint32_t pid;
17 + uint32_t ppid;
18 + uint32_t uid;
19 + uint32_t reserved0;
20 + uint64_t starttime;
21 + uint32_t comm_offset;
22 + uint32_t comm_length;
23 + uint32_t cgroup_path_offset;
24 + uint32_t cgroup_path_length;
25 + uint32_t cgroup_name_offset;
26 + uint32_t cgroup_name_length;
27 + uint16_t label_count;
28 + uint16_t reserved1;
29 +} nipc_apps_lookup_item_wire_t;
30 +
31 +#endif /* NETIPC_PROTOCOL_APPS_LOOKUP_INTERNAL_H */
src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_lookup.c new
+450
@@ -0,0 +1,450 @@
1 +#include "netipc_protocol_cgroups_lookup_internal.h"
2 +
3 +_Static_assert(sizeof(nipc_cgroups_lookup_item_wire_t) == NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE,
4 + "cgroups lookup item header must be 28 bytes");
5 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, layout_version) == 0, "");
6 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, status) == 2, "");
7 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, orchestrator) == 4, "");
8 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, reserved0) == 6, "");
9 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, path_offset) == 8, "");
10 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, path_length) == 12, "");
11 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, name_offset) == 16, "");
12 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, name_length) == 20, "");
13 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, label_count) == 24, "");
14 +_Static_assert(offsetof(nipc_cgroups_lookup_item_wire_t, reserved1) == 26, "");
15 +
16 +static nipc_error_t cgroups_lookup_validate_semantics(uint16_t status,
17 + uint16_t orchestrator,
18 + uint64_t path_len,
19 + uint64_t name_len,
20 + uint64_t label_count) {
21 + if (status != NIPC_CGROUP_LOOKUP_KNOWN &&
22 + status != NIPC_CGROUP_LOOKUP_UNKNOWN_RETRY_LATER &&
23 + status != NIPC_CGROUP_LOOKUP_UNKNOWN_PERMANENT)
24 + return NIPC_ERR_BAD_LAYOUT;
25 + if (path_len == 0)
26 + return NIPC_ERR_BAD_LAYOUT;
27 + if (status != NIPC_CGROUP_LOOKUP_KNOWN &&
28 + (orchestrator != 0 || name_len != 0 || label_count != 0))
29 + return NIPC_ERR_BAD_LAYOUT;
30 + return NIPC_OK;
31 +}
32 +
33 +/* ------------------------------------------------------------------ */
34 +/* Cgroups lookup request */
35 +/* ------------------------------------------------------------------ */
36 +
37 +size_t nipc_cgroups_lookup_req_encode(const nipc_str_view_t *paths,
38 + uint32_t item_count, void *buf,
39 + size_t buf_len) {
40 + if (mul_would_overflow((size_t)item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
41 + return 0;
42 +
43 + size_t dir_size = (size_t)item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
44 +#if SIZE_MAX <= UINT32_MAX
45 + if (dir_size > SIZE_MAX - NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE)
46 + return 0;
47 +#endif
48 + size_t packed_start = NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE + dir_size;
49 + if (buf_len < packed_start)
50 + return 0;
51 +
52 + uint8_t *p = (uint8_t *)buf;
53 + size_t data = packed_start;
54 +
55 + for (uint32_t i = 0; i < item_count; i++) {
56 + if (!paths ||
57 + nipc_lookup_source_string_invalid(paths[i].ptr, paths[i].len, true))
58 + return 0;
59 +
60 + size_t aligned = nipc_align8(data);
61 + uint64_t key_len_u64;
62 + if (nipc_lookup_add_u64_over_limit(paths[i].len, 1u, UINT32_MAX,
63 + &key_len_u64))
64 + return 0;
65 + size_t key_len = (size_t)key_len_u64;
66 + if (aligned < data || key_len > SIZE_MAX - aligned ||
67 + aligned + key_len > buf_len)
68 + return 0;
69 + size_t key_offset = aligned - packed_start;
70 + if (key_offset > UINT32_MAX || key_len > UINT32_MAX)
71 + return 0;
72 + if (aligned > data)
73 + memset(p + data, 0, aligned - data);
74 +
75 + nipc_lookup_dir_entry_t entry = {
76 + .offset = (uint32_t)key_offset,
77 + .length = (uint32_t)key_len,
78 + };
79 + memcpy(p + NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE +
80 + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE,
81 + &entry, sizeof(entry));
82 + memcpy(p + aligned, paths[i].ptr, paths[i].len);
83 + p[aligned + paths[i].len] = '\0';
84 + data = aligned + key_len;
85 + }
86 +
87 + nipc_lookup_req_header_wire_t hdr = {
88 + .layout_version = 1,
89 + .flags = 0,
90 + .item_count = item_count,
91 + .reserved0 = 0,
92 + .reserved1 = 0,
93 + };
94 + memcpy(p, &hdr, sizeof(hdr));
95 + return data;
96 +}
97 +
98 +nipc_error_t
99 +nipc_cgroups_lookup_req_decode(const void *buf, size_t buf_len,
100 + nipc_cgroups_lookup_req_view_t *out) {
101 + if (buf_len < NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE)
102 + return NIPC_ERR_TRUNCATED;
103 +
104 + nipc_lookup_req_header_wire_t hdr;
105 + memcpy(&hdr, buf, sizeof(hdr));
106 + if (hdr.layout_version != 1 || hdr.flags != 0 || hdr.reserved0 != 0 ||
107 + hdr.reserved1 != 0)
108 + return NIPC_ERR_BAD_LAYOUT;
109 +
110 + if (mul_would_overflow((size_t)hdr.item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
111 + return NIPC_ERR_BAD_ITEM_COUNT;
112 + size_t dir_size = (size_t)hdr.item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
113 + size_t dir_end = NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE + dir_size;
114 + if (dir_end > buf_len)
115 + return NIPC_ERR_TRUNCATED;
116 + size_t packed_area_len = buf_len - dir_end;
117 + if (packed_area_len > UINT32_MAX)
118 + return NIPC_ERR_BAD_ITEM_COUNT;
119 +
120 + const uint8_t *p = (const uint8_t *)buf;
121 + const uint8_t *dir = p + NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE;
122 + nipc_error_t err = nipc_lookup_validate_ordered_dir(
123 + dir, hdr.item_count, (uint32_t)packed_area_len, 2, false, 0);
124 + if (err != NIPC_OK)
125 + return err;
126 +
127 + const uint8_t *packed = p + dir_end;
128 + for (uint32_t i = 0; i < hdr.item_count; i++) {
129 + nipc_lookup_dir_entry_t entry;
130 + memcpy(&entry, dir + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE, sizeof(entry));
131 + const uint8_t *key = packed + entry.offset;
132 + if (key[entry.length - 1] != '\0')
133 + return NIPC_ERR_MISSING_NUL;
134 + if (nipc_lookup_bytes_have_nul(key, entry.length - 1))
135 + return NIPC_ERR_BAD_LAYOUT;
136 + }
137 +
138 + out->item_count = hdr.item_count;
139 + out->_payload = p;
140 + out->_payload_len = buf_len;
141 + return NIPC_OK;
142 +}
143 +
144 +nipc_error_t
145 +nipc_cgroups_lookup_req_item(const nipc_cgroups_lookup_req_view_t *view,
146 + uint32_t index,
147 + nipc_cgroups_lookup_req_item_t *out) {
148 + if (index >= view->item_count)
149 + return NIPC_ERR_OUT_OF_BOUNDS;
150 +
151 + /* Decode validates this, but item accessors are public and may be
152 + * called with manually constructed views. */
153 + if (mul_would_overflow((size_t)view->item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
154 + return NIPC_ERR_BAD_ITEM_COUNT;
155 +
156 + size_t dir_size = (size_t)view->item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
157 + size_t dir_end = NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE + dir_size;
158 + const uint8_t *dir = view->_payload + NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE;
159 + const uint8_t *packed = view->_payload + dir_end;
160 +
161 + nipc_lookup_dir_entry_t entry;
162 + memcpy(&entry, dir + (size_t)index * NIPC_LOOKUP_DIR_ENTRY_SIZE,
163 + sizeof(entry));
164 + out->path.ptr = (const char *)(packed + entry.offset);
165 + out->path.len = entry.length - 1;
166 + return NIPC_OK;
167 +}
168 +
169 +/* ------------------------------------------------------------------ */
170 +/* Cgroups lookup response */
171 +/* ------------------------------------------------------------------ */
172 +
173 +static nipc_error_t
174 +cgroups_lookup_decode_item_bytes(const uint8_t *item, uint32_t item_len,
175 + nipc_cgroups_lookup_item_view_t *out) {
176 + if (item_len < NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE)
177 + return NIPC_ERR_TRUNCATED;
178 +
179 + nipc_cgroups_lookup_item_wire_t wire;
180 + memcpy(&wire, item, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE);
181 +
182 + if (wire.layout_version != 1 || wire.reserved0 != 0 || wire.reserved1 != 0)
183 + return NIPC_ERR_BAD_LAYOUT;
184 +
185 + nipc_error_t err = cgroups_lookup_validate_semantics(
186 + wire.status, wire.orchestrator, wire.path_length, wire.name_length,
187 + wire.label_count);
188 + if (err != NIPC_OK)
189 + return err;
190 +
191 + nipc_str_view_t path, name;
192 + uint64_t path_end, name_end;
193 + err = nipc_lookup_string_view(
194 + item, item_len, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE, wire.path_offset,
195 + wire.path_length, &path, &path_end);
196 + if (err != NIPC_OK)
197 + return err;
198 + err = nipc_lookup_string_view(
199 + item, item_len, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE, wire.name_offset,
200 + wire.name_length, &name, &name_end);
201 + if (err != NIPC_OK)
202 + return err;
203 + if (nipc_lookup_ranges_overlap_u64(wire.path_offset, path_end,
204 + wire.name_offset, name_end))
205 + return NIPC_ERR_BAD_LAYOUT;
206 +
207 + uint64_t fixed_end = path_end > name_end ? path_end : name_end;
208 + uint32_t label_table_offset = 0;
209 + err = nipc_lookup_validate_labels(
210 + item, item_len, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE, wire.label_count,
211 + fixed_end, &label_table_offset);
212 + if (err != NIPC_OK)
213 + return err;
214 +
215 + if (out) {
216 + out->status = wire.status;
217 + out->orchestrator = wire.orchestrator;
218 + out->path = path;
219 + out->name = name;
220 + out->label_count = wire.label_count;
221 + out->_item = item;
222 + out->_item_len = item_len;
223 + out->_label_table_offset = label_table_offset;
224 + }
225 + return NIPC_OK;
226 +}
227 +
228 +nipc_error_t
229 +nipc_cgroups_lookup_resp_decode(const void *buf, size_t buf_len,
230 + nipc_cgroups_lookup_resp_view_t *out) {
231 + if (buf_len < NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE)
232 + return NIPC_ERR_TRUNCATED;
233 +
234 + nipc_lookup_resp_header_wire_t hdr;
235 + memcpy(&hdr, buf, sizeof(hdr));
236 + if (hdr.layout_version != 1 || hdr.flags != 0)
237 + return NIPC_ERR_BAD_LAYOUT;
238 +
239 + if (mul_would_overflow((size_t)hdr.item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
240 + return NIPC_ERR_BAD_ITEM_COUNT;
241 + size_t dir_size = (size_t)hdr.item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
242 + size_t dir_end = NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE + dir_size;
243 + if (dir_end > buf_len)
244 + return NIPC_ERR_TRUNCATED;
245 + size_t packed_area_len = buf_len - dir_end;
246 + if (packed_area_len > UINT32_MAX)
247 + return NIPC_ERR_BAD_ITEM_COUNT;
248 +
249 + const uint8_t *p = (const uint8_t *)buf;
250 + const uint8_t *dir = p + NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE;
251 + nipc_error_t err = nipc_lookup_validate_ordered_dir(
252 + dir, hdr.item_count, (uint32_t)packed_area_len,
253 + NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE, false, 0);
254 + if (err != NIPC_OK)
255 + return err;
256 +
257 + const uint8_t *packed = p + dir_end;
258 + for (uint32_t i = 0; i < hdr.item_count; i++) {
259 + nipc_lookup_dir_entry_t entry;
260 + memcpy(&entry, dir + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE, sizeof(entry));
261 + err = cgroups_lookup_decode_item_bytes(packed + entry.offset, entry.length,
262 + NULL);
263 + if (err != NIPC_OK)
264 + return err;
265 + }
266 +
267 + out->layout_version = hdr.layout_version;
268 + out->flags = hdr.flags;
269 + out->item_count = hdr.item_count;
270 + out->generation = hdr.generation;
271 + out->_payload = p;
272 + out->_payload_len = buf_len;
273 + return NIPC_OK;
274 +}
275 +
276 +nipc_error_t
277 +nipc_cgroups_lookup_resp_item(const nipc_cgroups_lookup_resp_view_t *view,
278 + uint32_t index,
279 + nipc_cgroups_lookup_item_view_t *out) {
280 + if (index >= view->item_count)
281 + return NIPC_ERR_OUT_OF_BOUNDS;
282 +
283 + /* Decode validates this, but item accessors are public and may be
284 + * called with manually constructed views. */
285 + if (mul_would_overflow((size_t)view->item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
286 + return NIPC_ERR_BAD_ITEM_COUNT;
287 +
288 + size_t dir_size = (size_t)view->item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
289 + size_t dir_end = NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE + dir_size;
290 + const uint8_t *dir = view->_payload + NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE;
291 + const uint8_t *packed = view->_payload + dir_end;
292 + nipc_lookup_dir_entry_t entry;
293 + memcpy(&entry, dir + (size_t)index * NIPC_LOOKUP_DIR_ENTRY_SIZE,
294 + sizeof(entry));
295 + return cgroups_lookup_decode_item_bytes(packed + entry.offset, entry.length,
296 + out);
297 +}
298 +
299 +nipc_error_t
300 +nipc_cgroups_lookup_item_label(const nipc_cgroups_lookup_item_view_t *item,
301 + uint32_t index, nipc_lookup_label_view_t *out) {
302 + return nipc_lookup_label_at(
303 + item->_item, item->_item_len, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE,
304 + item->label_count, item->_label_table_offset, index, out);
305 +}
306 +
307 +void nipc_cgroups_lookup_builder_init(nipc_cgroups_lookup_builder_t *b,
308 + void *buf, size_t buf_len,
309 + uint32_t max_items, uint64_t generation) {
310 + b->buf = (uint8_t *)buf;
311 + b->buf_len = buf_len;
312 + b->generation = generation;
313 + b->item_count = 0;
314 + b->max_items = max_items;
315 + b->error = NIPC_OK;
316 + if (mul_would_overflow((size_t)max_items, NIPC_LOOKUP_DIR_ENTRY_SIZE)) {
317 + b->data_offset = SIZE_MAX;
318 + } else {
319 + size_t dir_size = (size_t)max_items * NIPC_LOOKUP_DIR_ENTRY_SIZE;
320 +#if SIZE_MAX <= UINT32_MAX
321 + if (dir_size > SIZE_MAX - NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE) {
322 + b->data_offset = SIZE_MAX;
323 + } else
324 +#endif
325 + {
326 + b->data_offset = NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE + dir_size;
327 + }
328 + }
329 +}
330 +
331 +void nipc_cgroups_lookup_builder_set_generation(
332 + nipc_cgroups_lookup_builder_t *b, uint64_t generation) {
333 + b->generation = generation;
334 +}
335 +
336 +uint32_t nipc_cgroups_lookup_builder_estimate_max_items(size_t buf_len) {
337 + if (buf_len <= NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE)
338 + return 0;
339 + size_t min_item = nipc_align8(NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE + 2u + 1u);
340 + return (uint32_t)((buf_len - NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE) /
341 + (NIPC_LOOKUP_DIR_ENTRY_SIZE + min_item));
342 +}
343 +
344 +nipc_error_t nipc_cgroups_lookup_builder_add(
345 + nipc_cgroups_lookup_builder_t *b, uint16_t status, uint16_t orchestrator,
346 + const char *path, uint32_t path_len, const char *name, uint32_t name_len,
347 + const nipc_lookup_label_view_t *labels, uint16_t label_count) {
348 + if (b->item_count >= b->max_items) {
349 + b->error = NIPC_ERR_OVERFLOW;
350 + return b->error;
351 + }
352 + nipc_error_t err = cgroups_lookup_validate_semantics(
353 + status, orchestrator, path_len, name_len, label_count);
354 + if (err != NIPC_OK) {
355 + b->error = err;
356 + return b->error;
357 + }
358 +
359 + nipc_lookup_builder_string_t strings[] = {
360 + {.ptr = path, .len = path_len, .require_non_empty = true},
361 + {.ptr = name, .len = name_len, .require_non_empty = false},
362 + };
363 + err = nipc_lookup_builder_validate_strings(strings, 2);
364 + if (err != NIPC_OK) {
365 + b->error = err;
366 + return b->error;
367 + }
368 +
369 + nipc_lookup_builder_item_layout_t layout = {0};
370 + err = nipc_lookup_builder_layout_item(
371 + b->data_offset, b->buf_len, NIPC_CGROUPS_LOOKUP_ITEM_HDR_SIZE, strings, 2,
372 + labels, label_count, &layout);
373 + if (err != NIPC_OK) {
374 + b->error = err;
375 + return b->error;
376 + }
377 +
378 + size_t item_start = (size_t)layout.item_start;
379 + size_t item_size = (size_t)layout.item_size;
380 +
381 + if (item_start > b->data_offset)
382 + memset(b->buf + b->data_offset, 0, item_start - b->data_offset);
383 +
384 + uint8_t *item = b->buf + item_start;
385 + nipc_cgroups_lookup_item_wire_t wire = {
386 + .layout_version = 1,
387 + .status = status,
388 + .orchestrator = orchestrator,
389 + .reserved0 = 0,
390 + .path_offset = (uint32_t)strings[0].offset,
391 + .path_length = path_len,
392 + .name_offset = (uint32_t)strings[1].offset,
393 + .name_length = name_len,
394 + .label_count = label_count,
395 + .reserved1 = 0,
396 + };
397 + memcpy(item, &wire, sizeof(wire));
398 + nipc_lookup_builder_write_strings(item, strings, 2);
399 +
400 + if (label_count > 0) {
401 + size_t fixed_end = (size_t)layout.fixed_end;
402 + size_t table_start = (size_t)layout.table_start;
403 + if (table_start > fixed_end)
404 + memset(item + fixed_end, 0, table_start - fixed_end);
405 + nipc_lookup_write_labels(item, table_start, (size_t)layout.table_bytes,
406 + labels, label_count);
407 + }
408 +
409 + nipc_lookup_builder_write_dir_entry(b->buf, NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE,
410 + b->item_count, item_start, item_size);
411 +
412 + b->data_offset = item_start + item_size;
413 + b->item_count++;
414 + return NIPC_OK;
415 +}
416 +
417 +size_t nipc_cgroups_lookup_builder_finish(nipc_cgroups_lookup_builder_t *b) {
418 + return nipc_lookup_finish_common(
419 + b->buf, b->buf_len, b->item_count, b->data_offset,
420 + NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE, b->generation);
421 +}
422 +
423 +nipc_error_t nipc_dispatch_cgroups_lookup(
424 + const uint8_t *req, size_t req_len,
425 + uint8_t *resp, size_t resp_size, size_t *resp_len,
426 + nipc_cgroups_lookup_handler_fn handler, void *user)
427 +{
428 + nipc_cgroups_lookup_req_view_t request;
429 + nipc_error_t err = nipc_cgroups_lookup_req_decode(req, req_len, &request);
430 + if (err != NIPC_OK)
431 + return err;
432 +
433 + nipc_cgroups_lookup_builder_t builder;
434 + nipc_cgroups_lookup_builder_init(&builder, resp, resp_size,
435 + request.item_count, 0);
436 +
437 + if (!handler(user, &request, &builder)) {
438 + if (builder.error != NIPC_OK)
439 + return builder.error;
440 + return NIPC_ERR_HANDLER_FAILED;
441 + }
442 +
443 + if (builder.error != NIPC_OK)
444 + return builder.error;
445 + if (builder.item_count != request.item_count)
446 + return NIPC_ERR_BAD_ITEM_COUNT;
447 +
448 + *resp_len = nipc_cgroups_lookup_builder_finish(&builder);
449 + return (*resp_len > 0) ? NIPC_OK : NIPC_ERR_OVERFLOW;
450 +}
src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_lookup_internal.h new
+19
@@ -0,0 +1,19 @@
1 +#ifndef NETIPC_PROTOCOL_CGROUPS_LOOKUP_INTERNAL_H
2 +#define NETIPC_PROTOCOL_CGROUPS_LOOKUP_INTERNAL_H
3 +
4 +#include "netipc_protocol_lookup_common.h"
5 +
6 +typedef struct {
7 + uint16_t layout_version;
8 + uint16_t status;
9 + uint16_t orchestrator;
10 + uint16_t reserved0;
11 + uint32_t path_offset;
12 + uint32_t path_length;
13 + uint32_t name_offset;
14 + uint32_t name_length;
15 + uint16_t label_count;
16 + uint16_t reserved1;
17 +} nipc_cgroups_lookup_item_wire_t;
18 +
19 +#endif /* NETIPC_PROTOCOL_CGROUPS_LOOKUP_INTERNAL_H */
src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_snapshot.c new
+367
@@ -0,0 +1,367 @@
1 +#include "netipc_protocol_cgroups_snapshot_internal.h"
2 +
3 +/* Cgroups request (4 bytes) */
4 +_Static_assert(sizeof(nipc_cgroups_req_t) == 4,
5 + "nipc_cgroups_req_t must be 4 bytes");
6 +
7 +/* Cgroups snapshot response header (24 bytes) */
8 +_Static_assert(sizeof(nipc_cgroups_resp_header_t) == 24,
9 + "nipc_cgroups_resp_header_t must be 24 bytes");
10 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, layout_version) == 0, "");
11 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, flags) == 2, "");
12 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, item_count) == 4, "");
13 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, systemd_enabled) == 8, "");
14 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, reserved) == 12, "");
15 +_Static_assert(offsetof(nipc_cgroups_resp_header_t, generation) == 16, "");
16 +
17 +_Static_assert(sizeof(nipc_cgroups_item_wire_t) == 32,
18 + "nipc_cgroups_item_wire_t must be 32 bytes");
19 +
20 +/* ------------------------------------------------------------------ */
21 +/* Cgroups snapshot request (4 bytes) */
22 +/* ------------------------------------------------------------------ */
23 +
24 +size_t nipc_cgroups_req_encode(const nipc_cgroups_req_t *r,
25 + void *buf, size_t buf_len) {
26 + if (buf_len < sizeof(nipc_cgroups_req_t))
27 + return 0;
28 +
29 + memcpy(buf, r, sizeof(nipc_cgroups_req_t));
30 + return sizeof(nipc_cgroups_req_t);
31 +}
32 +
33 +nipc_error_t nipc_cgroups_req_decode(const void *buf, size_t buf_len,
34 + nipc_cgroups_req_t *out) {
35 + if (buf_len < sizeof(nipc_cgroups_req_t))
36 + return NIPC_ERR_TRUNCATED;
37 +
38 + memcpy(out, buf, sizeof(nipc_cgroups_req_t));
39 +
40 + if (out->layout_version != 1)
41 + return NIPC_ERR_BAD_LAYOUT;
42 + if (out->flags != 0)
43 + return NIPC_ERR_BAD_LAYOUT;
44 +
45 + return NIPC_OK;
46 +}
47 +
48 +/* ------------------------------------------------------------------ */
49 +/* Cgroups snapshot response decode */
50 +/* ------------------------------------------------------------------ */
51 +
52 +nipc_error_t nipc_cgroups_resp_decode(const void *buf, size_t buf_len,
53 + nipc_cgroups_resp_view_t *out) {
54 + if (buf_len < NIPC_CGROUPS_RESP_HDR_SIZE)
55 + return NIPC_ERR_TRUNCATED;
56 +
57 + nipc_cgroups_resp_header_t hdr;
58 + memcpy(&hdr, buf, sizeof(hdr));
59 +
60 + if (hdr.layout_version != 1)
61 + return NIPC_ERR_BAD_LAYOUT;
62 + if (hdr.flags != 0)
63 + return NIPC_ERR_BAD_LAYOUT;
64 + if (hdr.reserved != 0)
65 + return NIPC_ERR_BAD_LAYOUT;
66 +
67 + out->layout_version = hdr.layout_version;
68 + out->flags = hdr.flags;
69 + out->item_count = hdr.item_count;
70 + out->systemd_enabled = hdr.systemd_enabled;
71 + out->generation = hdr.generation;
72 +
73 + /* Validate directory fits (with overflow check) */
74 + if (mul_would_overflow((size_t)out->item_count, NIPC_CGROUPS_DIR_ENTRY_SIZE))
75 + return NIPC_ERR_BAD_ITEM_COUNT;
76 + size_t dir_size = (size_t)out->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
77 + size_t dir_end = NIPC_CGROUPS_RESP_HDR_SIZE + dir_size;
78 + if (dir_end > buf_len)
79 + return NIPC_ERR_TRUNCATED;
80 +
81 + size_t packed_area_len = buf_len - dir_end;
82 +
83 + /* Validate each directory entry */
84 + const uint8_t *dir = (const uint8_t *)buf + NIPC_CGROUPS_RESP_HDR_SIZE;
85 + for (uint32_t i = 0; i < out->item_count; i++) {
86 + nipc_batch_entry_t entry;
87 + memcpy(&entry, dir + i * sizeof(entry), sizeof(entry));
88 +
89 + if (entry.offset % NIPC_ALIGNMENT != 0)
90 + return NIPC_ERR_BAD_ALIGNMENT;
91 + if ((uint64_t)entry.offset + entry.length > packed_area_len)
92 + return NIPC_ERR_OUT_OF_BOUNDS;
93 + if (entry.length < NIPC_CGROUPS_ITEM_HDR_SIZE)
94 + return NIPC_ERR_TRUNCATED;
95 + }
96 +
97 + out->_payload = (const uint8_t *)buf;
98 + out->_payload_len = buf_len;
99 + return NIPC_OK;
100 +}
101 +
102 +nipc_error_t nipc_cgroups_resp_item(const nipc_cgroups_resp_view_t *view,
103 + uint32_t index,
104 + nipc_cgroups_item_view_t *out) {
105 + if (index >= view->item_count)
106 + return NIPC_ERR_OUT_OF_BOUNDS;
107 +
108 + /* Overflow already checked in nipc_cgroups_resp_decode, but
109 + * guard defensively since this is a public API. */
110 + if (mul_would_overflow((size_t)view->item_count, NIPC_CGROUPS_DIR_ENTRY_SIZE))
111 + return NIPC_ERR_BAD_ITEM_COUNT;
112 +
113 + size_t dir_start = NIPC_CGROUPS_RESP_HDR_SIZE;
114 + size_t dir_size = (size_t)view->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
115 + size_t packed_area_start = dir_start + dir_size;
116 +
117 + /* Read directory entry */
118 + nipc_batch_entry_t dir_entry;
119 + memcpy(&dir_entry,
120 + view->_payload + dir_start + index * sizeof(dir_entry),
121 + sizeof(dir_entry));
122 +
123 + const uint8_t *item = view->_payload + packed_area_start + dir_entry.offset;
124 + uint32_t item_len = dir_entry.length;
125 +
126 + /* Read the 32-byte item wire header in one copy */
127 + nipc_cgroups_item_wire_t wire;
128 + memcpy(&wire, item, sizeof(wire));
129 +
130 + if (wire.layout_version != 1)
131 + return NIPC_ERR_BAD_LAYOUT;
132 + if (wire.flags != 0)
133 + return NIPC_ERR_BAD_LAYOUT;
134 +
135 + /* Validate name string */
136 + if (wire.name_offset < NIPC_CGROUPS_ITEM_HDR_SIZE)
137 + return NIPC_ERR_OUT_OF_BOUNDS;
138 + if ((uint64_t)wire.name_offset + wire.name_length + 1 > item_len)
139 + return NIPC_ERR_OUT_OF_BOUNDS;
140 + if (item[wire.name_offset + wire.name_length] != '\0')
141 + return NIPC_ERR_MISSING_NUL;
142 +
143 + /* Validate path string */
144 + if (wire.path_offset < NIPC_CGROUPS_ITEM_HDR_SIZE)
145 + return NIPC_ERR_OUT_OF_BOUNDS;
146 + if ((uint64_t)wire.path_offset + wire.path_length + 1 > item_len)
147 + return NIPC_ERR_OUT_OF_BOUNDS;
148 + if (item[wire.path_offset + wire.path_length] != '\0')
149 + return NIPC_ERR_MISSING_NUL;
150 +
151 + /* Reject overlapping name and path regions (including NUL) */
152 + {
153 + uint64_t name_start = wire.name_offset;
154 + uint64_t name_end = name_start + wire.name_length + 1;
155 + uint64_t path_start = wire.path_offset;
156 + uint64_t path_end = path_start + wire.path_length + 1;
157 + if (name_start < path_end && path_start < name_end)
158 + return NIPC_ERR_BAD_LAYOUT;
159 + }
160 +
161 + out->layout_version = wire.layout_version;
162 + out->flags = wire.flags;
163 + out->hash = wire.hash;
164 + out->options = wire.options;
165 + out->enabled = wire.enabled;
166 + out->name.ptr = (const char *)(item + wire.name_offset);
167 + out->name.len = wire.name_length;
168 + out->path.ptr = (const char *)(item + wire.path_offset);
169 + out->path.len = wire.path_length;
170 +
171 + return NIPC_OK;
172 +}
173 +
174 +/* ------------------------------------------------------------------ */
175 +/* Cgroups snapshot response builder */
176 +/* */
177 +/* Layout during building (max_items directory slots reserved): */
178 +/* [24-byte header space] [max_items*8 directory] [packed items] */
179 +/* */
180 +/* Layout after finish (compacted to actual item_count): */
181 +/* [24-byte header] [item_count*8 directory] [packed items] */
182 +/* */
183 +/* If item_count < max_items, finish() shifts packed data left and */
184 +/* adjusts directory offsets accordingly. */
185 +/* ------------------------------------------------------------------ */
186 +
187 +void nipc_cgroups_builder_init(nipc_cgroups_builder_t *b,
188 + void *buf, size_t buf_len,
189 + uint32_t max_items,
190 + uint32_t systemd_enabled,
191 + uint64_t generation) {
192 + b->buf = (uint8_t *)buf;
193 + b->buf_len = buf_len;
194 + b->systemd_enabled = systemd_enabled;
195 + b->generation = generation;
196 + b->item_count = 0;
197 + b->max_items = max_items;
198 + b->error = NIPC_OK;
199 +
200 + /* Packed item data starts after reserved directory */
201 + b->data_offset = NIPC_CGROUPS_RESP_HDR_SIZE +
202 + (size_t)max_items * NIPC_CGROUPS_DIR_ENTRY_SIZE;
203 +}
204 +
205 +void nipc_cgroups_builder_set_header(nipc_cgroups_builder_t *b,
206 + uint32_t systemd_enabled,
207 + uint64_t generation) {
208 + b->systemd_enabled = systemd_enabled;
209 + b->generation = generation;
210 +}
211 +
212 +uint32_t nipc_cgroups_builder_estimate_max_items(size_t buf_len) {
213 + if (buf_len <= NIPC_CGROUPS_RESP_HDR_SIZE)
214 + return 0;
215 +
216 + size_t min_aligned_item = nipc_align8(NIPC_CGROUPS_ITEM_HDR_SIZE + 2u);
217 + return (uint32_t)((buf_len - NIPC_CGROUPS_RESP_HDR_SIZE) /
218 + (NIPC_CGROUPS_DIR_ENTRY_SIZE + min_aligned_item));
219 +}
220 +
221 +nipc_error_t nipc_cgroups_builder_add(nipc_cgroups_builder_t *b,
222 + uint32_t hash,
223 + uint32_t options,
224 + uint32_t enabled,
225 + const char *name, uint32_t name_len,
226 + const char *path, uint32_t path_len) {
227 + if (b->item_count >= b->max_items) {
228 + b->error = NIPC_ERR_OVERFLOW;
229 + return NIPC_ERR_OVERFLOW;
230 + }
231 +
232 + /* Align item start to 8 bytes */
233 + size_t item_start = nipc_align8(b->data_offset);
234 +
235 + /* Item payload: 32-byte header + name + NUL + path + NUL */
236 + size_t item_size = NIPC_CGROUPS_ITEM_HDR_SIZE +
237 + (size_t)name_len + 1 +
238 + (size_t)path_len + 1;
239 +
240 + if (item_start + item_size > b->buf_len) {
241 + b->error = NIPC_ERR_OVERFLOW;
242 + return NIPC_ERR_OVERFLOW;
243 + }
244 +
245 + /* Zero alignment padding */
246 + if (item_start > b->data_offset)
247 + memset(b->buf + b->data_offset, 0, item_start - b->data_offset);
248 +
249 + uint8_t *item = b->buf + item_start;
250 +
251 + /* Write item header as a single struct copy */
252 + nipc_cgroups_item_wire_t wire = {
253 + .layout_version = 1,
254 + .flags = 0,
255 + .hash = hash,
256 + .options = options,
257 + .enabled = enabled,
258 + .name_offset = NIPC_CGROUPS_ITEM_HDR_SIZE,
259 + .name_length = name_len,
260 + .path_offset = NIPC_CGROUPS_ITEM_HDR_SIZE + name_len + 1,
261 + .path_length = path_len,
262 + };
263 + memcpy(item, &wire, sizeof(wire));
264 +
265 + /* Write strings with NUL terminators */
266 + memcpy(item + wire.name_offset, name, name_len);
267 + item[wire.name_offset + name_len] = '\0';
268 + memcpy(item + wire.path_offset, path, path_len);
269 + item[wire.path_offset + path_len] = '\0';
270 +
271 + /* Write directory entry (absolute offset stored temporarily) */
272 + nipc_batch_entry_t dir_entry = {
273 + .offset = (uint32_t)item_start,
274 + .length = (uint32_t)item_size,
275 + };
276 + size_t dir_pos = NIPC_CGROUPS_RESP_HDR_SIZE +
277 + (size_t)b->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
278 + memcpy(b->buf + dir_pos, &dir_entry, sizeof(dir_entry));
279 +
280 + b->data_offset = item_start + item_size;
281 + b->item_count++;
282 + return NIPC_OK;
283 +}
284 +
285 +size_t nipc_cgroups_builder_finish(nipc_cgroups_builder_t *b) {
286 + uint8_t *p = b->buf;
287 +
288 + nipc_cgroups_resp_header_t hdr = {
289 + .layout_version = 1,
290 + .flags = 0,
291 + .item_count = b->item_count,
292 + .systemd_enabled = b->systemd_enabled,
293 + .reserved = 0,
294 + .generation = b->generation,
295 + };
296 +
297 + if (b->item_count == 0) {
298 + memcpy(p, &hdr, sizeof(hdr));
299 + return NIPC_CGROUPS_RESP_HDR_SIZE;
300 + }
301 +
302 + /* Where the decoder expects packed data to start */
303 + size_t final_packed_start = NIPC_CGROUPS_RESP_HDR_SIZE +
304 + (size_t)b->item_count * NIPC_CGROUPS_DIR_ENTRY_SIZE;
305 +
306 + /* Read the first directory entry to find where packed data actually begins */
307 + nipc_batch_entry_t first_entry;
308 + memcpy(&first_entry, p + NIPC_CGROUPS_RESP_HDR_SIZE, sizeof(first_entry));
309 + uint32_t first_item_abs = first_entry.offset;
310 +
311 + /* Guard against underflow if builder state is inconsistent */
312 + if (b->data_offset < first_item_abs) {
313 + hdr.item_count = 0;
314 + memcpy(p, &hdr, sizeof(hdr));
315 + return NIPC_CGROUPS_RESP_HDR_SIZE;
316 + }
317 +
318 + size_t packed_data_len = b->data_offset - first_item_abs;
319 +
320 + if (final_packed_start < first_item_abs) {
321 + memmove(p + final_packed_start, p + first_item_abs, packed_data_len);
322 + }
323 +
324 + /* Convert directory entries from absolute offsets to relative offsets */
325 + size_t dir_base = NIPC_CGROUPS_RESP_HDR_SIZE;
326 + for (uint32_t i = 0; i < b->item_count; i++) {
327 + size_t entry_pos = dir_base + (size_t)i * NIPC_CGROUPS_DIR_ENTRY_SIZE;
328 + nipc_batch_entry_t entry;
329 + memcpy(&entry, p + entry_pos, sizeof(entry));
330 + if (entry.offset < first_item_abs)
331 + continue; /* skip corrupted entry */
332 + entry.offset -= first_item_abs;
333 + memcpy(p + entry_pos, &entry, sizeof(entry));
334 + }
335 +
336 + /* Write snapshot header */
337 + memcpy(p, &hdr, sizeof(hdr));
338 +
339 + return final_packed_start + packed_data_len;
340 +}
341 +
342 +nipc_error_t nipc_dispatch_cgroups_snapshot(
343 + const uint8_t *req, size_t req_len,
344 + uint8_t *resp, size_t resp_size, size_t *resp_len,
345 + uint32_t max_items,
346 + nipc_cgroups_handler_fn handler, void *user)
347 +{
348 + nipc_cgroups_req_t request;
349 + nipc_error_t err = nipc_cgroups_req_decode(req, req_len, &request);
350 + if (err != NIPC_OK)
351 + return err;
352 +
353 + nipc_cgroups_builder_t builder;
354 + nipc_cgroups_builder_init(&builder, resp, resp_size, max_items, 0, 0);
355 +
356 + if (!handler(user, &request, &builder)) {
357 + if (builder.error != NIPC_OK)
358 + return builder.error;
359 + return NIPC_ERR_HANDLER_FAILED;
360 + }
361 +
362 + if (builder.error != NIPC_OK)
363 + return builder.error;
364 +
365 + *resp_len = nipc_cgroups_builder_finish(&builder);
366 + return (*resp_len > 0) ? NIPC_OK : NIPC_ERR_OVERFLOW;
367 +}
src/libnetdata/netipc/src/protocol/netipc_protocol_cgroups_snapshot_internal.h new
+19
@@ -0,0 +1,19 @@
1 +#ifndef NETIPC_PROTOCOL_CGROUPS_SNAPSHOT_INTERNAL_H
2 +#define NETIPC_PROTOCOL_CGROUPS_SNAPSHOT_INTERNAL_H
3 +
4 +#include "netipc_protocol_internal.h"
5 +
6 +/* Cgroups item wire header (internal, 32 bytes) */
7 +typedef struct {
8 + uint16_t layout_version;
9 + uint16_t flags;
10 + uint32_t hash;
11 + uint32_t options;
12 + uint32_t enabled;
13 + uint32_t name_offset;
14 + uint32_t name_length;
15 + uint32_t path_offset;
16 + uint32_t path_length;
17 +} nipc_cgroups_item_wire_t;
18 +
19 +#endif /* NETIPC_PROTOCOL_CGROUPS_SNAPSHOT_INTERNAL_H */
src/libnetdata/netipc/src/protocol/netipc_protocol_increment.c new
+37
@@ -0,0 +1,37 @@
1 +#include "netipc_protocol_internal.h"
2 +
3 +/* ------------------------------------------------------------------ */
4 +/* INCREMENT codec */
5 +/* ------------------------------------------------------------------ */
6 +
7 +size_t nipc_increment_encode(uint64_t value, void *buf, size_t buf_len) {
8 + if (buf_len < NIPC_INCREMENT_PAYLOAD_SIZE)
9 + return 0;
10 + memcpy(buf, &value, 8);
11 + return NIPC_INCREMENT_PAYLOAD_SIZE;
12 +}
13 +
14 +nipc_error_t nipc_increment_decode(const void *buf, size_t buf_len,
15 + uint64_t *value_out) {
16 + if (buf_len < NIPC_INCREMENT_PAYLOAD_SIZE)
17 + return NIPC_ERR_TRUNCATED;
18 + memcpy(value_out, buf, 8);
19 + return NIPC_OK;
20 +}
21 +
22 +bool nipc_dispatch_increment(
23 + const uint8_t *req, size_t req_len,
24 + uint8_t *resp, size_t resp_size, size_t *resp_len,
25 + nipc_increment_handler_fn handler, void *user)
26 +{
27 + uint64_t value;
28 + if (nipc_increment_decode(req, req_len, &value) != NIPC_OK)
29 + return false;
30 +
31 + uint64_t result;
32 + if (!handler(user, value, &result))
33 + return false;
34 +
35 + *resp_len = nipc_increment_encode(result, resp, resp_size);
36 + return *resp_len > 0;
37 +}
src/libnetdata/netipc/src/protocol/netipc_protocol_internal.h new
+19
@@ -0,0 +1,19 @@
1 +#ifndef NETIPC_PROTOCOL_INTERNAL_H
2 +#define NETIPC_PROTOCOL_INTERNAL_H
3 +
4 +#include "netipc/netipc_protocol.h"
5 +
6 +#include <stddef.h>
7 +#include <string.h>
8 +
9 +/*
10 + * Safe multiplication check: returns true if count * entry_size would
11 + * overflow size_t. Portable across 32-bit and 64-bit without triggering
12 + * -Wtype-limits.
13 + */
14 +static inline bool mul_would_overflow(size_t count, size_t entry_size)
15 +{
16 + return entry_size != 0 && count > SIZE_MAX / entry_size;
17 +}
18 +
19 +#endif /* NETIPC_PROTOCOL_INTERNAL_H */
src/libnetdata/netipc/src/protocol/netipc_protocol_lookup_common.c new
+404
@@ -0,0 +1,404 @@
1 +#include "netipc_protocol_lookup_common.h"
2 +
3 +_Static_assert(sizeof(nipc_lookup_req_header_wire_t) == NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE,
4 + "lookup request header must be 16 bytes");
5 +_Static_assert(sizeof(nipc_lookup_resp_header_wire_t) == NIPC_CGROUPS_LOOKUP_RESP_HDR_SIZE,
6 + "lookup response header must be 16 bytes");
7 +_Static_assert(sizeof(nipc_lookup_dir_entry_t) == NIPC_LOOKUP_DIR_ENTRY_SIZE,
8 + "lookup directory entry must be 8 bytes");
9 +_Static_assert(sizeof(nipc_lookup_label_entry_t) == NIPC_LOOKUP_LABEL_ENTRY_SIZE,
10 + "lookup label entry must be 16 bytes");
11 +
12 +_Static_assert(offsetof(nipc_lookup_req_header_wire_t, layout_version) == 0, "");
13 +_Static_assert(offsetof(nipc_lookup_req_header_wire_t, flags) == 2, "");
14 +_Static_assert(offsetof(nipc_lookup_req_header_wire_t, item_count) == 4, "");
15 +_Static_assert(offsetof(nipc_lookup_req_header_wire_t, reserved0) == 8, "");
16 +_Static_assert(offsetof(nipc_lookup_req_header_wire_t, reserved1) == 12, "");
17 +
18 +_Static_assert(offsetof(nipc_lookup_resp_header_wire_t, layout_version) == 0, "");
19 +_Static_assert(offsetof(nipc_lookup_resp_header_wire_t, flags) == 2, "");
20 +_Static_assert(offsetof(nipc_lookup_resp_header_wire_t, item_count) == 4, "");
21 +_Static_assert(offsetof(nipc_lookup_resp_header_wire_t, generation) == 8, "");
22 +
23 +_Static_assert(offsetof(nipc_lookup_dir_entry_t, offset) == 0, "");
24 +_Static_assert(offsetof(nipc_lookup_dir_entry_t, length) == 4, "");
25 +_Static_assert(offsetof(nipc_lookup_label_entry_t, key_offset) == 0, "");
26 +_Static_assert(offsetof(nipc_lookup_label_entry_t, key_length) == 4, "");
27 +_Static_assert(offsetof(nipc_lookup_label_entry_t, value_offset) == 8, "");
28 +_Static_assert(offsetof(nipc_lookup_label_entry_t, value_length) == 12, "");
29 +
30 +bool nipc_lookup_bytes_have_nul(const void *ptr, uint32_t len) {
31 + return len > 0 && memchr(ptr, '\0', len) != NULL;
32 +}
33 +
34 +bool nipc_lookup_source_string_invalid(const char *ptr, uint32_t len,
35 + bool require_non_empty) {
36 + if (require_non_empty && len == 0)
37 + return true;
38 + if (len > 0 && !ptr)
39 + return true;
40 + return ptr && nipc_lookup_bytes_have_nul(ptr, len);
41 +}
42 +
43 +static bool
44 +lookup_label_storage_add_u64(uint64_t *item_size,
45 + const nipc_lookup_label_view_t *label) {
46 + if (nipc_lookup_add_u64_over_limit(*item_size, label->key.len, UINT32_MAX,
47 + item_size))
48 + return false;
49 +
50 + if (nipc_lookup_add_u64_over_limit(*item_size, 1u, UINT32_MAX, item_size))
51 + return false;
52 +
53 + if (nipc_lookup_add_u64_over_limit(*item_size, label->value.len, UINT32_MAX,
54 + item_size))
55 + return false;
56 +
57 + if (nipc_lookup_add_u64_over_limit(*item_size, 1u, UINT32_MAX, item_size))
58 + return false;
59 +
60 + return true;
61 +}
62 +
63 +bool nipc_lookup_ranges_overlap_u64(uint64_t a_start, uint64_t a_end,
64 + uint64_t b_start, uint64_t b_end) {
65 + return a_start < b_end && b_start < a_end;
66 +}
67 +
68 +nipc_error_t nipc_lookup_string_view(const uint8_t *item, uint32_t item_len,
69 + uint32_t hdr_size, uint32_t offset,
70 + uint32_t length, nipc_str_view_t *out,
71 + uint64_t *end_out) {
72 + if (offset < hdr_size)
73 + return NIPC_ERR_OUT_OF_BOUNDS;
74 +
75 + uint64_t end;
76 + if (nipc_lookup_add_u64_over_limit(offset, length, item_len, &end) ||
77 + nipc_lookup_add_u64_over_limit(end, 1, item_len, &end))
78 + return NIPC_ERR_OUT_OF_BOUNDS;
79 +
80 + if (item[offset + length] != '\0')
81 + return NIPC_ERR_MISSING_NUL;
82 + if (nipc_lookup_bytes_have_nul(item + offset, length))
83 + return NIPC_ERR_BAD_LAYOUT;
84 +
85 + if (out) {
86 + out->ptr = (const char *)(item + offset);
87 + out->len = length;
88 + }
89 + if (end_out)
90 + *end_out = end;
91 + return NIPC_OK;
92 +}
93 +
94 +nipc_error_t nipc_lookup_validate_ordered_dir(const uint8_t *dir,
95 + uint32_t item_count,
96 + uint32_t packed_area_len,
97 + uint32_t min_len, bool exact_len,
98 + uint32_t exact_value) {
99 + uint64_t prev_end = 0;
100 +
101 + for (uint32_t i = 0; i < item_count; i++) {
102 + nipc_lookup_dir_entry_t entry;
103 + memcpy(&entry, dir + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE, sizeof(entry));
104 +
105 + if (entry.offset % NIPC_ALIGNMENT != 0)
106 + return NIPC_ERR_BAD_ALIGNMENT;
107 + if (exact_len && entry.length != exact_value)
108 + return NIPC_ERR_BAD_LAYOUT;
109 + if (!exact_len && entry.length < min_len)
110 + return NIPC_ERR_BAD_LAYOUT;
111 +
112 + uint64_t end;
113 + if (nipc_lookup_add_u64_over_limit(entry.offset, entry.length,
114 + packed_area_len, &end))
115 + return NIPC_ERR_OUT_OF_BOUNDS;
116 + if (i > 0 && entry.offset < prev_end)
117 + return NIPC_ERR_BAD_LAYOUT;
118 + prev_end = end;
119 + }
120 +
121 + return NIPC_OK;
122 +}
123 +
124 +nipc_error_t nipc_lookup_validate_labels(const uint8_t *item, uint32_t item_len,
125 + uint32_t hdr_size,
126 + uint16_t label_count,
127 + uint64_t fixed_end,
128 + uint32_t *label_table_offset_out) {
129 + if (label_count == 0) {
130 + if (fixed_end != item_len)
131 + return NIPC_ERR_BAD_LAYOUT;
132 + if (label_table_offset_out)
133 + *label_table_offset_out = (uint32_t)fixed_end;
134 + return NIPC_OK;
135 + }
136 +
137 + uint64_t table_start;
138 + if (nipc_lookup_align8_u64_over_limit(fixed_end, UINT32_MAX, &table_start) ||
139 + table_start > item_len)
140 + return NIPC_ERR_OUT_OF_BOUNDS;
141 +
142 + for (uint64_t i = fixed_end; i < table_start; i++) {
143 + if (item[i] != 0)
144 + return NIPC_ERR_BAD_LAYOUT;
145 + }
146 +
147 + uint64_t table_bytes = (uint64_t)label_count * NIPC_LOOKUP_LABEL_ENTRY_SIZE;
148 + uint64_t after_table;
149 + if (nipc_lookup_add_u64_over_limit(table_start, table_bytes, item_len,
150 + &after_table))
151 + return NIPC_ERR_OUT_OF_BOUNDS;
152 +
153 + uint64_t expected = after_table;
154 + for (uint32_t i = 0; i < label_count; i++) {
155 + nipc_lookup_label_entry_t entry;
156 + memcpy(&entry,
157 + item + table_start + (uint64_t)i * NIPC_LOOKUP_LABEL_ENTRY_SIZE,
158 + sizeof(entry));
159 +
160 + if (entry.key_length == 0)
161 + return NIPC_ERR_BAD_LAYOUT;
162 + if (entry.key_offset != expected)
163 + return NIPC_ERR_BAD_LAYOUT;
164 +
165 + uint64_t key_end;
166 + nipc_error_t err =
167 + nipc_lookup_string_view(item, item_len, hdr_size, entry.key_offset,
168 + entry.key_length, NULL, &key_end);
169 + if (err != NIPC_OK)
170 + return err;
171 + expected = key_end;
172 +
173 + if (entry.value_offset != expected)
174 + return NIPC_ERR_BAD_LAYOUT;
175 + uint64_t value_end;
176 + err = nipc_lookup_string_view(item, item_len, hdr_size, entry.value_offset,
177 + entry.value_length, NULL, &value_end);
178 + if (err != NIPC_OK)
179 + return err;
180 + expected = value_end;
181 + }
182 +
183 + if (expected != item_len)
184 + return NIPC_ERR_BAD_LAYOUT;
185 + if (label_table_offset_out)
186 + *label_table_offset_out = (uint32_t)table_start;
187 + return NIPC_OK;
188 +}
189 +
190 +nipc_error_t nipc_lookup_label_at(const uint8_t *item, uint32_t item_len,
191 + uint32_t hdr_size, uint16_t label_count,
192 + uint32_t label_table_offset, uint32_t index,
193 + nipc_lookup_label_view_t *out) {
194 + if (index >= label_count)
195 + return NIPC_ERR_OUT_OF_BOUNDS;
196 +
197 + uint64_t entry_pos = (uint64_t)label_table_offset +
198 + (uint64_t)index * NIPC_LOOKUP_LABEL_ENTRY_SIZE;
199 + if (entry_pos + NIPC_LOOKUP_LABEL_ENTRY_SIZE > item_len)
200 + return NIPC_ERR_OUT_OF_BOUNDS;
201 +
202 + nipc_lookup_label_entry_t entry;
203 + memcpy(&entry, item + entry_pos, sizeof(entry));
204 +
205 + uint64_t ignored;
206 + nipc_error_t err =
207 + nipc_lookup_string_view(item, item_len, hdr_size, entry.key_offset,
208 + entry.key_length, &out->key, &ignored);
209 + if (err != NIPC_OK)
210 + return err;
211 + return nipc_lookup_string_view(item, item_len, hdr_size, entry.value_offset,
212 + entry.value_length, &out->value, &ignored);
213 +}
214 +
215 +void nipc_lookup_write_labels(uint8_t *item, size_t table_start,
216 + size_t table_bytes,
217 + const nipc_lookup_label_view_t *labels,
218 + uint16_t label_count) {
219 + size_t next = table_start + table_bytes;
220 + for (uint32_t i = 0; i < label_count; i++) {
221 + nipc_lookup_label_entry_t entry = {
222 + .key_offset = (uint32_t)next,
223 + .key_length = labels[i].key.len,
224 + .value_offset = (uint32_t)(next + labels[i].key.len + 1u),
225 + .value_length = labels[i].value.len,
226 + };
227 + memcpy(item + table_start + (size_t)i * NIPC_LOOKUP_LABEL_ENTRY_SIZE,
228 + &entry, sizeof(entry));
229 + memcpy(item + entry.key_offset, labels[i].key.ptr, labels[i].key.len);
230 + item[entry.key_offset + labels[i].key.len] = '\0';
231 + if (labels[i].value.len > 0)
232 + memcpy(item + entry.value_offset, labels[i].value.ptr,
233 + labels[i].value.len);
234 + item[entry.value_offset + labels[i].value.len] = '\0';
235 + next = entry.value_offset + labels[i].value.len + 1u;
236 + }
237 +}
238 +
239 +nipc_error_t nipc_lookup_builder_validate_strings(
240 + const nipc_lookup_builder_string_t *strings, uint32_t string_count) {
241 + for (uint32_t i = 0; i < string_count; i++) {
242 + if (nipc_lookup_source_string_invalid(strings[i].ptr, strings[i].len,
243 + strings[i].require_non_empty))
244 + return NIPC_ERR_BAD_LAYOUT;
245 + }
246 + return NIPC_OK;
247 +}
248 +
249 +static nipc_error_t
250 +lookup_builder_layout_strings(uint32_t fixed_header_size,
251 + nipc_lookup_builder_string_t *strings,
252 + uint32_t string_count, uint64_t *fixed_end_out) {
253 + uint64_t cursor = fixed_header_size;
254 +
255 + for (uint32_t i = 0; i < string_count; i++) {
256 + strings[i].offset = cursor;
257 + if (nipc_lookup_add_u64_over_limit(cursor, strings[i].len, UINT32_MAX,
258 + &cursor) ||
259 + nipc_lookup_add_u64_over_limit(cursor, 1u, UINT32_MAX, &cursor))
260 + return NIPC_ERR_OVERFLOW;
261 + }
262 +
263 + *fixed_end_out = cursor;
264 + return NIPC_OK;
265 +}
266 +
267 +static nipc_error_t lookup_builder_layout_labels(
268 + uint64_t fixed_end, const nipc_lookup_label_view_t *labels,
269 + uint16_t label_count, nipc_lookup_builder_item_layout_t *layout) {
270 + layout->table_start = fixed_end;
271 + layout->table_bytes = 0;
272 + layout->item_size = fixed_end;
273 +
274 + if (label_count == 0)
275 + return NIPC_OK;
276 +
277 + if (!labels)
278 + return NIPC_ERR_OVERFLOW;
279 +
280 + if (nipc_lookup_align8_u64_over_limit(fixed_end, UINT32_MAX,
281 + &layout->table_start))
282 + return NIPC_ERR_OVERFLOW;
283 +
284 + layout->table_bytes = (uint64_t)label_count * NIPC_LOOKUP_LABEL_ENTRY_SIZE;
285 + if (nipc_lookup_add_u64_over_limit(layout->table_start, layout->table_bytes,
286 + UINT32_MAX, &layout->item_size))
287 + return NIPC_ERR_OVERFLOW;
288 +
289 + for (uint32_t i = 0; i < label_count; i++) {
290 + if (nipc_lookup_source_string_invalid(labels[i].key.ptr, labels[i].key.len,
291 + true) ||
292 + nipc_lookup_source_string_invalid(labels[i].value.ptr,
293 + labels[i].value.len, false))
294 + return NIPC_ERR_BAD_LAYOUT;
295 +
296 + if (!lookup_label_storage_add_u64(&layout->item_size, &labels[i]))
297 + return NIPC_ERR_OVERFLOW;
298 + }
299 +
300 + return NIPC_OK;
301 +}
302 +
303 +nipc_error_t nipc_lookup_builder_layout_item(
304 + size_t data_offset, size_t buf_len, uint32_t fixed_header_size,
305 + nipc_lookup_builder_string_t *strings, uint32_t string_count,
306 + const nipc_lookup_label_view_t *labels, uint16_t label_count,
307 + nipc_lookup_builder_item_layout_t *layout) {
308 + if (nipc_lookup_align8_u64_over_limit((uint64_t)data_offset, UINT32_MAX,
309 + &layout->item_start))
310 + return NIPC_ERR_OVERFLOW;
311 +
312 + nipc_error_t err = lookup_builder_layout_strings(
313 + fixed_header_size, strings, string_count, &layout->fixed_end);
314 + if (err != NIPC_OK)
315 + return err;
316 +
317 + err = lookup_builder_layout_labels(layout->fixed_end, labels, label_count,
318 + layout);
319 + if (err != NIPC_OK)
320 + return err;
321 +
322 + uint64_t buf_len_u64 = (uint64_t)buf_len;
323 + if (layout->item_size > buf_len_u64 ||
324 + layout->item_start > buf_len_u64 - layout->item_size)
325 + return NIPC_ERR_OVERFLOW;
326 +
327 + return NIPC_OK;
328 +}
329 +
330 +void nipc_lookup_builder_write_strings(
331 + uint8_t *item, const nipc_lookup_builder_string_t *strings,
332 + uint32_t string_count) {
333 + for (uint32_t i = 0; i < string_count; i++) {
334 + size_t offset = (size_t)strings[i].offset;
335 + if (strings[i].len > 0)
336 + memcpy(item + offset, strings[i].ptr, strings[i].len);
337 + item[offset + strings[i].len] = '\0';
338 + }
339 +}
340 +
341 +void nipc_lookup_builder_write_dir_entry(uint8_t *buf,
342 + size_t response_header_size,
343 + uint32_t item_count, size_t item_start,
344 + size_t item_size) {
345 + nipc_lookup_dir_entry_t dir_entry = {
346 + .offset = (uint32_t)item_start,
347 + .length = (uint32_t)item_size,
348 + };
349 + size_t dir_pos =
350 + response_header_size + (size_t)item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
351 + memcpy(buf + dir_pos, &dir_entry, sizeof(dir_entry));
352 +}
353 +
354 +size_t nipc_lookup_finish_common(uint8_t *p, size_t buf_len,
355 + uint32_t item_count, size_t data_offset,
356 + size_t header_size, uint64_t generation) {
357 + nipc_lookup_resp_header_wire_t hdr = {
358 + .layout_version = 1,
359 + .flags = 0,
360 + .item_count = item_count,
361 + .generation = generation,
362 + };
363 +
364 + if (buf_len < header_size)
365 + return 0;
366 +
367 + if (item_count == 0) {
368 + memcpy(p, &hdr, sizeof(hdr));
369 + return header_size;
370 + }
371 +
372 + if (mul_would_overflow((size_t)item_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
373 + return 0;
374 + size_t dir_size = (size_t)item_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
375 + if (header_size > SIZE_MAX - dir_size)
376 + return 0;
377 + size_t final_packed_start = header_size + dir_size;
378 + nipc_lookup_dir_entry_t first_entry;
379 + memcpy(&first_entry, p + header_size, sizeof(first_entry));
380 + uint32_t first_item_abs = first_entry.offset;
381 +
382 + if (data_offset < first_item_abs) {
383 + hdr.item_count = 0;
384 + memcpy(p, &hdr, sizeof(hdr));
385 + return header_size;
386 + }
387 +
388 + size_t packed_data_len = data_offset - first_item_abs;
389 + if (final_packed_start < first_item_abs)
390 + memmove(p + final_packed_start, p + first_item_abs, packed_data_len);
391 +
392 + for (uint32_t i = 0; i < item_count; i++) {
393 + size_t entry_pos = header_size + (size_t)i * NIPC_LOOKUP_DIR_ENTRY_SIZE;
394 + nipc_lookup_dir_entry_t entry;
395 + memcpy(&entry, p + entry_pos, sizeof(entry));
396 + if (entry.offset < first_item_abs)
397 + return 0;
398 + entry.offset -= first_item_abs;
399 + memcpy(p + entry_pos, &entry, sizeof(entry));
400 + }
401 +
402 + memcpy(p, &hdr, sizeof(hdr));
403 + return final_packed_start + packed_data_len;
404 +}
src/libnetdata/netipc/src/protocol/netipc_protocol_lookup_common.h new
+105
@@ -0,0 +1,105 @@
1 +#ifndef NETIPC_PROTOCOL_LOOKUP_COMMON_H
2 +#define NETIPC_PROTOCOL_LOOKUP_COMMON_H
3 +
4 +#include "netipc_protocol_internal.h"
5 +
6 +typedef struct {
7 + uint16_t layout_version;
8 + uint16_t flags;
9 + uint32_t item_count;
10 + uint32_t reserved0;
11 + uint32_t reserved1;
12 +} nipc_lookup_req_header_wire_t;
13 +
14 +typedef struct {
15 + uint16_t layout_version;
16 + uint16_t flags;
17 + uint32_t item_count;
18 + uint64_t generation;
19 +} nipc_lookup_resp_header_wire_t;
20 +
21 +typedef struct {
22 + const char *ptr;
23 + uint32_t len;
24 + bool require_non_empty;
25 + uint64_t offset;
26 +} nipc_lookup_builder_string_t;
27 +
28 +typedef struct {
29 + uint64_t item_start;
30 + uint64_t fixed_end;
31 + uint64_t table_start;
32 + uint64_t table_bytes;
33 + uint64_t item_size;
34 +} nipc_lookup_builder_item_layout_t;
35 +
36 +static inline bool nipc_lookup_add_u64_over_limit(uint64_t a, uint64_t b,
37 + uint64_t limit,
38 + uint64_t *out) {
39 + if (UINT64_MAX - a < b)
40 + return true;
41 + uint64_t value = a + b;
42 + if (value > limit)
43 + return true;
44 + if (out)
45 + *out = value;
46 + return false;
47 +}
48 +
49 +static inline bool nipc_lookup_align8_u64_over_limit(uint64_t value,
50 + uint64_t limit,
51 + uint64_t *out) {
52 + if (nipc_lookup_add_u64_over_limit(value, NIPC_ALIGNMENT - 1u, limit, &value))
53 + return true;
54 + value &= ~(uint64_t)(NIPC_ALIGNMENT - 1u);
55 + if (out)
56 + *out = value;
57 + return false;
58 +}
59 +
60 +bool nipc_lookup_bytes_have_nul(const void *ptr, uint32_t len);
61 +bool nipc_lookup_source_string_invalid(const char *ptr, uint32_t len,
62 + bool require_non_empty);
63 +bool nipc_lookup_ranges_overlap_u64(uint64_t a_start, uint64_t a_end,
64 + uint64_t b_start, uint64_t b_end);
65 +nipc_error_t nipc_lookup_string_view(const uint8_t *item, uint32_t item_len,
66 + uint32_t hdr_size, uint32_t offset,
67 + uint32_t length, nipc_str_view_t *out,
68 + uint64_t *end_out);
69 +nipc_error_t nipc_lookup_validate_ordered_dir(const uint8_t *dir,
70 + uint32_t item_count,
71 + uint32_t packed_area_len,
72 + uint32_t min_len, bool exact_len,
73 + uint32_t exact_value);
74 +nipc_error_t nipc_lookup_validate_labels(const uint8_t *item, uint32_t item_len,
75 + uint32_t hdr_size,
76 + uint16_t label_count,
77 + uint64_t fixed_end,
78 + uint32_t *label_table_offset_out);
79 +nipc_error_t nipc_lookup_label_at(const uint8_t *item, uint32_t item_len,
80 + uint32_t hdr_size, uint16_t label_count,
81 + uint32_t label_table_offset, uint32_t index,
82 + nipc_lookup_label_view_t *out);
83 +void nipc_lookup_write_labels(uint8_t *item, size_t table_start,
84 + size_t table_bytes,
85 + const nipc_lookup_label_view_t *labels,
86 + uint16_t label_count);
87 +nipc_error_t nipc_lookup_builder_validate_strings(
88 + const nipc_lookup_builder_string_t *strings, uint32_t string_count);
89 +nipc_error_t nipc_lookup_builder_layout_item(
90 + size_t data_offset, size_t buf_len, uint32_t fixed_header_size,
91 + nipc_lookup_builder_string_t *strings, uint32_t string_count,
92 + const nipc_lookup_label_view_t *labels, uint16_t label_count,
93 + nipc_lookup_builder_item_layout_t *layout);
94 +void nipc_lookup_builder_write_strings(
95 + uint8_t *item, const nipc_lookup_builder_string_t *strings,
96 + uint32_t string_count);
97 +void nipc_lookup_builder_write_dir_entry(uint8_t *buf,
98 + size_t response_header_size,
99 + uint32_t item_count, size_t item_start,
100 + size_t item_size);
101 +size_t nipc_lookup_finish_common(uint8_t *p, size_t buf_len,
102 + uint32_t item_count, size_t data_offset,
103 + size_t header_size, uint64_t generation);
104 +
105 +#endif /* NETIPC_PROTOCOL_LOOKUP_COMMON_H */
src/libnetdata/netipc/src/protocol/netipc_protocol_string_reverse.c new
+74
@@ -0,0 +1,74 @@
1 +#include "netipc_protocol_internal.h"
2 +
3 +/* ------------------------------------------------------------------ */
4 +/* STRING_REVERSE codec */
5 +/* ------------------------------------------------------------------ */
6 +
7 +size_t nipc_string_reverse_encode(const char *str, uint32_t str_len,
8 + void *buf, size_t buf_len) {
9 + /* Guard against size_t overflow only where uint32_t can exceed size_t. */
10 +#if SIZE_MAX <= UINT32_MAX
11 + if ((size_t)str_len > SIZE_MAX - (size_t)NIPC_STRING_REVERSE_HDR_SIZE - 1u)
12 + return 0;
13 +#endif
14 +
15 + size_t total = NIPC_STRING_REVERSE_HDR_SIZE + str_len + 1;
16 + if (buf_len < total)
17 + return 0;
18 +
19 + uint8_t *p = (uint8_t *)buf;
20 + uint32_t offset = NIPC_STRING_REVERSE_HDR_SIZE;
21 + memcpy(p + 0, &offset, 4);
22 + memcpy(p + 4, &str_len, 4);
23 + if (str_len > 0)
24 + memcpy(p + offset, str, str_len);
25 + p[offset + str_len] = '\0';
26 + return total;
27 +}
28 +
29 +nipc_error_t nipc_string_reverse_decode(const void *buf, size_t buf_len,
30 + nipc_string_reverse_view_t *view_out) {
31 + if (buf_len < NIPC_STRING_REVERSE_HDR_SIZE)
32 + return NIPC_ERR_TRUNCATED;
33 +
34 + const uint8_t *p = (const uint8_t *)buf;
35 + uint32_t str_offset, str_length;
36 + memcpy(&str_offset, p + 0, 4);
37 + memcpy(&str_length, p + 4, 4);
38 +
39 + if ((uint64_t)str_offset + str_length + 1 > buf_len)
40 + return NIPC_ERR_OUT_OF_BOUNDS;
41 +
42 + if (p[str_offset + str_length] != '\0')
43 + return NIPC_ERR_MISSING_NUL;
44 +
45 + view_out->str = (const char *)(p + str_offset);
46 + view_out->str_len = str_length;
47 + return NIPC_OK;
48 +}
49 +
50 +bool nipc_dispatch_string_reverse(
51 + const uint8_t *req, size_t req_len,
52 + uint8_t *resp, size_t resp_size, size_t *resp_len,
53 + nipc_string_reverse_handler_fn handler, void *user)
54 +{
55 + nipc_string_reverse_view_t view;
56 + if (nipc_string_reverse_decode(req, req_len, &view) != NIPC_OK)
57 + return false;
58 +
59 + /* The handler writes the response string into scratch space that is
60 + * already positioned at the codec payload offset. */
61 + uint32_t capacity = (resp_size > NIPC_STRING_REVERSE_HDR_SIZE + 1)
62 + ? (uint32_t)(resp_size - NIPC_STRING_REVERSE_HDR_SIZE - 1)
63 + : 0;
64 + char *scratch = (char *)(resp + NIPC_STRING_REVERSE_HDR_SIZE);
65 +
66 + uint32_t response_str_len = 0;
67 + if (!handler(user, view.str, view.str_len,
68 + scratch, capacity, &response_str_len))
69 + return false;
70 +
71 + *resp_len = nipc_string_reverse_encode(scratch, response_str_len,
72 + resp, resp_size);
73 + return *resp_len > 0;
74 +}
src/libnetdata/netipc/src/service/netipc_service.c
+40 -1667
@@ -13,6 +13,9 @@
13 #include "netipc/netipc_protocol.h"
14 #include "netipc/netipc_uds.h"
15 #include "netipc/netipc_shm.h"
16 +#include "netipc_service_common.h"
17 +#include "netipc_service_platform.h"
18 +#include "netipc_service_posix_internal.h"
19
20 #include <errno.h>
21 #include <poll.h>
@@ -23,30 +26,28 @@
26 #include <time.h>
27 #include <unistd.h>
28
26 -/* Poll timeout for server loops: 100ms between shutdown checks */
27 -#define SERVER_POLL_TIMEOUT_MS 100
28 -#define NIPC_CLIENT_BUF_DEFAULT 65536u
29 #define CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS 5u
30 #define CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS 5000u
31 +#define CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS 5u
32 +#define CLIENT_CALL_RECONNECT_DRAIN_MS (SERVER_POLL_TIMEOUT_MS + 50u)
33 +#define CLIENT_CALL_RECONNECT_RETRIES 20u
34 +
35
32 -enum {
33 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL = 1,
34 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL,
35 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL,
36 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL,
37 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL,
38 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL,
39 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL,
40 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL,
41 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_THREAD_CREATE_INTERNAL,
42 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL,
43 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
44 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
45 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
46 -};
36
37 static uint64_t g_posix_service_test_fault_state = 0;
38
39 +void nipc_service_posix_sleep_us(unsigned int usec)
40 +{
41 + struct timespec req = {
42 + .tv_sec = (time_t)(usec / 1000000u),
43 + .tv_nsec = (long)(usec % 1000000u) * 1000L,
44 + };
45 +
46 + while (nanosleep(&req, &req) == -1 && errno == EINTR) {
47 + // Retry with the remaining interval after signal interruption.
48 + }
49 +}
50 +
51 static uint64_t posix_service_fault_state_make(int site, uint32_t skip_matches)
52 {
53 return ((uint64_t)skip_matches << 32) | (uint32_t)site;
@@ -88,20 +89,30 @@ static bool service_test_should_fail(int site)
89 }
90 }
91
91 -static void *service_malloc(size_t size, int fault_site)
92 +void *nipc_service_posix_malloc(size_t size, int fault_site)
93 {
94 if (service_test_should_fail(fault_site))
95 return NULL;
96 return malloc(size);
97 }
98
98 -static void *service_calloc(size_t count, size_t size, int fault_site)
99 +void *nipc_service_platform_malloc(size_t size, int fault_site)
100 +{
101 + return nipc_service_posix_malloc(size, fault_site);
102 +}
103 +
104 +void *nipc_service_posix_calloc(size_t count, size_t size, int fault_site)
105 {
106 if (service_test_should_fail(fault_site))
107 return NULL;
108 return calloc(count, size);
109 }
110
111 +void *nipc_service_platform_calloc(size_t count, size_t size, int fault_site)
112 +{
113 + return nipc_service_posix_calloc(count, size, fault_site);
114 +}
115 +
116 static void *service_realloc(void *ptr, size_t size, int fault_site)
117 {
118 if (service_test_should_fail(fault_site))
@@ -109,7 +120,7 @@ static void *service_realloc(void *ptr, size_t size, int fault_site)
120 return realloc(ptr, size);
121 }
122
112 -static int service_pthread_create(pthread_t *thread,
123 +int nipc_service_posix_pthread_create(pthread_t *thread,
124 const pthread_attr_t *attr,
125 void *(*start_routine)(void *),
126 void *arg)
@@ -126,20 +137,12 @@ static uint64_t monotonic_time_ms(void)
137 return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
138 }
139
129 -static uint32_t next_power_of_2_u32(uint32_t n)
140 +uint64_t nipc_service_platform_monotonic_ms(void)
141 {
131 - if (n < 16)
132 - return 16;
133 - n--;
134 - n |= n >> 1;
135 - n |= n >> 2;
136 - n |= n >> 4;
137 - n |= n >> 8;
138 - n |= n >> 16;
139 - return n + 1;
142 + return monotonic_time_ms();
143 }
144
142 -static bool ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int fault_site)
145 +bool nipc_service_posix_ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int fault_site)
146 {
147 if (*buf && *buf_size >= need)
148 return true;
@@ -153,1641 +156,11 @@ static bool ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int faul
156 return true;
157 }
158
156 -static bool header_payload_len(size_t payload_len, size_t *msg_len_out)
157 -{
158 -#if SIZE_MAX <= UINT32_MAX
159 - if (payload_len > SIZE_MAX - NIPC_HEADER_LEN)
160 - return false;
161 -#endif
162 -
163 - *msg_len_out = NIPC_HEADER_LEN + payload_len;
164 - return true;
165 -}
166 -
167 -static bool header_payload_len_u32(uint32_t payload_len, uint32_t *msg_len_out)
168 -{
169 - if (payload_len > UINT32_MAX - NIPC_HEADER_LEN)
170 - return false;
171 -
172 - *msg_len_out = payload_len + NIPC_HEADER_LEN;
173 - return true;
174 -}
175 -
176 -static void client_note_request_capacity(nipc_client_ctx_t *ctx, uint32_t payload_len)
177 -{
178 - uint32_t grown = next_power_of_2_u32(payload_len);
179 - if (grown > NIPC_MAX_PAYLOAD_CAP)
180 - grown = NIPC_MAX_PAYLOAD_CAP;
181 - if (grown > ctx->transport_config.max_request_payload_bytes)
182 - ctx->transport_config.max_request_payload_bytes = grown;
183 -}
184 -
185 -static void client_note_response_capacity(nipc_client_ctx_t *ctx, uint32_t payload_len)
186 -{
187 - uint32_t grown = next_power_of_2_u32(payload_len);
188 - if (grown > NIPC_MAX_PAYLOAD_CAP)
189 - grown = NIPC_MAX_PAYLOAD_CAP;
190 - if (grown > ctx->transport_config.max_response_payload_bytes)
191 - ctx->transport_config.max_response_payload_bytes = grown;
192 -}
193 -
194 -static bool client_prepare_session_buffers(nipc_client_ctx_t *ctx)
195 -{
196 - size_t response_need;
197 - if (!header_payload_len(ctx->session.max_response_payload_bytes, &response_need))
198 - return false;
199 - if (response_need < NIPC_HEADER_LEN + 1024u)
200 - response_need = NIPC_HEADER_LEN + 1024u;
201 -
202 - if (!ensure_buffer(&ctx->response_buf, &ctx->response_buf_size, response_need,
203 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL))
204 - return false;
205 -
206 - if (ctx->session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
207 - ctx->session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
208 - size_t send_need;
209 - if (!header_payload_len(ctx->session.max_request_payload_bytes, &send_need))
210 - return false;
211 - if (!ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, send_need,
212 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL))
213 - return false;
214 - }
215 -
216 - return true;
217 -}
218 -
219 -static uint32_t cgroups_request_payload_default(void)
220 -{
221 - return 16u;
222 -}
223 -
224 -static uint32_t cgroups_response_payload_default(void)
225 -{
226 - return NIPC_CLIENT_BUF_DEFAULT;
227 -}
228 -
229 -static nipc_uds_client_config_t service_client_config_to_transport(
230 - const nipc_client_config_t *config)
231 -{
232 - nipc_uds_client_config_t transport = {0};
233 -
234 - if (!config)
235 - return transport;
236 -
237 - transport.supported_profiles = config->supported_profiles;
238 - transport.preferred_profiles = config->preferred_profiles;
239 - transport.max_request_batch_items = config->max_request_batch_items;
240 - transport.max_response_payload_bytes = config->max_response_payload_bytes;
241 - transport.max_response_batch_items = config->max_request_batch_items;
242 - transport.auth_token = config->auth_token;
243 -
244 - return transport;
245 -}
246 -
247 -static nipc_uds_server_config_t service_server_config_to_transport(
248 - const nipc_server_config_t *config)
249 -{
250 - nipc_uds_server_config_t transport = {0};
251 -
252 - if (!config)
253 - return transport;
254 -
255 - transport.supported_profiles = config->supported_profiles;
256 - transport.preferred_profiles = config->preferred_profiles;
257 - transport.max_request_batch_items = config->max_request_batch_items;
258 - transport.max_response_payload_bytes = config->max_response_payload_bytes;
259 - transport.max_response_batch_items = config->max_request_batch_items;
260 - transport.auth_token = config->auth_token;
261 -
262 - return transport;
263 -}
264 -
265 -/* ------------------------------------------------------------------ */
266 -/* Internal: client connection helpers */
267 -/* ------------------------------------------------------------------ */
268 -
269 -/* Tear down the current connection (UDS session + SHM if any). */
270 -static void client_disconnect(nipc_client_ctx_t *ctx)
271 -{
272 - if (ctx->shm) {
273 - nipc_shm_close(ctx->shm);
274 - free(ctx->shm);
275 - ctx->shm = NULL;
276 - }
277 -
278 - if (ctx->session_valid) {
279 - nipc_uds_close_session(&ctx->session);
280 - ctx->session_valid = false;
281 - }
282 -}
283 -
284 -static void client_disable_shm_profiles(nipc_client_ctx_t *ctx)
285 -{
286 - ctx->transport_config.supported_profiles &=
287 - ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
288 - ctx->transport_config.preferred_profiles &=
289 - ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
290 -}
291 -
292 -/* Attempt a full connection: UDS connect + handshake, then SHM upgrade
293 - * if negotiated. Returns the new state. */
294 -static nipc_client_state_t client_try_connect(nipc_client_ctx_t *ctx)
295 -{
296 - nipc_uds_session_t session;
297 - memset(&session, 0, sizeof(session));
298 - session.fd = -1;
299 -
300 - nipc_uds_error_t err = nipc_uds_connect(
301 - ctx->run_dir, ctx->service_name,
302 - &ctx->transport_config, &session);
303 -
304 - switch (err) {
305 - case NIPC_UDS_OK:
306 - break;
307 - case NIPC_UDS_ERR_CONNECT:
308 - return NIPC_CLIENT_NOT_FOUND;
309 - case NIPC_UDS_ERR_AUTH_FAILED:
310 - return NIPC_CLIENT_AUTH_FAILED;
311 - case NIPC_UDS_ERR_NO_PROFILE:
312 - case NIPC_UDS_ERR_INCOMPATIBLE:
313 - return NIPC_CLIENT_INCOMPATIBLE;
314 - default:
315 - return NIPC_CLIENT_DISCONNECTED;
316 - }
317 -
318 - ctx->session = session;
319 - ctx->session_valid = true;
320 -
321 - if (!client_prepare_session_buffers(ctx)) {
322 - nipc_uds_close_session(&ctx->session);
323 - ctx->session_valid = false;
324 - return NIPC_CLIENT_DISCONNECTED;
325 - }
326 -
327 - /* SHM upgrade if negotiated */
328 - if (session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
329 - session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
330 -
331 - nipc_shm_ctx_t *shm = service_calloc(
332 - 1, sizeof(nipc_shm_ctx_t),
333 - NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL);
334 - if (!shm) {
335 - nipc_uds_close_session(&ctx->session);
336 - ctx->session_valid = false;
337 - return NIPC_CLIENT_DISCONNECTED;
338 - }
339 - {
340 - /* Retry attach: server creates the SHM region after
341 - * the UDS handshake, so it may not exist yet. */
342 - nipc_shm_error_t serr = NIPC_SHM_ERR_NOT_READY;
343 - uint64_t deadline_ms = monotonic_time_ms() + CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS;
344 - for (;;) {
345 - serr = nipc_shm_client_attach(
346 - ctx->run_dir, ctx->service_name,
347 - session.session_id, shm);
348 - if (serr == NIPC_SHM_OK)
349 - break;
350 - if (serr != NIPC_SHM_ERR_NOT_READY &&
351 - serr != NIPC_SHM_ERR_OPEN &&
352 - serr != NIPC_SHM_ERR_BAD_MAGIC)
353 - break;
354 - if (monotonic_time_ms() >= deadline_ms)
355 - break;
356 - usleep(CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS * 1000u);
357 - }
358 -
359 - if (serr == NIPC_SHM_OK) {
360 - ctx->shm = shm;
361 - } else {
362 - /* SHM attach failed after negotiation. Close that session,
363 - * blacklist SHM for this client context, and retry
364 - * baseline via a new handshake. */
365 - free(shm);
366 - nipc_uds_close_session(&ctx->session);
367 - ctx->session_valid = false;
368 - client_disable_shm_profiles(ctx);
369 - if (ctx->transport_config.supported_profiles == 0)
370 - return NIPC_CLIENT_DISCONNECTED;
371 - return client_try_connect(ctx);
372 - }
373 - }
374 - }
375 -
376 - return NIPC_CLIENT_READY;
377 -}
378 -
379 -/* ------------------------------------------------------------------ */
380 -/* Internal: send/receive via the active transport */
381 -/* ------------------------------------------------------------------ */
382 -
383 -/*
384 - * Send a complete message (header + payload) using whichever transport
385 - * is active: SHM if negotiated, UDS otherwise.
386 - */
387 -static nipc_error_t transport_send(nipc_client_ctx_t *ctx,
388 - nipc_header_t *hdr,
389 - const void *payload,
390 - size_t payload_len)
391 -{
392 - if (payload_len > UINT32_MAX)
393 - return NIPC_ERR_OVERFLOW;
394 -
395 - if (ctx->shm) {
396 - if (payload_len > ctx->session.max_request_payload_bytes) {
397 - client_note_request_capacity(ctx, (uint32_t)payload_len);
398 - return NIPC_ERR_OVERFLOW;
399 - }
400 -
401 - size_t msg_len;
402 - if (!header_payload_len(payload_len, &msg_len))
403 - return NIPC_ERR_OVERFLOW;
404 -
405 - uint8_t *msg = ctx->send_buf;
406 - if (!msg || msg_len > ctx->send_buf_size)
407 - return NIPC_ERR_OVERFLOW;
408 -
409 - hdr->magic = NIPC_MAGIC_MSG;
410 - hdr->version = NIPC_VERSION;
411 - hdr->header_len = NIPC_HEADER_LEN;
412 - hdr->payload_len = (uint32_t)payload_len;
413 -
414 - nipc_header_encode(hdr, msg, NIPC_HEADER_LEN);
415 - if (payload_len > 0)
416 - memcpy(msg + NIPC_HEADER_LEN, payload, payload_len);
417 -
418 - nipc_shm_error_t serr = nipc_shm_send(ctx->shm, msg, msg_len);
419 - if (serr == NIPC_SHM_ERR_MSG_TOO_LARGE) {
420 - client_note_request_capacity(ctx, (uint32_t)payload_len);
421 - return NIPC_ERR_OVERFLOW;
422 - }
423 - return (serr == NIPC_SHM_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
424 - }
425 -
426 - /* UDS path */
427 - nipc_uds_error_t uerr = nipc_uds_send(&ctx->session, hdr,
428 - payload, payload_len);
429 - if (uerr == NIPC_UDS_ERR_LIMIT_EXCEEDED) {
430 - client_note_request_capacity(ctx, (uint32_t)payload_len);
431 - return NIPC_ERR_OVERFLOW;
432 - }
433 - return (uerr == NIPC_UDS_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
434 -}
435 -
436 -/*
437 - * Receive a complete message. For SHM, reads from the SHM region.
438 - * For UDS, reads from the socket into the caller's buffer.
439 - *
440 - * On success, hdr_out is filled, and payload_out + payload_len_out
441 - * point to the payload bytes (valid until next receive).
442 - */
443 -static nipc_error_t transport_receive(nipc_client_ctx_t *ctx,
444 - void *buf, size_t buf_size,
445 - nipc_header_t *hdr_out,
446 - const void **payload_out,
447 - size_t *payload_len_out)
448 -{
449 - if (ctx->shm) {
450 - size_t msg_len;
451 - nipc_shm_error_t serr = nipc_shm_receive(ctx->shm, buf, buf_size,
452 - &msg_len, 30000);
453 - if (serr != NIPC_SHM_OK)
454 - return NIPC_ERR_TRUNCATED;
455 -
456 - if (msg_len < NIPC_HEADER_LEN)
457 - return NIPC_ERR_TRUNCATED;
458 -
459 - nipc_error_t perr = nipc_header_decode(buf, msg_len, hdr_out);
460 - if (perr != NIPC_OK)
461 - return perr;
462 -
463 - *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
464 - *payload_len_out = msg_len - NIPC_HEADER_LEN;
465 - return NIPC_OK;
466 - }
467 -
468 - /* UDS path */
469 - nipc_uds_error_t uerr = nipc_uds_receive(&ctx->session, buf, buf_size,
470 - hdr_out, payload_out,
471 - payload_len_out);
472 - return (uerr == NIPC_UDS_OK) ? NIPC_OK : NIPC_ERR_TRUNCATED;
473 -}
474 -
475 -/* ------------------------------------------------------------------ */
476 -/* Internal: generic raw call (send request, receive response) */
477 -/* ------------------------------------------------------------------ */
478 -
479 -/*
480 - * Single-attempt raw call: build envelope, send, receive, validate
481 - * envelope. The caller handles encode before and decode after.
482 - *
483 - * On success, response_payload_out and response_len_out point into the
484 - * internal client response buffer (valid until next call on this context).
485 - */
486 -static nipc_error_t do_raw_call(nipc_client_ctx_t *ctx,
487 - uint16_t method_code,
488 - const void *request_payload,
489 - size_t request_len,
490 - const void **response_payload_out,
491 - size_t *response_len_out)
492 -{
493 - nipc_header_t hdr = {0};
494 - hdr.kind = NIPC_KIND_REQUEST;
495 - hdr.code = method_code;
496 - hdr.flags = 0;
497 - hdr.item_count = 1;
498 - hdr.message_id = (uint64_t)(ctx->call_count + 1);
499 - hdr.transport_status = NIPC_STATUS_OK;
500 -
501 - nipc_error_t err = transport_send(ctx, &hdr, request_payload, request_len);
502 - if (err != NIPC_OK) {
503 - return err;
504 - }
505 -
506 - nipc_header_t resp_hdr;
507 - err = transport_receive(ctx, ctx->response_buf, ctx->response_buf_size,
508 - &resp_hdr, response_payload_out, response_len_out);
509 - if (err != NIPC_OK) {
510 - return err;
511 - }
512 -
513 - if (resp_hdr.kind != NIPC_KIND_RESPONSE)
514 - return NIPC_ERR_BAD_KIND;
515 - if (resp_hdr.code != method_code)
516 - return NIPC_ERR_BAD_LAYOUT;
517 - if (resp_hdr.message_id != hdr.message_id)
518 - return NIPC_ERR_BAD_LAYOUT;
519 -
520 - switch (resp_hdr.transport_status) {
521 - case NIPC_STATUS_OK:
522 - break;
523 - case NIPC_STATUS_LIMIT_EXCEEDED:
524 - if (ctx->session.max_response_payload_bytes > 0) {
525 - uint32_t current = ctx->session.max_response_payload_bytes;
526 - client_note_response_capacity(
527 - ctx, current >= UINT32_MAX / 2u ? UINT32_MAX : current * 2u);
528 - }
529 - return NIPC_ERR_OVERFLOW;
530 - case NIPC_STATUS_UNSUPPORTED:
531 - return NIPC_ERR_BAD_LAYOUT;
532 - case NIPC_STATUS_BAD_ENVELOPE:
533 - case NIPC_STATUS_INTERNAL_ERROR:
534 - default:
535 - return NIPC_ERR_BAD_LAYOUT;
536 - }
537 -
538 - return NIPC_OK;
539 -}
540 -
541 -/*
542 - * Generic call-with-retry:
543 - * - ordinary failures reconnect and retry once
544 - * - overflow-driven resize recovery may reconnect repeatedly until
545 - * negotiated capacities grow or recovery fails
546 - * The caller provides a function pointer for the single-attempt logic.
547 - */
548 -typedef nipc_error_t (*nipc_attempt_fn)(nipc_client_ctx_t *ctx, void *state);
549 -
550 -static nipc_error_t call_with_retry(nipc_client_ctx_t *ctx,
551 - nipc_attempt_fn attempt,
552 - void *state)
553 -{
554 - if (ctx->state != NIPC_CLIENT_READY) {
555 - ctx->error_count++;
556 - return NIPC_ERR_NOT_READY;
557 - }
558 -
559 - /* Cap overflow-driven retries: payloads grow by powers of 2, so 8
560 - * retries allows ~256x growth from the initial negotiated size. */
561 - int overflow_retries = 0;
562 - for (;;) {
563 - uint32_t prev_req = ctx->session.max_request_payload_bytes;
564 - uint32_t prev_resp = ctx->session.max_response_payload_bytes;
565 - uint32_t prev_cfg_req = ctx->transport_config.max_request_payload_bytes;
566 - uint32_t prev_cfg_resp = ctx->transport_config.max_response_payload_bytes;
567 -
568 - nipc_error_t err = attempt(ctx, state);
569 - if (err == NIPC_OK) {
570 - ctx->call_count++;
571 - return NIPC_OK;
572 - }
573 -
574 - if (err != NIPC_ERR_OVERFLOW) {
575 - client_disconnect(ctx);
576 - ctx->state = NIPC_CLIENT_BROKEN;
577 - ctx->state = client_try_connect(ctx);
578 - if (ctx->state != NIPC_CLIENT_READY) {
579 - ctx->error_count++;
580 - return err;
581 - }
582 -
583 - ctx->reconnect_count++;
584 - err = attempt(ctx, state);
585 - if (err == NIPC_OK) {
586 - ctx->call_count++;
587 - return NIPC_OK;
588 - }
589 -
590 - client_disconnect(ctx);
591 - ctx->state = NIPC_CLIENT_BROKEN;
592 - ctx->error_count++;
593 - return err;
594 - }
595 -
596 - client_disconnect(ctx);
597 - ctx->state = NIPC_CLIENT_BROKEN;
598 - ctx->state = client_try_connect(ctx);
599 - if (ctx->state != NIPC_CLIENT_READY) {
600 - ctx->error_count++;
601 - return err;
602 - }
603 - ctx->reconnect_count++;
604 -
605 - if (ctx->session.max_request_payload_bytes <= prev_req &&
606 - ctx->session.max_response_payload_bytes <= prev_resp &&
607 - ctx->transport_config.max_request_payload_bytes <= prev_cfg_req &&
608 - ctx->transport_config.max_response_payload_bytes <= prev_cfg_resp) {
609 - client_disconnect(ctx);
610 - ctx->state = NIPC_CLIENT_BROKEN;
611 - ctx->error_count++;
612 - return err;
613 - }
614 -
615 - if (++overflow_retries >= 8) {
616 - client_disconnect(ctx);
617 - ctx->state = NIPC_CLIENT_BROKEN;
618 - ctx->error_count++;
619 - return err;
620 - }
621 - }
622 -}
623 -
624 -/* ------------------------------------------------------------------ */
625 -/* Internal: single attempt at a cgroups snapshot call */
626 -/* ------------------------------------------------------------------ */
627 -
628 -typedef struct {
629 - nipc_cgroups_resp_view_t *view_out;
630 -} cgroups_call_state_t;
631 -
632 -static nipc_error_t do_cgroups_attempt(nipc_client_ctx_t *ctx, void *state)
633 -{
634 - cgroups_call_state_t *s = (cgroups_call_state_t *)state;
635 -
636 - nipc_cgroups_req_t req = { .layout_version = 1, .flags = 0 };
637 - uint8_t req_buf[4];
638 - size_t req_len = nipc_cgroups_req_encode(&req, req_buf, sizeof(req_buf));
639 - if (req_len == 0)
640 - return NIPC_ERR_TRUNCATED;
641 -
642 - const void *payload;
643 - size_t payload_len;
644 - nipc_error_t err = do_raw_call(ctx, NIPC_METHOD_CGROUPS_SNAPSHOT,
645 - req_buf, req_len,
646 - &payload, &payload_len);
647 - if (err != NIPC_OK)
648 - return err;
649 -
650 - return nipc_cgroups_resp_decode(payload, payload_len, s->view_out);
651 -}
652 -
653 -/* ------------------------------------------------------------------ */
654 -/* Public API: client lifecycle */
655 -/* ------------------------------------------------------------------ */
656 -
657 -void nipc_client_init(nipc_client_ctx_t *ctx,
658 - const char *run_dir,
659 - const char *service_name,
660 - const nipc_client_config_t *config)
661 -{
662 - memset(ctx, 0, sizeof(*ctx));
663 - ctx->state = NIPC_CLIENT_DISCONNECTED;
664 - ctx->session.fd = -1;
665 - ctx->session_valid = false;
666 - ctx->shm = NULL;
667 -
668 - if (run_dir) {
669 - size_t len = strlen(run_dir);
670 - if (len >= sizeof(ctx->run_dir))
671 - len = sizeof(ctx->run_dir) - 1;
672 - memcpy(ctx->run_dir, run_dir, len);
673 - ctx->run_dir[len] = '\0';
674 - }
675 -
676 - if (service_name) {
677 - size_t len = strlen(service_name);
678 - if (len >= sizeof(ctx->service_name))
679 - len = sizeof(ctx->service_name) - 1;
680 - memcpy(ctx->service_name, service_name, len);
681 - ctx->service_name[len] = '\0';
682 - }
683 -
684 - ctx->transport_config = service_client_config_to_transport(config);
685 - if (ctx->transport_config.max_request_payload_bytes == 0)
686 - ctx->transport_config.max_request_payload_bytes = cgroups_request_payload_default();
687 - if (ctx->transport_config.max_response_payload_bytes == 0)
688 - ctx->transport_config.max_response_payload_bytes = cgroups_response_payload_default();
689 -}
690 -
691 -bool nipc_client_refresh(nipc_client_ctx_t *ctx)
692 -{
693 - nipc_client_state_t old_state = ctx->state;
694 -
695 - switch (ctx->state) {
696 - case NIPC_CLIENT_DISCONNECTED:
697 - case NIPC_CLIENT_NOT_FOUND:
698 - /* Attempt to connect */
699 - ctx->state = NIPC_CLIENT_CONNECTING;
700 - ctx->state = client_try_connect(ctx);
701 - if (ctx->state == NIPC_CLIENT_READY)
702 - ctx->connect_count++;
703 - break;
704 -
705 - case NIPC_CLIENT_BROKEN:
706 - /* Reconnect: tear down old connection first */
707 - client_disconnect(ctx);
708 - ctx->state = NIPC_CLIENT_CONNECTING;
709 - ctx->state = client_try_connect(ctx);
710 - if (ctx->state == NIPC_CLIENT_READY)
711 - ctx->reconnect_count++;
712 - break;
713 -
714 - case NIPC_CLIENT_READY:
715 - case NIPC_CLIENT_CONNECTING:
716 - case NIPC_CLIENT_AUTH_FAILED:
717 - case NIPC_CLIENT_INCOMPATIBLE:
718 - /* No action needed */
719 - break;
720 - }
721 -
722 - return ctx->state != old_state;
723 -}
724 -
725 -void nipc_client_status(const nipc_client_ctx_t *ctx,
726 - nipc_client_status_t *out)
159 +bool nipc_service_platform_ensure_client_send_buffer(nipc_client_ctx_t *ctx,
160 + size_t need)
161 {
728 - out->state = ctx->state;
729 - out->connect_count = ctx->connect_count;
730 - out->reconnect_count = ctx->reconnect_count;
731 - out->call_count = ctx->call_count;
732 - out->error_count = ctx->error_count;
162 + return nipc_service_posix_ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, need,
163 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL);
164 }
165
735 -void nipc_client_close(nipc_client_ctx_t *ctx)
736 -{
737 - client_disconnect(ctx);
738 - free(ctx->response_buf);
739 - free(ctx->send_buf);
740 - ctx->response_buf = NULL;
741 - ctx->send_buf = NULL;
742 - ctx->response_buf_size = 0;
743 - ctx->send_buf_size = 0;
744 - ctx->state = NIPC_CLIENT_DISCONNECTED;
745 -}
746 -
747 -/* ------------------------------------------------------------------ */
748 -/* Public API: typed cgroups snapshot call */
749 -/* ------------------------------------------------------------------ */
750 -
751 -nipc_error_t nipc_client_call_cgroups_snapshot(
752 - nipc_client_ctx_t *ctx,
753 - nipc_cgroups_resp_view_t *view_out)
754 -{
755 - cgroups_call_state_t state = {
756 - .view_out = view_out,
757 - };
758 - return call_with_retry(ctx, do_cgroups_attempt, &state);
759 -}
760 -
761 -/* ------------------------------------------------------------------ */
762 -/* Internal: managed server session handler */
763 -/* ------------------------------------------------------------------ */
764 -
765 -/*
766 - * Wait for data on a file descriptor with periodic shutdown checks.
767 - * Returns: 1 = data ready, 0 = server stopping, -1 = error/hangup.
768 - */
769 -static int poll_with_shutdown(int fd, bool *running)
770 -{
771 - while (__atomic_load_n(running, __ATOMIC_RELAXED)) {
772 - struct pollfd pfd = { .fd = fd, .events = POLLIN };
773 - int ret = poll(&pfd, 1, SERVER_POLL_TIMEOUT_MS);
774 -
775 - if (ret < 0) {
776 - if (errno == EINTR)
777 - continue;
778 - return -1;
779 - }
780 -
781 - if (ret == 0)
782 - continue; /* timeout, check running flag */
783 -
784 - if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
785 - return -1;
786 -
787 - if (pfd.revents & POLLIN)
788 - return 1;
789 - }
790 - return 0;
791 -}
792 -
793 -static uint32_t server_snapshot_max_items(size_t response_buf_size,
794 - const nipc_cgroups_service_handler_t *service_handler)
795 -{
796 - if (service_handler->snapshot_max_items != 0)
797 - return service_handler->snapshot_max_items;
798 - return nipc_cgroups_builder_estimate_max_items(response_buf_size);
799 -}
800 -
801 -static void server_note_request_capacity(nipc_managed_server_t *server,
802 - uint32_t payload_len)
803 -{
804 - uint32_t grown = next_power_of_2_u32(payload_len);
805 - uint32_t current = __atomic_load_n(&server->learned_request_payload_bytes,
806 - __ATOMIC_RELAXED);
807 - while (grown > current &&
808 - !__atomic_compare_exchange_n(&server->learned_request_payload_bytes,
809 - &current, grown, false,
810 - __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {
811 - }
812 -}
813 -
814 -static void server_note_response_capacity(nipc_managed_server_t *server,
815 - uint32_t payload_len)
816 -{
817 - uint32_t grown = next_power_of_2_u32(payload_len);
818 - uint32_t current = __atomic_load_n(&server->learned_response_payload_bytes,
819 - __ATOMIC_RELAXED);
820 - while (grown > current &&
821 - !__atomic_compare_exchange_n(&server->learned_response_payload_bytes,
822 - &current, grown, false,
823 - __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {
824 - }
825 -}
826 -
827 -static nipc_error_t server_typed_dispatch(void *user,
828 - const nipc_header_t *request_hdr,
829 - const uint8_t *request_payload,
830 - size_t request_len,
831 - uint8_t *response_buf,
832 - size_t response_buf_size,
833 - size_t *response_len_out)
834 -{
835 - nipc_managed_server_t *server = (nipc_managed_server_t *)user;
836 - nipc_cgroups_service_handler_t *service_handler = &server->service_handler;
837 - (void)request_hdr;
838 -
839 - if (!service_handler->handle)
840 - return NIPC_ERR_HANDLER_FAILED;
841 -
842 - return nipc_dispatch_cgroups_snapshot(
843 - request_payload, request_len,
844 - response_buf, response_buf_size, response_len_out,
845 - server_snapshot_max_items(response_buf_size, service_handler),
846 - service_handler->handle, service_handler->user);
847 -}
848 -
849 -/*
850 - * Handle one client session: read requests, dispatch to handler,
851 - * send responses. Each session gets its own response buffer.
852 - * Runs until the client disconnects or server stops.
853 - */
854 -static void server_handle_session(nipc_managed_server_t *server,
855 - nipc_uds_session_t *session,
856 - nipc_shm_ctx_t *shm,
857 - uint8_t *resp_buf,
858 - size_t resp_buf_size)
859 -{
860 - /* Allocate recv buffer based on negotiated max request size */
861 - size_t recv_size;
862 - if (!header_payload_len(session->max_request_payload_bytes, &recv_size))
863 - return;
864 - if (recv_size < NIPC_HEADER_LEN + 1024u)
865 - recv_size = NIPC_HEADER_LEN + 1024u;
866 - uint8_t *recv_buf = service_malloc(
867 - recv_size, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL);
868 - if (!recv_buf)
869 - return;
870 -
871 - while (__atomic_load_n(&server->running, __ATOMIC_RELAXED)) {
872 - nipc_header_t hdr;
873 - const void *payload;
874 - size_t payload_len;
875 -
876 - /* Receive request via the active transport */
877 - if (shm) {
878 - size_t msg_len;
879 - nipc_shm_error_t serr = nipc_shm_receive(shm, recv_buf, recv_size,
880 - &msg_len, SERVER_POLL_TIMEOUT_MS);
881 - if (serr == NIPC_SHM_ERR_TIMEOUT)
882 - continue; /* check running flag */
883 - if (serr != NIPC_SHM_OK)
884 - break;
885 - if (msg_len < NIPC_HEADER_LEN)
886 - break;
887 -
888 - nipc_error_t perr = nipc_header_decode(recv_buf, msg_len, &hdr);
889 - if (perr != NIPC_OK)
890 - break;
891 -
892 - payload = recv_buf + NIPC_HEADER_LEN;
893 - payload_len = msg_len - NIPC_HEADER_LEN;
894 - } else {
895 - /* Poll the session fd before blocking on receive */
896 - int pr = poll_with_shutdown(session->fd, &server->running);
897 - if (pr <= 0)
898 - break; /* shutdown or error */
899 -
900 - nipc_uds_error_t uerr = nipc_uds_receive(
901 - session, recv_buf, recv_size,
902 - &hdr, &payload, &payload_len);
903 - if (uerr == NIPC_UDS_ERR_LIMIT_EXCEEDED) {
904 - if (hdr.kind == NIPC_KIND_REQUEST) {
905 - if (hdr.payload_len > 0)
906 - server_note_request_capacity(server, hdr.payload_len);
907 -
908 - nipc_header_t resp_hdr = {0};
909 - resp_hdr.kind = NIPC_KIND_RESPONSE;
910 - resp_hdr.code = hdr.code;
911 - resp_hdr.message_id = hdr.message_id;
912 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
913 - resp_hdr.item_count = 1;
914 - resp_hdr.flags = 0;
915 -
916 - if (nipc_uds_send(session, &resp_hdr, NULL, 0) != NIPC_UDS_OK)
917 - break;
918 - }
919 - break;
920 - }
921 - if (uerr != NIPC_UDS_OK)
922 - break;
923 - }
924 -
925 - /* Protocol violation: unexpected message kind terminates session */
926 - if (hdr.kind != NIPC_KIND_REQUEST)
927 - break;
928 -
929 - if (hdr.code != server->expected_method_code) {
930 - nipc_header_t resp_hdr = {0};
931 - resp_hdr.kind = NIPC_KIND_RESPONSE;
932 - resp_hdr.code = hdr.code;
933 - resp_hdr.message_id = hdr.message_id;
934 - resp_hdr.transport_status = NIPC_STATUS_UNSUPPORTED;
935 - resp_hdr.item_count = 1;
936 - resp_hdr.flags = 0;
937 -
938 - if (shm) {
939 - uint8_t msg[NIPC_HEADER_LEN];
940 - resp_hdr.magic = NIPC_MAGIC_MSG;
941 - resp_hdr.version = NIPC_VERSION;
942 - resp_hdr.header_len = NIPC_HEADER_LEN;
943 - resp_hdr.payload_len = 0;
944 - nipc_header_encode(&resp_hdr, msg, sizeof(msg));
945 - if (nipc_shm_send(shm, msg, sizeof(msg)) != NIPC_SHM_OK)
946 - break;
947 - } else {
948 - if (nipc_uds_send(session, &resp_hdr, NULL, 0) != NIPC_UDS_OK)
949 - break;
950 - }
951 - continue;
952 - }
953 -
954 - if (payload_len <= UINT32_MAX)
955 - server_note_request_capacity(server, (uint32_t)payload_len);
956 -
957 - /* Dispatch: one request kind per service endpoint. */
958 - size_t response_len = 0;
959 - nipc_error_t dispatch_err = server->handler(
960 - server->handler_user,
961 - &hdr,
962 - (const uint8_t *)payload, payload_len,
963 - resp_buf, resp_buf_size,
964 - &response_len);
965 -
966 - /* Build response header */
967 - nipc_header_t resp_hdr = {0};
968 - resp_hdr.kind = NIPC_KIND_RESPONSE;
969 - resp_hdr.code = hdr.code;
970 - resp_hdr.message_id = hdr.message_id;
971 - if ((hdr.flags & NIPC_FLAG_BATCH) && hdr.item_count >= 1) {
972 - resp_hdr.item_count = hdr.item_count;
973 - resp_hdr.flags = NIPC_FLAG_BATCH;
974 - } else {
975 - resp_hdr.item_count = 1;
976 - resp_hdr.flags = 0;
977 - }
978 -
979 - switch (dispatch_err) {
980 - case NIPC_OK:
981 - if (response_len > resp_buf_size ||
982 - response_len > session->max_response_payload_bytes ||
983 - response_len > SIZE_MAX - NIPC_HEADER_LEN) {
984 - server_note_response_capacity(
985 - server,
986 - response_len >= UINT32_MAX ? UINT32_MAX : (uint32_t)response_len);
987 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
988 - response_len = 0;
989 - } else {
990 - if (response_len <= UINT32_MAX)
991 - server_note_response_capacity(server, (uint32_t)response_len);
992 - resp_hdr.transport_status = NIPC_STATUS_OK;
993 - }
994 - break;
995 - case NIPC_ERR_OVERFLOW:
996 - if (session->max_response_payload_bytes >= UINT32_MAX / 2u)
997 - server_note_response_capacity(server, UINT32_MAX);
998 - else
999 - server_note_response_capacity(server,
1000 - session->max_response_payload_bytes * 2u);
1001 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
1002 - response_len = 0;
1003 - break;
1004 - case NIPC_ERR_TRUNCATED:
1005 - case NIPC_ERR_BAD_LAYOUT:
1006 - case NIPC_ERR_OUT_OF_BOUNDS:
1007 - case NIPC_ERR_MISSING_NUL:
1008 - case NIPC_ERR_BAD_ALIGNMENT:
1009 - case NIPC_ERR_BAD_ITEM_COUNT:
1010 - resp_hdr.transport_status = NIPC_STATUS_BAD_ENVELOPE;
1011 - response_len = 0;
1012 - break;
1013 - case NIPC_ERR_HANDLER_FAILED:
1014 - default:
1015 - resp_hdr.transport_status = NIPC_STATUS_INTERNAL_ERROR;
1016 - response_len = 0;
1017 - break;
1018 - }
1019 -
1020 - /* Send response via the active transport */
1021 - if (shm) {
1022 - size_t msg_len;
1023 - if (!header_payload_len(response_len, &msg_len))
1024 - break;
1025 -
1026 - resp_hdr.magic = NIPC_MAGIC_MSG;
1027 - resp_hdr.version = NIPC_VERSION;
1028 - resp_hdr.header_len = NIPC_HEADER_LEN;
1029 - resp_hdr.payload_len = (uint32_t)response_len;
1030 -
1031 - /* Use a stack buffer for small responses, heap for large ones */
1032 - uint8_t stack_msg[4096];
1033 - uint8_t *msg = (msg_len <= sizeof(stack_msg)) ? stack_msg :
1034 - service_malloc(msg_len, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
1035 - if (!msg)
1036 - break;
1037 -
1038 - nipc_header_encode(&resp_hdr, msg, NIPC_HEADER_LEN);
1039 - if (response_len > 0)
1040 - memcpy(msg + NIPC_HEADER_LEN, resp_buf, response_len);
1041 -
1042 - nipc_shm_error_t serr = nipc_shm_send(shm, msg, msg_len);
1043 - if (msg != stack_msg)
1044 - free(msg);
1045 - if (serr != NIPC_SHM_OK)
1046 - break;
1047 - } else {
1048 - nipc_uds_error_t uerr = nipc_uds_send(
1049 - session, &resp_hdr, resp_buf, response_len);
1050 - if (uerr != NIPC_UDS_OK)
1051 - break;
1052 - }
1053 -
1054 - if (dispatch_err == NIPC_ERR_OVERFLOW)
1055 - break;
1056 - }
1057 -
1058 - free(recv_buf);
1059 -}
1060 -
1061 -/* ------------------------------------------------------------------ */
1062 -/* Internal: per-session handler thread */
1063 -/* ------------------------------------------------------------------ */
1064 -
1065 -/* Thread function: handles one client session from accept to disconnect. */
1066 -static void *session_handler_thread(void *arg)
1067 -{
1068 - nipc_session_ctx_t *sctx = (nipc_session_ctx_t *)arg;
1069 - nipc_managed_server_t *server = sctx->server;
1070 -
1071 - /* Allocate a per-session response buffer */
1072 - size_t resp_size = (size_t)sctx->session.max_response_payload_bytes;
1073 - if (resp_size < 1024u)
1074 - resp_size = 1024u;
1075 - uint8_t *resp_buf = service_malloc(
1076 - resp_size, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
1077 - if (resp_buf) {
1078 - server_handle_session(server, &sctx->session, sctx->shm,
1079 - resp_buf, resp_size);
1080 - free(resp_buf);
1081 - }
1082 -
1083 - /* Cleanup SHM and session */
1084 - if (sctx->shm) {
1085 - nipc_shm_destroy(sctx->shm);
1086 - free(sctx->shm);
1087 - }
1088 - nipc_uds_close_session(&sctx->session);
1089 -
1090 - /* Mark inactive so the acceptor's reap loop (or server destroy)
1091 - * can join this thread and free sctx. Do NOT remove from the
1092 - * tracking array here — the reap/destroy path owns that. */
1093 - __atomic_store_n(&sctx->active, false, __ATOMIC_RELEASE);
1094 - return NULL;
1095 -}
1096 -
1097 -/* ------------------------------------------------------------------ */
1098 -/* Internal: reap finished session threads */
1099 -/* ------------------------------------------------------------------ */
1100 -
1101 -/* Reap all finished (inactive) session threads. Called with lock held. */
1102 -static void server_reap_sessions_locked(nipc_managed_server_t *server)
1103 -{
1104 - int i = 0;
1105 - while (i < server->session_count) {
1106 - nipc_session_ctx_t *s = server->sessions[i];
1107 - if (!__atomic_load_n(&s->active, __ATOMIC_ACQUIRE)) {
1108 - pthread_join(s->thread, NULL);
1109 - /* Swap with last, free */
1110 - server->sessions[i] = server->sessions[server->session_count - 1];
1111 - server->session_count--;
1112 - free(s);
1113 - } else {
1114 - i++;
1115 - }
1116 - }
1117 -
1118 -}
1119 -
1120 -static void server_destroy_precreated_shm(nipc_shm_ctx_t **shm)
1121 -{
1122 - if (!shm || !*shm)
1123 - return;
1124 - nipc_shm_destroy(*shm);
1125 - free(*shm);
1126 - *shm = NULL;
1127 -}
1128 -
1129 -static bool server_prepare_accept_config(nipc_managed_server_t *server,
1130 - uint64_t sid,
1131 - nipc_uds_server_config_t *cfg_out,
1132 - nipc_shm_ctx_t **shm_out)
1133 -{
1134 - *cfg_out = server->base_config;
1135 - cfg_out->max_request_payload_bytes =
1136 - __atomic_load_n(&server->learned_request_payload_bytes, __ATOMIC_ACQUIRE);
1137 - cfg_out->max_response_payload_bytes =
1138 - __atomic_load_n(&server->learned_response_payload_bytes, __ATOMIC_ACQUIRE);
1139 - *shm_out = NULL;
1140 -
1141 - uint32_t shm_profiles = cfg_out->supported_profiles &
1142 - (NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
1143 - if (shm_profiles == 0)
1144 - return true;
1145 -
1146 - uint32_t request_capacity;
1147 - uint32_t response_capacity;
1148 - if (!header_payload_len_u32(NIPC_MAX_PAYLOAD_CAP, &request_capacity) ||
1149 - !header_payload_len_u32(cfg_out->max_response_payload_bytes, &response_capacity)) {
1150 - cfg_out->supported_profiles &= ~shm_profiles;
1151 - cfg_out->preferred_profiles &= ~shm_profiles;
1152 - return cfg_out->supported_profiles != 0;
1153 - }
1154 -
1155 - nipc_shm_ctx_t *shm = service_calloc(
1156 - 1, sizeof(nipc_shm_ctx_t),
1157 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL);
1158 - if (!shm)
1159 - return false;
1160 -
1161 - /* HELLO has not been read yet, so the request segment must cover any
1162 - * client proposal the handshake may legally echo back. */
1163 - nipc_shm_error_t serr = nipc_shm_server_create(
1164 - server->run_dir, server->service_name,
1165 - sid,
1166 - request_capacity,
1167 - response_capacity,
1168 - shm);
1169 - if (serr == NIPC_SHM_OK) {
1170 - *shm_out = shm;
1171 - return true;
1172 - }
1173 -
1174 - free(shm);
1175 - cfg_out->supported_profiles &= ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
1176 - cfg_out->preferred_profiles &= ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
1177 - return cfg_out->supported_profiles != 0;
1178 -}
1179 -
1180 -/* ------------------------------------------------------------------ */
1181 -/* Public API: managed server */
1182 -/* ------------------------------------------------------------------ */
1183 -
1184 -static nipc_error_t server_init_raw(nipc_managed_server_t *server,
1185 - const char *run_dir,
1186 - const char *service_name,
1187 - const nipc_uds_server_config_t *config,
1188 - int worker_count,
1189 - uint16_t expected_method_code,
1190 - nipc_server_handler_fn handler,
1191 - void *user)
1192 -{
1193 - if (!server)
1194 - return NIPC_ERR_BAD_LAYOUT;
1195 -
1196 - memset(server, 0, sizeof(*server));
1197 - server->listener.fd = -1;
1198 - __atomic_store_n(&server->running, false, __ATOMIC_RELAXED);
1199 - server->acceptor_started = false;
1200 -
1201 - if (!run_dir || !service_name || !config || !handler)
1202 - return NIPC_ERR_BAD_LAYOUT;
1203 -
1204 - if (worker_count < 1)
1205 - worker_count = 1;
1206 -
1207 - /* Store config */
1208 - {
1209 - size_t len = strlen(run_dir);
1210 - if (len >= sizeof(server->run_dir))
1211 - len = sizeof(server->run_dir) - 1;
1212 - memcpy(server->run_dir, run_dir, len);
1213 - server->run_dir[len] = '\0';
1214 - }
1215 - {
1216 - size_t len = strlen(service_name);
1217 - if (len >= sizeof(server->service_name))
1218 - len = sizeof(server->service_name) - 1;
1219 - memcpy(server->service_name, service_name, len);
1220 - server->service_name[len] = '\0';
1221 - }
1222 -
1223 - server->handler = handler;
1224 - server->handler_user = user;
1225 - server->worker_count = worker_count;
1226 - server->expected_method_code = expected_method_code;
1227 - server->base_config = *config;
1228 - server->learned_request_payload_bytes =
1229 - (config && config->max_request_payload_bytes > 0)
1230 - ? config->max_request_payload_bytes
1231 - : NIPC_MAX_PAYLOAD_DEFAULT;
1232 - server->learned_response_payload_bytes =
1233 - (config && config->max_response_payload_bytes > 0)
1234 - ? config->max_response_payload_bytes
1235 - : NIPC_MAX_PAYLOAD_DEFAULT;
1236 -
1237 - /* Initialize session tracking */
1238 - server->session_capacity = worker_count * 2; /* room for slots being reaped */
1239 - if (server->session_capacity < 16)
1240 - server->session_capacity = 16;
1241 - server->sessions = service_calloc((size_t)server->session_capacity,
1242 - sizeof(nipc_session_ctx_t *),
1243 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL);
1244 - if (!server->sessions)
1245 - return NIPC_ERR_OVERFLOW;
1246 - server->session_count = 0;
1247 - server->next_session_id = 1; /* spec: monotonic counter starting at 1 */
1248 - pthread_mutex_init(&server->sessions_lock, NULL);
1249 -
1250 - /* Clean up stale SHM regions from previous crashes (spec requirement:
1251 - * runs once at server startup, before the listener begins accepting). */
1252 - nipc_shm_cleanup_stale(run_dir, service_name);
1253 -
1254 - /* Start listening via L1 */
1255 - nipc_uds_error_t uerr = nipc_uds_listen(
1256 - run_dir, service_name, config, &server->listener);
1257 - if (uerr != NIPC_UDS_OK) {
1258 - free(server->sessions);
1259 - server->sessions = NULL;
1260 - pthread_mutex_destroy(&server->sessions_lock);
1261 - return NIPC_ERR_BAD_LAYOUT;
1262 - }
1263 -
1264 - return NIPC_OK;
1265 -}
1266 -
1267 -nipc_error_t nipc_server_init_typed(nipc_managed_server_t *server,
1268 - const char *run_dir,
1269 - const char *service_name,
1270 - const nipc_server_config_t *config,
1271 - int worker_count,
1272 - const nipc_cgroups_service_handler_t *service_handler)
1273 -{
1274 - if (!service_handler)
1275 - return NIPC_ERR_BAD_LAYOUT;
1276 -
1277 - nipc_uds_server_config_t typed_cfg = service_server_config_to_transport(config);
1278 - if (typed_cfg.max_request_payload_bytes == 0)
1279 - typed_cfg.max_request_payload_bytes = cgroups_request_payload_default();
1280 - if (typed_cfg.max_response_payload_bytes == 0)
1281 - typed_cfg.max_response_payload_bytes = cgroups_response_payload_default();
1282 -
1283 - nipc_error_t err = server_init_raw(server, run_dir, service_name,
1284 - &typed_cfg, worker_count,
1285 - NIPC_METHOD_CGROUPS_SNAPSHOT,
1286 - server_typed_dispatch, server);
1287 - if (err != NIPC_OK)
1288 - return err;
1289 -
1290 - server->service_handler = *service_handler;
1291 - return NIPC_OK;
1292 -}
1293 -
1294 -nipc_error_t nipc_server_init_raw_for_tests(nipc_managed_server_t *server,
1295 - const char *run_dir,
1296 - const char *service_name,
1297 - const nipc_uds_server_config_t *config,
1298 - int worker_count,
1299 - uint16_t expected_method_code,
1300 - nipc_server_handler_fn handler,
1301 - void *user)
1302 -{
1303 - return server_init_raw(server, run_dir, service_name, config,
1304 - worker_count, expected_method_code, handler, user);
1305 -}
1306 -
1307 -void nipc_server_run(nipc_managed_server_t *server)
1308 -{
1309 - __atomic_store_n(&server->running, true, __ATOMIC_RELEASE);
1310 -
1311 - while (__atomic_load_n(&server->running, __ATOMIC_RELAXED)) {
1312 - /* Poll the listener fd before blocking on accept */
1313 - int pr = poll_with_shutdown(server->listener.fd, &server->running);
1314 - if (pr <= 0)
1315 - break; /* shutdown or error */
1316 -
1317 - /* Accept one client via L1 */
1318 - nipc_uds_session_t session;
1319 - memset(&session, 0, sizeof(session));
1320 - session.fd = -1;
1321 -
1322 - uint64_t sid = server->next_session_id++;
1323 - nipc_uds_server_config_t accept_cfg;
1324 - nipc_shm_ctx_t *prepared_shm = NULL;
1325 - if (!server_prepare_accept_config(server, sid, &accept_cfg, &prepared_shm)) {
1326 - usleep(10000);
1327 - continue;
1328 - }
1329 -
1330 - server->listener.config = accept_cfg;
1331 - nipc_uds_error_t uerr = nipc_uds_accept(
1332 - &server->listener, sid, &session);
1333 - if (uerr != NIPC_UDS_OK) {
1334 - server_destroy_precreated_shm(&prepared_shm);
1335 - if (!__atomic_load_n(&server->running, __ATOMIC_RELAXED))
1336 - break;
1337 - usleep(10000);
1338 - continue;
1339 - }
1340 -
1341 - server_note_request_capacity(server, session.max_request_payload_bytes);
1342 - server_note_response_capacity(server, session.max_response_payload_bytes);
1343 -
1344 - /* Enforce worker_count limit: reap finished sessions, check count */
1345 - pthread_mutex_lock(&server->sessions_lock);
1346 - server_reap_sessions_locked(server);
1347 -
1348 - if (server->session_count >= server->worker_count) {
1349 - /* At capacity: reject this client by closing the session */
1350 - pthread_mutex_unlock(&server->sessions_lock);
1351 - server_destroy_precreated_shm(&prepared_shm);
1352 - nipc_uds_close_session(&session);
1353 - continue;
1354 - }
1355 -
1356 - /* SHM profile guarantee: only negotiate SHM for sessions that already
1357 - * have a prepared per-session SHM region. */
1358 - nipc_shm_ctx_t *shm = prepared_shm;
1359 - if (session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
1360 - session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
1361 - if (!shm) {
1362 - pthread_mutex_unlock(&server->sessions_lock);
1363 - nipc_uds_close_session(&session);
1364 - continue;
1365 - }
1366 - } else {
1367 - server_destroy_precreated_shm(&prepared_shm);
1368 - shm = NULL;
1369 - }
1370 -
1371 - /* Create session context */
1372 - nipc_session_ctx_t *sctx = service_calloc(
1373 - 1, sizeof(nipc_session_ctx_t),
1374 - NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL);
1375 - if (!sctx) {
1376 - if (shm) { nipc_shm_destroy(shm); free(shm); }
1377 - pthread_mutex_unlock(&server->sessions_lock);
1378 - nipc_uds_close_session(&session);
1379 - continue;
1380 - }
1381 -
1382 - sctx->server = server;
1383 - sctx->session = session;
1384 - sctx->shm = shm;
1385 - sctx->id = sid;
1386 - __atomic_store_n(&sctx->active, true, __ATOMIC_RELAXED);
1387 -
1388 - server->sessions[server->session_count++] = sctx;
1389 - pthread_mutex_unlock(&server->sessions_lock);
1390 -
1391 - /* Spawn handler thread for this session */
1392 - int rc = service_pthread_create(&sctx->thread, NULL,
1393 - session_handler_thread, sctx);
1394 - if (rc != 0) {
1395 - /* Thread creation failed: clean up */
1396 - pthread_mutex_lock(&server->sessions_lock);
1397 - /* Remove the sctx we just added */
1398 - for (int i = 0; i < server->session_count; i++) {
1399 - if (server->sessions[i] == sctx) {
1400 - server->sessions[i] = server->sessions[server->session_count - 1];
1401 - server->session_count--;
1402 - break;
1403 - }
1404 - }
1405 - pthread_mutex_unlock(&server->sessions_lock);
1406 -
1407 - if (shm) { nipc_shm_destroy(shm); free(shm); }
1408 - nipc_uds_close_session(&session);
1409 - free(sctx);
1410 - }
1411 - }
1412 -}
1413 -
1414 -void nipc_server_stop(nipc_managed_server_t *server)
1415 -{
1416 - __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
1417 -}
1418 -
1419 -bool nipc_server_drain(nipc_managed_server_t *server, uint32_t timeout_ms)
1420 -{
1421 - /* 1. Stop accepting new clients.
1422 - * Do NOT close the listener here — the run loop may still be
1423 - * polling on listener.fd. Setting the flag is enough; the run
1424 - * loop will exit on its next poll timeout (100ms). The listener
1425 - * is closed later by nipc_server_destroy(). */
1426 - __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
1427 -
1428 - /* 2. Wait for in-flight sessions to complete */
1429 - bool all_drained = true;
1430 - if (server->sessions) {
1431 - struct timespec deadline;
1432 - clock_gettime(CLOCK_MONOTONIC, &deadline);
1433 - deadline.tv_sec += timeout_ms / 1000;
1434 - deadline.tv_nsec += (timeout_ms % 1000) * 1000000L;
1435 - if (deadline.tv_nsec >= 1000000000L) {
1436 - deadline.tv_sec++;
1437 - deadline.tv_nsec -= 1000000000L;
1438 - }
1439 -
1440 - /* Poll until all sessions are inactive or timeout */
1441 - while (1) {
1442 - pthread_mutex_lock(&server->sessions_lock);
1443 - int active_count = 0;
1444 - for (int i = 0; i < server->session_count; i++) {
1445 - if (__atomic_load_n(&server->sessions[i]->active,
1446 - __ATOMIC_ACQUIRE))
1447 - active_count++;
1448 - }
1449 - pthread_mutex_unlock(&server->sessions_lock);
1450 -
1451 - if (active_count == 0)
1452 - break;
1453 -
1454 - struct timespec now;
1455 - clock_gettime(CLOCK_MONOTONIC, &now);
1456 - if (now.tv_sec > deadline.tv_sec ||
1457 - (now.tv_sec == deadline.tv_sec &&
1458 - now.tv_nsec >= deadline.tv_nsec)) {
1459 - /* Timeout: force-close session fds to unblock recv.
1460 - * Closing the fd causes poll/recv to return error,
1461 - * which terminates the session handler loop. */
1462 - pthread_mutex_lock(&server->sessions_lock);
1463 - for (int i = 0; i < server->session_count; i++) {
1464 - nipc_session_ctx_t *s = server->sessions[i];
1465 - if (__atomic_load_n(&s->active, __ATOMIC_ACQUIRE)) {
1466 - if (s->session.fd >= 0) {
1467 - shutdown(s->session.fd, SHUT_RDWR);
1468 - }
1469 - }
1470 - }
1471 - pthread_mutex_unlock(&server->sessions_lock);
1472 - all_drained = false;
1473 - break;
1474 - }
1475 -
1476 - usleep(5000); /* 5ms poll interval */
1477 - }
1478 -
1479 - /* 3. Join all session threads (finished or not) */
1480 - pthread_mutex_lock(&server->sessions_lock);
1481 - for (int i = 0; i < server->session_count; i++) {
1482 - nipc_session_ctx_t *s = server->sessions[i];
1483 - pthread_mutex_unlock(&server->sessions_lock);
1484 - pthread_join(s->thread, NULL);
1485 - free(s);
1486 - pthread_mutex_lock(&server->sessions_lock);
1487 - }
1488 - server->session_count = 0;
1489 - pthread_mutex_unlock(&server->sessions_lock);
1490 -
1491 - free(server->sessions);
1492 - server->sessions = NULL;
1493 - server->session_capacity = 0;
1494 - pthread_mutex_destroy(&server->sessions_lock);
1495 - }
1496 -
1497 - server->worker_count = 0;
1498 - return all_drained;
1499 -}
1500 -
1501 -void nipc_server_destroy(nipc_managed_server_t *server)
1502 -{
1503 - __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
1504 - nipc_uds_close_listener(&server->listener);
1505 -
1506 - /* Join all active session threads */
1507 - if (server->sessions) {
1508 - pthread_mutex_lock(&server->sessions_lock);
1509 - for (int i = 0; i < server->session_count; i++) {
1510 - nipc_session_ctx_t *s = server->sessions[i];
1511 - pthread_mutex_unlock(&server->sessions_lock);
1512 - pthread_join(s->thread, NULL);
1513 - free(s);
1514 - pthread_mutex_lock(&server->sessions_lock);
1515 - }
1516 - server->session_count = 0;
1517 - pthread_mutex_unlock(&server->sessions_lock);
1518 -
1519 - free(server->sessions);
1520 - server->sessions = NULL;
1521 - server->session_capacity = 0;
1522 - pthread_mutex_destroy(&server->sessions_lock);
1523 - }
1524 -
1525 - server->worker_count = 0;
1526 -}
1527 -
1528 -/* ------------------------------------------------------------------ */
1529 -/* L3: Client-side cgroups snapshot cache */
1530 -/* ------------------------------------------------------------------ */
1531 -
1532 -/* Free all owned strings in cache items and the items array itself. */
1533 -static void cache_free_items(nipc_cgroups_cache_item_t *items, uint32_t count)
1534 -{
1535 - if (!items)
1536 - return;
1537 -
1538 - for (uint32_t i = 0; i < count; i++) {
1539 - free(items[i].name);
1540 - free(items[i].path);
1541 - }
1542 - free(items);
1543 -}
1544 -
1545 -/* Hash a name string (djb2). Combined with item hash for bucket index. */
1546 -static uint32_t cache_hash_name(const char *name)
1547 -{
1548 - uint32_t h = 5381;
1549 - for (const unsigned char *p = (const unsigned char *)name; *p; p++)
1550 - h = ((h << 5) + h) + *p;
1551 - return h;
1552 -}
1553 -
1554 -/*
1555 - * Build the open-addressing hash table from the items array.
1556 - * Uses (item.hash ^ name_hash) as the probe key.
1557 - * Load factor <= 0.5 (bucket_count >= 2 * item_count).
1558 - */
1559 -static bool cache_build_hashtable(nipc_cgroups_cache_t *cache)
1560 -{
1561 - free(cache->buckets);
1562 - cache->buckets = NULL;
1563 - cache->bucket_count = 0;
1564 -
1565 - if (cache->item_count == 0)
1566 - return true;
1567 -
1568 - uint32_t bcount = next_power_of_2_u32(cache->item_count * 2);
1569 - nipc_cgroups_hash_bucket_t *buckets = service_calloc(bcount,
1570 - sizeof(nipc_cgroups_hash_bucket_t),
1571 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL);
1572 - if (!buckets)
1573 - return false;
1574 -
1575 - uint32_t mask = bcount - 1;
1576 - for (uint32_t i = 0; i < cache->item_count; i++) {
1577 - uint32_t key = cache->items[i].hash ^ cache_hash_name(cache->items[i].name);
1578 - uint32_t slot = key & mask;
1579 -
1580 - /* Linear probe for an empty bucket */
1581 - while (buckets[slot].used)
1582 - slot = (slot + 1) & mask;
1583 -
1584 - buckets[slot].index = i;
1585 - buckets[slot].used = true;
1586 - }
1587 -
1588 - cache->buckets = buckets;
1589 - cache->bucket_count = bcount;
1590 - return true;
1591 -}
1592 -
1593 -/*
1594 - * Build a new cache from a decoded snapshot view. Copies all strings
1595 - * from the ephemeral view into owned heap allocations.
1596 - *
1597 - * Returns the new items array and sets *count_out. Returns NULL on
1598 - * allocation failure.
1599 - */
1600 -static nipc_cgroups_cache_item_t *cache_build_items(
1601 - const nipc_cgroups_resp_view_t *view,
1602 - uint32_t *count_out)
1603 -{
1604 - uint32_t n = view->item_count;
1605 - *count_out = 0;
1606 -
1607 - if (n == 0)
1608 - return NULL; /* empty snapshot is valid */
1609 -
1610 - nipc_cgroups_cache_item_t *items = service_calloc(
1611 - n, sizeof(nipc_cgroups_cache_item_t),
1612 - NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL);
1613 - if (!items)
1614 - return NULL;
1615 -
1616 - for (uint32_t i = 0; i < n; i++) {
1617 - nipc_cgroups_item_view_t iv;
1618 - nipc_error_t err = nipc_cgroups_resp_item(view, i, &iv);
1619 - if (err != NIPC_OK) {
1620 - /* Malformed item: abort build, free partial */
1621 - cache_free_items(items, i);
1622 - return NULL;
1623 - }
1624 -
1625 - items[i].hash = iv.hash;
1626 - items[i].options = iv.options;
1627 - items[i].enabled = iv.enabled;
1628 -
1629 - /* Copy name (add NUL terminator) */
1630 - items[i].name = service_malloc(
1631 - iv.name.len + 1, NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL);
1632 - if (!items[i].name) {
1633 - cache_free_items(items, i);
1634 - return NULL;
1635 - }
1636 - if (iv.name.len > 0)
1637 - memcpy(items[i].name, iv.name.ptr, iv.name.len);
1638 - items[i].name[iv.name.len] = '\0';
1639 -
1640 - /* Copy path (add NUL terminator) */
1641 - items[i].path = service_malloc(
1642 - iv.path.len + 1, NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL);
1643 - if (!items[i].path) {
1644 - free(items[i].name);
1645 - cache_free_items(items, i);
1646 - return NULL;
1647 - }
1648 - if (iv.path.len > 0)
1649 - memcpy(items[i].path, iv.path.ptr, iv.path.len);
1650 - items[i].path[iv.path.len] = '\0';
1651 - }
1652 -
1653 - *count_out = n;
1654 - return items;
1655 -}
1656 -
1657 -void nipc_cgroups_cache_init(nipc_cgroups_cache_t *cache,
1658 - const char *run_dir,
1659 - const char *service_name,
1660 - const nipc_client_config_t *config)
1661 -{
1662 - memset(cache, 0, sizeof(*cache));
1663 -
1664 - nipc_client_init(&cache->client, run_dir, service_name, config);
1665 -
1666 - cache->items = NULL;
1667 - cache->item_count = 0;
1668 - cache->systemd_enabled = 0;
1669 - cache->generation = 0;
1670 - cache->populated = false;
1671 - cache->buckets = NULL;
1672 - cache->bucket_count = 0;
1673 - cache->refresh_success_count = 0;
1674 - cache->refresh_failure_count = 0;
1675 -
1676 - cache->response_buf = NULL;
1677 - cache->response_buf_size = 0;
1678 -}
1679 -
1680 -bool nipc_cgroups_cache_refresh(nipc_cgroups_cache_t *cache)
1681 -{
1682 - /* Drive L2 connection lifecycle */
1683 - nipc_client_refresh(&cache->client);
1684 -
1685 - /* Attempt snapshot call */
1686 - nipc_cgroups_resp_view_t view;
1687 - nipc_error_t err = nipc_client_call_cgroups_snapshot(&cache->client, &view);
1688 -
1689 - if (err != NIPC_OK) {
1690 - /* Refresh failed -- preserve previous cache */
1691 - cache->refresh_failure_count++;
1692 - return false;
1693 - }
1694 -
1695 - /* Build new cache from the snapshot view */
1696 - uint32_t new_count = 0;
1697 - nipc_cgroups_cache_item_t *new_items = NULL;
1698 -
1699 - if (view.item_count > 0) {
1700 - new_items = cache_build_items(&view, &new_count);
1701 - if (!new_items && view.item_count > 0) {
1702 - /* Build failed (allocation error) -- preserve old cache */
1703 - cache->refresh_failure_count++;
1704 - return false;
1705 - }
1706 - }
1707 -
1708 - /* Replace old cache with new one */
1709 - cache_free_items(cache->items, cache->item_count);
1710 - cache->items = new_items;
1711 - cache->item_count = new_count;
1712 - cache->systemd_enabled = view.systemd_enabled;
1713 - cache->generation = view.generation;
1714 - cache->populated = true;
1715 - cache->refresh_success_count++;
1716 -
1717 - /* Record monotonic timestamp */
1718 - struct timespec ts;
1719 - clock_gettime(CLOCK_MONOTONIC, &ts);
1720 - cache->last_refresh_ts = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
1721 -
1722 - /* Rebuild hash table for O(1) lookup */
1723 - cache_build_hashtable(cache);
1724 -
1725 - return true;
1726 -}
1727 -
1728 -const nipc_cgroups_cache_item_t *nipc_cgroups_cache_lookup(
1729 - const nipc_cgroups_cache_t *cache,
1730 - uint32_t hash,
1731 - const char *name)
1732 -{
1733 - if (!cache->populated || !cache->items || !name)
1734 - return NULL;
1735 -
1736 - /* Use hash table if available, fall back to linear scan */
1737 - if (cache->buckets && cache->bucket_count > 0) {
1738 - uint32_t key = hash ^ cache_hash_name(name);
1739 - uint32_t mask = cache->bucket_count - 1;
1740 - uint32_t slot = key & mask;
1741 -
1742 - while (cache->buckets[slot].used) {
1743 - uint32_t idx = cache->buckets[slot].index;
1744 - if (cache->items[idx].hash == hash &&
1745 - strcmp(cache->items[idx].name, name) == 0) {
1746 - return &cache->items[idx];
1747 - }
1748 - slot = (slot + 1) & mask;
1749 - }
1750 - return NULL;
1751 - }
1752 -
1753 - /* Fallback linear scan (hash table allocation failed) */
1754 - for (uint32_t i = 0; i < cache->item_count; i++) {
1755 - if (cache->items[i].hash == hash &&
1756 - strcmp(cache->items[i].name, name) == 0) {
1757 - return &cache->items[i];
1758 - }
1759 - }
1760 -
1761 - return NULL;
1762 -}
1763 -
1764 -void nipc_cgroups_cache_status(const nipc_cgroups_cache_t *cache,
1765 - nipc_cgroups_cache_status_t *out)
1766 -{
1767 - out->populated = cache->populated;
1768 - out->item_count = cache->item_count;
1769 - out->systemd_enabled = cache->systemd_enabled;
1770 - out->generation = cache->generation;
1771 - out->refresh_success_count = cache->refresh_success_count;
1772 - out->refresh_failure_count = cache->refresh_failure_count;
1773 - out->connection_state = cache->client.state;
1774 - out->last_refresh_ts = cache->last_refresh_ts;
1775 -}
1776 -
1777 -void nipc_cgroups_cache_close(nipc_cgroups_cache_t *cache)
1778 -{
1779 - cache_free_items(cache->items, cache->item_count);
1780 - cache->items = NULL;
1781 - cache->item_count = 0;
1782 - cache->populated = false;
1783 -
1784 - free(cache->buckets);
1785 - cache->buckets = NULL;
1786 - cache->bucket_count = 0;
1787 -
1788 - free(cache->response_buf);
1789 - cache->response_buf = NULL;
1790 - cache->response_buf_size = 0;
1791 -
1792 - nipc_client_close(&cache->client);
1793 -}
166 +/* Managed server implementation lives in netipc_service_posix_server.c. */
src/libnetdata/netipc/src/service/netipc_service_apps_lookup.c new
+141
@@ -0,0 +1,141 @@
1 +#include "netipc_service_platform.h"
2 +
3 +#include "netipc/netipc_protocol.h"
4 +
5 +static bool apps_lookup_request_size(uint32_t pid_count, size_t *size_out)
6 +{
7 + if (nipc_service_common_mul_would_overflow(
8 + (size_t)pid_count, NIPC_LOOKUP_DIR_ENTRY_SIZE) ||
9 + nipc_service_common_mul_would_overflow(
10 + (size_t)pid_count, NIPC_APPS_LOOKUP_KEY_SIZE))
11 + return false;
12 +
13 + size_t dir = (size_t)pid_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
14 + size_t keys = (size_t)pid_count * NIPC_APPS_LOOKUP_KEY_SIZE;
15 +#if SIZE_MAX <= UINT32_MAX
16 + if (NIPC_APPS_LOOKUP_REQ_HDR_SIZE > SIZE_MAX - dir ||
17 + NIPC_APPS_LOOKUP_REQ_HDR_SIZE + dir > SIZE_MAX - keys)
18 + return false;
19 +#endif
20 + *size_out = NIPC_APPS_LOOKUP_REQ_HDR_SIZE + dir + keys;
21 + return true;
22 +}
23 +
24 +static nipc_error_t apps_lookup_dispatch(void *user,
25 + const nipc_header_t *request_hdr,
26 + const uint8_t *request_payload,
27 + size_t request_len,
28 + uint8_t *response_buf,
29 + size_t response_buf_size,
30 + size_t *response_len_out)
31 +{
32 + nipc_apps_lookup_service_handler_t *service_handler =
33 + (nipc_apps_lookup_service_handler_t *)user;
34 + (void)request_hdr;
35 +
36 + if (!service_handler->handle)
37 + return NIPC_ERR_HANDLER_FAILED;
38 +
39 + return nipc_dispatch_apps_lookup(
40 + request_payload, request_len,
41 + response_buf, response_buf_size, response_len_out,
42 + service_handler->handle, service_handler->user);
43 +}
44 +
45 +typedef struct {
46 + const uint32_t *pids;
47 + uint32_t pid_count;
48 + nipc_apps_lookup_resp_view_t *view_out;
49 +} apps_lookup_call_state_t;
50 +
51 +static nipc_error_t do_apps_lookup_attempt(nipc_client_ctx_t *ctx, void *state)
52 +{
53 + apps_lookup_call_state_t *s = (apps_lookup_call_state_t *)state;
54 +
55 + size_t req_size;
56 + if (!apps_lookup_request_size(s->pid_count, &req_size))
57 + return NIPC_ERR_OVERFLOW;
58 + if (req_size > UINT32_MAX)
59 + return NIPC_ERR_OVERFLOW;
60 + if (ctx->session.max_request_payload_bytes > 0 &&
61 + req_size > ctx->session.max_request_payload_bytes) {
62 + nipc_service_common_client_note_request_capacity(ctx, (uint32_t)req_size);
63 + return NIPC_ERR_OVERFLOW;
64 + }
65 + if (!nipc_service_platform_ensure_client_send_buffer(ctx, req_size))
66 + return NIPC_ERR_OVERFLOW;
67 +
68 + size_t req_len = nipc_apps_lookup_req_encode(
69 + s->pids, s->pid_count, ctx->send_buf, ctx->send_buf_size);
70 + if (req_len == 0)
71 + return NIPC_ERR_BAD_LAYOUT;
72 +
73 + const void *payload;
74 + size_t payload_len;
75 + nipc_error_t err = nipc_service_platform_do_raw_call(
76 + ctx, NIPC_METHOD_APPS_LOOKUP, ctx->send_buf, req_len,
77 + &payload, &payload_len);
78 + if (err != NIPC_OK)
79 + return err;
80 +
81 + err = nipc_apps_lookup_resp_decode(payload, payload_len, s->view_out);
82 + if (err != NIPC_OK)
83 + return err;
84 + if (s->view_out->item_count != s->pid_count)
85 + return NIPC_ERR_BAD_ITEM_COUNT;
86 + for (uint32_t i = 0; i < s->pid_count; i++) {
87 + nipc_apps_lookup_item_view_t item;
88 + err = nipc_apps_lookup_resp_item(s->view_out, i, &item);
89 + if (err != NIPC_OK)
90 + return err;
91 + if (item.pid != s->pids[i])
92 + return NIPC_ERR_BAD_LAYOUT;
93 + }
94 + return NIPC_OK;
95 +}
96 +
97 +nipc_error_t nipc_client_call_apps_lookup(
98 + nipc_client_ctx_t *ctx,
99 + const uint32_t *pids,
100 + uint32_t pid_count,
101 + nipc_apps_lookup_resp_view_t *view_out)
102 +{
103 + apps_lookup_call_state_t state = {
104 + .pids = pids,
105 + .pid_count = pid_count,
106 + .view_out = view_out,
107 + };
108 + return nipc_service_platform_call_with_retry(
109 + ctx, do_apps_lookup_attempt, &state);
110 +}
111 +
112 +nipc_error_t nipc_server_init_apps_lookup(
113 + nipc_managed_server_t *server,
114 + const char *run_dir,
115 + const char *service_name,
116 + const nipc_server_config_t *config,
117 + int worker_count,
118 + const nipc_apps_lookup_service_handler_t *service_handler)
119 +{
120 + if (!service_handler)
121 + return NIPC_ERR_BAD_LAYOUT;
122 +
123 + nipc_service_platform_server_config_t typed_cfg;
124 + nipc_service_platform_server_config_from_service(&typed_cfg, config);
125 + if (typed_cfg.max_request_payload_bytes == 0)
126 + typed_cfg.max_request_payload_bytes =
127 + nipc_service_common_response_payload_default();
128 + if (typed_cfg.max_response_payload_bytes == 0)
129 + typed_cfg.max_response_payload_bytes =
130 + nipc_service_common_response_payload_default();
131 +
132 + nipc_error_t err = nipc_service_platform_server_init_raw(
133 + server, run_dir, service_name, &typed_cfg, worker_count,
134 + NIPC_METHOD_APPS_LOOKUP, apps_lookup_dispatch,
135 + &server->typed_handler.apps_lookup);
136 + if (err != NIPC_OK)
137 + return err;
138 +
139 + server->typed_handler.apps_lookup = *service_handler;
140 + return NIPC_OK;
141 +}
src/libnetdata/netipc/src/service/netipc_service_cgroups_cache.c new
+48
@@ -0,0 +1,48 @@
1 +#include "netipc_service_cgroups_cache_common.h"
2 +#include "netipc_service_platform.h"
3 +
4 +static const nipc_service_common_cache_ops_t cgroups_cache_ops = {
5 + .malloc_fn = nipc_service_platform_malloc,
6 + .calloc_fn = nipc_service_platform_calloc,
7 + .monotonic_ms_fn = nipc_service_platform_monotonic_ms,
8 + .cache_buckets_fault_site =
9 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL,
10 + .cache_items_fault_site =
11 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
12 + .cache_item_name_fault_site =
13 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
14 + .cache_item_path_fault_site =
15 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
16 +};
17 +
18 +void nipc_cgroups_cache_init(nipc_cgroups_cache_t *cache,
19 + const char *run_dir,
20 + const char *service_name,
21 + const nipc_client_config_t *config)
22 +{
23 + nipc_service_common_cgroups_cache_init(cache, run_dir, service_name, config);
24 +}
25 +
26 +bool nipc_cgroups_cache_refresh(nipc_cgroups_cache_t *cache)
27 +{
28 + return nipc_service_common_cgroups_cache_refresh(cache, &cgroups_cache_ops);
29 +}
30 +
31 +const nipc_cgroups_cache_item_t *nipc_cgroups_cache_lookup(
32 + const nipc_cgroups_cache_t *cache,
33 + uint32_t hash,
34 + const char *name)
35 +{
36 + return nipc_service_common_cgroups_cache_lookup(cache, hash, name);
37 +}
38 +
39 +void nipc_cgroups_cache_status(const nipc_cgroups_cache_t *cache,
40 + nipc_cgroups_cache_status_t *out)
41 +{
42 + nipc_service_common_cgroups_cache_status(cache, out);
43 +}
44 +
45 +void nipc_cgroups_cache_close(nipc_cgroups_cache_t *cache)
46 +{
47 + nipc_service_common_cgroups_cache_close(cache);
48 +}
src/libnetdata/netipc/src/service/netipc_service_cgroups_cache_common.c new
+223
@@ -0,0 +1,223 @@
1 +#include "netipc_service_cgroups_cache_common.h"
2 +
3 +#include "netipc/netipc_protocol.h"
4 +
5 +#include <stdlib.h>
6 +#include <string.h>
7 +
8 +static void cache_free_items(nipc_cgroups_cache_item_t *items, uint32_t count)
9 +{
10 + if (!items)
11 + return;
12 +
13 + for (uint32_t i = 0; i < count; i++) {
14 + free(items[i].name);
15 + free(items[i].path);
16 + }
17 + free(items);
18 +}
19 +
20 +static uint32_t cache_hash_name(const char *name)
21 +{
22 + uint32_t h = 5381;
23 + for (const unsigned char *p = (const unsigned char *)name; *p; p++)
24 + h = ((h << 5) + h) + *p;
25 + return h;
26 +}
27 +
28 +static bool cache_build_hashtable(nipc_cgroups_cache_t *cache,
29 + const nipc_service_common_cache_ops_t *ops)
30 +{
31 + free(cache->buckets);
32 + cache->buckets = NULL;
33 + cache->bucket_count = 0;
34 +
35 + if (cache->item_count == 0)
36 + return true;
37 +
38 + uint32_t bcount = nipc_service_common_next_power_of_2_u32(cache->item_count * 2);
39 + nipc_cgroups_hash_bucket_t *buckets = ops->calloc_fn(
40 + bcount, sizeof(nipc_cgroups_hash_bucket_t),
41 + ops->cache_buckets_fault_site);
42 + if (!buckets)
43 + return false;
44 +
45 + uint32_t mask = bcount - 1;
46 + for (uint32_t i = 0; i < cache->item_count; i++) {
47 + uint32_t key = cache->items[i].hash ^ cache_hash_name(cache->items[i].name);
48 + uint32_t slot = key & mask;
49 +
50 + while (buckets[slot].used)
51 + slot = (slot + 1) & mask;
52 +
53 + buckets[slot].index = i;
54 + buckets[slot].used = true;
55 + }
56 +
57 + cache->buckets = buckets;
58 + cache->bucket_count = bcount;
59 + return true;
60 +}
61 +
62 +static nipc_cgroups_cache_item_t *cache_build_items(
63 + const nipc_cgroups_resp_view_t *view,
64 + const nipc_service_common_cache_ops_t *ops,
65 + uint32_t *count_out)
66 +{
67 + uint32_t n = view->item_count;
68 + *count_out = 0;
69 +
70 + if (n == 0)
71 + return NULL;
72 +
73 + nipc_cgroups_cache_item_t *items = ops->calloc_fn(
74 + n, sizeof(nipc_cgroups_cache_item_t),
75 + ops->cache_items_fault_site);
76 + if (!items)
77 + return NULL;
78 +
79 + for (uint32_t i = 0; i < n; i++) {
80 + nipc_cgroups_item_view_t iv;
81 + nipc_error_t err = nipc_cgroups_resp_item(view, i, &iv);
82 + if (err != NIPC_OK) {
83 + cache_free_items(items, i);
84 + return NULL;
85 + }
86 +
87 + items[i].hash = iv.hash;
88 + items[i].options = iv.options;
89 + items[i].enabled = iv.enabled;
90 +
91 + items[i].name = ops->malloc_fn(
92 + iv.name.len + 1, ops->cache_item_name_fault_site);
93 + if (!items[i].name) {
94 + cache_free_items(items, i);
95 + return NULL;
96 + }
97 + if (iv.name.len > 0)
98 + memcpy(items[i].name, iv.name.ptr, iv.name.len);
99 + items[i].name[iv.name.len] = '\0';
100 +
101 + items[i].path = ops->malloc_fn(
102 + iv.path.len + 1, ops->cache_item_path_fault_site);
103 + if (!items[i].path) {
104 + free(items[i].name);
105 + cache_free_items(items, i);
106 + return NULL;
107 + }
108 + if (iv.path.len > 0)
109 + memcpy(items[i].path, iv.path.ptr, iv.path.len);
110 + items[i].path[iv.path.len] = '\0';
111 + }
112 +
113 + *count_out = n;
114 + return items;
115 +}
116 +
117 +void nipc_service_common_cgroups_cache_init(nipc_cgroups_cache_t *cache,
118 + const char *run_dir,
119 + const char *service_name,
120 + const nipc_client_config_t *config)
121 +{
122 + memset(cache, 0, sizeof(*cache));
123 + nipc_client_init(&cache->client, run_dir, service_name, config);
124 +}
125 +
126 +bool nipc_service_common_cgroups_cache_refresh(
127 + nipc_cgroups_cache_t *cache,
128 + const nipc_service_common_cache_ops_t *ops)
129 +{
130 + nipc_client_refresh(&cache->client);
131 +
132 + nipc_cgroups_resp_view_t view;
133 + nipc_error_t err = nipc_client_call_cgroups_snapshot(&cache->client, &view);
134 + if (err != NIPC_OK) {
135 + cache->refresh_failure_count++;
136 + return false;
137 + }
138 +
139 + uint32_t new_count = 0;
140 + nipc_cgroups_cache_item_t *new_items = NULL;
141 + if (view.item_count > 0) {
142 + new_items = cache_build_items(&view, ops, &new_count);
143 + if (!new_items) {
144 + cache->refresh_failure_count++;
145 + return false;
146 + }
147 + }
148 +
149 + cache_free_items(cache->items, cache->item_count);
150 + cache->items = new_items;
151 + cache->item_count = new_count;
152 + cache->systemd_enabled = view.systemd_enabled;
153 + cache->generation = view.generation;
154 + cache->populated = true;
155 + cache->refresh_success_count++;
156 + cache->last_refresh_ts = ops->monotonic_ms_fn();
157 + cache_build_hashtable(cache, ops);
158 +
159 + return true;
160 +}
161 +
162 +const nipc_cgroups_cache_item_t *nipc_service_common_cgroups_cache_lookup(
163 + const nipc_cgroups_cache_t *cache,
164 + uint32_t hash,
165 + const char *name)
166 +{
167 + if (!cache->populated || !cache->items || !name)
168 + return NULL;
169 +
170 + if (cache->buckets && cache->bucket_count > 0) {
171 + uint32_t key = hash ^ cache_hash_name(name);
172 + uint32_t mask = cache->bucket_count - 1;
173 + uint32_t slot = key & mask;
174 +
175 + while (cache->buckets[slot].used) {
176 + uint32_t idx = cache->buckets[slot].index;
177 + if (cache->items[idx].hash == hash &&
178 + strcmp(cache->items[idx].name, name) == 0)
179 + return &cache->items[idx];
180 + slot = (slot + 1) & mask;
181 + }
182 + return NULL;
183 + }
184 +
185 + for (uint32_t i = 0; i < cache->item_count; i++) {
186 + if (cache->items[i].hash == hash &&
187 + strcmp(cache->items[i].name, name) == 0)
188 + return &cache->items[i];
189 + }
190 +
191 + return NULL;
192 +}
193 +
194 +void nipc_service_common_cgroups_cache_status(const nipc_cgroups_cache_t *cache,
195 + nipc_cgroups_cache_status_t *out)
196 +{
197 + out->populated = cache->populated;
198 + out->item_count = cache->item_count;
199 + out->systemd_enabled = cache->systemd_enabled;
200 + out->generation = cache->generation;
201 + out->refresh_success_count = cache->refresh_success_count;
202 + out->refresh_failure_count = cache->refresh_failure_count;
203 + out->connection_state = cache->client.state;
204 + out->last_refresh_ts = cache->last_refresh_ts;
205 +}
206 +
207 +void nipc_service_common_cgroups_cache_close(nipc_cgroups_cache_t *cache)
208 +{
209 + cache_free_items(cache->items, cache->item_count);
210 + cache->items = NULL;
211 + cache->item_count = 0;
212 + cache->populated = false;
213 +
214 + free(cache->buckets);
215 + cache->buckets = NULL;
216 + cache->bucket_count = 0;
217 +
218 + free(cache->response_buf);
219 + cache->response_buf = NULL;
220 + cache->response_buf_size = 0;
221 +
222 + nipc_client_close(&cache->client);
223 +}
src/libnetdata/netipc/src/service/netipc_service_cgroups_cache_common.h new
+29
@@ -0,0 +1,29 @@
1 +#ifndef NETIPC_SERVICE_CGROUPS_CACHE_COMMON_H
2 +#define NETIPC_SERVICE_CGROUPS_CACHE_COMMON_H
3 +
4 +#include "netipc_service_common.h"
5 +
6 +#ifdef __cplusplus
7 +extern "C" {
8 +#endif
9 +
10 +void nipc_service_common_cgroups_cache_init(nipc_cgroups_cache_t *cache,
11 + const char *run_dir,
12 + const char *service_name,
13 + const nipc_client_config_t *config);
14 +bool nipc_service_common_cgroups_cache_refresh(
15 + nipc_cgroups_cache_t *cache,
16 + const nipc_service_common_cache_ops_t *ops);
17 +const nipc_cgroups_cache_item_t *nipc_service_common_cgroups_cache_lookup(
18 + const nipc_cgroups_cache_t *cache,
19 + uint32_t hash,
20 + const char *name);
21 +void nipc_service_common_cgroups_cache_status(const nipc_cgroups_cache_t *cache,
22 + nipc_cgroups_cache_status_t *out);
23 +void nipc_service_common_cgroups_cache_close(nipc_cgroups_cache_t *cache);
24 +
25 +#ifdef __cplusplus
26 +}
27 +#endif
28 +
29 +#endif /* NETIPC_SERVICE_CGROUPS_CACHE_COMMON_H */
src/libnetdata/netipc/src/service/netipc_service_cgroups_lookup.c new
+148
@@ -0,0 +1,148 @@
1 +#include "netipc_service_platform.h"
2 +
3 +#include "netipc/netipc_protocol.h"
4 +
5 +#include <string.h>
6 +
7 +static bool cgroups_lookup_request_size(const nipc_str_view_t *paths,
8 + uint32_t path_count,
9 + size_t *size_out)
10 +{
11 + if (nipc_service_common_mul_would_overflow(
12 + (size_t)path_count, NIPC_LOOKUP_DIR_ENTRY_SIZE))
13 + return false;
14 +
15 + size_t data = NIPC_CGROUPS_LOOKUP_REQ_HDR_SIZE +
16 + (size_t)path_count * NIPC_LOOKUP_DIR_ENTRY_SIZE;
17 + for (uint32_t i = 0; i < path_count; i++) {
18 + size_t aligned = nipc_align8(data);
19 + if (aligned < data)
20 + return false;
21 + if (!paths || paths[i].len > SIZE_MAX - aligned - 1u)
22 + return false;
23 + data = aligned + (size_t)paths[i].len + 1u;
24 + }
25 + *size_out = data;
26 + return true;
27 +}
28 +
29 +static nipc_error_t cgroups_lookup_dispatch(void *user,
30 + const nipc_header_t *request_hdr,
31 + const uint8_t *request_payload,
32 + size_t request_len,
33 + uint8_t *response_buf,
34 + size_t response_buf_size,
35 + size_t *response_len_out)
36 +{
37 + nipc_cgroups_lookup_service_handler_t *service_handler =
38 + (nipc_cgroups_lookup_service_handler_t *)user;
39 + (void)request_hdr;
40 +
41 + if (!service_handler->handle)
42 + return NIPC_ERR_HANDLER_FAILED;
43 +
44 + return nipc_dispatch_cgroups_lookup(
45 + request_payload, request_len,
46 + response_buf, response_buf_size, response_len_out,
47 + service_handler->handle, service_handler->user);
48 +}
49 +
50 +typedef struct {
51 + const nipc_str_view_t *paths;
52 + uint32_t path_count;
53 + nipc_cgroups_lookup_resp_view_t *view_out;
54 +} cgroups_lookup_call_state_t;
55 +
56 +static nipc_error_t do_cgroups_lookup_attempt(nipc_client_ctx_t *ctx,
57 + void *state)
58 +{
59 + cgroups_lookup_call_state_t *s = (cgroups_lookup_call_state_t *)state;
60 +
61 + size_t req_size;
62 + if (!cgroups_lookup_request_size(s->paths, s->path_count, &req_size))
63 + return NIPC_ERR_OVERFLOW;
64 + if (req_size > UINT32_MAX)
65 + return NIPC_ERR_OVERFLOW;
66 + if (ctx->session.max_request_payload_bytes > 0 &&
67 + req_size > ctx->session.max_request_payload_bytes) {
68 + nipc_service_common_client_note_request_capacity(ctx, (uint32_t)req_size);
69 + return NIPC_ERR_OVERFLOW;
70 + }
71 + if (!nipc_service_platform_ensure_client_send_buffer(ctx, req_size))
72 + return NIPC_ERR_OVERFLOW;
73 +
74 + size_t req_len = nipc_cgroups_lookup_req_encode(
75 + s->paths, s->path_count, ctx->send_buf, ctx->send_buf_size);
76 + if (req_len == 0)
77 + return NIPC_ERR_BAD_LAYOUT;
78 +
79 + const void *payload;
80 + size_t payload_len;
81 + nipc_error_t err = nipc_service_platform_do_raw_call(
82 + ctx, NIPC_METHOD_CGROUPS_LOOKUP, ctx->send_buf, req_len,
83 + &payload, &payload_len);
84 + if (err != NIPC_OK)
85 + return err;
86 +
87 + err = nipc_cgroups_lookup_resp_decode(payload, payload_len, s->view_out);
88 + if (err != NIPC_OK)
89 + return err;
90 + if (s->view_out->item_count != s->path_count)
91 + return NIPC_ERR_BAD_ITEM_COUNT;
92 + for (uint32_t i = 0; i < s->path_count; i++) {
93 + nipc_cgroups_lookup_item_view_t item;
94 + err = nipc_cgroups_lookup_resp_item(s->view_out, i, &item);
95 + if (err != NIPC_OK)
96 + return err;
97 + if (item.path.len != s->paths[i].len ||
98 + memcmp(item.path.ptr, s->paths[i].ptr, item.path.len) != 0)
99 + return NIPC_ERR_BAD_LAYOUT;
100 + }
101 + return NIPC_OK;
102 +}
103 +
104 +nipc_error_t nipc_client_call_cgroups_lookup(
105 + nipc_client_ctx_t *ctx,
106 + const nipc_str_view_t *paths,
107 + uint32_t path_count,
108 + nipc_cgroups_lookup_resp_view_t *view_out)
109 +{
110 + cgroups_lookup_call_state_t state = {
111 + .paths = paths,
112 + .path_count = path_count,
113 + .view_out = view_out,
114 + };
115 + return nipc_service_platform_call_with_retry(
116 + ctx, do_cgroups_lookup_attempt, &state);
117 +}
118 +
119 +nipc_error_t nipc_server_init_cgroups_lookup(
120 + nipc_managed_server_t *server,
121 + const char *run_dir,
122 + const char *service_name,
123 + const nipc_server_config_t *config,
124 + int worker_count,
125 + const nipc_cgroups_lookup_service_handler_t *service_handler)
126 +{
127 + if (!service_handler)
128 + return NIPC_ERR_BAD_LAYOUT;
129 +
130 + nipc_service_platform_server_config_t typed_cfg;
131 + nipc_service_platform_server_config_from_service(&typed_cfg, config);
132 + if (typed_cfg.max_request_payload_bytes == 0)
133 + typed_cfg.max_request_payload_bytes =
134 + nipc_service_common_response_payload_default();
135 + if (typed_cfg.max_response_payload_bytes == 0)
136 + typed_cfg.max_response_payload_bytes =
137 + nipc_service_common_response_payload_default();
138 +
139 + nipc_error_t err = nipc_service_platform_server_init_raw(
140 + server, run_dir, service_name, &typed_cfg, worker_count,
141 + NIPC_METHOD_CGROUPS_LOOKUP, cgroups_lookup_dispatch,
142 + &server->typed_handler.cgroups_lookup);
143 + if (err != NIPC_OK)
144 + return err;
145 +
146 + server->typed_handler.cgroups_lookup = *service_handler;
147 + return NIPC_OK;
148 +}
src/libnetdata/netipc/src/service/netipc_service_cgroups_snapshot.c new
+102
@@ -0,0 +1,102 @@
1 +#include "netipc_service_platform.h"
2 +
3 +#include "netipc/netipc_protocol.h"
4 +
5 +static uint32_t snapshot_max_items(
6 + size_t response_buf_size,
7 + const nipc_cgroups_service_handler_t *service_handler)
8 +{
9 + if (service_handler->snapshot_max_items != 0)
10 + return service_handler->snapshot_max_items;
11 + return nipc_cgroups_builder_estimate_max_items(response_buf_size);
12 +}
13 +
14 +static nipc_error_t cgroups_snapshot_dispatch(void *user,
15 + const nipc_header_t *request_hdr,
16 + const uint8_t *request_payload,
17 + size_t request_len,
18 + uint8_t *response_buf,
19 + size_t response_buf_size,
20 + size_t *response_len_out)
21 +{
22 + nipc_cgroups_service_handler_t *service_handler =
23 + (nipc_cgroups_service_handler_t *)user;
24 + (void)request_hdr;
25 +
26 + if (!service_handler->handle)
27 + return NIPC_ERR_HANDLER_FAILED;
28 +
29 + return nipc_dispatch_cgroups_snapshot(
30 + request_payload, request_len,
31 + response_buf, response_buf_size, response_len_out,
32 + snapshot_max_items(response_buf_size, service_handler),
33 + service_handler->handle, service_handler->user);
34 +}
35 +
36 +typedef struct {
37 + nipc_cgroups_resp_view_t *view_out;
38 +} cgroups_snapshot_call_state_t;
39 +
40 +static nipc_error_t do_cgroups_snapshot_attempt(nipc_client_ctx_t *ctx,
41 + void *state)
42 +{
43 + cgroups_snapshot_call_state_t *s = (cgroups_snapshot_call_state_t *)state;
44 +
45 + nipc_cgroups_req_t req = { .layout_version = 1, .flags = 0 };
46 + uint8_t req_buf[4];
47 + size_t req_len = nipc_cgroups_req_encode(&req, req_buf, sizeof(req_buf));
48 + if (req_len == 0)
49 + return NIPC_ERR_TRUNCATED;
50 +
51 + const void *payload;
52 + size_t payload_len;
53 + nipc_error_t err = nipc_service_platform_do_raw_call(
54 + ctx, NIPC_METHOD_CGROUPS_SNAPSHOT, req_buf, req_len,
55 + &payload, &payload_len);
56 + if (err != NIPC_OK)
57 + return err;
58 +
59 + return nipc_cgroups_resp_decode(payload, payload_len, s->view_out);
60 +}
61 +
62 +nipc_error_t nipc_client_call_cgroups_snapshot(
63 + nipc_client_ctx_t *ctx,
64 + nipc_cgroups_resp_view_t *view_out)
65 +{
66 + cgroups_snapshot_call_state_t state = {
67 + .view_out = view_out,
68 + };
69 + return nipc_service_platform_call_with_retry(
70 + ctx, do_cgroups_snapshot_attempt, &state);
71 +}
72 +
73 +nipc_error_t nipc_server_init_typed(
74 + nipc_managed_server_t *server,
75 + const char *run_dir,
76 + const char *service_name,
77 + const nipc_server_config_t *config,
78 + int worker_count,
79 + const nipc_cgroups_service_handler_t *service_handler)
80 +{
81 + if (!service_handler)
82 + return NIPC_ERR_BAD_LAYOUT;
83 +
84 + nipc_service_platform_server_config_t typed_cfg;
85 + nipc_service_platform_server_config_from_service(&typed_cfg, config);
86 + if (typed_cfg.max_request_payload_bytes == 0)
87 + typed_cfg.max_request_payload_bytes =
88 + nipc_service_common_request_payload_default();
89 + if (typed_cfg.max_response_payload_bytes == 0)
90 + typed_cfg.max_response_payload_bytes =
91 + nipc_service_common_response_payload_default();
92 +
93 + nipc_error_t err = nipc_service_platform_server_init_raw(
94 + server, run_dir, service_name, &typed_cfg, worker_count,
95 + NIPC_METHOD_CGROUPS_SNAPSHOT, cgroups_snapshot_dispatch,
96 + &server->typed_handler.cgroups_snapshot);
97 + if (err != NIPC_OK)
98 + return err;
99 +
100 + server->typed_handler.cgroups_snapshot = *service_handler;
101 + return NIPC_OK;
102 +}
src/libnetdata/netipc/src/service/netipc_service_common.c new
+576
@@ -0,0 +1,576 @@
1 +#include "netipc_service_common.h"
2 +
3 +#include "netipc/netipc_protocol.h"
4 +
5 +#include <stdlib.h>
6 +#include <string.h>
7 +
8 +#define NIPC_SERVICE_RESPONSE_BUF_DEFAULT 65536u
9 +
10 +uint32_t nipc_service_common_next_power_of_2_u32(uint32_t n)
11 +{
12 + if (n < 16)
13 + return 16;
14 + n--;
15 + n |= n >> 1;
16 + n |= n >> 2;
17 + n |= n >> 4;
18 + n |= n >> 8;
19 + n |= n >> 16;
20 + return n + 1;
21 +}
22 +
23 +bool nipc_service_common_header_payload_len(size_t payload_len,
24 + size_t *msg_len_out)
25 +{
26 +#if SIZE_MAX <= UINT32_MAX
27 + if (payload_len > SIZE_MAX - NIPC_HEADER_LEN)
28 + return false;
29 +#endif
30 +
31 + *msg_len_out = NIPC_HEADER_LEN + payload_len;
32 + return true;
33 +}
34 +
35 +bool nipc_service_common_header_payload_len_u32(uint32_t payload_len,
36 + uint32_t *msg_len_out)
37 +{
38 + if (payload_len > UINT32_MAX - NIPC_HEADER_LEN)
39 + return false;
40 +
41 + *msg_len_out = payload_len + NIPC_HEADER_LEN;
42 + return true;
43 +}
44 +
45 +bool nipc_service_common_mul_would_overflow(size_t count, size_t size)
46 +{
47 + return size != 0 && count > SIZE_MAX / size;
48 +}
49 +
50 +uint32_t nipc_service_common_request_payload_default(void)
51 +{
52 + return NIPC_MAX_PAYLOAD_DEFAULT;
53 +}
54 +
55 +uint32_t nipc_service_common_response_payload_default(void)
56 +{
57 + return NIPC_SERVICE_RESPONSE_BUF_DEFAULT;
58 +}
59 +
60 +/* Typed services expose one batch-count knob; level 1 keeps request/response counts symmetric. */
61 +uint32_t nipc_service_common_typed_response_batch_items(uint32_t max_request_batch_items)
62 +{
63 + return max_request_batch_items;
64 +}
65 +
66 +void nipc_service_common_copy_cstr_field(char *dst, size_t dst_size, const char *src)
67 +{
68 + if (!dst || dst_size == 0)
69 + return;
70 +
71 + dst[0] = '\0';
72 + if (!src)
73 + return;
74 +
75 + size_t len = 0;
76 + size_t max_copy = dst_size - 1;
77 + while (len < max_copy && src[len] != '\0')
78 + len++;
79 +
80 + memcpy(dst, src, len);
81 + dst[len] = '\0';
82 +}
83 +
84 +bool nipc_service_common_client_transport_fields(
85 + nipc_service_common_transport_fields_t *fields,
86 + const nipc_client_config_t *config)
87 +{
88 + memset(fields, 0, sizeof(*fields));
89 + if (!config)
90 + return false;
91 +
92 + fields->supported_profiles = config->supported_profiles;
93 + fields->preferred_profiles = config->preferred_profiles;
94 + fields->max_request_batch_items = config->max_request_batch_items;
95 + fields->max_response_payload_bytes = config->max_response_payload_bytes;
96 + fields->max_response_batch_items =
97 + nipc_service_common_typed_response_batch_items(config->max_request_batch_items);
98 + fields->auth_token = config->auth_token;
99 + return true;
100 +}
101 +
102 +bool nipc_service_common_server_transport_fields(
103 + nipc_service_common_transport_fields_t *fields,
104 + const nipc_server_config_t *config)
105 +{
106 + memset(fields, 0, sizeof(*fields));
107 + if (!config)
108 + return false;
109 +
110 + fields->supported_profiles = config->supported_profiles;
111 + fields->preferred_profiles = config->preferred_profiles;
112 + fields->max_request_batch_items = config->max_request_batch_items;
113 + fields->max_response_payload_bytes = config->max_response_payload_bytes;
114 + fields->max_response_batch_items =
115 + nipc_service_common_typed_response_batch_items(config->max_request_batch_items);
116 + fields->auth_token = config->auth_token;
117 + return true;
118 +}
119 +
120 +void nipc_service_common_client_init(nipc_client_ctx_t *ctx,
121 + const char *run_dir,
122 + const char *service_name)
123 +{
124 + memset(ctx, 0, sizeof(*ctx));
125 + ctx->state = NIPC_CLIENT_DISCONNECTED;
126 + ctx->session_valid = false;
127 + ctx->shm = NULL;
128 + nipc_service_common_copy_cstr_field(ctx->run_dir, sizeof(ctx->run_dir), run_dir);
129 + nipc_service_common_copy_cstr_field(ctx->service_name, sizeof(ctx->service_name), service_name);
130 +}
131 +
132 +void nipc_service_common_client_status(const nipc_client_ctx_t *ctx,
133 + nipc_client_status_t *out)
134 +{
135 + out->state = ctx->state;
136 + out->connect_count = ctx->connect_count;
137 + out->reconnect_count = ctx->reconnect_count;
138 + out->call_count = ctx->call_count;
139 + out->error_count = ctx->error_count;
140 +}
141 +
142 +void nipc_service_common_client_close_buffers(nipc_client_ctx_t *ctx)
143 +{
144 + free(ctx->response_buf);
145 + free(ctx->send_buf);
146 + ctx->response_buf = NULL;
147 + ctx->send_buf = NULL;
148 + ctx->response_buf_size = 0;
149 + ctx->send_buf_size = 0;
150 + ctx->state = NIPC_CLIENT_DISCONNECTED;
151 +}
152 +
153 +bool nipc_service_common_client_refresh(nipc_client_ctx_t *ctx,
154 + const nipc_service_common_client_ops_t *ops)
155 +{
156 + nipc_client_state_t old_state = ctx->state;
157 +
158 + switch (ctx->state) {
159 + case NIPC_CLIENT_DISCONNECTED:
160 + case NIPC_CLIENT_NOT_FOUND:
161 + ctx->state = NIPC_CLIENT_CONNECTING;
162 + ctx->state = ops->try_connect(ctx);
163 + if (ctx->state == NIPC_CLIENT_READY)
164 + ctx->connect_count++;
165 + break;
166 +
167 + case NIPC_CLIENT_BROKEN:
168 + ops->disconnect(ctx);
169 + ctx->state = NIPC_CLIENT_CONNECTING;
170 + ctx->state = ops->try_connect(ctx);
171 + if (ctx->state == NIPC_CLIENT_READY)
172 + ctx->reconnect_count++;
173 + break;
174 +
175 + case NIPC_CLIENT_READY:
176 + case NIPC_CLIENT_CONNECTING:
177 + case NIPC_CLIENT_AUTH_FAILED:
178 + case NIPC_CLIENT_INCOMPATIBLE:
179 + break;
180 + }
181 +
182 + return ctx->state != old_state;
183 +}
184 +
185 +void nipc_service_common_client_note_request_capacity(nipc_client_ctx_t *ctx,
186 + uint32_t payload_len)
187 +{
188 + uint32_t grown = nipc_service_common_next_power_of_2_u32(payload_len);
189 + if (grown > NIPC_MAX_PAYLOAD_CAP)
190 + grown = NIPC_MAX_PAYLOAD_CAP;
191 + if (grown > ctx->transport_config.max_request_payload_bytes)
192 + ctx->transport_config.max_request_payload_bytes = grown;
193 +}
194 +
195 +void nipc_service_common_client_note_response_capacity(nipc_client_ctx_t *ctx,
196 + uint32_t payload_len)
197 +{
198 + uint32_t grown = nipc_service_common_next_power_of_2_u32(payload_len);
199 + if (grown > NIPC_MAX_PAYLOAD_CAP)
200 + grown = NIPC_MAX_PAYLOAD_CAP;
201 + if (grown > ctx->transport_config.max_response_payload_bytes)
202 + ctx->transport_config.max_response_payload_bytes = grown;
203 +}
204 +
205 +nipc_error_t nipc_service_common_client_prepare_shm_request(
206 + nipc_client_ctx_t *ctx,
207 + nipc_header_t *hdr,
208 + const void *payload,
209 + size_t payload_len,
210 + uint8_t **msg_out,
211 + size_t *msg_len_out)
212 +{
213 + if (payload_len > UINT32_MAX)
214 + return NIPC_ERR_OVERFLOW;
215 +
216 + if (payload_len > ctx->session.max_request_payload_bytes) {
217 + nipc_service_common_client_note_request_capacity(
218 + ctx, (uint32_t)payload_len);
219 + return NIPC_ERR_OVERFLOW;
220 + }
221 +
222 + size_t msg_len;
223 + if (!nipc_service_common_header_payload_len(payload_len, &msg_len))
224 + return NIPC_ERR_OVERFLOW;
225 +
226 + uint8_t *msg = ctx->send_buf;
227 + if (!msg || msg_len > ctx->send_buf_size)
228 + return NIPC_ERR_OVERFLOW;
229 +
230 + if (payload_len > 0)
231 + memmove(msg + NIPC_HEADER_LEN, payload, payload_len);
232 +
233 + hdr->magic = NIPC_MAGIC_MSG;
234 + hdr->version = NIPC_VERSION;
235 + hdr->header_len = NIPC_HEADER_LEN;
236 + hdr->payload_len = (uint32_t)payload_len;
237 +
238 + nipc_header_encode(hdr, msg, NIPC_HEADER_LEN);
239 +
240 + *msg_out = msg;
241 + *msg_len_out = msg_len;
242 + return NIPC_OK;
243 +}
244 +
245 +nipc_error_t nipc_service_common_client_parse_shm_response(
246 + void *buf,
247 + size_t msg_len,
248 + nipc_header_t *hdr_out,
249 + const void **payload_out,
250 + size_t *payload_len_out)
251 +{
252 + if (msg_len < NIPC_HEADER_LEN)
253 + return NIPC_ERR_TRUNCATED;
254 +
255 + nipc_error_t perr = nipc_header_decode(buf, msg_len, hdr_out);
256 + if (perr != NIPC_OK)
257 + return perr;
258 +
259 + *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
260 + *payload_len_out = msg_len - NIPC_HEADER_LEN;
261 + return NIPC_OK;
262 +}
263 +
264 +nipc_error_t nipc_service_common_response_status_to_error(nipc_client_ctx_t *ctx,
265 + const nipc_header_t *resp_hdr)
266 +{
267 + switch (resp_hdr->transport_status) {
268 + case NIPC_STATUS_OK:
269 + return NIPC_OK;
270 + case NIPC_STATUS_LIMIT_EXCEEDED:
271 + if (ctx->session.max_response_payload_bytes > 0) {
272 + uint32_t current = ctx->session.max_response_payload_bytes;
273 + nipc_service_common_client_note_response_capacity(
274 + ctx, current >= UINT32_MAX / 2u ? UINT32_MAX : current * 2u);
275 + }
276 + return NIPC_ERR_OVERFLOW;
277 + case NIPC_STATUS_UNSUPPORTED:
278 + return NIPC_ERR_BAD_LAYOUT;
279 + case NIPC_STATUS_BAD_ENVELOPE:
280 + case NIPC_STATUS_INTERNAL_ERROR:
281 + default:
282 + return NIPC_ERR_BAD_LAYOUT;
283 + }
284 +}
285 +
286 +nipc_error_t nipc_service_common_do_raw_call(
287 + nipc_client_ctx_t *ctx,
288 + uint16_t method_code,
289 + const void *request_payload,
290 + size_t request_len,
291 + const void **response_payload_out,
292 + size_t *response_len_out,
293 + nipc_service_common_transport_send_fn send_fn,
294 + nipc_service_common_transport_receive_fn receive_fn)
295 +{
296 + nipc_header_t hdr = {0};
297 + hdr.kind = NIPC_KIND_REQUEST;
298 + hdr.code = method_code;
299 + hdr.flags = 0;
300 + hdr.item_count = 1;
301 + hdr.message_id = (uint64_t)(ctx->call_count + 1);
302 + hdr.transport_status = NIPC_STATUS_OK;
303 +
304 + nipc_error_t err = send_fn(ctx, &hdr, request_payload, request_len);
305 + if (err != NIPC_OK)
306 + return err;
307 +
308 + nipc_header_t resp_hdr;
309 + err = receive_fn(ctx, ctx->response_buf, ctx->response_buf_size,
310 + &resp_hdr, response_payload_out, response_len_out);
311 + if (err != NIPC_OK)
312 + return err;
313 +
314 + if (resp_hdr.kind != NIPC_KIND_RESPONSE)
315 + return NIPC_ERR_BAD_KIND;
316 + if (resp_hdr.code != method_code)
317 + return NIPC_ERR_BAD_LAYOUT;
318 + if (resp_hdr.message_id != hdr.message_id)
319 + return NIPC_ERR_BAD_LAYOUT;
320 + return nipc_service_common_response_status_to_error(ctx, &resp_hdr);
321 +}
322 +
323 +nipc_error_t nipc_service_common_call_with_retry(
324 + nipc_client_ctx_t *ctx,
325 + nipc_service_common_attempt_fn attempt,
326 + void *state,
327 + const nipc_service_common_client_ops_t *ops)
328 +{
329 + if (ctx->state != NIPC_CLIENT_READY) {
330 + ctx->error_count++;
331 + return NIPC_ERR_NOT_READY;
332 + }
333 +
334 + int overflow_retries = 0;
335 + for (;;) {
336 + uint32_t prev_req = ctx->session.max_request_payload_bytes;
337 + uint32_t prev_resp = ctx->session.max_response_payload_bytes;
338 + uint32_t prev_cfg_req = ctx->transport_config.max_request_payload_bytes;
339 + uint32_t prev_cfg_resp = ctx->transport_config.max_response_payload_bytes;
340 +
341 + nipc_error_t err = attempt(ctx, state);
342 + if (err == NIPC_OK) {
343 + ctx->call_count++;
344 + return NIPC_OK;
345 + }
346 +
347 + if (err != NIPC_ERR_OVERFLOW) {
348 + ops->disconnect(ctx);
349 + ctx->state = NIPC_CLIENT_BROKEN;
350 + if (ops->reconnect_drain_ms > 0 && ops->sleep_ms)
351 + ops->sleep_ms(ops->reconnect_drain_ms);
352 + if (!ops->reconnect_for_call(ctx)) {
353 + ctx->error_count++;
354 + return err;
355 + }
356 +
357 + ctx->reconnect_count++;
358 + if (ops->sleep_ms && ops->reconnect_retry_interval_ms > 0)
359 + ops->sleep_ms(ops->reconnect_retry_interval_ms);
360 + err = attempt(ctx, state);
361 + if (err == NIPC_OK) {
362 + ctx->call_count++;
363 + return NIPC_OK;
364 + }
365 +
366 + ops->disconnect(ctx);
367 + ctx->state = NIPC_CLIENT_BROKEN;
368 + ctx->error_count++;
369 + return err;
370 + }
371 +
372 + ops->disconnect(ctx);
373 + ctx->state = NIPC_CLIENT_BROKEN;
374 + if (!ops->reconnect_for_call(ctx)) {
375 + ctx->error_count++;
376 + return err;
377 + }
378 + ctx->reconnect_count++;
379 +
380 + if (ctx->session.max_request_payload_bytes <= prev_req &&
381 + ctx->session.max_response_payload_bytes <= prev_resp &&
382 + ctx->transport_config.max_request_payload_bytes <= prev_cfg_req &&
383 + ctx->transport_config.max_response_payload_bytes <= prev_cfg_resp) {
384 + ops->disconnect(ctx);
385 + ctx->state = NIPC_CLIENT_BROKEN;
386 + ctx->error_count++;
387 + return err;
388 + }
389 +
390 + if (++overflow_retries >= 8) {
391 + ops->disconnect(ctx);
392 + ctx->state = NIPC_CLIENT_BROKEN;
393 + ctx->error_count++;
394 + return err;
395 + }
396 + if (ops->sleep_ms && ops->reconnect_retry_interval_ms > 0)
397 + ops->sleep_ms(ops->reconnect_retry_interval_ms);
398 + }
399 +}
400 +
401 +nipc_error_t nipc_service_common_server_init_base(
402 + nipc_managed_server_t *server,
403 + const char *run_dir,
404 + const char *service_name,
405 + int worker_count,
406 + uint16_t expected_method_code,
407 + nipc_server_handler_fn handler,
408 + void *user,
409 + uint32_t max_request_payload_bytes,
410 + uint32_t max_response_payload_bytes)
411 +{
412 + if (!run_dir || !service_name || !handler)
413 + return NIPC_ERR_BAD_LAYOUT;
414 +
415 + if (worker_count < 1)
416 + worker_count = 1;
417 +
418 + nipc_service_common_copy_cstr_field(server->run_dir, sizeof(server->run_dir),
419 + run_dir);
420 + nipc_service_common_copy_cstr_field(server->service_name,
421 + sizeof(server->service_name),
422 + service_name);
423 +
424 + server->handler = handler;
425 + server->handler_user = user;
426 + server->worker_count = worker_count;
427 + server->expected_method_code = expected_method_code;
428 + server->learned_request_payload_bytes =
429 + max_request_payload_bytes > 0
430 + ? max_request_payload_bytes
431 + : NIPC_MAX_PAYLOAD_DEFAULT;
432 + server->learned_response_payload_bytes =
433 + max_response_payload_bytes > 0
434 + ? max_response_payload_bytes
435 + : NIPC_MAX_PAYLOAD_DEFAULT;
436 + server->session_capacity = worker_count * 2;
437 + if (server->session_capacity < 16)
438 + server->session_capacity = 16;
439 + server->session_count = 0;
440 + server->next_session_id = 1;
441 + return NIPC_OK;
442 +}
443 +
444 +nipc_error_t nipc_service_common_server_alloc_sessions(
445 + nipc_managed_server_t *server,
446 + nipc_service_common_calloc_fn calloc_fn,
447 + int fault_site)
448 +{
449 + server->sessions = calloc_fn((size_t)server->session_capacity,
450 + sizeof(nipc_session_ctx_t *),
451 + fault_site);
452 + return server->sessions ? NIPC_OK : NIPC_ERR_OVERFLOW;
453 +}
454 +
455 +void nipc_service_common_server_note_request_capacity(nipc_managed_server_t *server,
456 + uint32_t payload_len)
457 +{
458 + uint32_t grown = nipc_service_common_next_power_of_2_u32(payload_len);
459 +#if defined(_WIN32) || defined(__MSYS__)
460 + uint32_t current = server->learned_request_payload_bytes;
461 + while (grown > current) {
462 + uint32_t previous = (uint32_t)InterlockedCompareExchange(
463 + (volatile LONG *)&server->learned_request_payload_bytes,
464 + (LONG)grown, (LONG)current);
465 + if (previous == current)
466 + break;
467 + current = previous;
468 + }
469 +#else
470 + uint32_t current = __atomic_load_n(&server->learned_request_payload_bytes,
471 + __ATOMIC_RELAXED);
472 + while (grown > current &&
473 + !__atomic_compare_exchange_n(&server->learned_request_payload_bytes,
474 + &current, grown, false,
475 + __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {
476 + }
477 +#endif
478 +}
479 +
480 +void nipc_service_common_server_note_response_capacity(nipc_managed_server_t *server,
481 + uint32_t payload_len)
482 +{
483 + uint32_t grown = nipc_service_common_next_power_of_2_u32(payload_len);
484 +#if defined(_WIN32) || defined(__MSYS__)
485 + uint32_t current = server->learned_response_payload_bytes;
486 + while (grown > current) {
487 + uint32_t previous = (uint32_t)InterlockedCompareExchange(
488 + (volatile LONG *)&server->learned_response_payload_bytes,
489 + (LONG)grown, (LONG)current);
490 + if (previous == current)
491 + break;
492 + current = previous;
493 + }
494 +#else
495 + uint32_t current = __atomic_load_n(&server->learned_response_payload_bytes,
496 + __ATOMIC_RELAXED);
497 + while (grown > current &&
498 + !__atomic_compare_exchange_n(&server->learned_response_payload_bytes,
499 + &current, grown, false,
500 + __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {
501 + }
502 +#endif
503 +}
504 +
505 +void nipc_service_common_prepare_response_header(const nipc_header_t *request_hdr,
506 + nipc_header_t *resp_hdr)
507 +{
508 + memset(resp_hdr, 0, sizeof(*resp_hdr));
509 + resp_hdr->kind = NIPC_KIND_RESPONSE;
510 + resp_hdr->code = request_hdr->code;
511 + resp_hdr->message_id = request_hdr->message_id;
512 + if ((request_hdr->flags & NIPC_FLAG_BATCH) && request_hdr->item_count >= 1) {
513 + resp_hdr->item_count = request_hdr->item_count;
514 + resp_hdr->flags = NIPC_FLAG_BATCH;
515 + } else {
516 + resp_hdr->item_count = 1;
517 + resp_hdr->flags = 0;
518 + }
519 +}
520 +
521 +void nipc_service_common_apply_dispatch_result(nipc_managed_server_t *server,
522 + nipc_error_t dispatch_err,
523 + size_t resp_buf_size,
524 + uint32_t max_response_payload_bytes,
525 + bool check_header_room,
526 + nipc_header_t *resp_hdr,
527 + size_t *response_len,
528 + bool *close_after_response)
529 +{
530 + *close_after_response = false;
531 +
532 + switch (dispatch_err) {
533 + case NIPC_OK:
534 + if (*response_len > resp_buf_size ||
535 + *response_len > max_response_payload_bytes ||
536 + (check_header_room && *response_len > SIZE_MAX - NIPC_HEADER_LEN)) {
537 + nipc_service_common_server_note_response_capacity(
538 + server, *response_len >= UINT32_MAX ? UINT32_MAX : (uint32_t)*response_len);
539 + resp_hdr->transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
540 + *close_after_response = true;
541 + *response_len = 0;
542 + } else {
543 + if (*response_len <= UINT32_MAX)
544 + nipc_service_common_server_note_response_capacity(
545 + server, (uint32_t)*response_len);
546 + resp_hdr->transport_status = NIPC_STATUS_OK;
547 + }
548 + break;
549 + case NIPC_ERR_OVERFLOW:
550 + if (max_response_payload_bytes >= UINT32_MAX / 2u)
551 + nipc_service_common_server_note_response_capacity(server, UINT32_MAX);
552 + else
553 + nipc_service_common_server_note_response_capacity(
554 + server, max_response_payload_bytes * 2u);
555 + resp_hdr->transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
556 + *close_after_response = true;
557 + *response_len = 0;
558 + break;
559 + case NIPC_ERR_TRUNCATED:
560 + case NIPC_ERR_BAD_LAYOUT:
561 + case NIPC_ERR_OUT_OF_BOUNDS:
562 + case NIPC_ERR_MISSING_NUL:
563 + case NIPC_ERR_BAD_ALIGNMENT:
564 + case NIPC_ERR_BAD_ITEM_COUNT:
565 + resp_hdr->transport_status = NIPC_STATUS_BAD_ENVELOPE;
566 + *close_after_response = true;
567 + *response_len = 0;
568 + break;
569 + case NIPC_ERR_HANDLER_FAILED:
570 + default:
571 + resp_hdr->transport_status = NIPC_STATUS_INTERNAL_ERROR;
572 + *close_after_response = true;
573 + *response_len = 0;
574 + break;
575 + }
576 +}
src/libnetdata/netipc/src/service/netipc_service_common.h new
+162
@@ -0,0 +1,162 @@
1 +#ifndef NETIPC_SERVICE_COMMON_H
2 +#define NETIPC_SERVICE_COMMON_H
3 +
4 +#include "netipc/netipc_service.h"
5 +
6 +#include <stdbool.h>
7 +#include <stddef.h>
8 +#include <stdint.h>
9 +
10 +#ifdef __cplusplus
11 +extern "C" {
12 +#endif
13 +
14 +typedef struct {
15 + void *(*malloc_fn)(size_t size, int fault_site);
16 + void *(*calloc_fn)(size_t count, size_t size, int fault_site);
17 + uint64_t (*monotonic_ms_fn)(void);
18 + int cache_buckets_fault_site;
19 + int cache_items_fault_site;
20 + int cache_item_name_fault_site;
21 + int cache_item_path_fault_site;
22 +} nipc_service_common_cache_ops_t;
23 +
24 +typedef nipc_error_t (*nipc_service_common_attempt_fn)(nipc_client_ctx_t *ctx,
25 + void *state);
26 +typedef nipc_error_t (*nipc_service_common_transport_send_fn)(
27 + nipc_client_ctx_t *ctx,
28 + nipc_header_t *hdr,
29 + const void *payload,
30 + size_t payload_len);
31 +typedef nipc_error_t (*nipc_service_common_transport_receive_fn)(
32 + nipc_client_ctx_t *ctx,
33 + void *buf,
34 + size_t buf_size,
35 + nipc_header_t *hdr_out,
36 + const void **payload_out,
37 + size_t *payload_len_out);
38 +
39 +typedef struct {
40 + void (*disconnect)(nipc_client_ctx_t *ctx);
41 + nipc_client_state_t (*try_connect)(nipc_client_ctx_t *ctx);
42 + bool (*reconnect_for_call)(nipc_client_ctx_t *ctx);
43 + void (*sleep_ms)(uint32_t ms);
44 + uint32_t reconnect_drain_ms;
45 + uint32_t reconnect_retry_interval_ms;
46 +} nipc_service_common_client_ops_t;
47 +
48 +typedef void *(*nipc_service_common_calloc_fn)(size_t count,
49 + size_t size,
50 + int fault_site);
51 +
52 +typedef struct {
53 + uint32_t supported_profiles;
54 + uint32_t preferred_profiles;
55 + uint32_t max_request_batch_items;
56 + uint32_t max_response_payload_bytes;
57 + uint32_t max_response_batch_items;
58 + uint64_t auth_token;
59 +} nipc_service_common_transport_fields_t;
60 +
61 +#define NIPC_SERVICE_COMMON_APPLY_TRANSPORT_FIELDS(dst, fields) do { \
62 + (dst)->supported_profiles = (fields)->supported_profiles; \
63 + (dst)->preferred_profiles = (fields)->preferred_profiles; \
64 + (dst)->max_request_batch_items = (fields)->max_request_batch_items; \
65 + (dst)->max_response_payload_bytes = (fields)->max_response_payload_bytes; \
66 + (dst)->max_response_batch_items = (fields)->max_response_batch_items; \
67 + (dst)->auth_token = (fields)->auth_token; \
68 +} while (0)
69 +
70 +uint32_t nipc_service_common_next_power_of_2_u32(uint32_t n);
71 +bool nipc_service_common_header_payload_len(size_t payload_len,
72 + size_t *msg_len_out);
73 +bool nipc_service_common_header_payload_len_u32(uint32_t payload_len,
74 + uint32_t *msg_len_out);
75 +bool nipc_service_common_mul_would_overflow(size_t count, size_t size);
76 +uint32_t nipc_service_common_request_payload_default(void);
77 +uint32_t nipc_service_common_response_payload_default(void);
78 +uint32_t nipc_service_common_typed_response_batch_items(uint32_t max_request_batch_items);
79 +void nipc_service_common_copy_cstr_field(char *dst, size_t dst_size, const char *src);
80 +bool nipc_service_common_client_transport_fields(
81 + nipc_service_common_transport_fields_t *fields,
82 + const nipc_client_config_t *config);
83 +bool nipc_service_common_server_transport_fields(
84 + nipc_service_common_transport_fields_t *fields,
85 + const nipc_server_config_t *config);
86 +
87 +void nipc_service_common_client_init(nipc_client_ctx_t *ctx,
88 + const char *run_dir,
89 + const char *service_name);
90 +void nipc_service_common_client_status(const nipc_client_ctx_t *ctx,
91 + nipc_client_status_t *out);
92 +void nipc_service_common_client_close_buffers(nipc_client_ctx_t *ctx);
93 +bool nipc_service_common_client_refresh(nipc_client_ctx_t *ctx,
94 + const nipc_service_common_client_ops_t *ops);
95 +void nipc_service_common_client_note_request_capacity(nipc_client_ctx_t *ctx,
96 + uint32_t payload_len);
97 +void nipc_service_common_client_note_response_capacity(nipc_client_ctx_t *ctx,
98 + uint32_t payload_len);
99 +nipc_error_t nipc_service_common_client_prepare_shm_request(
100 + nipc_client_ctx_t *ctx,
101 + nipc_header_t *hdr,
102 + const void *payload,
103 + size_t payload_len,
104 + uint8_t **msg_out,
105 + size_t *msg_len_out);
106 +nipc_error_t nipc_service_common_client_parse_shm_response(
107 + void *buf,
108 + size_t msg_len,
109 + nipc_header_t *hdr_out,
110 + const void **payload_out,
111 + size_t *payload_len_out);
112 +nipc_error_t nipc_service_common_response_status_to_error(nipc_client_ctx_t *ctx,
113 + const nipc_header_t *resp_hdr);
114 +nipc_error_t nipc_service_common_do_raw_call(
115 + nipc_client_ctx_t *ctx,
116 + uint16_t method_code,
117 + const void *request_payload,
118 + size_t request_len,
119 + const void **response_payload_out,
120 + size_t *response_len_out,
121 + nipc_service_common_transport_send_fn send_fn,
122 + nipc_service_common_transport_receive_fn receive_fn);
123 +nipc_error_t nipc_service_common_call_with_retry(
124 + nipc_client_ctx_t *ctx,
125 + nipc_service_common_attempt_fn attempt,
126 + void *state,
127 + const nipc_service_common_client_ops_t *ops);
128 +
129 +void nipc_service_common_server_note_request_capacity(nipc_managed_server_t *server,
130 + uint32_t payload_len);
131 +void nipc_service_common_server_note_response_capacity(nipc_managed_server_t *server,
132 + uint32_t payload_len);
133 +nipc_error_t nipc_service_common_server_init_base(
134 + nipc_managed_server_t *server,
135 + const char *run_dir,
136 + const char *service_name,
137 + int worker_count,
138 + uint16_t expected_method_code,
139 + nipc_server_handler_fn handler,
140 + void *user,
141 + uint32_t max_request_payload_bytes,
142 + uint32_t max_response_payload_bytes);
143 +nipc_error_t nipc_service_common_server_alloc_sessions(
144 + nipc_managed_server_t *server,
145 + nipc_service_common_calloc_fn calloc_fn,
146 + int fault_site);
147 +void nipc_service_common_prepare_response_header(const nipc_header_t *request_hdr,
148 + nipc_header_t *resp_hdr);
149 +void nipc_service_common_apply_dispatch_result(nipc_managed_server_t *server,
150 + nipc_error_t dispatch_err,
151 + size_t resp_buf_size,
152 + uint32_t max_response_payload_bytes,
153 + bool check_header_room,
154 + nipc_header_t *resp_hdr,
155 + size_t *response_len,
156 + bool *close_after_response);
157 +
158 +#ifdef __cplusplus
159 +}
160 +#endif
161 +
162 +#endif /* NETIPC_SERVICE_COMMON_H */
src/libnetdata/netipc/src/service/netipc_service_platform.h new
+63
@@ -0,0 +1,63 @@
1 +#ifndef NETIPC_SERVICE_PLATFORM_H
2 +#define NETIPC_SERVICE_PLATFORM_H
3 +
4 +#include "netipc_service_common.h"
5 +
6 +#ifdef __cplusplus
7 +extern "C" {
8 +#endif
9 +
10 +#if defined(_WIN32) || defined(__MSYS__)
11 +typedef nipc_np_server_config_t nipc_service_platform_server_config_t;
12 +#else
13 +typedef nipc_uds_server_config_t nipc_service_platform_server_config_t;
14 +#endif
15 +
16 +typedef nipc_service_common_attempt_fn nipc_service_platform_attempt_fn;
17 +
18 +enum {
19 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL = 10,
20 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
21 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
22 + NIPC_SERVICE_PLATFORM_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
23 +};
24 +
25 +void *nipc_service_platform_malloc(size_t size, int fault_site);
26 +void *nipc_service_platform_calloc(size_t count, size_t size, int fault_site);
27 +uint64_t nipc_service_platform_monotonic_ms(void);
28 +
29 +bool nipc_service_platform_ensure_client_send_buffer(nipc_client_ctx_t *ctx,
30 + size_t need);
31 +
32 +nipc_error_t nipc_service_platform_do_raw_call(
33 + nipc_client_ctx_t *ctx,
34 + uint16_t method_code,
35 + const void *request_payload,
36 + size_t request_len,
37 + const void **response_payload_out,
38 + size_t *response_len_out);
39 +
40 +nipc_error_t nipc_service_platform_call_with_retry(
41 + nipc_client_ctx_t *ctx,
42 + nipc_service_platform_attempt_fn attempt,
43 + void *state);
44 +
45 +void nipc_service_platform_server_config_from_service(
46 + nipc_service_platform_server_config_t *transport,
47 + const nipc_server_config_t *config);
48 +
49 +nipc_error_t nipc_service_platform_server_init_raw(
50 + nipc_managed_server_t *server,
51 + const char *run_dir,
52 + const char *service_name,
53 + const nipc_service_platform_server_config_t *config,
54 + int worker_count,
55 + uint16_t expected_method_code,
56 + nipc_server_handler_fn handler,
57 + void *user);
58 +
59 +#ifdef __cplusplus
60 +}
61 +#endif
62 +
63 +#endif /* NETIPC_SERVICE_PLATFORM_H */
src/libnetdata/netipc/src/service/netipc_service_posix_client.c new
+97
@@ -0,0 +1,97 @@
1 +/*
2 + * netipc_service_posix_client.c - POSIX public service client API.
3 + */
4 +
5 +#include "netipc/netipc_service.h"
6 +#include "netipc/netipc_protocol.h"
7 +#include "netipc/netipc_uds.h"
8 +#include "netipc/netipc_shm.h"
9 +#include "netipc_service_common.h"
10 +#include "netipc_service_platform.h"
11 +#include "netipc_service_posix_internal.h"
12 +
13 +#include <stdint.h>
14 +#include <stdlib.h>
15 +#include <string.h>
16 +
17 +static nipc_uds_client_config_t service_client_config_to_transport(
18 + const nipc_client_config_t *config)
19 +{
20 + nipc_uds_client_config_t transport = {0};
21 + nipc_service_common_transport_fields_t fields;
22 +
23 + if (!nipc_service_common_client_transport_fields(&fields, config))
24 + return transport;
25 +
26 + NIPC_SERVICE_COMMON_APPLY_TRANSPORT_FIELDS(&transport, &fields);
27 + return transport;
28 +}
29 +
30 +static void client_sleep_ms(uint32_t ms)
31 +{
32 + nipc_service_posix_sleep_us(ms * 1000u);
33 +}
34 +
35 +const nipc_service_common_client_ops_t *nipc_service_posix_client_ops(void)
36 +{
37 + static const nipc_service_common_client_ops_t ops = {
38 + .disconnect = nipc_service_posix_client_disconnect,
39 + .try_connect = nipc_service_posix_client_try_connect,
40 + .reconnect_for_call = nipc_service_posix_client_reconnect_for_call,
41 + .sleep_ms = client_sleep_ms,
42 + .reconnect_drain_ms = CLIENT_CALL_RECONNECT_DRAIN_MS,
43 + .reconnect_retry_interval_ms = CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS,
44 + };
45 + return &ops;
46 +}
47 +
48 +void nipc_service_platform_server_config_from_service(
49 + nipc_service_platform_server_config_t *transport,
50 + const nipc_server_config_t *config)
51 +{
52 + memset(transport, 0, sizeof(*transport));
53 + nipc_service_common_transport_fields_t fields;
54 +
55 + if (!nipc_service_common_server_transport_fields(&fields, config))
56 + return;
57 +
58 + NIPC_SERVICE_COMMON_APPLY_TRANSPORT_FIELDS(transport, &fields);
59 +}
60 +
61 +/* ------------------------------------------------------------------ */
62 +/* Public API: client lifecycle */
63 +/* ------------------------------------------------------------------ */
64 +
65 +void nipc_client_init(nipc_client_ctx_t *ctx,
66 + const char *run_dir,
67 + const char *service_name,
68 + const nipc_client_config_t *config)
69 +{
70 + nipc_service_common_client_init(ctx, run_dir, service_name);
71 + ctx->session.fd = -1;
72 +
73 + ctx->transport_config = service_client_config_to_transport(config);
74 + if (ctx->transport_config.max_request_payload_bytes == 0)
75 + ctx->transport_config.max_request_payload_bytes =
76 + nipc_service_common_request_payload_default();
77 + if (ctx->transport_config.max_response_payload_bytes == 0)
78 + ctx->transport_config.max_response_payload_bytes =
79 + nipc_service_common_response_payload_default();
80 +}
81 +
82 +bool nipc_client_refresh(nipc_client_ctx_t *ctx)
83 +{
84 + return nipc_service_common_client_refresh(ctx, nipc_service_posix_client_ops());
85 +}
86 +
87 +void nipc_client_status(const nipc_client_ctx_t *ctx,
88 + nipc_client_status_t *out)
89 +{
90 + nipc_service_common_client_status(ctx, out);
91 +}
92 +
93 +void nipc_client_close(nipc_client_ctx_t *ctx)
94 +{
95 + nipc_service_posix_client_disconnect(ctx);
96 + nipc_service_common_client_close_buffers(ctx);
97 +}
src/libnetdata/netipc/src/service/netipc_service_posix_client_call.c new
+130
@@ -0,0 +1,130 @@
1 +/*
2 + * netipc_service_posix_client_call.c - POSIX raw client call flow.
3 + */
4 +
5 +#include "netipc/netipc_service.h"
6 +#include "netipc/netipc_protocol.h"
7 +#include "netipc/netipc_uds.h"
8 +#include "netipc/netipc_shm.h"
9 +#include "netipc_service_common.h"
10 +#include "netipc_service_platform.h"
11 +#include "netipc_service_posix_internal.h"
12 +
13 +#include <stdint.h>
14 +#include <stdlib.h>
15 +#include <string.h>
16 +
17 +/* ------------------------------------------------------------------ */
18 +/* Internal: send/receive via the active transport */
19 +/* ------------------------------------------------------------------ */
20 +
21 +/*
22 + * Send a complete message (header + payload) using whichever transport
23 + * is active: SHM if negotiated, UDS otherwise.
24 + */
25 +static nipc_error_t transport_send(nipc_client_ctx_t *ctx,
26 + nipc_header_t *hdr,
27 + const void *payload,
28 + size_t payload_len)
29 +{
30 + if (payload_len > UINT32_MAX)
31 + return NIPC_ERR_OVERFLOW;
32 +
33 + if (ctx->shm) {
34 + uint8_t *msg;
35 + size_t msg_len;
36 + nipc_error_t perr = nipc_service_common_client_prepare_shm_request(
37 + ctx, hdr, payload, payload_len, &msg, &msg_len);
38 + if (perr != NIPC_OK)
39 + return perr;
40 +
41 + nipc_shm_error_t serr = nipc_shm_send(ctx->shm, msg, msg_len);
42 + if (serr == NIPC_SHM_ERR_MSG_TOO_LARGE) {
43 + nipc_service_common_client_note_request_capacity(
44 + ctx, (uint32_t)payload_len);
45 + return NIPC_ERR_OVERFLOW;
46 + }
47 + return (serr == NIPC_SHM_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
48 + }
49 +
50 + /* UDS path */
51 + nipc_uds_error_t uerr = nipc_uds_send(&ctx->session, hdr,
52 + payload, payload_len);
53 + if (uerr == NIPC_UDS_ERR_LIMIT_EXCEEDED) {
54 + nipc_service_common_client_note_request_capacity(
55 + ctx, (uint32_t)payload_len);
56 + return NIPC_ERR_OVERFLOW;
57 + }
58 + return (uerr == NIPC_UDS_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
59 +}
60 +
61 +/*
62 + * Receive a complete message. For SHM, reads from the SHM region.
63 + * For UDS, reads from the socket into the caller's buffer.
64 + *
65 + * On success, hdr_out is filled, and payload_out + payload_len_out
66 + * point to the payload bytes (valid until next receive).
67 + */
68 +static nipc_error_t transport_receive(nipc_client_ctx_t *ctx,
69 + void *buf, size_t buf_size,
70 + nipc_header_t *hdr_out,
71 + const void **payload_out,
72 + size_t *payload_len_out)
73 +{
74 + if (ctx->shm) {
75 + size_t msg_len;
76 + nipc_shm_error_t serr = nipc_shm_receive(ctx->shm, buf, buf_size,
77 + &msg_len, 30000);
78 + if (serr != NIPC_SHM_OK)
79 + return NIPC_ERR_TRUNCATED;
80 +
81 + return nipc_service_common_client_parse_shm_response(
82 + buf, msg_len, hdr_out, payload_out, payload_len_out);
83 + }
84 +
85 + /* UDS path */
86 + nipc_uds_error_t uerr = nipc_uds_receive(&ctx->session, buf, buf_size,
87 + hdr_out, payload_out,
88 + payload_len_out);
89 + return (uerr == NIPC_UDS_OK) ? NIPC_OK : NIPC_ERR_TRUNCATED;
90 +}
91 +
92 +/* ------------------------------------------------------------------ */
93 +/* Internal: generic raw call (send request, receive response) */
94 +/* ------------------------------------------------------------------ */
95 +
96 +/*
97 + * Single-attempt raw call: build envelope, send, receive, validate
98 + * envelope. The caller handles encode before and decode after.
99 + *
100 + * On success, response_payload_out and response_len_out point into the
101 + * internal client response buffer (valid until next call on this context).
102 + */
103 +nipc_error_t nipc_service_platform_do_raw_call(nipc_client_ctx_t *ctx,
104 + uint16_t method_code,
105 + const void *request_payload,
106 + size_t request_len,
107 + const void **response_payload_out,
108 + size_t *response_len_out)
109 +{
110 + return nipc_service_common_do_raw_call(
111 + ctx, method_code, request_payload, request_len,
112 + response_payload_out, response_len_out,
113 + transport_send, transport_receive);
114 +}
115 +
116 +/*
117 + * Generic call-with-retry:
118 + * - ordinary failures reconnect and retry once
119 + * - overflow-driven resize recovery may reconnect repeatedly until
120 + * negotiated capacities grow or recovery fails
121 + * The caller provides a function pointer for the single-attempt logic.
122 + */
123 +nipc_error_t nipc_service_platform_call_with_retry(
124 + nipc_client_ctx_t *ctx,
125 + nipc_service_platform_attempt_fn attempt,
126 + void *state)
127 +{
128 + return nipc_service_common_call_with_retry(
129 + ctx, attempt, state, nipc_service_posix_client_ops());
130 +}
src/libnetdata/netipc/src/service/netipc_service_posix_client_connect.c new
+172
@@ -0,0 +1,172 @@
1 +/*
2 + * netipc_service_posix_client_connect.c - POSIX client connection management.
3 + */
4 +
5 +#include "netipc/netipc_service.h"
6 +#include "netipc/netipc_protocol.h"
7 +#include "netipc/netipc_uds.h"
8 +#include "netipc/netipc_shm.h"
9 +#include "netipc_service_common.h"
10 +#include "netipc_service_platform.h"
11 +#include "netipc_service_posix_internal.h"
12 +
13 +#include <stdint.h>
14 +#include <stdlib.h>
15 +#include <string.h>
16 +
17 +static bool client_prepare_session_buffers(nipc_client_ctx_t *ctx)
18 +{
19 + size_t response_need;
20 + if (!nipc_service_common_header_payload_len(
21 + ctx->session.max_response_payload_bytes, &response_need))
22 + return false;
23 + if (response_need < NIPC_HEADER_LEN + 1024u)
24 + response_need = NIPC_HEADER_LEN + 1024u;
25 +
26 + if (!nipc_service_posix_ensure_buffer(&ctx->response_buf, &ctx->response_buf_size, response_need,
27 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL))
28 + return false;
29 +
30 + if (ctx->session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
31 + ctx->session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
32 + size_t send_need;
33 + if (!nipc_service_common_header_payload_len(
34 + ctx->session.max_request_payload_bytes, &send_need))
35 + return false;
36 + if (!nipc_service_posix_ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, send_need,
37 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL))
38 + return false;
39 + }
40 +
41 + return true;
42 +}
43 +
44 +/* ------------------------------------------------------------------ */
45 +/* Internal: client connection helpers */
46 +/* ------------------------------------------------------------------ */
47 +
48 +/* Tear down the current connection (UDS session + SHM if any). */
49 +void nipc_service_posix_client_disconnect(nipc_client_ctx_t *ctx)
50 +{
51 + if (ctx->shm) {
52 + nipc_shm_close(ctx->shm);
53 + free(ctx->shm);
54 + ctx->shm = NULL;
55 + }
56 +
57 + if (ctx->session_valid) {
58 + nipc_uds_close_session(&ctx->session);
59 + ctx->session_valid = false;
60 + }
61 +}
62 +
63 +static void client_disable_shm_profiles(nipc_client_ctx_t *ctx)
64 +{
65 + ctx->transport_config.supported_profiles &=
66 + ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
67 + ctx->transport_config.preferred_profiles &=
68 + ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
69 +}
70 +
71 +/* Attempt a full connection: UDS connect + handshake, then SHM upgrade
72 + * if negotiated. Returns the new state. */
73 +nipc_client_state_t nipc_service_posix_client_try_connect(nipc_client_ctx_t *ctx)
74 +{
75 + nipc_uds_session_t session;
76 + memset(&session, 0, sizeof(session));
77 + session.fd = -1;
78 +
79 + nipc_uds_error_t err = nipc_uds_connect(
80 + ctx->run_dir, ctx->service_name,
81 + &ctx->transport_config, &session);
82 +
83 + switch (err) {
84 + case NIPC_UDS_OK:
85 + break;
86 + case NIPC_UDS_ERR_CONNECT:
87 + return NIPC_CLIENT_NOT_FOUND;
88 + case NIPC_UDS_ERR_AUTH_FAILED:
89 + return NIPC_CLIENT_AUTH_FAILED;
90 + case NIPC_UDS_ERR_NO_PROFILE:
91 + case NIPC_UDS_ERR_INCOMPATIBLE:
92 + return NIPC_CLIENT_INCOMPATIBLE;
93 + default:
94 + return NIPC_CLIENT_DISCONNECTED;
95 + }
96 +
97 + ctx->session = session;
98 + ctx->session_valid = true;
99 +
100 + if (!client_prepare_session_buffers(ctx)) {
101 + nipc_uds_close_session(&ctx->session);
102 + ctx->session_valid = false;
103 + return NIPC_CLIENT_DISCONNECTED;
104 + }
105 +
106 + /* SHM upgrade if negotiated */
107 + if (session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
108 + session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
109 +
110 + nipc_shm_ctx_t *shm = nipc_service_posix_calloc(
111 + 1, sizeof(nipc_shm_ctx_t),
112 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL);
113 + if (!shm) {
114 + nipc_uds_close_session(&ctx->session);
115 + ctx->session_valid = false;
116 + return NIPC_CLIENT_DISCONNECTED;
117 + }
118 + {
119 + /* Retry attach: server creates the SHM region after
120 + * the UDS handshake, so it may not exist yet. */
121 + nipc_shm_error_t serr = NIPC_SHM_ERR_NOT_READY;
122 + uint64_t deadline_ms = nipc_service_platform_monotonic_ms() + CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS;
123 + for (;;) {
124 + serr = nipc_shm_client_attach(
125 + ctx->run_dir, ctx->service_name,
126 + session.session_id, shm);
127 + if (serr == NIPC_SHM_OK)
128 + break;
129 + if (serr != NIPC_SHM_ERR_NOT_READY &&
130 + serr != NIPC_SHM_ERR_OPEN &&
131 + serr != NIPC_SHM_ERR_BAD_MAGIC)
132 + break;
133 + if (nipc_service_platform_monotonic_ms() >= deadline_ms)
134 + break;
135 + nipc_service_posix_sleep_us(CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS * 1000u);
136 + }
137 +
138 + if (serr == NIPC_SHM_OK) {
139 + ctx->shm = shm;
140 + } else {
141 + /* SHM attach failed after negotiation. Close that session,
142 + * blacklist SHM for this client context, and retry
143 + * baseline via a new handshake. */
144 + free(shm);
145 + nipc_uds_close_session(&ctx->session);
146 + ctx->session_valid = false;
147 + client_disable_shm_profiles(ctx);
148 + if (ctx->transport_config.supported_profiles == 0)
149 + return NIPC_CLIENT_DISCONNECTED;
150 + return nipc_service_posix_client_try_connect(ctx);
151 + }
152 + }
153 + }
154 +
155 + return NIPC_CLIENT_READY;
156 +}
157 +
158 +bool nipc_service_posix_client_reconnect_for_call(nipc_client_ctx_t *ctx)
159 +{
160 + for (uint32_t i = 0; i < CLIENT_CALL_RECONNECT_RETRIES; i++) {
161 + ctx->state = nipc_service_posix_client_try_connect(ctx);
162 + if (ctx->state == NIPC_CLIENT_READY)
163 + return true;
164 + if (ctx->state == NIPC_CLIENT_AUTH_FAILED ||
165 + ctx->state == NIPC_CLIENT_INCOMPATIBLE)
166 + return false;
167 + if (i + 1u < CLIENT_CALL_RECONNECT_RETRIES)
168 + nipc_service_posix_sleep_us(CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS * 1000u);
169 + }
170 +
171 + return false;
172 +}
src/libnetdata/netipc/src/service/netipc_service_posix_internal.h new
+51
@@ -0,0 +1,51 @@
1 +#ifndef NETIPC_SERVICE_POSIX_INTERNAL_H
2 +#define NETIPC_SERVICE_POSIX_INTERNAL_H
3 +
4 +#include "netipc_service_platform.h"
5 +
6 +#include <pthread.h>
7 +#include <stdbool.h>
8 +#include <stddef.h>
9 +#include <stdint.h>
10 +
11 +#define SERVER_POLL_TIMEOUT_MS 100
12 +#define CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS 5u
13 +#define CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS 5000u
14 +#define CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS 5u
15 +#define CLIENT_CALL_RECONNECT_DRAIN_MS (SERVER_POLL_TIMEOUT_MS + 50u)
16 +#define CLIENT_CALL_RECONNECT_RETRIES 20u
17 +
18 +enum {
19 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL = 1,
20 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL,
21 + NIPC_POSIX_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL,
22 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL,
23 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL,
24 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL,
25 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL,
26 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL,
27 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_THREAD_CREATE_INTERNAL,
28 + NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL,
29 + NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
30 + NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
31 + NIPC_POSIX_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
32 +};
33 +
34 +void nipc_service_posix_sleep_us(unsigned int usec);
35 +const nipc_service_common_client_ops_t *nipc_service_posix_client_ops(void);
36 +void nipc_service_posix_client_disconnect(nipc_client_ctx_t *ctx);
37 +nipc_client_state_t nipc_service_posix_client_try_connect(nipc_client_ctx_t *ctx);
38 +bool nipc_service_posix_client_reconnect_for_call(nipc_client_ctx_t *ctx);
39 +void *nipc_service_posix_malloc(size_t size, int fault_site);
40 +void *nipc_service_posix_calloc(size_t count, size_t size, int fault_site);
41 +bool nipc_service_posix_ensure_buffer(uint8_t **buf, size_t *buf_size,
42 + size_t need, int fault_site);
43 +int nipc_service_posix_poll_with_shutdown(int fd, bool *running);
44 +void *nipc_service_posix_session_handler_thread(void *arg);
45 +void nipc_service_posix_server_reap_sessions_locked(nipc_managed_server_t *server);
46 +int nipc_service_posix_pthread_create(pthread_t *thread,
47 + const pthread_attr_t *attr,
48 + void *(*start_routine)(void *),
49 + void *arg);
50 +
51 +#endif /* NETIPC_SERVICE_POSIX_INTERNAL_H */
src/libnetdata/netipc/src/service/netipc_service_posix_server.c new
+385
@@ -0,0 +1,385 @@
1 +/*
2 + * netipc_service_posix_server.c - POSIX managed server orchestration.
3 + */
4 +
5 +#include "netipc/netipc_service.h"
6 +#include "netipc/netipc_protocol.h"
7 +#include "netipc/netipc_uds.h"
8 +#include "netipc/netipc_shm.h"
9 +#include "netipc_service_common.h"
10 +#include "netipc_service_platform.h"
11 +#include "netipc_service_posix_internal.h"
12 +
13 +#include <errno.h>
14 +#include <poll.h>
15 +#include <stdint.h>
16 +#include <stdlib.h>
17 +#include <string.h>
18 +#include <sys/socket.h>
19 +#include <time.h>
20 +#include <unistd.h>
21 +
22 +/* ------------------------------------------------------------------ */
23 +/* Internal: managed server session handler */
24 +/* ------------------------------------------------------------------ */
25 +
26 +/*
27 + * Wait for data on a file descriptor with periodic shutdown checks.
28 + * Returns: 1 = data ready, 0 = server stopping, -1 = error/hangup.
29 + */
30 +static void server_destroy_precreated_shm(nipc_shm_ctx_t **shm)
31 +{
32 + if (!shm || !*shm)
33 + return;
34 + nipc_shm_destroy(*shm);
35 + free(*shm);
36 + *shm = NULL;
37 +}
38 +
39 +static bool server_prepare_accept_config(nipc_managed_server_t *server,
40 + uint64_t sid,
41 + nipc_uds_server_config_t *cfg_out,
42 + nipc_shm_ctx_t **shm_out)
43 +{
44 + *cfg_out = server->base_config;
45 + cfg_out->max_request_payload_bytes =
46 + __atomic_load_n(&server->learned_request_payload_bytes, __ATOMIC_ACQUIRE);
47 + cfg_out->max_response_payload_bytes =
48 + __atomic_load_n(&server->learned_response_payload_bytes, __ATOMIC_ACQUIRE);
49 + *shm_out = NULL;
50 +
51 + uint32_t shm_profiles = cfg_out->supported_profiles &
52 + (NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
53 + if (shm_profiles == 0)
54 + return true;
55 +
56 + uint32_t request_capacity;
57 + uint32_t response_capacity;
58 + if (!nipc_service_common_header_payload_len_u32(
59 + NIPC_MAX_PAYLOAD_CAP, &request_capacity) ||
60 + !nipc_service_common_header_payload_len_u32(
61 + cfg_out->max_response_payload_bytes, &response_capacity)) {
62 + cfg_out->supported_profiles &= ~shm_profiles;
63 + cfg_out->preferred_profiles &= ~shm_profiles;
64 + return cfg_out->supported_profiles != 0;
65 + }
66 +
67 + nipc_shm_ctx_t *shm = nipc_service_posix_calloc(
68 + 1, sizeof(nipc_shm_ctx_t),
69 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL);
70 + if (!shm)
71 + return false;
72 +
73 + /* HELLO has not been read yet, so the request segment must cover any
74 + * client proposal the handshake may legally echo back. */
75 + nipc_shm_error_t serr = nipc_shm_server_create(
76 + server->run_dir, server->service_name,
77 + sid,
78 + request_capacity,
79 + response_capacity,
80 + shm);
81 + if (serr == NIPC_SHM_OK) {
82 + *shm_out = shm;
83 + return true;
84 + }
85 +
86 + free(shm);
87 + cfg_out->supported_profiles &= ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
88 + cfg_out->preferred_profiles &= ~(NIPC_PROFILE_SHM_HYBRID | NIPC_PROFILE_SHM_FUTEX);
89 + return cfg_out->supported_profiles != 0;
90 +}
91 +
92 +/* ------------------------------------------------------------------ */
93 +/* Public API: managed server */
94 +/* ------------------------------------------------------------------ */
95 +
96 +nipc_error_t nipc_service_platform_server_init_raw(
97 + nipc_managed_server_t *server,
98 + const char *run_dir,
99 + const char *service_name,
100 + const nipc_service_platform_server_config_t *config,
101 + int worker_count,
102 + uint16_t expected_method_code,
103 + nipc_server_handler_fn handler,
104 + void *user)
105 +{
106 + if (!server)
107 + return NIPC_ERR_BAD_LAYOUT;
108 +
109 + memset(server, 0, sizeof(*server));
110 + server->listener.fd = -1;
111 + __atomic_store_n(&server->running, false, __ATOMIC_RELAXED);
112 + server->acceptor_started = false;
113 +
114 + if (!config)
115 + return NIPC_ERR_BAD_LAYOUT;
116 +
117 + nipc_error_t ierr = nipc_service_common_server_init_base(
118 + server, run_dir, service_name, worker_count, expected_method_code,
119 + handler, user, config->max_request_payload_bytes,
120 + config->max_response_payload_bytes);
121 + if (ierr != NIPC_OK)
122 + return ierr;
123 + server->base_config = *config;
124 + ierr = nipc_service_common_server_alloc_sessions(
125 + server, nipc_service_posix_calloc,
126 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL);
127 + if (ierr != NIPC_OK)
128 + return ierr;
129 + pthread_mutex_init(&server->sessions_lock, NULL);
130 +
131 + /* Clean up stale SHM regions from previous crashes (spec requirement:
132 + * runs once at server startup, before the listener begins accepting). */
133 + nipc_shm_cleanup_stale(run_dir, service_name);
134 +
135 + /* Start listening via L1 */
136 + nipc_uds_error_t uerr = nipc_uds_listen(
137 + run_dir, service_name, config, &server->listener);
138 + if (uerr != NIPC_UDS_OK) {
139 + free(server->sessions);
140 + server->sessions = NULL;
141 + pthread_mutex_destroy(&server->sessions_lock);
142 + return NIPC_ERR_BAD_LAYOUT;
143 + }
144 +
145 + return NIPC_OK;
146 +}
147 +
148 +nipc_error_t nipc_server_init_raw_for_tests(nipc_managed_server_t *server,
149 + const char *run_dir,
150 + const char *service_name,
151 + const nipc_uds_server_config_t *config,
152 + int worker_count,
153 + uint16_t expected_method_code,
154 + nipc_server_handler_fn handler,
155 + void *user)
156 +{
157 + return nipc_service_platform_server_init_raw(
158 + server, run_dir, service_name, config,
159 + worker_count, expected_method_code, handler, user);
160 +}
161 +
162 +void nipc_server_run(nipc_managed_server_t *server)
163 +{
164 + __atomic_store_n(&server->running, true, __ATOMIC_RELEASE);
165 +
166 + while (__atomic_load_n(&server->running, __ATOMIC_RELAXED)) {
167 + /* Poll the listener fd before blocking on accept */
168 + int pr = nipc_service_posix_poll_with_shutdown(server->listener.fd, &server->running);
169 + if (pr <= 0)
170 + break; /* shutdown or error */
171 +
172 + /* Accept one client via L1 */
173 + nipc_uds_session_t session;
174 + memset(&session, 0, sizeof(session));
175 + session.fd = -1;
176 +
177 + uint64_t sid = server->next_session_id++;
178 + nipc_uds_server_config_t accept_cfg;
179 + nipc_shm_ctx_t *prepared_shm = NULL;
180 + if (!server_prepare_accept_config(server, sid, &accept_cfg, &prepared_shm)) {
181 + nipc_service_posix_sleep_us(10000);
182 + continue;
183 + }
184 +
185 + server->listener.config = accept_cfg;
186 + nipc_uds_error_t uerr = nipc_uds_accept(
187 + &server->listener, sid, &session);
188 + if (uerr != NIPC_UDS_OK) {
189 + server_destroy_precreated_shm(&prepared_shm);
190 + if (!__atomic_load_n(&server->running, __ATOMIC_RELAXED))
191 + break;
192 + nipc_service_posix_sleep_us(10000);
193 + continue;
194 + }
195 +
196 + nipc_service_common_server_note_request_capacity(
197 + server, session.max_request_payload_bytes);
198 + nipc_service_common_server_note_response_capacity(
199 + server, session.max_response_payload_bytes);
200 +
201 + /* Enforce worker_count limit: reap finished sessions, check count */
202 + pthread_mutex_lock(&server->sessions_lock);
203 + nipc_service_posix_server_reap_sessions_locked(server);
204 +
205 + if (server->session_count >= server->worker_count) {
206 + /* At capacity: reject this client by closing the session */
207 + pthread_mutex_unlock(&server->sessions_lock);
208 + server_destroy_precreated_shm(&prepared_shm);
209 + nipc_uds_close_session(&session);
210 + continue;
211 + }
212 +
213 + /* SHM profile guarantee: only negotiate SHM for sessions that already
214 + * have a prepared per-session SHM region. */
215 + nipc_shm_ctx_t *shm = prepared_shm;
216 + if (session.selected_profile == NIPC_PROFILE_SHM_HYBRID ||
217 + session.selected_profile == NIPC_PROFILE_SHM_FUTEX) {
218 + if (!shm) {
219 + pthread_mutex_unlock(&server->sessions_lock);
220 + nipc_uds_close_session(&session);
221 + continue;
222 + }
223 + } else {
224 + server_destroy_precreated_shm(&prepared_shm);
225 + shm = NULL;
226 + }
227 +
228 + /* Create session context */
229 + nipc_session_ctx_t *sctx = nipc_service_posix_calloc(
230 + 1, sizeof(nipc_session_ctx_t),
231 + NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL);
232 + if (!sctx) {
233 + if (shm) { nipc_shm_destroy(shm); free(shm); }
234 + pthread_mutex_unlock(&server->sessions_lock);
235 + nipc_uds_close_session(&session);
236 + continue;
237 + }
238 +
239 + /* The caller-owned server object stays live until destroy joins sessions. */
240 +// codeql[cpp/stack-address-escape]
241 + sctx->server = server;
242 + sctx->session = session;
243 + sctx->shm = shm;
244 + sctx->id = sid;
245 + __atomic_store_n(&sctx->active, true, __ATOMIC_RELAXED);
246 +
247 + server->sessions[server->session_count++] = sctx;
248 + pthread_mutex_unlock(&server->sessions_lock);
249 +
250 + /* Spawn handler thread for this session */
251 + int rc = nipc_service_posix_pthread_create(&sctx->thread, NULL,
252 + nipc_service_posix_session_handler_thread, sctx);
253 + if (rc != 0) {
254 + /* Thread creation failed: clean up */
255 + pthread_mutex_lock(&server->sessions_lock);
256 + /* Remove the sctx we just added */
257 + for (int i = 0; i < server->session_count; i++) {
258 + if (server->sessions[i] == sctx) {
259 + server->sessions[i] = server->sessions[server->session_count - 1];
260 + server->session_count--;
261 + break;
262 + }
263 + }
264 + pthread_mutex_unlock(&server->sessions_lock);
265 +
266 + if (shm) { nipc_shm_destroy(shm); free(shm); }
267 + nipc_uds_close_session(&session);
268 + free(sctx);
269 + }
270 + }
271 +}
272 +
273 +void nipc_server_stop(nipc_managed_server_t *server)
274 +{
275 + __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
276 +}
277 +
278 +bool nipc_server_drain(nipc_managed_server_t *server, uint32_t timeout_ms)
279 +{
280 + /* 1. Stop accepting new clients.
281 + * Do NOT close the listener here — the run loop may still be
282 + * polling on listener.fd. Setting the flag is enough; the run
283 + * loop will exit on its next poll timeout (100ms). The listener
284 + * is closed later by nipc_server_destroy(). */
285 + __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
286 +
287 + /* 2. Wait for in-flight sessions to complete */
288 + bool all_drained = true;
289 + if (server->sessions) {
290 + struct timespec deadline;
291 + clock_gettime(CLOCK_MONOTONIC, &deadline);
292 + deadline.tv_sec += timeout_ms / 1000;
293 + deadline.tv_nsec += (timeout_ms % 1000) * 1000000L;
294 + if (deadline.tv_nsec >= 1000000000L) {
295 + deadline.tv_sec++;
296 + deadline.tv_nsec -= 1000000000L;
297 + }
298 +
299 + /* Poll until all sessions are inactive or timeout */
300 + while (1) {
301 + pthread_mutex_lock(&server->sessions_lock);
302 + int active_count = 0;
303 + for (int i = 0; i < server->session_count; i++) {
304 + if (__atomic_load_n(&server->sessions[i]->active,
305 + __ATOMIC_ACQUIRE))
306 + active_count++;
307 + }
308 + pthread_mutex_unlock(&server->sessions_lock);
309 +
310 + if (active_count == 0)
311 + break;
312 +
313 + struct timespec now;
314 + clock_gettime(CLOCK_MONOTONIC, &now);
315 + if (now.tv_sec > deadline.tv_sec ||
316 + (now.tv_sec == deadline.tv_sec &&
317 + now.tv_nsec >= deadline.tv_nsec)) {
318 + /* Timeout: force-close session fds to unblock recv.
319 + * Closing the fd causes poll/recv to return error,
320 + * which terminates the session handler loop. */
321 + pthread_mutex_lock(&server->sessions_lock);
322 + for (int i = 0; i < server->session_count; i++) {
323 + nipc_session_ctx_t *s = server->sessions[i];
324 + if (__atomic_load_n(&s->active, __ATOMIC_ACQUIRE)) {
325 + if (s->session.fd >= 0) {
326 + shutdown(s->session.fd, SHUT_RDWR);
327 + }
328 + }
329 + }
330 + pthread_mutex_unlock(&server->sessions_lock);
331 + all_drained = false;
332 + break;
333 + }
334 +
335 + nipc_service_posix_sleep_us(5000); /* 5ms poll interval */
336 + }
337 +
338 + /* 3. Join all session threads (finished or not) */
339 + pthread_mutex_lock(&server->sessions_lock);
340 + for (int i = 0; i < server->session_count; i++) {
341 + nipc_session_ctx_t *s = server->sessions[i];
342 + pthread_mutex_unlock(&server->sessions_lock);
343 + pthread_join(s->thread, NULL);
344 + free(s);
345 + pthread_mutex_lock(&server->sessions_lock);
346 + }
347 + server->session_count = 0;
348 + pthread_mutex_unlock(&server->sessions_lock);
349 +
350 + free(server->sessions);
351 + server->sessions = NULL;
352 + server->session_capacity = 0;
353 + pthread_mutex_destroy(&server->sessions_lock);
354 + }
355 +
356 + server->worker_count = 0;
357 + return all_drained;
358 +}
359 +
360 +void nipc_server_destroy(nipc_managed_server_t *server)
361 +{
362 + __atomic_store_n(&server->running, false, __ATOMIC_RELEASE);
363 + nipc_uds_close_listener(&server->listener);
364 +
365 + /* Join all active session threads */
366 + if (server->sessions) {
367 + pthread_mutex_lock(&server->sessions_lock);
368 + for (int i = 0; i < server->session_count; i++) {
369 + nipc_session_ctx_t *s = server->sessions[i];
370 + pthread_mutex_unlock(&server->sessions_lock);
371 + pthread_join(s->thread, NULL);
372 + free(s);
373 + pthread_mutex_lock(&server->sessions_lock);
374 + }
375 + server->session_count = 0;
376 + pthread_mutex_unlock(&server->sessions_lock);
377 +
378 + free(server->sessions);
379 + server->sessions = NULL;
380 + server->session_capacity = 0;
381 + pthread_mutex_destroy(&server->sessions_lock);
382 + }
383 +
384 + server->worker_count = 0;
385 +}
src/libnetdata/netipc/src/service/netipc_service_posix_server_session.c new
+319
@@ -0,0 +1,319 @@
1 +/*
2 + * netipc_service_posix_server_session.c - POSIX managed server session loop.
3 + */
4 +
5 +#include "netipc/netipc_service.h"
6 +#include "netipc/netipc_protocol.h"
7 +#include "netipc/netipc_uds.h"
8 +#include "netipc/netipc_shm.h"
9 +#include "netipc_service_common.h"
10 +#include "netipc_service_posix_internal.h"
11 +
12 +#include <errno.h>
13 +#include <poll.h>
14 +#include <stdint.h>
15 +#include <stdlib.h>
16 +#include <string.h>
17 +#include <sys/socket.h>
18 +
19 +int nipc_service_posix_poll_with_shutdown(int fd, bool *running)
20 +{
21 + while (__atomic_load_n(running, __ATOMIC_RELAXED)) {
22 + struct pollfd pfd = { .fd = fd, .events = POLLIN };
23 + int ret = poll(&pfd, 1, SERVER_POLL_TIMEOUT_MS);
24 +
25 + if (ret < 0) {
26 + if (errno == EINTR)
27 + continue;
28 + return -1;
29 + }
30 +
31 + if (ret == 0)
32 + continue; /* timeout, check running flag */
33 +
34 + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
35 + return -1;
36 +
37 + if (pfd.revents & POLLIN)
38 + return 1;
39 + }
40 + return 0;
41 +}
42 +
43 +static bool session_peer_disconnected(int fd)
44 +{
45 + if (fd < 0)
46 + return true;
47 +
48 + struct pollfd pfd = {
49 + .fd = fd,
50 + .events = POLLIN | POLLHUP | POLLERR,
51 + };
52 +#ifdef POLLRDHUP
53 + pfd.events |= POLLRDHUP;
54 +#endif
55 +
56 + int ret;
57 + do {
58 + ret = poll(&pfd, 1, 0);
59 + } while (ret < 0 && errno == EINTR);
60 +
61 + if (ret < 0)
62 + return true;
63 + if (ret == 0)
64 + return false;
65 +
66 + short disconnected = POLLHUP | POLLERR | POLLNVAL;
67 +#ifdef POLLRDHUP
68 + disconnected |= POLLRDHUP;
69 +#endif
70 + if (pfd.revents & disconnected)
71 + return true;
72 +
73 + if (pfd.revents & POLLIN) {
74 + char ch;
75 + ssize_t n;
76 + do {
77 + n = recv(fd, &ch, sizeof(ch), MSG_PEEK | MSG_DONTWAIT);
78 + } while (n < 0 && errno == EINTR);
79 +
80 + if (n == 0)
81 + return true;
82 + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
83 + return true;
84 + }
85 +
86 + return false;
87 +}
88 +
89 +/*
90 + * Handle one client session: read requests, dispatch to handler,
91 + * send responses. Each session gets its own response buffer.
92 + * Runs until the client disconnects or server stops.
93 + */
94 +static void server_handle_session(nipc_managed_server_t *server,
95 + nipc_uds_session_t *session,
96 + nipc_shm_ctx_t *shm,
97 + uint8_t *resp_buf,
98 + size_t resp_buf_size)
99 +{
100 + /* Allocate recv buffer based on negotiated max request size */
101 + size_t recv_size;
102 + if (!nipc_service_common_header_payload_len(
103 + session->max_request_payload_bytes, &recv_size))
104 + return;
105 + if (recv_size < NIPC_HEADER_LEN + 1024u)
106 + recv_size = NIPC_HEADER_LEN + 1024u;
107 + uint8_t *recv_buf = nipc_service_posix_malloc(
108 + recv_size, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL);
109 + if (!recv_buf)
110 + return;
111 +
112 + while (__atomic_load_n(&server->running, __ATOMIC_RELAXED)) {
113 + nipc_header_t hdr;
114 + const void *payload;
115 + size_t payload_len;
116 +
117 + /* Receive request via the active transport */
118 + if (shm) {
119 + size_t msg_len;
120 + nipc_shm_error_t serr = nipc_shm_receive(shm, recv_buf, recv_size,
121 + &msg_len, SERVER_POLL_TIMEOUT_MS);
122 + if (serr == NIPC_SHM_ERR_TIMEOUT) {
123 + if (session_peer_disconnected(session->fd))
124 + break;
125 + continue; /* check running flag */
126 + }
127 + if (serr != NIPC_SHM_OK)
128 + break;
129 + if (msg_len < NIPC_HEADER_LEN)
130 + break;
131 +
132 + nipc_error_t perr = nipc_header_decode(recv_buf, msg_len, &hdr);
133 + if (perr != NIPC_OK)
134 + break;
135 +
136 + payload = recv_buf + NIPC_HEADER_LEN;
137 + payload_len = msg_len - NIPC_HEADER_LEN;
138 + } else {
139 + /* Poll the session fd before blocking on receive */
140 + int pr = nipc_service_posix_poll_with_shutdown(session->fd, &server->running);
141 + if (pr <= 0)
142 + break; /* shutdown or error */
143 +
144 + nipc_uds_error_t uerr = nipc_uds_receive(
145 + session, recv_buf, recv_size,
146 + &hdr, &payload, &payload_len);
147 + if (uerr == NIPC_UDS_ERR_LIMIT_EXCEEDED) {
148 + if (hdr.kind == NIPC_KIND_REQUEST) {
149 + if (hdr.payload_len > 0)
150 + nipc_service_common_server_note_request_capacity(
151 + server, hdr.payload_len);
152 +
153 + nipc_header_t resp_hdr = {0};
154 + resp_hdr.kind = NIPC_KIND_RESPONSE;
155 + resp_hdr.code = hdr.code;
156 + resp_hdr.message_id = hdr.message_id;
157 + resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
158 + resp_hdr.item_count = 1;
159 + resp_hdr.flags = 0;
160 +
161 + if (nipc_uds_send(session, &resp_hdr, NULL, 0) != NIPC_UDS_OK)
162 + break;
163 + }
164 + break;
165 + }
166 + if (uerr != NIPC_UDS_OK)
167 + break;
168 + }
169 +
170 + /* Protocol violation: unexpected message kind terminates session */
171 + if (hdr.kind != NIPC_KIND_REQUEST)
172 + break;
173 +
174 + if (hdr.code != server->expected_method_code) {
175 + nipc_header_t resp_hdr = {0};
176 + resp_hdr.kind = NIPC_KIND_RESPONSE;
177 + resp_hdr.code = hdr.code;
178 + resp_hdr.message_id = hdr.message_id;
179 + resp_hdr.transport_status = NIPC_STATUS_UNSUPPORTED;
180 + resp_hdr.item_count = 1;
181 + resp_hdr.flags = 0;
182 +
183 + if (shm) {
184 + uint8_t msg[NIPC_HEADER_LEN];
185 + resp_hdr.magic = NIPC_MAGIC_MSG;
186 + resp_hdr.version = NIPC_VERSION;
187 + resp_hdr.header_len = NIPC_HEADER_LEN;
188 + resp_hdr.payload_len = 0;
189 + nipc_header_encode(&resp_hdr, msg, sizeof(msg));
190 + if (nipc_shm_send(shm, msg, sizeof(msg)) != NIPC_SHM_OK)
191 + break;
192 + } else {
193 + if (nipc_uds_send(session, &resp_hdr, NULL, 0) != NIPC_UDS_OK)
194 + break;
195 + }
196 + continue;
197 + }
198 +
199 + if (payload_len <= UINT32_MAX)
200 + nipc_service_common_server_note_request_capacity(
201 + server, (uint32_t)payload_len);
202 +
203 + /* Dispatch: one request kind per service endpoint. */
204 + size_t response_len = 0;
205 + nipc_error_t dispatch_err = server->handler(
206 + server->handler_user,
207 + &hdr,
208 + (const uint8_t *)payload, payload_len,
209 + resp_buf, resp_buf_size,
210 + &response_len);
211 +
212 + /* Build response header */
213 + nipc_header_t resp_hdr;
214 + bool close_after_response = false;
215 + nipc_service_common_prepare_response_header(&hdr, &resp_hdr);
216 + nipc_service_common_apply_dispatch_result(
217 + server, dispatch_err, resp_buf_size,
218 + session->max_response_payload_bytes, false,
219 + &resp_hdr, &response_len, &close_after_response);
220 +
221 + /* Send response via the active transport */
222 + if (shm) {
223 + size_t msg_len;
224 + if (!nipc_service_common_header_payload_len(response_len, &msg_len))
225 + break;
226 +
227 + resp_hdr.magic = NIPC_MAGIC_MSG;
228 + resp_hdr.version = NIPC_VERSION;
229 + resp_hdr.header_len = NIPC_HEADER_LEN;
230 + resp_hdr.payload_len = (uint32_t)response_len;
231 +
232 + /* Use a stack buffer for small responses, heap for large ones */
233 + uint8_t stack_msg[4096];
234 + uint8_t *msg = (msg_len <= sizeof(stack_msg)) ? stack_msg :
235 + nipc_service_posix_malloc(msg_len, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
236 + if (!msg)
237 + break;
238 +
239 + nipc_header_encode(&resp_hdr, msg, NIPC_HEADER_LEN);
240 + if (response_len > 0)
241 + memcpy(msg + NIPC_HEADER_LEN, resp_buf, response_len);
242 +
243 + nipc_shm_error_t serr = nipc_shm_send(shm, msg, msg_len);
244 + if (msg != stack_msg)
245 + free(msg);
246 + if (serr != NIPC_SHM_OK)
247 + break;
248 + } else {
249 + nipc_uds_error_t uerr = nipc_uds_send(
250 + session, &resp_hdr, resp_buf, response_len);
251 + if (uerr != NIPC_UDS_OK)
252 + break;
253 + }
254 +
255 + if (close_after_response)
256 + break;
257 + }
258 +
259 + free(recv_buf);
260 +}
261 +
262 +/* ------------------------------------------------------------------ */
263 +/* Internal: per-session handler thread */
264 +/* ------------------------------------------------------------------ */
265 +
266 +/* Thread function: handles one client session from accept to disconnect. */
267 +void *nipc_service_posix_session_handler_thread(void *arg)
268 +{
269 + nipc_session_ctx_t *sctx = (nipc_session_ctx_t *)arg;
270 + nipc_managed_server_t *server = sctx->server;
271 +
272 + /* Allocate a per-session response buffer */
273 + size_t resp_size = (size_t)sctx->session.max_response_payload_bytes;
274 + if (resp_size < 1024u)
275 + resp_size = 1024u;
276 + uint8_t *resp_buf = nipc_service_posix_malloc(
277 + resp_size, NIPC_POSIX_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
278 + if (resp_buf) {
279 + server_handle_session(server, &sctx->session, sctx->shm,
280 + resp_buf, resp_size);
281 + free(resp_buf);
282 + }
283 +
284 + /* Cleanup SHM and session */
285 + if (sctx->shm) {
286 + nipc_shm_destroy(sctx->shm);
287 + free(sctx->shm);
288 + }
289 + nipc_uds_close_session(&sctx->session);
290 +
291 + /* Mark inactive so the acceptor's reap loop (or server destroy)
292 + * can join this thread and free sctx. Do NOT remove from the
293 + * tracking array here — the reap/destroy path owns that. */
294 + __atomic_store_n(&sctx->active, false, __ATOMIC_RELEASE);
295 + return NULL;
296 +}
297 +
298 +/* ------------------------------------------------------------------ */
299 +/* Internal: reap finished session threads */
300 +/* ------------------------------------------------------------------ */
301 +
302 +/* Reap all finished (inactive) session threads. Called with lock held. */
303 +void nipc_service_posix_server_reap_sessions_locked(nipc_managed_server_t *server)
304 +{
305 + int i = 0;
306 + while (i < server->session_count) {
307 + nipc_session_ctx_t *s = server->sessions[i];
308 + if (!__atomic_load_n(&s->active, __ATOMIC_ACQUIRE)) {
309 + pthread_join(s->thread, NULL);
310 + /* Swap with last, free */
311 + server->sessions[i] = server->sessions[server->session_count - 1];
312 + server->session_count--;
313 + free(s);
314 + } else {
315 + i++;
316 + }
317 + }
318 +
319 +}
src/libnetdata/netipc/src/service/netipc_service_win.c
+29 -1698
@@ -15,6 +15,9 @@
15 #include "netipc/netipc_protocol.h"
16 #include "netipc/netipc_named_pipe.h"
17 #include "netipc/netipc_win_shm.h"
18 +#include "netipc_service_common.h"
19 +#include "netipc_service_platform.h"
20 +#include "netipc_service_win_internal.h"
21
22 #include <stdint.h>
23 #include <stdlib.h>
@@ -22,27 +25,12 @@
25 #include <process.h>
26 #include <windows.h>
27
25 -/* WaitForSingleObject timeout for server poll loops (ms) */
26 -#define SERVER_POLL_TIMEOUT_MS 100
27 -#define NIPC_CLIENT_BUF_DEFAULT 65536u
28 #define CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS 5u
29 #define CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS 5000u
30 +#define CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS 5u
31 +#define CLIENT_CALL_RECONNECT_RETRIES 20u
32 +
33
31 -enum {
32 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL = 1,
33 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL,
34 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL,
35 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL,
36 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL,
37 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL,
38 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL,
39 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL,
40 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_THREAD_CREATE_INTERNAL,
41 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL,
42 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
43 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
44 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
45 -};
34
35 static uint64_t g_win_service_test_fault_state = 0;
36
@@ -87,20 +75,30 @@ static bool service_test_should_fail(int site)
75 }
76 }
77
90 -static void *service_malloc(size_t size, int fault_site)
78 +void *nipc_service_win_malloc(size_t size, int fault_site)
79 {
80 if (service_test_should_fail(fault_site))
81 return NULL;
82 return malloc(size);
83 }
84
97 -static void *service_calloc(size_t count, size_t size, int fault_site)
85 +void *nipc_service_platform_malloc(size_t size, int fault_site)
86 +{
87 + return nipc_service_win_malloc(size, fault_site);
88 +}
89 +
90 +void *nipc_service_win_calloc(size_t count, size_t size, int fault_site)
91 {
92 if (service_test_should_fail(fault_site))
93 return NULL;
94 return calloc(count, size);
95 }
96
97 +void *nipc_service_platform_calloc(size_t count, size_t size, int fault_site)
98 +{
99 + return nipc_service_win_calloc(count, size, fault_site);
100 +}
101 +
102 static void *service_realloc(void *ptr, size_t size, int fault_site)
103 {
104 if (service_test_should_fail(fault_site))
@@ -108,7 +106,7 @@ static void *service_realloc(void *ptr, size_t size, int fault_site)
106 return realloc(ptr, size);
107 }
108
111 -static uintptr_t service_beginthreadex(void *security,
109 +uintptr_t nipc_service_win_beginthreadex(void *security,
110 unsigned stack_size,
111 unsigned (__stdcall *start_address)(void *),
112 void *arglist,
@@ -136,20 +134,7 @@ static uintptr_t service_beginthreadex(void *security,
134 #endif
135 }
136
139 -static uint32_t next_power_of_2_u32(uint32_t n)
140 -{
141 - if (n < 16)
142 - return 16;
143 - n--;
144 - n |= n >> 1;
145 - n |= n >> 2;
146 - n |= n >> 4;
147 - n |= n >> 8;
148 - n |= n >> 16;
149 - return n + 1;
150 -}
151 -
152 -static bool ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int fault_site)
137 +bool nipc_service_win_ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int fault_site)
138 {
139 if (*buf && *buf_size >= need)
140 return true;
@@ -163,1677 +148,23 @@ static bool ensure_buffer(uint8_t **buf, size_t *buf_size, size_t need, int faul
148 return true;
149 }
150
166 -static bool header_payload_len(size_t payload_len, size_t *msg_len_out)
167 -{
168 -#if SIZE_MAX <= UINT32_MAX
169 - if (payload_len > SIZE_MAX - NIPC_HEADER_LEN)
170 - return false;
171 -#endif
172 -
173 - *msg_len_out = NIPC_HEADER_LEN + payload_len;
174 - return true;
175 -}
176 -
177 -static bool header_payload_len_u32(uint32_t payload_len, uint32_t *msg_len_out)
178 -{
179 - if (payload_len > UINT32_MAX - NIPC_HEADER_LEN)
180 - return false;
181 -
182 - *msg_len_out = payload_len + NIPC_HEADER_LEN;
183 - return true;
184 -}
185 -
186 -static void client_note_request_capacity(nipc_client_ctx_t *ctx, uint32_t payload_len)
187 -{
188 - uint32_t grown = next_power_of_2_u32(payload_len);
189 - if (grown > NIPC_MAX_PAYLOAD_CAP)
190 - grown = NIPC_MAX_PAYLOAD_CAP;
191 - if (grown > ctx->transport_config.max_request_payload_bytes)
192 - ctx->transport_config.max_request_payload_bytes = grown;
193 -}
194 -
195 -static void client_note_response_capacity(nipc_client_ctx_t *ctx, uint32_t payload_len)
196 -{
197 - uint32_t grown = next_power_of_2_u32(payload_len);
198 - if (grown > NIPC_MAX_PAYLOAD_CAP)
199 - grown = NIPC_MAX_PAYLOAD_CAP;
200 - if (grown > ctx->transport_config.max_response_payload_bytes)
201 - ctx->transport_config.max_response_payload_bytes = grown;
202 -}
203 -
204 -static bool client_prepare_session_buffers(nipc_client_ctx_t *ctx)
205 -{
206 - size_t response_need;
207 - if (!header_payload_len(ctx->session.max_response_payload_bytes, &response_need))
208 - return false;
209 - if (response_need < NIPC_HEADER_LEN + 1024u)
210 - response_need = NIPC_HEADER_LEN + 1024u;
211 -
212 - if (!ensure_buffer(&ctx->response_buf, &ctx->response_buf_size, response_need,
213 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL))
214 - return false;
215 -
216 - if (ctx->session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
217 - ctx->session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
218 - size_t send_need;
219 - if (!header_payload_len(ctx->session.max_request_payload_bytes, &send_need))
220 - return false;
221 - if (!ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, send_need,
222 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL))
223 - return false;
224 - }
225 -
226 - return true;
227 -}
228 -
229 -static uint32_t cgroups_request_payload_default(void)
230 -{
231 - return 16u;
232 -}
233 -
234 -static uint32_t cgroups_response_payload_default(void)
235 -{
236 - return NIPC_CLIENT_BUF_DEFAULT;
237 -}
238 -
239 -static nipc_np_client_config_t service_client_config_to_transport(
240 - const nipc_client_config_t *config)
241 -{
242 - nipc_np_client_config_t transport = {0};
243 -
244 - if (!config)
245 - return transport;
246 -
247 - transport.supported_profiles = config->supported_profiles;
248 - transport.preferred_profiles = config->preferred_profiles;
249 - transport.max_request_batch_items = config->max_request_batch_items;
250 - transport.max_response_payload_bytes = config->max_response_payload_bytes;
251 - transport.max_response_batch_items = config->max_request_batch_items;
252 - transport.auth_token = config->auth_token;
253 -
254 - return transport;
255 -}
256 -
257 -static nipc_np_server_config_t service_server_config_to_transport(
258 - const nipc_server_config_t *config)
259 -{
260 - nipc_np_server_config_t transport = {0};
261 -
262 - if (!config)
263 - return transport;
264 -
265 - transport.supported_profiles = config->supported_profiles;
266 - transport.preferred_profiles = config->preferred_profiles;
267 - transport.max_request_batch_items = config->max_request_batch_items;
268 - transport.max_response_payload_bytes = config->max_response_payload_bytes;
269 - transport.max_response_batch_items = config->max_request_batch_items;
270 - transport.auth_token = config->auth_token;
271 -
272 - return transport;
273 -}
274 -
275 -static void server_note_request_capacity(nipc_managed_server_t *server,
276 - uint32_t payload_len);
277 -static void server_note_response_capacity(nipc_managed_server_t *server,
278 - uint32_t payload_len);
279 -
280 -/* ------------------------------------------------------------------ */
281 -/* Internal: client connection helpers */
282 -/* ------------------------------------------------------------------ */
283 -
284 -/* Tear down the current connection (Named Pipe session + Win SHM). */
285 -static void client_disconnect(nipc_client_ctx_t *ctx)
286 -{
287 - if (ctx->shm) {
288 - nipc_win_shm_close(ctx->shm);
289 - free(ctx->shm);
290 - ctx->shm = NULL;
291 - }
292 -
293 - if (ctx->session_valid) {
294 - nipc_np_close_session(&ctx->session);
295 - ctx->session_valid = false;
296 - }
297 -}
298 -
299 -static void client_disable_shm_profiles(nipc_client_ctx_t *ctx)
300 -{
301 - ctx->transport_config.supported_profiles &=
302 - ~(NIPC_WIN_SHM_PROFILE_HYBRID | NIPC_WIN_SHM_PROFILE_BUSYWAIT);
303 - ctx->transport_config.preferred_profiles &=
304 - ~(NIPC_WIN_SHM_PROFILE_HYBRID | NIPC_WIN_SHM_PROFILE_BUSYWAIT);
305 -}
306 -
307 -/* Attempt a full connection: Named Pipe connect + handshake, then
308 - * Win SHM upgrade if negotiated. Returns the new state. */
309 -static nipc_client_state_t client_try_connect(nipc_client_ctx_t *ctx)
310 -{
311 - nipc_np_session_t session;
312 - memset(&session, 0, sizeof(session));
313 - session.pipe = INVALID_HANDLE_VALUE;
314 -
315 - nipc_np_error_t err = nipc_np_connect(
316 - ctx->run_dir, ctx->service_name,
317 - &ctx->transport_config, &session);
318 -
319 - switch (err) {
320 - case NIPC_NP_OK:
321 - break;
322 - case NIPC_NP_ERR_CONNECT:
323 - return NIPC_CLIENT_NOT_FOUND;
324 - case NIPC_NP_ERR_AUTH_FAILED:
325 - return NIPC_CLIENT_AUTH_FAILED;
326 - case NIPC_NP_ERR_NO_PROFILE:
327 - case NIPC_NP_ERR_INCOMPATIBLE:
328 - return NIPC_CLIENT_INCOMPATIBLE;
329 - default:
330 - return NIPC_CLIENT_DISCONNECTED;
331 - }
332 -
333 - ctx->session = session;
334 - ctx->session_valid = true;
335 -
336 - if (!client_prepare_session_buffers(ctx)) {
337 - nipc_np_close_session(&ctx->session);
338 - ctx->session_valid = false;
339 - return NIPC_CLIENT_DISCONNECTED;
340 - }
341 -
342 - /* Win SHM upgrade if negotiated */
343 - if (session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
344 - session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
345 -
346 - nipc_win_shm_ctx_t *shm = service_calloc(
347 - 1, sizeof(nipc_win_shm_ctx_t),
348 - NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL);
349 - if (!shm) {
350 - nipc_np_close_session(&ctx->session);
351 - ctx->session_valid = false;
352 - return NIPC_CLIENT_DISCONNECTED;
353 - }
354 - {
355 - /* Retry attach: the server prepares SHM before accepting the
356 - * handshake, but filesystem/object visibility may lag briefly. */
357 - nipc_win_shm_error_t serr = NIPC_WIN_SHM_ERR_OPEN_MAPPING;
358 - ULONGLONG deadline_ms = GetTickCount64() + CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS;
359 - for (;;) {
360 - serr = nipc_win_shm_client_attach(
361 - ctx->run_dir, ctx->service_name,
362 - ctx->transport_config.auth_token,
363 - session.session_id,
364 - session.selected_profile,
365 - shm);
366 - if (serr == NIPC_WIN_SHM_OK)
367 - break;
368 - if (serr != NIPC_WIN_SHM_ERR_OPEN_MAPPING &&
369 - serr != NIPC_WIN_SHM_ERR_OPEN_EVENT &&
370 - serr != NIPC_WIN_SHM_ERR_BAD_MAGIC)
371 - break;
372 - if (GetTickCount64() >= deadline_ms)
373 - break;
374 - Sleep(CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS);
375 - }
376 -
377 - if (serr == NIPC_WIN_SHM_OK) {
378 - ctx->shm = shm;
379 - } else {
380 - /* WinSHM attach failed after negotiation. Close that
381 - * session, blacklist WinSHM for this client context, and
382 - * retry baseline via a new handshake. */
383 - free(shm);
384 - nipc_np_close_session(&ctx->session);
385 - ctx->session_valid = false;
386 - client_disable_shm_profiles(ctx);
387 - if (ctx->transport_config.supported_profiles == 0)
388 - return NIPC_CLIENT_DISCONNECTED;
389 - return client_try_connect(ctx);
390 - }
391 - }
392 - }
393 -
394 - return NIPC_CLIENT_READY;
395 -}
396 -
397 -/* ------------------------------------------------------------------ */
398 -/* Internal: send/receive via the active transport */
399 -/* ------------------------------------------------------------------ */
400 -
401 -static nipc_error_t transport_send(nipc_client_ctx_t *ctx,
402 - nipc_header_t *hdr,
403 - const void *payload,
404 - size_t payload_len)
405 -{
406 - if (payload_len > UINT32_MAX)
407 - return NIPC_ERR_OVERFLOW;
408 -
409 - if (ctx->shm) {
410 - if (payload_len > ctx->session.max_request_payload_bytes) {
411 - client_note_request_capacity(ctx, (uint32_t)payload_len);
412 - return NIPC_ERR_OVERFLOW;
413 - }
414 -
415 - size_t msg_len;
416 - if (!header_payload_len(payload_len, &msg_len))
417 - return NIPC_ERR_OVERFLOW;
418 -
419 - uint8_t *msg = ctx->send_buf;
420 - if (!msg || msg_len > ctx->send_buf_size)
421 - return NIPC_ERR_OVERFLOW;
422 -
423 - hdr->magic = NIPC_MAGIC_MSG;
424 - hdr->version = NIPC_VERSION;
425 - hdr->header_len = NIPC_HEADER_LEN;
426 - hdr->payload_len = (uint32_t)payload_len;
427 -
428 - nipc_header_encode(hdr, msg, NIPC_HEADER_LEN);
429 - if (payload_len > 0)
430 - memcpy(msg + NIPC_HEADER_LEN, payload, payload_len);
431 -
432 - nipc_win_shm_error_t serr = nipc_win_shm_send(ctx->shm, msg, msg_len);
433 - if (serr == NIPC_WIN_SHM_ERR_MSG_TOO_LARGE) {
434 - client_note_request_capacity(ctx, (uint32_t)payload_len);
435 - return NIPC_ERR_OVERFLOW;
436 - }
437 - return (serr == NIPC_WIN_SHM_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
438 - }
439 -
440 - /* Named Pipe path */
441 - nipc_np_error_t uerr = nipc_np_send(&ctx->session, hdr,
442 - payload, payload_len);
443 - if (uerr == NIPC_NP_ERR_LIMIT_EXCEEDED) {
444 - client_note_request_capacity(ctx, (uint32_t)payload_len);
445 - return NIPC_ERR_OVERFLOW;
446 - }
447 - return (uerr == NIPC_NP_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
448 -}
449 -
450 -static nipc_error_t transport_receive(nipc_client_ctx_t *ctx,
451 - void *buf, size_t buf_size,
452 - nipc_header_t *hdr_out,
453 - const void **payload_out,
454 - size_t *payload_len_out)
455 -{
456 - if (ctx->shm) {
457 - size_t msg_len;
458 - nipc_win_shm_error_t serr = nipc_win_shm_receive(ctx->shm, buf, buf_size,
459 - &msg_len, 30000);
460 - if (serr != NIPC_WIN_SHM_OK)
461 - return NIPC_ERR_TRUNCATED;
462 -
463 - if (msg_len < NIPC_HEADER_LEN)
464 - return NIPC_ERR_TRUNCATED;
465 -
466 - nipc_error_t perr = nipc_header_decode(buf, msg_len, hdr_out);
467 - if (perr != NIPC_OK)
468 - return perr;
469 -
470 - *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
471 - *payload_len_out = msg_len - NIPC_HEADER_LEN;
472 - return NIPC_OK;
473 - }
474 -
475 - /* Named Pipe path */
476 - nipc_np_error_t uerr = nipc_np_receive(&ctx->session, buf, buf_size,
477 - hdr_out, payload_out,
478 - payload_len_out);
479 - return (uerr == NIPC_NP_OK) ? NIPC_OK : NIPC_ERR_TRUNCATED;
480 -}
481 -
482 -/* ------------------------------------------------------------------ */
483 -/* Internal: generic raw call (send request, receive response) */
484 -/* ------------------------------------------------------------------ */
485 -
486 -/*
487 - * Single-attempt raw call: build envelope, send, receive, validate
488 - * envelope. The caller handles encode before and decode after.
489 - *
490 - * On success, response_payload_out and response_len_out point into the
491 - * internal client response buffer (valid until next call on this context).
492 - */
493 -static nipc_error_t do_raw_call(nipc_client_ctx_t *ctx,
494 - uint16_t method_code,
495 - const void *request_payload,
496 - size_t request_len,
497 - const void **response_payload_out,
498 - size_t *response_len_out)
499 -{
500 - nipc_header_t hdr = {0};
501 - hdr.kind = NIPC_KIND_REQUEST;
502 - hdr.code = method_code;
503 - hdr.flags = 0;
504 - hdr.item_count = 1;
505 - hdr.message_id = (uint64_t)(ctx->call_count + 1);
506 - hdr.transport_status = NIPC_STATUS_OK;
507 -
508 - nipc_error_t err = transport_send(ctx, &hdr, request_payload, request_len);
509 - if (err != NIPC_OK)
510 - return err;
511 -
512 - nipc_header_t resp_hdr;
513 - err = transport_receive(ctx, ctx->response_buf, ctx->response_buf_size,
514 - &resp_hdr, response_payload_out, response_len_out);
515 - if (err != NIPC_OK)
516 - return err;
517 -
518 - if (resp_hdr.kind != NIPC_KIND_RESPONSE)
519 - return NIPC_ERR_BAD_KIND;
520 - if (resp_hdr.code != method_code)
521 - return NIPC_ERR_BAD_LAYOUT;
522 - if (resp_hdr.message_id != hdr.message_id)
523 - return NIPC_ERR_BAD_LAYOUT;
524 - switch (resp_hdr.transport_status) {
525 - case NIPC_STATUS_OK:
526 - break;
527 - case NIPC_STATUS_LIMIT_EXCEEDED:
528 - if (ctx->session.max_response_payload_bytes > 0) {
529 - uint32_t current = ctx->session.max_response_payload_bytes;
530 - client_note_response_capacity(
531 - ctx, current >= UINT32_MAX / 2u ? UINT32_MAX : current * 2u);
532 - }
533 - return NIPC_ERR_OVERFLOW;
534 - case NIPC_STATUS_UNSUPPORTED:
535 - return NIPC_ERR_BAD_LAYOUT;
536 - case NIPC_STATUS_BAD_ENVELOPE:
537 - case NIPC_STATUS_INTERNAL_ERROR:
538 - default:
539 - return NIPC_ERR_BAD_LAYOUT;
540 - }
541 -
542 - return NIPC_OK;
543 -}
544 -
545 -/*
546 - * Generic call-with-retry:
547 - * - ordinary failures reconnect and retry once
548 - * - overflow-driven resize recovery may reconnect repeatedly until
549 - * negotiated capacities grow or recovery fails
550 - * The caller provides a function pointer for the single-attempt logic.
551 - */
552 -typedef nipc_error_t (*nipc_attempt_fn)(nipc_client_ctx_t *ctx, void *state);
553 -
554 -static nipc_error_t call_with_retry(nipc_client_ctx_t *ctx,
555 - nipc_attempt_fn attempt,
556 - void *state)
557 -{
558 - if (ctx->state != NIPC_CLIENT_READY) {
559 - ctx->error_count++;
560 - return NIPC_ERR_NOT_READY;
561 - }
562 -
563 - /* Cap overflow-driven retries: payloads grow by powers of 2, so 8
564 - * retries allows ~256x growth from the initial negotiated size. */
565 - int overflow_retries = 0;
566 - for (;;) {
567 - uint32_t prev_req = ctx->session.max_request_payload_bytes;
568 - uint32_t prev_resp = ctx->session.max_response_payload_bytes;
569 - uint32_t prev_cfg_req = ctx->transport_config.max_request_payload_bytes;
570 - uint32_t prev_cfg_resp = ctx->transport_config.max_response_payload_bytes;
571 -
572 - nipc_error_t err = attempt(ctx, state);
573 - if (err == NIPC_OK) {
574 - ctx->call_count++;
575 - return NIPC_OK;
576 - }
577 -
578 - if (err != NIPC_ERR_OVERFLOW) {
579 - client_disconnect(ctx);
580 - ctx->state = NIPC_CLIENT_BROKEN;
581 - ctx->state = client_try_connect(ctx);
582 - if (ctx->state != NIPC_CLIENT_READY) {
583 - ctx->error_count++;
584 - return err;
585 - }
586 -
587 - ctx->reconnect_count++;
588 - err = attempt(ctx, state);
589 - if (err == NIPC_OK) {
590 - ctx->call_count++;
591 - return NIPC_OK;
592 - }
593 -
594 - client_disconnect(ctx);
595 - ctx->state = NIPC_CLIENT_BROKEN;
596 - ctx->error_count++;
597 - return err;
598 - }
599 -
600 - client_disconnect(ctx);
601 - ctx->state = NIPC_CLIENT_BROKEN;
602 - ctx->state = client_try_connect(ctx);
603 - if (ctx->state != NIPC_CLIENT_READY) {
604 - ctx->error_count++;
605 - return err;
606 - }
607 - ctx->reconnect_count++;
608 -
609 - if (ctx->session.max_request_payload_bytes <= prev_req &&
610 - ctx->session.max_response_payload_bytes <= prev_resp &&
611 - ctx->transport_config.max_request_payload_bytes <= prev_cfg_req &&
612 - ctx->transport_config.max_response_payload_bytes <= prev_cfg_resp) {
613 - client_disconnect(ctx);
614 - ctx->state = NIPC_CLIENT_BROKEN;
615 - ctx->error_count++;
616 - return err;
617 - }
618 -
619 - if (++overflow_retries >= 8) {
620 - client_disconnect(ctx);
621 - ctx->state = NIPC_CLIENT_BROKEN;
622 - ctx->error_count++;
623 - return err;
624 - }
625 - }
626 -}
627 -
628 -/* ------------------------------------------------------------------ */
629 -/* Internal: single attempt at a cgroups snapshot call */
630 -/* ------------------------------------------------------------------ */
631 -
632 -typedef struct {
633 - nipc_cgroups_resp_view_t *view_out;
634 -} cgroups_call_state_t;
635 -
636 -static nipc_error_t do_cgroups_attempt(nipc_client_ctx_t *ctx, void *state)
637 -{
638 - cgroups_call_state_t *s = (cgroups_call_state_t *)state;
639 -
640 - nipc_cgroups_req_t req = { .layout_version = 1, .flags = 0 };
641 - uint8_t req_buf[4];
642 - size_t req_len = nipc_cgroups_req_encode(&req, req_buf, sizeof(req_buf));
643 - if (req_len == 0)
644 - return NIPC_ERR_TRUNCATED;
645 -
646 - const void *payload;
647 - size_t payload_len;
648 - nipc_error_t err = do_raw_call(ctx, NIPC_METHOD_CGROUPS_SNAPSHOT,
649 - req_buf, req_len,
650 - &payload, &payload_len);
651 - if (err != NIPC_OK)
652 - return err;
653 -
654 - return nipc_cgroups_resp_decode(payload, payload_len, s->view_out);
655 -}
656 -
657 -/* ------------------------------------------------------------------ */
658 -/* Public API: client lifecycle */
659 -/* ------------------------------------------------------------------ */
660 -
661 -void nipc_client_init(nipc_client_ctx_t *ctx,
662 - const char *run_dir,
663 - const char *service_name,
664 - const nipc_client_config_t *config)
665 -{
666 - memset(ctx, 0, sizeof(*ctx));
667 - ctx->state = NIPC_CLIENT_DISCONNECTED;
668 - ctx->session.pipe = INVALID_HANDLE_VALUE;
669 - ctx->session_valid = false;
670 - ctx->shm = NULL;
671 -
672 - if (run_dir) {
673 - size_t len = strlen(run_dir);
674 - if (len >= sizeof(ctx->run_dir))
675 - len = sizeof(ctx->run_dir) - 1;
676 - memcpy(ctx->run_dir, run_dir, len);
677 - ctx->run_dir[len] = '\0';
678 - }
679 -
680 - if (service_name) {
681 - size_t len = strlen(service_name);
682 - if (len >= sizeof(ctx->service_name))
683 - len = sizeof(ctx->service_name) - 1;
684 - memcpy(ctx->service_name, service_name, len);
685 - ctx->service_name[len] = '\0';
686 - }
687 -
688 - ctx->transport_config = service_client_config_to_transport(config);
689 - if (ctx->transport_config.max_request_payload_bytes == 0)
690 - ctx->transport_config.max_request_payload_bytes = cgroups_request_payload_default();
691 - if (ctx->transport_config.max_response_payload_bytes == 0)
692 - ctx->transport_config.max_response_payload_bytes = cgroups_response_payload_default();
693 -}
694 -
695 -bool nipc_client_refresh(nipc_client_ctx_t *ctx)
696 -{
697 - nipc_client_state_t old_state = ctx->state;
698 -
699 - switch (ctx->state) {
700 - case NIPC_CLIENT_DISCONNECTED:
701 - case NIPC_CLIENT_NOT_FOUND:
702 - ctx->state = NIPC_CLIENT_CONNECTING;
703 - ctx->state = client_try_connect(ctx);
704 - if (ctx->state == NIPC_CLIENT_READY)
705 - ctx->connect_count++;
706 - break;
707 -
708 - case NIPC_CLIENT_BROKEN:
709 - client_disconnect(ctx);
710 - ctx->state = NIPC_CLIENT_CONNECTING;
711 - ctx->state = client_try_connect(ctx);
712 - if (ctx->state == NIPC_CLIENT_READY)
713 - ctx->reconnect_count++;
714 - break;
715 -
716 - case NIPC_CLIENT_READY:
717 - case NIPC_CLIENT_CONNECTING:
718 - case NIPC_CLIENT_AUTH_FAILED:
719 - case NIPC_CLIENT_INCOMPATIBLE:
720 - break;
721 - }
722 -
723 - return ctx->state != old_state;
724 -}
725 -
726 -void nipc_client_status(const nipc_client_ctx_t *ctx,
727 - nipc_client_status_t *out)
728 -{
729 - out->state = ctx->state;
730 - out->connect_count = ctx->connect_count;
731 - out->reconnect_count = ctx->reconnect_count;
732 - out->call_count = ctx->call_count;
733 - out->error_count = ctx->error_count;
734 -}
735 -
736 -void nipc_client_close(nipc_client_ctx_t *ctx)
737 -{
738 - client_disconnect(ctx);
739 - free(ctx->response_buf);
740 - free(ctx->send_buf);
741 - ctx->response_buf = NULL;
742 - ctx->send_buf = NULL;
743 - ctx->response_buf_size = 0;
744 - ctx->send_buf_size = 0;
745 - ctx->state = NIPC_CLIENT_DISCONNECTED;
746 -}
747 -
748 -/* ------------------------------------------------------------------ */
749 -/* Public API: typed cgroups snapshot call */
750 -/* ------------------------------------------------------------------ */
751 -
752 -nipc_error_t nipc_client_call_cgroups_snapshot(
753 - nipc_client_ctx_t *ctx,
754 - nipc_cgroups_resp_view_t *view_out)
755 -{
756 - cgroups_call_state_t state = {
757 - .view_out = view_out,
758 - };
759 - return call_with_retry(ctx, do_cgroups_attempt, &state);
760 -}
761 -
762 -/* ------------------------------------------------------------------ */
763 -/* Internal: managed server session handler */
764 -/* ------------------------------------------------------------------ */
765 -
766 -/*
767 - * Handle one client session: read requests, dispatch to handler,
768 - * send responses. Each session gets its own response buffer.
769 - * Runs until the client disconnects or server stops.
770 - */
771 -static void server_handle_session(nipc_managed_server_t *server,
772 - nipc_np_session_t *session,
773 - nipc_win_shm_ctx_t *shm,
774 - uint8_t *resp_buf,
775 - size_t resp_buf_size)
776 -{
777 - /* Dynamically allocate recv buffer based on negotiated max */
778 - size_t recv_size;
779 - if (!header_payload_len(session->max_request_payload_bytes, &recv_size))
780 - return;
781 - if (recv_size < NIPC_HEADER_LEN + 1024u)
782 - recv_size = NIPC_HEADER_LEN + 1024u;
783 - uint8_t *recv_buf = service_malloc(
784 - recv_size, NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL);
785 - if (!recv_buf)
786 - return;
787 -
788 - while (InterlockedCompareExchange(&server->running, 0, 0)) {
789 - nipc_header_t hdr;
790 - const void *payload;
791 - size_t payload_len;
792 -
793 - /* Receive request via the active transport */
794 - if (shm) {
795 - size_t msg_len;
796 - nipc_win_shm_error_t serr = nipc_win_shm_receive(shm, recv_buf, recv_size,
797 - &msg_len, SERVER_POLL_TIMEOUT_MS);
798 - if (serr == NIPC_WIN_SHM_ERR_TIMEOUT)
799 - continue;
800 - if (serr != NIPC_WIN_SHM_OK)
801 - break;
802 - if (msg_len < NIPC_HEADER_LEN)
803 - break;
804 -
805 - nipc_error_t perr = nipc_header_decode(recv_buf, msg_len, &hdr);
806 - if (perr != NIPC_OK)
807 - break;
808 -
809 - payload = recv_buf + NIPC_HEADER_LEN;
810 - payload_len = msg_len - NIPC_HEADER_LEN;
811 - } else {
812 - /* Named Pipe path: wait for readability first, then receive.
813 - * This mirrors the Go/Rust Windows server loops and avoids
814 - * relying on a blocking ReadFile wake-up for each ping-pong
815 - * request. */
816 - bool readable = false;
817 - nipc_np_error_t werr = nipc_np_wait_readable(
818 - session, SERVER_POLL_TIMEOUT_MS, &readable);
819 - if (werr == NIPC_NP_ERR_DISCONNECTED)
820 - break;
821 - if (werr != NIPC_NP_OK)
822 - break;
823 - if (!readable)
824 - continue;
825 -
826 - nipc_np_error_t uerr = nipc_np_receive(
827 - session, recv_buf, recv_size,
828 - &hdr, &payload, &payload_len);
829 - if (uerr == NIPC_NP_ERR_LIMIT_EXCEEDED) {
830 - if (hdr.kind == NIPC_KIND_REQUEST) {
831 - if (hdr.payload_len > 0)
832 - server_note_request_capacity(server, hdr.payload_len);
833 -
834 - nipc_header_t resp_hdr = {0};
835 - resp_hdr.kind = NIPC_KIND_RESPONSE;
836 - resp_hdr.code = hdr.code;
837 - resp_hdr.message_id = hdr.message_id;
838 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
839 - resp_hdr.item_count = 1;
840 - resp_hdr.flags = 0;
841 -
842 - if (nipc_np_send(session, &resp_hdr, NULL, 0) != NIPC_NP_OK)
843 - break;
844 - }
845 - break;
846 - }
847 - if (uerr != NIPC_NP_OK)
848 - break;
849 - }
850 -
851 - /* Protocol violation: unexpected message kind terminates session */
852 - if (hdr.kind != NIPC_KIND_REQUEST)
853 - break;
854 -
855 - if (hdr.code != server->expected_method_code) {
856 - nipc_header_t resp_hdr = {0};
857 - resp_hdr.kind = NIPC_KIND_RESPONSE;
858 - resp_hdr.code = hdr.code;
859 - resp_hdr.message_id = hdr.message_id;
860 - resp_hdr.transport_status = NIPC_STATUS_UNSUPPORTED;
861 - resp_hdr.item_count = 1;
862 - resp_hdr.flags = 0;
863 -
864 - if (shm) {
865 - uint8_t msg[NIPC_HEADER_LEN];
866 - resp_hdr.magic = NIPC_MAGIC_MSG;
867 - resp_hdr.version = NIPC_VERSION;
868 - resp_hdr.header_len = NIPC_HEADER_LEN;
869 - resp_hdr.payload_len = 0;
870 - nipc_header_encode(&resp_hdr, msg, sizeof(msg));
871 - if (nipc_win_shm_send(shm, msg, sizeof(msg)) != NIPC_WIN_SHM_OK)
872 - break;
873 - } else {
874 - if (nipc_np_send(session, &resp_hdr, NULL, 0) != NIPC_NP_OK)
875 - break;
876 - }
877 - continue;
878 - }
879 -
880 - if (payload_len <= UINT32_MAX)
881 - server_note_request_capacity(server, (uint32_t)payload_len);
882 -
883 - /* Dispatch: one request kind per service endpoint. */
884 - size_t response_len = 0;
885 - nipc_error_t dispatch_err = server->handler(
886 - server->handler_user,
887 - &hdr,
888 - (const uint8_t *)payload, payload_len,
889 - resp_buf, resp_buf_size,
890 - &response_len);
891 -
892 - /* Build response header */
893 - nipc_header_t resp_hdr = {0};
894 - resp_hdr.kind = NIPC_KIND_RESPONSE;
895 - resp_hdr.code = hdr.code;
896 - resp_hdr.message_id = hdr.message_id;
897 - if ((hdr.flags & NIPC_FLAG_BATCH) && hdr.item_count >= 1) {
898 - resp_hdr.item_count = hdr.item_count;
899 - resp_hdr.flags = NIPC_FLAG_BATCH;
900 - } else {
901 - resp_hdr.item_count = 1;
902 - resp_hdr.flags = 0;
903 - }
904 -
905 - switch (dispatch_err) {
906 - case NIPC_OK:
907 - if (response_len > resp_buf_size ||
908 - response_len > session->max_response_payload_bytes ||
909 - response_len > SIZE_MAX - NIPC_HEADER_LEN) {
910 - server_note_response_capacity(
911 - server,
912 - response_len >= UINT32_MAX ? UINT32_MAX : (uint32_t)response_len);
913 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
914 - response_len = 0;
915 - } else {
916 - if (response_len <= UINT32_MAX)
917 - server_note_response_capacity(server, (uint32_t)response_len);
918 - resp_hdr.transport_status = NIPC_STATUS_OK;
919 - }
920 - break;
921 - case NIPC_ERR_OVERFLOW:
922 - if (session->max_response_payload_bytes >= UINT32_MAX / 2u)
923 - server_note_response_capacity(server, UINT32_MAX);
924 - else
925 - server_note_response_capacity(server,
926 - session->max_response_payload_bytes * 2u);
927 - resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
928 - response_len = 0;
929 - break;
930 - case NIPC_ERR_TRUNCATED:
931 - case NIPC_ERR_BAD_LAYOUT:
932 - case NIPC_ERR_OUT_OF_BOUNDS:
933 - case NIPC_ERR_MISSING_NUL:
934 - case NIPC_ERR_BAD_ALIGNMENT:
935 - case NIPC_ERR_BAD_ITEM_COUNT:
936 - resp_hdr.transport_status = NIPC_STATUS_BAD_ENVELOPE;
937 - response_len = 0;
938 - break;
939 - case NIPC_ERR_HANDLER_FAILED:
940 - default:
941 - resp_hdr.transport_status = NIPC_STATUS_INTERNAL_ERROR;
942 - response_len = 0;
943 - break;
944 - }
945 -
946 - /* Send response via the active transport */
947 - if (shm) {
948 - size_t msg_len;
949 - if (!header_payload_len(response_len, &msg_len))
950 - break;
951 -
952 - resp_hdr.magic = NIPC_MAGIC_MSG;
953 - resp_hdr.version = NIPC_VERSION;
954 - resp_hdr.header_len = NIPC_HEADER_LEN;
955 - resp_hdr.payload_len = (uint32_t)response_len;
956 -
957 - uint8_t stack_msg[4096];
958 - uint8_t *msg = (msg_len <= sizeof(stack_msg)) ? stack_msg : malloc(msg_len);
959 - if (!msg)
960 - break;
961 -
962 - nipc_header_encode(&resp_hdr, msg, NIPC_HEADER_LEN);
963 - if (response_len > 0)
964 - memcpy(msg + NIPC_HEADER_LEN, resp_buf, response_len);
965 -
966 - nipc_win_shm_error_t serr = nipc_win_shm_send(shm, msg, msg_len);
967 - if (msg != stack_msg)
968 - free(msg);
969 - if (serr != NIPC_WIN_SHM_OK)
970 - break;
971 - } else {
972 - nipc_np_error_t uerr = nipc_np_send(
973 - session, &resp_hdr, resp_buf, response_len);
974 - if (uerr != NIPC_NP_OK)
975 - break;
976 - }
977 -
978 - if (dispatch_err == NIPC_ERR_OVERFLOW)
979 - break;
980 - }
981 -
982 - free(recv_buf);
983 -}
984 -
985 -static uint32_t server_snapshot_max_items(size_t response_buf_size,
986 - const nipc_cgroups_service_handler_t *service_handler)
987 -{
988 - if (service_handler->snapshot_max_items != 0)
989 - return service_handler->snapshot_max_items;
990 - return nipc_cgroups_builder_estimate_max_items(response_buf_size);
991 -}
992 -
993 -static void server_note_request_capacity(nipc_managed_server_t *server,
994 - uint32_t payload_len)
995 -{
996 - uint32_t grown = next_power_of_2_u32(payload_len);
997 - uint32_t current = server->learned_request_payload_bytes;
998 - while (grown > current) {
999 - uint32_t previous = (uint32_t)InterlockedCompareExchange(
1000 - (volatile LONG *)&server->learned_request_payload_bytes,
1001 - (LONG)grown, (LONG)current);
1002 - if (previous == current)
1003 - break;
1004 - current = previous;
1005 - }
1006 -}
1007 -
1008 -static void server_note_response_capacity(nipc_managed_server_t *server,
1009 - uint32_t payload_len)
1010 -{
1011 - uint32_t grown = next_power_of_2_u32(payload_len);
1012 - uint32_t current = server->learned_response_payload_bytes;
1013 - while (grown > current) {
1014 - uint32_t previous = (uint32_t)InterlockedCompareExchange(
1015 - (volatile LONG *)&server->learned_response_payload_bytes,
1016 - (LONG)grown, (LONG)current);
1017 - if (previous == current)
1018 - break;
1019 - current = previous;
1020 - }
1021 -}
1022 -
1023 -static nipc_error_t server_typed_dispatch(void *user,
1024 - const nipc_header_t *request_hdr,
1025 - const uint8_t *request_payload,
1026 - size_t request_len,
1027 - uint8_t *response_buf,
1028 - size_t response_buf_size,
1029 - size_t *response_len_out)
1030 -{
1031 - nipc_managed_server_t *server = (nipc_managed_server_t *)user;
1032 - nipc_cgroups_service_handler_t *service_handler = &server->service_handler;
1033 - (void)request_hdr;
1034 -
1035 - if (!service_handler->handle)
1036 - return NIPC_ERR_HANDLER_FAILED;
1037 -
1038 - return nipc_dispatch_cgroups_snapshot(
1039 - request_payload, request_len,
1040 - response_buf, response_buf_size, response_len_out,
1041 - server_snapshot_max_items(response_buf_size, service_handler),
1042 - service_handler->handle, service_handler->user);
1043 -}
1044 -
1045 -/* ------------------------------------------------------------------ */
1046 -/* Internal: per-session handler thread */
1047 -/* ------------------------------------------------------------------ */
1048 -
1049 -/* Thread function: handles one client session from accept to disconnect. */
1050 -static unsigned __stdcall session_handler_thread(void *arg)
1051 -{
1052 - nipc_session_ctx_t *sctx = (nipc_session_ctx_t *)arg;
1053 - nipc_managed_server_t *server = sctx->server;
1054 - /* Allocate a per-session response buffer */
1055 - size_t resp_size = (size_t)sctx->session.max_response_payload_bytes;
1056 - if (resp_size < 1024u)
1057 - resp_size = 1024u;
1058 - uint8_t *resp_buf = service_malloc(
1059 - resp_size, NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
1060 - if (resp_buf) {
1061 - server_handle_session(server, &sctx->session, sctx->shm,
1062 - resp_buf, resp_size);
1063 - free(resp_buf);
1064 - }
1065 -
1066 - /* Cleanup SHM and session */
1067 - if (sctx->shm) {
1068 - nipc_win_shm_destroy(sctx->shm);
1069 - free(sctx->shm);
1070 - }
1071 - nipc_np_close_session(&sctx->session);
1072 -
1073 - /* Mark inactive; the reap/destroy path owns removal from the array */
1074 - InterlockedExchange((volatile LONG *)&sctx->active, 0);
1075 - return 0;
1076 -}
1077 -
1078 -/* ------------------------------------------------------------------ */
1079 -/* Internal: reap finished session threads */
1080 -/* ------------------------------------------------------------------ */
1081 -
1082 -/* Reap all finished (inactive) session threads. Called with lock held. */
1083 -static void server_reap_sessions_locked(nipc_managed_server_t *server)
1084 -{
1085 - int i = 0;
1086 - while (i < server->session_count) {
1087 - nipc_session_ctx_t *s = server->sessions[i];
1088 - if (!InterlockedCompareExchange((volatile LONG *)&s->active, 0, 0)) {
1089 - WaitForSingleObject(s->thread, INFINITE);
1090 - CloseHandle(s->thread);
1091 - /* Swap with last, free */
1092 - server->sessions[i] = server->sessions[server->session_count - 1];
1093 - server->session_count--;
1094 - free(s);
1095 - } else {
1096 - i++;
1097 - }
1098 - }
1099 -
1100 -}
1101 -
1102 -typedef struct {
1103 - nipc_win_shm_ctx_t *hybrid;
1104 - nipc_win_shm_ctx_t *busywait;
1105 -} prepared_win_shm_t;
1106 -
1107 -static void server_destroy_prepared_win_shm(prepared_win_shm_t *prepared)
1108 -{
1109 - if (!prepared)
1110 - return;
1111 - if (prepared->hybrid) {
1112 - nipc_win_shm_destroy(prepared->hybrid);
1113 - free(prepared->hybrid);
1114 - prepared->hybrid = NULL;
1115 - }
1116 - if (prepared->busywait) {
1117 - nipc_win_shm_destroy(prepared->busywait);
1118 - free(prepared->busywait);
1119 - prepared->busywait = NULL;
1120 - }
1121 -}
1122 -
1123 -static nipc_win_shm_ctx_t *server_take_prepared_win_shm(prepared_win_shm_t *prepared,
1124 - uint32_t profile)
1125 -{
1126 - if (!prepared)
1127 - return NULL;
1128 - if (profile == NIPC_WIN_SHM_PROFILE_HYBRID) {
1129 - nipc_win_shm_ctx_t *ctx = prepared->hybrid;
1130 - prepared->hybrid = NULL;
1131 - return ctx;
1132 - }
1133 - if (profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
1134 - nipc_win_shm_ctx_t *ctx = prepared->busywait;
1135 - prepared->busywait = NULL;
1136 - return ctx;
1137 - }
1138 - return NULL;
1139 -}
1140 -
1141 -static bool server_prepare_accept_config(nipc_managed_server_t *server,
1142 - uint64_t sid,
1143 - nipc_np_server_config_t *cfg_out,
1144 - prepared_win_shm_t *prepared)
1145 -{
1146 - *cfg_out = server->base_config;
1147 - cfg_out->max_request_payload_bytes = server->learned_request_payload_bytes;
1148 - cfg_out->max_response_payload_bytes = server->learned_response_payload_bytes;
1149 - memset(prepared, 0, sizeof(*prepared));
1150 -
1151 - uint32_t shm_profiles = cfg_out->supported_profiles &
1152 - (NIPC_WIN_SHM_PROFILE_HYBRID |
1153 - NIPC_WIN_SHM_PROFILE_BUSYWAIT);
1154 - if (shm_profiles == 0)
1155 - return true;
1156 -
1157 - uint32_t request_capacity;
1158 - uint32_t response_capacity;
1159 - if (!header_payload_len_u32(NIPC_MAX_PAYLOAD_CAP, &request_capacity) ||
1160 - !header_payload_len_u32(cfg_out->max_response_payload_bytes, &response_capacity)) {
1161 - cfg_out->supported_profiles &= ~shm_profiles;
1162 - cfg_out->preferred_profiles &= ~shm_profiles;
1163 - return cfg_out->supported_profiles != 0;
1164 - }
1165 -
1166 - const uint32_t profiles[] = {
1167 - NIPC_WIN_SHM_PROFILE_HYBRID,
1168 - NIPC_WIN_SHM_PROFILE_BUSYWAIT,
1169 - };
1170 - for (size_t i = 0; i < sizeof(profiles) / sizeof(profiles[0]); i++) {
1171 - uint32_t profile = profiles[i];
1172 - if (!(cfg_out->supported_profiles & profile))
1173 - continue;
1174 -
1175 - nipc_win_shm_ctx_t *ctx = service_calloc(
1176 - 1, sizeof(nipc_win_shm_ctx_t),
1177 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL);
1178 - if (!ctx)
1179 - continue;
1180 -
1181 - /* HELLO has not been read yet, so the request segment must cover any
1182 - * client proposal the handshake may legally echo back. */
1183 - nipc_win_shm_error_t serr = nipc_win_shm_server_create(
1184 - server->run_dir, server->service_name,
1185 - server->auth_token,
1186 - sid,
1187 - profile,
1188 - request_capacity,
1189 - response_capacity,
1190 - ctx);
1191 - if (serr == NIPC_WIN_SHM_OK) {
1192 - if (profile == NIPC_WIN_SHM_PROFILE_HYBRID)
1193 - prepared->hybrid = ctx;
1194 - else
1195 - prepared->busywait = ctx;
1196 - continue;
1197 - }
1198 -
1199 - free(ctx);
1200 - cfg_out->supported_profiles &= ~profile;
1201 - cfg_out->preferred_profiles &= ~profile;
1202 - }
1203 -
1204 - return cfg_out->supported_profiles != 0;
1205 -}
1206 -
1207 -static void server_wake_listener(nipc_managed_server_t *server)
1208 -{
1209 - if (server->listener.pipe == INVALID_HANDLE_VALUE ||
1210 - server->listener.pipe == NULL ||
1211 - server->listener.pipe_name[0] == L'\0')
1212 - return;
1213 -
1214 - HANDLE wake = CreateFileW(
1215 - server->listener.pipe_name,
1216 - GENERIC_READ | GENERIC_WRITE,
1217 - 0,
1218 - NULL,
1219 - OPEN_EXISTING,
1220 - 0,
1221 - NULL);
1222 - if (wake != INVALID_HANDLE_VALUE)
1223 - CloseHandle(wake);
1224 -}
1225 -
1226 -/* Ask active session threads to leave synchronous ReadFile/WriteFile waits.
1227 - * This is safer than targeting pipe handles from another thread because the
1228 - * thread handle stays valid until the owner joins it. */
1229 -static void server_cancel_active_session_io_locked(nipc_managed_server_t *server)
1230 -{
1231 - for (int i = 0; i < server->session_count; i++) {
1232 - nipc_session_ctx_t *s = server->sessions[i];
1233 - if (!s)
1234 - continue;
1235 - if (!InterlockedCompareExchange((volatile LONG *)&s->active, 0, 0))
1236 - continue;
1237 - if (s->thread == NULL || s->thread == INVALID_HANDLE_VALUE)
1238 - continue;
1239 - CancelSynchronousIo(s->thread);
1240 - }
1241 -}
1242 -
1243 -/* ------------------------------------------------------------------ */
1244 -/* Public API: managed server */
1245 -/* ------------------------------------------------------------------ */
1246 -
1247 -static nipc_error_t server_init_raw(nipc_managed_server_t *server,
1248 - const char *run_dir,
1249 - const char *service_name,
1250 - const nipc_np_server_config_t *config,
1251 - int worker_count,
1252 - uint16_t expected_method_code,
1253 - nipc_server_handler_fn handler,
1254 - void *user)
1255 -{
1256 - if (!server)
1257 - return NIPC_ERR_BAD_LAYOUT;
1258 -
1259 - memset(server, 0, sizeof(*server));
1260 - server->listener.pipe = INVALID_HANDLE_VALUE;
1261 - InterlockedExchange(&server->running, 0);
1262 -
1263 - if (!run_dir || !service_name || !config || !handler)
1264 - return NIPC_ERR_BAD_LAYOUT;
1265 -
1266 - if (worker_count < 1)
1267 - worker_count = 1;
1268 -
1269 - /* Store config */
1270 - {
1271 - size_t len = strlen(run_dir);
1272 - if (len >= sizeof(server->run_dir))
1273 - len = sizeof(server->run_dir) - 1;
1274 - memcpy(server->run_dir, run_dir, len);
1275 - server->run_dir[len] = '\0';
1276 - }
1277 - {
1278 - size_t len = strlen(service_name);
1279 - if (len >= sizeof(server->service_name))
1280 - len = sizeof(server->service_name) - 1;
1281 - memcpy(server->service_name, service_name, len);
1282 - server->service_name[len] = '\0';
1283 - }
1284 -
1285 - server->handler = handler;
1286 - server->handler_user = user;
1287 - server->worker_count = worker_count;
1288 - server->expected_method_code = expected_method_code;
1289 - server->base_config = *config;
1290 - server->learned_request_payload_bytes =
1291 - (config && config->max_request_payload_bytes > 0)
1292 - ? config->max_request_payload_bytes
1293 - : NIPC_MAX_PAYLOAD_DEFAULT;
1294 - server->learned_response_payload_bytes =
1295 - (config && config->max_response_payload_bytes > 0)
1296 - ? config->max_response_payload_bytes
1297 - : NIPC_MAX_PAYLOAD_DEFAULT;
1298 - server->auth_token = config ? config->auth_token : 0;
1299 -
1300 -
1301 - /* Initialize session tracking */
1302 - server->session_capacity = worker_count * 2;
1303 - if (server->session_capacity < 16)
1304 - server->session_capacity = 16;
1305 - server->sessions = service_calloc(
1306 - (size_t)server->session_capacity, sizeof(nipc_session_ctx_t *),
1307 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL);
1308 - if (!server->sessions)
1309 - return NIPC_ERR_OVERFLOW;
1310 - server->session_count = 0;
1311 - server->next_session_id = 1; /* spec: monotonic counter starting at 1 */
1312 - InitializeCriticalSection(&server->sessions_lock);
1313 -
1314 - /* Clean up stale SHM kernel objects from previous crashes (no-op on
1315 - * Windows but maintains API symmetry with the POSIX transport). */
1316 - nipc_win_shm_cleanup_stale(run_dir, service_name);
1317 -
1318 - /* Start listening via L1 */
1319 - nipc_np_error_t uerr = nipc_np_listen(
1320 - run_dir, service_name, config, &server->listener);
1321 - if (uerr != NIPC_NP_OK) {
1322 - free(server->sessions);
1323 - server->sessions = NULL;
1324 - DeleteCriticalSection(&server->sessions_lock);
1325 - return NIPC_ERR_BAD_LAYOUT;
1326 - }
1327 -
1328 - return NIPC_OK;
1329 -}
1330 -
1331 -nipc_error_t nipc_server_init_typed(nipc_managed_server_t *server,
1332 - const char *run_dir,
1333 - const char *service_name,
1334 - const nipc_server_config_t *config,
1335 - int worker_count,
1336 - const nipc_cgroups_service_handler_t *service_handler)
1337 -{
1338 - if (!service_handler)
1339 - return NIPC_ERR_BAD_LAYOUT;
1340 -
1341 - nipc_np_server_config_t typed_cfg = service_server_config_to_transport(config);
1342 - if (typed_cfg.max_request_payload_bytes == 0)
1343 - typed_cfg.max_request_payload_bytes = cgroups_request_payload_default();
1344 - if (typed_cfg.max_response_payload_bytes == 0)
1345 - typed_cfg.max_response_payload_bytes = cgroups_response_payload_default();
1346 -
1347 - nipc_error_t err = server_init_raw(server, run_dir, service_name,
1348 - &typed_cfg, worker_count,
1349 - NIPC_METHOD_CGROUPS_SNAPSHOT,
1350 - server_typed_dispatch, server);
1351 - if (err != NIPC_OK)
1352 - return err;
1353 -
1354 - server->service_handler = *service_handler;
1355 - return NIPC_OK;
1356 -}
1357 -
1358 -nipc_error_t nipc_server_init_raw_for_tests(nipc_managed_server_t *server,
1359 - const char *run_dir,
1360 - const char *service_name,
1361 - const nipc_np_server_config_t *config,
1362 - int worker_count,
1363 - uint16_t expected_method_code,
1364 - nipc_server_handler_fn handler,
1365 - void *user)
1366 -{
1367 - return server_init_raw(server, run_dir, service_name, config,
1368 - worker_count, expected_method_code, handler, user);
1369 -}
1370 -
1371 -void nipc_server_run(nipc_managed_server_t *server)
1372 -{
1373 - InterlockedExchange(&server->accept_loop_active, 1);
1374 - InterlockedExchange(&server->running, 1);
1375 -
1376 - while (InterlockedCompareExchange(&server->running, 0, 0)) {
1377 - /* Accept one client via L1 (blocking with internal timeout) */
1378 - nipc_np_session_t session;
1379 - memset(&session, 0, sizeof(session));
1380 - session.pipe = INVALID_HANDLE_VALUE;
1381 -
1382 - uint64_t sid = server->next_session_id++;
1383 - nipc_np_server_config_t accept_cfg;
1384 - prepared_win_shm_t prepared_shm;
1385 - if (!server_prepare_accept_config(server, sid, &accept_cfg, &prepared_shm)) {
1386 - Sleep(10);
1387 - continue;
1388 - }
1389 -
1390 - server->listener.config = accept_cfg;
1391 - nipc_np_error_t uerr = nipc_np_accept(&server->listener, sid, &session);
1392 - if (uerr != NIPC_NP_OK) {
1393 - server_destroy_prepared_win_shm(&prepared_shm);
1394 - if (!InterlockedCompareExchange(&server->running, 0, 0))
1395 - break;
1396 - Sleep(10);
1397 - continue;
1398 - }
1399 -
1400 - server_note_request_capacity(server, session.max_request_payload_bytes);
1401 - server_note_response_capacity(server, session.max_response_payload_bytes);
1402 -
1403 - /* Enforce worker_count limit: reap finished sessions, check count */
1404 - EnterCriticalSection(&server->sessions_lock);
1405 - server_reap_sessions_locked(server);
1406 -
1407 - if (server->session_count >= server->worker_count) {
1408 - /* At capacity: reject this client by closing the session */
1409 - LeaveCriticalSection(&server->sessions_lock);
1410 - server_destroy_prepared_win_shm(&prepared_shm);
1411 - nipc_np_close_session(&session);
1412 - continue;
1413 - }
1414 -
1415 - /* SHM profile guarantee: only negotiate SHM for sessions backed by
1416 - * prepared per-session kernel objects for the selected profile. */
1417 - nipc_win_shm_ctx_t *shm = NULL;
1418 - if (session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
1419 - session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
1420 - shm = server_take_prepared_win_shm(&prepared_shm, session.selected_profile);
1421 - if (!shm) {
1422 - server_destroy_prepared_win_shm(&prepared_shm);
1423 - LeaveCriticalSection(&server->sessions_lock);
1424 - nipc_np_close_session(&session);
1425 - continue;
1426 - }
1427 - server_destroy_prepared_win_shm(&prepared_shm);
1428 - } else {
1429 - server_destroy_prepared_win_shm(&prepared_shm);
1430 - }
1431 -
1432 - /* Create session context */
1433 - nipc_session_ctx_t *sctx = service_calloc(
1434 - 1, sizeof(nipc_session_ctx_t),
1435 - NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL);
1436 - if (!sctx) {
1437 - LeaveCriticalSection(&server->sessions_lock);
1438 - if (shm) { nipc_win_shm_destroy(shm); free(shm); }
1439 - nipc_np_close_session(&session);
1440 - continue;
1441 - }
1442 -
1443 - sctx->server = server;
1444 - sctx->session = session;
1445 - sctx->shm = shm;
1446 - sctx->id = sid;
1447 - InterlockedExchange((volatile LONG *)&sctx->active, 1);
1448 -
1449 - server->sessions[server->session_count++] = sctx;
1450 - LeaveCriticalSection(&server->sessions_lock);
1451 -
1452 - /* Spawn handler thread for this session */
1453 - unsigned tid_unused;
1454 - sctx->thread = (HANDLE)service_beginthreadex(
1455 - NULL, 0, session_handler_thread, sctx, 0, &tid_unused);
1456 - if (sctx->thread == 0) {
1457 - /* Thread creation failed: clean up */
1458 - EnterCriticalSection(&server->sessions_lock);
1459 - for (int i = 0; i < server->session_count; i++) {
1460 - if (server->sessions[i] == sctx) {
1461 - server->sessions[i] = server->sessions[server->session_count - 1];
1462 - server->session_count--;
1463 - break;
1464 - }
1465 - }
1466 - LeaveCriticalSection(&server->sessions_lock);
1467 -
1468 - if (shm) { nipc_win_shm_destroy(shm); free(shm); }
1469 - nipc_np_close_session(&session);
1470 - free(sctx);
1471 - }
1472 - }
1473 -
1474 - InterlockedExchange(&server->accept_loop_active, 0);
1475 - nipc_np_close_listener(&server->listener);
1476 -}
1477 -
1478 -void nipc_server_stop(nipc_managed_server_t *server)
1479 -{
1480 - InterlockedExchange(&server->running, 0);
1481 - if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
1482 - server_wake_listener(server);
1483 - else
1484 - nipc_np_close_listener(&server->listener);
1485 -
1486 - if (server->sessions) {
1487 - EnterCriticalSection(&server->sessions_lock);
1488 - server_cancel_active_session_io_locked(server);
1489 - LeaveCriticalSection(&server->sessions_lock);
1490 - }
1491 -}
1492 -
1493 -bool nipc_server_drain(nipc_managed_server_t *server, uint32_t timeout_ms)
1494 -{
1495 - /* 1. Stop accepting new clients */
1496 - InterlockedExchange(&server->running, 0);
1497 - if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
1498 - server_wake_listener(server);
1499 - else
1500 - nipc_np_close_listener(&server->listener);
1501 -
1502 - /* 2. Wait for in-flight sessions to complete */
1503 - bool all_drained = true;
1504 - if (server->sessions) {
1505 - ULONGLONG deadline = GetTickCount64() + timeout_ms;
1506 -
1507 - /* Poll until all sessions are inactive or timeout */
1508 - while (1) {
1509 - EnterCriticalSection(&server->sessions_lock);
1510 - int active_count = 0;
1511 - for (int i = 0; i < server->session_count; i++) {
1512 - if (InterlockedCompareExchange(
1513 - (volatile LONG *)&server->sessions[i]->active, 0, 0))
1514 - active_count++;
1515 - }
1516 - LeaveCriticalSection(&server->sessions_lock);
1517 -
1518 - if (active_count == 0)
1519 - break;
1520 -
1521 - if (GetTickCount64() >= deadline) {
1522 - /* Timeout: cancel synchronous session I/O to unblock threads. */
1523 - EnterCriticalSection(&server->sessions_lock);
1524 - server_cancel_active_session_io_locked(server);
1525 - LeaveCriticalSection(&server->sessions_lock);
1526 - all_drained = false;
1527 - break;
1528 - }
1529 -
1530 - Sleep(5); /* 5ms poll interval */
1531 - }
1532 -
1533 - /* 3. Join all session threads */
1534 - EnterCriticalSection(&server->sessions_lock);
1535 - for (int i = 0; i < server->session_count; i++) {
1536 - nipc_session_ctx_t *s = server->sessions[i];
1537 - LeaveCriticalSection(&server->sessions_lock);
1538 - WaitForSingleObject(s->thread, INFINITE);
1539 - CloseHandle(s->thread);
1540 - free(s);
1541 - EnterCriticalSection(&server->sessions_lock);
1542 - }
1543 - server->session_count = 0;
1544 - LeaveCriticalSection(&server->sessions_lock);
1545 -
1546 - free(server->sessions);
1547 - server->sessions = NULL;
1548 - server->session_capacity = 0;
1549 - DeleteCriticalSection(&server->sessions_lock);
1550 - }
1551 -
1552 - server->worker_count = 0;
1553 -
1554 - return all_drained;
1555 -}
1556 -
1557 -void nipc_server_destroy(nipc_managed_server_t *server)
1558 -{
1559 - InterlockedExchange(&server->running, 0);
1560 - if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
1561 - server_wake_listener(server);
1562 - else
1563 - nipc_np_close_listener(&server->listener);
1564 -
1565 - /* Join all active session threads */
1566 - if (server->sessions) {
1567 - EnterCriticalSection(&server->sessions_lock);
1568 - server_cancel_active_session_io_locked(server);
1569 - for (int i = 0; i < server->session_count; i++) {
1570 - nipc_session_ctx_t *s = server->sessions[i];
1571 - LeaveCriticalSection(&server->sessions_lock);
1572 - WaitForSingleObject(s->thread, INFINITE);
1573 - CloseHandle(s->thread);
1574 - free(s);
1575 - EnterCriticalSection(&server->sessions_lock);
1576 - }
1577 - server->session_count = 0;
1578 - LeaveCriticalSection(&server->sessions_lock);
1579 -
1580 - free(server->sessions);
1581 - server->sessions = NULL;
1582 - server->session_capacity = 0;
1583 - DeleteCriticalSection(&server->sessions_lock);
1584 - }
1585 -
1586 - server->worker_count = 0;
1587 -
1588 -}
1589 -
1590 -/* ------------------------------------------------------------------ */
1591 -/* L3: Client-side cgroups snapshot cache */
1592 -/* ------------------------------------------------------------------ */
1593 -
1594 -/* Free all owned strings in cache items and the items array itself. */
1595 -static void cache_free_items(nipc_cgroups_cache_item_t *items, uint32_t count)
1596 -{
1597 - if (!items)
1598 - return;
1599 -
1600 - for (uint32_t i = 0; i < count; i++) {
1601 - free(items[i].name);
1602 - free(items[i].path);
1603 - }
1604 - free(items);
1605 -}
1606 -
1607 -/* Hash a name string (djb2). Combined with item hash for bucket index. */
1608 -static uint32_t cache_hash_name(const char *name)
1609 -{
1610 - uint32_t h = 5381;
1611 - for (const unsigned char *p = (const unsigned char *)name; *p; p++)
1612 - h = ((h << 5) + h) + *p;
1613 - return h;
1614 -}
1615 -
1616 -/*
1617 - * Build the open-addressing hash table from the items array.
1618 - * Uses (item.hash ^ name_hash) as the probe key.
1619 - * Load factor <= 0.5 (bucket_count >= 2 * item_count).
1620 - */
1621 -static bool cache_build_hashtable(nipc_cgroups_cache_t *cache)
1622 -{
1623 - free(cache->buckets);
1624 - cache->buckets = NULL;
1625 - cache->bucket_count = 0;
1626 -
1627 - if (cache->item_count == 0)
1628 - return true;
1629 -
1630 - uint32_t bcount = next_power_of_2_u32(cache->item_count * 2);
1631 - nipc_cgroups_hash_bucket_t *buckets = service_calloc(
1632 - bcount, sizeof(nipc_cgroups_hash_bucket_t),
1633 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL);
1634 - if (!buckets)
1635 - return false;
1636 -
1637 - uint32_t mask = bcount - 1;
1638 - for (uint32_t i = 0; i < cache->item_count; i++) {
1639 - uint32_t key = cache->items[i].hash ^ cache_hash_name(cache->items[i].name);
1640 - uint32_t slot = key & mask;
1641 -
1642 - /* Linear probe for an empty bucket */
1643 - while (buckets[slot].used)
1644 - slot = (slot + 1) & mask;
1645 -
1646 - buckets[slot].index = i;
1647 - buckets[slot].used = true;
1648 - }
1649 -
1650 - cache->buckets = buckets;
1651 - cache->bucket_count = bcount;
1652 - return true;
1653 -}
1654 -
1655 -static nipc_cgroups_cache_item_t *cache_build_items(
1656 - const nipc_cgroups_resp_view_t *view,
1657 - uint32_t *count_out)
151 +static uint64_t monotonic_time_ms(void)
152 {
1659 - uint32_t n = view->item_count;
1660 - *count_out = 0;
1661 -
1662 - if (n == 0)
1663 - return NULL;
1664 -
1665 - nipc_cgroups_cache_item_t *items = service_calloc(
1666 - n, sizeof(nipc_cgroups_cache_item_t),
1667 - NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL);
1668 - if (!items)
1669 - return NULL;
1670 -
1671 - for (uint32_t i = 0; i < n; i++) {
1672 - nipc_cgroups_item_view_t iv;
1673 - nipc_error_t err = nipc_cgroups_resp_item(view, i, &iv);
1674 - if (err != NIPC_OK) {
1675 - cache_free_items(items, i);
1676 - return NULL;
1677 - }
1678 -
1679 - items[i].hash = iv.hash;
1680 - items[i].options = iv.options;
1681 - items[i].enabled = iv.enabled;
1682 -
1683 - items[i].name = service_malloc(
1684 - iv.name.len + 1, NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL);
1685 - if (!items[i].name) {
1686 - cache_free_items(items, i);
1687 - return NULL;
1688 - }
1689 - if (iv.name.len > 0)
1690 - memcpy(items[i].name, iv.name.ptr, iv.name.len);
1691 - items[i].name[iv.name.len] = '\0';
1692 -
1693 - items[i].path = service_malloc(
1694 - iv.path.len + 1, NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL);
1695 - if (!items[i].path) {
1696 - free(items[i].name);
1697 - cache_free_items(items, i);
1698 - return NULL;
1699 - }
1700 - if (iv.path.len > 0)
1701 - memcpy(items[i].path, iv.path.ptr, iv.path.len);
1702 - items[i].path[iv.path.len] = '\0';
1703 - }
1704 -
1705 - *count_out = n;
1706 - return items;
1707 -}
1708 -
1709 -void nipc_cgroups_cache_init(nipc_cgroups_cache_t *cache,
1710 - const char *run_dir,
1711 - const char *service_name,
1712 - const nipc_client_config_t *config)
1713 -{
1714 - memset(cache, 0, sizeof(*cache));
1715 -
1716 - nipc_client_init(&cache->client, run_dir, service_name, config);
1717 -
1718 - cache->items = NULL;
1719 - cache->item_count = 0;
1720 - cache->systemd_enabled = 0;
1721 - cache->generation = 0;
1722 - cache->populated = false;
1723 - cache->buckets = NULL;
1724 - cache->bucket_count = 0;
1725 - cache->refresh_success_count = 0;
1726 - cache->refresh_failure_count = 0;
1727 -
1728 - cache->response_buf = NULL;
1729 - cache->response_buf_size = 0;
1730 -}
1731 -
1732 -bool nipc_cgroups_cache_refresh(nipc_cgroups_cache_t *cache)
1733 -{
1734 - nipc_client_refresh(&cache->client);
1735 -
1736 - nipc_cgroups_resp_view_t view;
1737 - nipc_error_t err = nipc_client_call_cgroups_snapshot(&cache->client, &view);
1738 -
1739 - if (err != NIPC_OK) {
1740 - cache->refresh_failure_count++;
1741 - return false;
1742 - }
1743 -
1744 - uint32_t new_count = 0;
1745 - nipc_cgroups_cache_item_t *new_items = NULL;
1746 -
1747 - if (view.item_count > 0) {
1748 - new_items = cache_build_items(&view, &new_count);
1749 - if (!new_items && view.item_count > 0) {
1750 - cache->refresh_failure_count++;
1751 - return false;
1752 - }
1753 - }
1754 -
1755 - cache_free_items(cache->items, cache->item_count);
1756 - cache->items = new_items;
1757 - cache->item_count = new_count;
1758 - cache->systemd_enabled = view.systemd_enabled;
1759 - cache->generation = view.generation;
1760 - cache->populated = true;
1761 - cache->refresh_success_count++;
1762 -
1763 - /* Record monotonic timestamp (GetTickCount64 is always available on Windows) */
1764 - cache->last_refresh_ts = GetTickCount64();
1765 -
1766 - /* Rebuild hash table for O(1) lookup */
1767 - cache_build_hashtable(cache);
1768 -
1769 - return true;
153 + return GetTickCount64();
154 }
155
1772 -const nipc_cgroups_cache_item_t *nipc_cgroups_cache_lookup(
1773 - const nipc_cgroups_cache_t *cache,
1774 - uint32_t hash,
1775 - const char *name)
156 +uint64_t nipc_service_platform_monotonic_ms(void)
157 {
1777 - if (!cache->populated || !cache->items || !name)
1778 - return NULL;
1779 -
1780 - /* Use hash table if available, fall back to linear scan */
1781 - if (cache->buckets && cache->bucket_count > 0) {
1782 - uint32_t key = hash ^ cache_hash_name(name);
1783 - uint32_t mask = cache->bucket_count - 1;
1784 - uint32_t slot = key & mask;
1785 -
1786 - while (cache->buckets[slot].used) {
1787 - uint32_t idx = cache->buckets[slot].index;
1788 - if (cache->items[idx].hash == hash &&
1789 - strcmp(cache->items[idx].name, name) == 0) {
1790 - return &cache->items[idx];
1791 - }
1792 - slot = (slot + 1) & mask;
1793 - }
1794 - return NULL;
1795 - }
1796 -
1797 - /* Fallback linear scan (hash table allocation failed) */
1798 - for (uint32_t i = 0; i < cache->item_count; i++) {
1799 - if (cache->items[i].hash == hash &&
1800 - strcmp(cache->items[i].name, name) == 0) {
1801 - return &cache->items[i];
1802 - }
1803 - }
1804 -
1805 - return NULL;
158 + return monotonic_time_ms();
159 }
160
1808 -void nipc_cgroups_cache_status(const nipc_cgroups_cache_t *cache,
1809 - nipc_cgroups_cache_status_t *out)
161 +bool nipc_service_platform_ensure_client_send_buffer(nipc_client_ctx_t *ctx,
162 + size_t need)
163 {
1811 - out->populated = cache->populated;
1812 - out->item_count = cache->item_count;
1813 - out->systemd_enabled = cache->systemd_enabled;
1814 - out->generation = cache->generation;
1815 - out->refresh_success_count = cache->refresh_success_count;
1816 - out->refresh_failure_count = cache->refresh_failure_count;
1817 - out->connection_state = cache->client.state;
1818 - out->last_refresh_ts = cache->last_refresh_ts;
164 + return nipc_service_win_ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, need,
165 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL);
166 }
167
1821 -void nipc_cgroups_cache_close(nipc_cgroups_cache_t *cache)
1822 -{
1823 - cache_free_items(cache->items, cache->item_count);
1824 - cache->items = NULL;
1825 - cache->item_count = 0;
1826 - cache->populated = false;
1827 -
1828 - free(cache->buckets);
1829 - cache->buckets = NULL;
1830 - cache->bucket_count = 0;
1831 -
1832 - free(cache->response_buf);
1833 - cache->response_buf = NULL;
1834 - cache->response_buf_size = 0;
1835 -
1836 - nipc_client_close(&cache->client);
1837 -}
168 +/* Managed server implementation lives in netipc_service_win_server.c. */
169
170 #endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/service/netipc_service_win_client.c new
+102
@@ -0,0 +1,102 @@
1 +/*
2 + * netipc_service_win_client.c - WIN public service client API.
3 + */
4 +
5 +#if defined(_WIN32) || defined(__MSYS__)
6 +
7 +#include "netipc/netipc_service.h"
8 +#include "netipc/netipc_protocol.h"
9 +#include "netipc/netipc_named_pipe.h"
10 +#include "netipc/netipc_win_shm.h"
11 +#include "netipc_service_common.h"
12 +#include "netipc_service_platform.h"
13 +#include "netipc_service_win_internal.h"
14 +
15 +#include <stdint.h>
16 +#include <stdlib.h>
17 +#include <string.h>
18 +#include <windows.h>
19 +
20 +static nipc_np_client_config_t service_client_config_to_transport(
21 + const nipc_client_config_t *config)
22 +{
23 + nipc_np_client_config_t transport = {0};
24 + nipc_service_common_transport_fields_t fields;
25 +
26 + if (!nipc_service_common_client_transport_fields(&fields, config))
27 + return transport;
28 +
29 + NIPC_SERVICE_COMMON_APPLY_TRANSPORT_FIELDS(&transport, &fields);
30 + return transport;
31 +}
32 +
33 +static void client_sleep_ms(uint32_t ms)
34 +{
35 + Sleep(ms);
36 +}
37 +
38 +const nipc_service_common_client_ops_t *nipc_service_win_client_ops(void)
39 +{
40 + static const nipc_service_common_client_ops_t ops = {
41 + .disconnect = nipc_service_win_client_disconnect,
42 + .try_connect = nipc_service_win_client_try_connect,
43 + .reconnect_for_call = nipc_service_win_client_reconnect_for_call,
44 + .sleep_ms = client_sleep_ms,
45 + .reconnect_drain_ms = 0,
46 + .reconnect_retry_interval_ms = CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS,
47 + };
48 + return &ops;
49 +}
50 +
51 +void nipc_service_platform_server_config_from_service(
52 + nipc_service_platform_server_config_t *transport,
53 + const nipc_server_config_t *config)
54 +{
55 + memset(transport, 0, sizeof(*transport));
56 + nipc_service_common_transport_fields_t fields;
57 +
58 + if (!nipc_service_common_server_transport_fields(&fields, config))
59 + return;
60 +
61 + NIPC_SERVICE_COMMON_APPLY_TRANSPORT_FIELDS(transport, &fields);
62 +}
63 +
64 +/* ------------------------------------------------------------------ */
65 +/* Public API: client lifecycle */
66 +/* ------------------------------------------------------------------ */
67 +
68 +void nipc_client_init(nipc_client_ctx_t *ctx,
69 + const char *run_dir,
70 + const char *service_name,
71 + const nipc_client_config_t *config)
72 +{
73 + nipc_service_common_client_init(ctx, run_dir, service_name);
74 + ctx->session.pipe = INVALID_HANDLE_VALUE;
75 +
76 + ctx->transport_config = service_client_config_to_transport(config);
77 + if (ctx->transport_config.max_request_payload_bytes == 0)
78 + ctx->transport_config.max_request_payload_bytes =
79 + nipc_service_common_request_payload_default();
80 + if (ctx->transport_config.max_response_payload_bytes == 0)
81 + ctx->transport_config.max_response_payload_bytes =
82 + nipc_service_common_response_payload_default();
83 +}
84 +
85 +bool nipc_client_refresh(nipc_client_ctx_t *ctx)
86 +{
87 + return nipc_service_common_client_refresh(ctx, nipc_service_win_client_ops());
88 +}
89 +
90 +void nipc_client_status(const nipc_client_ctx_t *ctx,
91 + nipc_client_status_t *out)
92 +{
93 + nipc_service_common_client_status(ctx, out);
94 +}
95 +
96 +void nipc_client_close(nipc_client_ctx_t *ctx)
97 +{
98 + nipc_service_win_client_disconnect(ctx);
99 + nipc_service_common_client_close_buffers(ctx);
100 +}
101 +
102 +#endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/service/netipc_service_win_client_call.c new
+125
@@ -0,0 +1,125 @@
1 +/*
2 + * netipc_service_win_client_call.c - WIN raw client call flow.
3 + */
4 +
5 +#if defined(_WIN32) || defined(__MSYS__)
6 +
7 +#include "netipc/netipc_service.h"
8 +#include "netipc/netipc_protocol.h"
9 +#include "netipc/netipc_named_pipe.h"
10 +#include "netipc/netipc_win_shm.h"
11 +#include "netipc_service_common.h"
12 +#include "netipc_service_platform.h"
13 +#include "netipc_service_win_internal.h"
14 +
15 +#include <stdint.h>
16 +#include <stdlib.h>
17 +#include <string.h>
18 +#include <windows.h>
19 +
20 +/* ------------------------------------------------------------------ */
21 +/* Internal: send/receive via the active transport */
22 +/* ------------------------------------------------------------------ */
23 +
24 +static nipc_error_t transport_send(nipc_client_ctx_t *ctx,
25 + nipc_header_t *hdr,
26 + const void *payload,
27 + size_t payload_len)
28 +{
29 + if (payload_len > UINT32_MAX)
30 + return NIPC_ERR_OVERFLOW;
31 +
32 + if (ctx->shm) {
33 + uint8_t *msg;
34 + size_t msg_len;
35 + nipc_error_t perr = nipc_service_common_client_prepare_shm_request(
36 + ctx, hdr, payload, payload_len, &msg, &msg_len);
37 + if (perr != NIPC_OK)
38 + return perr;
39 +
40 + nipc_win_shm_error_t serr = nipc_win_shm_send(ctx->shm, msg, msg_len);
41 + if (serr == NIPC_WIN_SHM_ERR_MSG_TOO_LARGE) {
42 + nipc_service_common_client_note_request_capacity(
43 + ctx, (uint32_t)payload_len);
44 + return NIPC_ERR_OVERFLOW;
45 + }
46 + return (serr == NIPC_WIN_SHM_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
47 + }
48 +
49 + /* Named Pipe path */
50 + nipc_np_error_t uerr = nipc_np_send(&ctx->session, hdr,
51 + payload, payload_len);
52 + if (uerr == NIPC_NP_ERR_LIMIT_EXCEEDED) {
53 + nipc_service_common_client_note_request_capacity(
54 + ctx, (uint32_t)payload_len);
55 + return NIPC_ERR_OVERFLOW;
56 + }
57 + return (uerr == NIPC_NP_OK) ? NIPC_OK : NIPC_ERR_NOT_READY;
58 +}
59 +
60 +static nipc_error_t transport_receive(nipc_client_ctx_t *ctx,
61 + void *buf, size_t buf_size,
62 + nipc_header_t *hdr_out,
63 + const void **payload_out,
64 + size_t *payload_len_out)
65 +{
66 + if (ctx->shm) {
67 + size_t msg_len;
68 + nipc_win_shm_error_t serr = nipc_win_shm_receive(ctx->shm, buf, buf_size,
69 + &msg_len, 30000);
70 + if (serr != NIPC_WIN_SHM_OK)
71 + return NIPC_ERR_TRUNCATED;
72 +
73 + return nipc_service_common_client_parse_shm_response(
74 + buf, msg_len, hdr_out, payload_out, payload_len_out);
75 + }
76 +
77 + /* Named Pipe path */
78 + nipc_np_error_t uerr = nipc_np_receive(&ctx->session, buf, buf_size,
79 + hdr_out, payload_out,
80 + payload_len_out);
81 + return (uerr == NIPC_NP_OK) ? NIPC_OK : NIPC_ERR_TRUNCATED;
82 +}
83 +
84 +/* ------------------------------------------------------------------ */
85 +/* Internal: generic raw call (send request, receive response) */
86 +/* ------------------------------------------------------------------ */
87 +
88 +/*
89 + * Single-attempt raw call: build envelope, send, receive, validate
90 + * envelope. The caller handles encode before and decode after.
91 + *
92 + * On success, response_payload_out and response_len_out point into the
93 + * internal client response buffer (valid until next call on this context).
94 + */
95 +nipc_error_t nipc_service_platform_do_raw_call(nipc_client_ctx_t *ctx,
96 + uint16_t method_code,
97 + const void *request_payload,
98 + size_t request_len,
99 + const void **response_payload_out,
100 + size_t *response_len_out)
101 +{
102 + return nipc_service_common_do_raw_call(
103 + ctx, method_code, request_payload, request_len,
104 + response_payload_out, response_len_out,
105 + transport_send, transport_receive);
106 +}
107 +
108 +/*
109 + * Generic call-with-retry:
110 + * - ordinary failures reconnect and retry once
111 + * - overflow-driven resize recovery may reconnect repeatedly until
112 + * negotiated capacities grow or recovery fails
113 + * The caller provides a function pointer for the single-attempt logic.
114 + */
115 +nipc_error_t nipc_service_platform_call_with_retry(
116 + nipc_client_ctx_t *ctx,
117 + nipc_service_platform_attempt_fn attempt,
118 + void *state)
119 +{
120 + return nipc_service_common_call_with_retry(
121 + ctx, attempt, state, nipc_service_win_client_ops());
122 +}
123 +
124 +
125 +#endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/service/netipc_service_win_client_connect.c new
+181
@@ -0,0 +1,181 @@
1 +/*
2 + * netipc_service_win_client_connect.c - WIN client connection management.
3 + */
4 +
5 +#if defined(_WIN32) || defined(__MSYS__)
6 +
7 +#include "netipc/netipc_service.h"
8 +#include "netipc/netipc_protocol.h"
9 +#include "netipc/netipc_named_pipe.h"
10 +#include "netipc/netipc_win_shm.h"
11 +#include "netipc_service_common.h"
12 +#include "netipc_service_platform.h"
13 +#include "netipc_service_win_internal.h"
14 +
15 +#include <stdint.h>
16 +#include <stdlib.h>
17 +#include <string.h>
18 +#include <windows.h>
19 +
20 +static bool client_prepare_session_buffers(nipc_client_ctx_t *ctx)
21 +{
22 + size_t response_need;
23 + if (!nipc_service_common_header_payload_len(
24 + ctx->session.max_response_payload_bytes, &response_need))
25 + return false;
26 + if (response_need < NIPC_HEADER_LEN + 1024u)
27 + response_need = NIPC_HEADER_LEN + 1024u;
28 +
29 + if (!nipc_service_win_ensure_buffer(&ctx->response_buf, &ctx->response_buf_size, response_need,
30 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL))
31 + return false;
32 +
33 + if (ctx->session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
34 + ctx->session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
35 + size_t send_need;
36 + if (!nipc_service_common_header_payload_len(
37 + ctx->session.max_request_payload_bytes, &send_need))
38 + return false;
39 + if (!nipc_service_win_ensure_buffer(&ctx->send_buf, &ctx->send_buf_size, send_need,
40 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL))
41 + return false;
42 + }
43 +
44 + return true;
45 +}
46 +
47 +/* ------------------------------------------------------------------ */
48 +/* Internal: client connection helpers */
49 +/* ------------------------------------------------------------------ */
50 +
51 +/* Tear down the current connection (Named Pipe session + Win SHM). */
52 +void nipc_service_win_client_disconnect(nipc_client_ctx_t *ctx)
53 +{
54 + if (ctx->shm) {
55 + nipc_win_shm_close(ctx->shm);
56 + free(ctx->shm);
57 + ctx->shm = NULL;
58 + }
59 +
60 + if (ctx->session_valid) {
61 + nipc_np_close_session(&ctx->session);
62 + ctx->session_valid = false;
63 + }
64 +}
65 +
66 +static void client_disable_shm_profiles(nipc_client_ctx_t *ctx)
67 +{
68 + ctx->transport_config.supported_profiles &=
69 + ~(NIPC_WIN_SHM_PROFILE_HYBRID | NIPC_WIN_SHM_PROFILE_BUSYWAIT);
70 + ctx->transport_config.preferred_profiles &=
71 + ~(NIPC_WIN_SHM_PROFILE_HYBRID | NIPC_WIN_SHM_PROFILE_BUSYWAIT);
72 +}
73 +
74 +/* Attempt a full connection: Named Pipe connect + handshake, then
75 + * Win SHM upgrade if negotiated. Returns the new state. */
76 +nipc_client_state_t nipc_service_win_client_try_connect(nipc_client_ctx_t *ctx)
77 +{
78 + nipc_np_session_t session;
79 + memset(&session, 0, sizeof(session));
80 + session.pipe = INVALID_HANDLE_VALUE;
81 +
82 + nipc_np_error_t err = nipc_np_connect(
83 + ctx->run_dir, ctx->service_name,
84 + &ctx->transport_config, &session);
85 +
86 + switch (err) {
87 + case NIPC_NP_OK:
88 + break;
89 + case NIPC_NP_ERR_CONNECT:
90 + return NIPC_CLIENT_NOT_FOUND;
91 + case NIPC_NP_ERR_AUTH_FAILED:
92 + return NIPC_CLIENT_AUTH_FAILED;
93 + case NIPC_NP_ERR_NO_PROFILE:
94 + case NIPC_NP_ERR_INCOMPATIBLE:
95 + return NIPC_CLIENT_INCOMPATIBLE;
96 + default:
97 + return NIPC_CLIENT_DISCONNECTED;
98 + }
99 +
100 + ctx->session = session;
101 + ctx->session_valid = true;
102 +
103 + if (!client_prepare_session_buffers(ctx)) {
104 + nipc_np_close_session(&ctx->session);
105 + ctx->session_valid = false;
106 + return NIPC_CLIENT_DISCONNECTED;
107 + }
108 +
109 + /* Win SHM upgrade if negotiated */
110 + if (session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
111 + session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
112 +
113 + nipc_win_shm_ctx_t *shm = nipc_service_win_calloc(
114 + 1, sizeof(nipc_win_shm_ctx_t),
115 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL);
116 + if (!shm) {
117 + nipc_np_close_session(&ctx->session);
118 + ctx->session_valid = false;
119 + return NIPC_CLIENT_DISCONNECTED;
120 + }
121 + {
122 + /* Retry attach: the server prepares SHM before accepting the
123 + * handshake, but filesystem/object visibility may lag briefly. */
124 + nipc_win_shm_error_t serr = NIPC_WIN_SHM_ERR_OPEN_MAPPING;
125 + ULONGLONG deadline_ms = GetTickCount64() + CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS;
126 + for (;;) {
127 + serr = nipc_win_shm_client_attach(
128 + ctx->run_dir, ctx->service_name,
129 + ctx->transport_config.auth_token,
130 + session.session_id,
131 + session.selected_profile,
132 + shm);
133 + if (serr == NIPC_WIN_SHM_OK)
134 + break;
135 + if (serr != NIPC_WIN_SHM_ERR_OPEN_MAPPING &&
136 + serr != NIPC_WIN_SHM_ERR_OPEN_EVENT &&
137 + serr != NIPC_WIN_SHM_ERR_BAD_MAGIC)
138 + break;
139 + if (GetTickCount64() >= deadline_ms)
140 + break;
141 + Sleep(CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS);
142 + }
143 +
144 + if (serr == NIPC_WIN_SHM_OK) {
145 + ctx->shm = shm;
146 + } else {
147 + /* WinSHM attach failed after negotiation. Close that
148 + * session, blacklist WinSHM for this client context, and
149 + * retry baseline via a new handshake. */
150 + free(shm);
151 + nipc_np_close_session(&ctx->session);
152 + ctx->session_valid = false;
153 + client_disable_shm_profiles(ctx);
154 + if (ctx->transport_config.supported_profiles == 0)
155 + return NIPC_CLIENT_DISCONNECTED;
156 + return nipc_service_win_client_try_connect(ctx);
157 + }
158 + }
159 + }
160 +
161 + return NIPC_CLIENT_READY;
162 +}
163 +
164 +bool nipc_service_win_client_reconnect_for_call(nipc_client_ctx_t *ctx)
165 +{
166 + for (uint32_t i = 0; i < CLIENT_CALL_RECONNECT_RETRIES; i++) {
167 + ctx->state = nipc_service_win_client_try_connect(ctx);
168 + if (ctx->state == NIPC_CLIENT_READY)
169 + return true;
170 + if (ctx->state == NIPC_CLIENT_AUTH_FAILED ||
171 + ctx->state == NIPC_CLIENT_INCOMPATIBLE)
172 + return false;
173 + if (i + 1u < CLIENT_CALL_RECONNECT_RETRIES)
174 + Sleep(CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS);
175 + }
176 +
177 + return false;
178 +}
179 +
180 +
181 +#endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/service/netipc_service_win_internal.h new
+55
@@ -0,0 +1,55 @@
1 +#ifndef NETIPC_SERVICE_WIN_INTERNAL_H
2 +#define NETIPC_SERVICE_WIN_INTERNAL_H
3 +
4 +#if defined(_WIN32) || defined(__MSYS__)
5 +
6 +#include "netipc_service_platform.h"
7 +
8 +#include <process.h>
9 +#include <stdbool.h>
10 +#include <stddef.h>
11 +#include <stdint.h>
12 +#include <windows.h>
13 +
14 +#define SERVER_POLL_TIMEOUT_MS 100
15 +#define CLIENT_SHM_ATTACH_RETRY_INTERVAL_MS 5u
16 +#define CLIENT_SHM_ATTACH_RETRY_TIMEOUT_MS 5000u
17 +#define CLIENT_CALL_RECONNECT_RETRY_INTERVAL_MS 5u
18 +#define CLIENT_CALL_RECONNECT_RETRIES 20u
19 +
20 +enum {
21 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_RESPONSE_BUF_REALLOC_INTERNAL = 1,
22 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SEND_BUF_REALLOC_INTERNAL,
23 + NIPC_WIN_SERVICE_TEST_FAULT_CLIENT_SHM_CTX_CALLOC_INTERNAL,
24 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL,
25 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL,
26 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL,
27 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL,
28 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL,
29 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_THREAD_CREATE_INTERNAL,
30 + NIPC_WIN_SERVICE_TEST_FAULT_CACHE_BUCKETS_CALLOC_INTERNAL,
31 + NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEMS_CALLOC_INTERNAL,
32 + NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_NAME_MALLOC_INTERNAL,
33 + NIPC_WIN_SERVICE_TEST_FAULT_CACHE_ITEM_PATH_MALLOC_INTERNAL,
34 +};
35 +
36 +const nipc_service_common_client_ops_t *nipc_service_win_client_ops(void);
37 +void nipc_service_win_client_disconnect(nipc_client_ctx_t *ctx);
38 +nipc_client_state_t nipc_service_win_client_try_connect(nipc_client_ctx_t *ctx);
39 +bool nipc_service_win_client_reconnect_for_call(nipc_client_ctx_t *ctx);
40 +void *nipc_service_win_malloc(size_t size, int fault_site);
41 +void *nipc_service_win_calloc(size_t count, size_t size, int fault_site);
42 +bool nipc_service_win_ensure_buffer(uint8_t **buf, size_t *buf_size,
43 + size_t need, int fault_site);
44 +unsigned __stdcall nipc_service_win_session_handler_thread(void *arg);
45 +void nipc_service_win_server_reap_sessions_locked(nipc_managed_server_t *server);
46 +uintptr_t nipc_service_win_beginthreadex(void *security,
47 + unsigned stack_size,
48 + unsigned (__stdcall *start_address)(void *),
49 + void *arglist,
50 + unsigned initflag,
51 + unsigned *thrdaddr);
52 +
53 +#endif /* _WIN32 || __MSYS__ */
54 +
55 +#endif /* NETIPC_SERVICE_WIN_INTERNAL_H */
src/libnetdata/netipc/src/service/netipc_service_win_server.c new
+464
@@ -0,0 +1,464 @@
1 +/*
2 + * netipc_service_win_server.c - Windows managed server orchestration.
3 + */
4 +
5 +#if defined(_WIN32) || defined(__MSYS__)
6 +
7 +#include "netipc/netipc_service.h"
8 +#include "netipc/netipc_protocol.h"
9 +#include "netipc/netipc_named_pipe.h"
10 +#include "netipc/netipc_win_shm.h"
11 +#include "netipc_service_common.h"
12 +#include "netipc_service_platform.h"
13 +#include "netipc_service_win_internal.h"
14 +
15 +#include <stdint.h>
16 +#include <stdlib.h>
17 +#include <string.h>
18 +#include <windows.h>
19 +
20 +/* ------------------------------------------------------------------ */
21 +/* Internal: managed server session handler */
22 +/* ------------------------------------------------------------------ */
23 +
24 +/*
25 + * Handle one client session: read requests, dispatch to handler,
26 + * send responses. Each session gets its own response buffer.
27 + * Runs until the client disconnects or server stops.
28 + */
29 +typedef struct {
30 + nipc_win_shm_ctx_t *hybrid;
31 + nipc_win_shm_ctx_t *busywait;
32 +} prepared_win_shm_t;
33 +
34 +static void server_destroy_prepared_win_shm(prepared_win_shm_t *prepared)
35 +{
36 + if (!prepared)
37 + return;
38 + if (prepared->hybrid) {
39 + nipc_win_shm_destroy(prepared->hybrid);
40 + free(prepared->hybrid);
41 + prepared->hybrid = NULL;
42 + }
43 + if (prepared->busywait) {
44 + nipc_win_shm_destroy(prepared->busywait);
45 + free(prepared->busywait);
46 + prepared->busywait = NULL;
47 + }
48 +}
49 +
50 +static nipc_win_shm_ctx_t *server_take_prepared_win_shm(prepared_win_shm_t *prepared,
51 + uint32_t profile)
52 +{
53 + if (!prepared)
54 + return NULL;
55 + if (profile == NIPC_WIN_SHM_PROFILE_HYBRID) {
56 + nipc_win_shm_ctx_t *ctx = prepared->hybrid;
57 + prepared->hybrid = NULL;
58 + return ctx;
59 + }
60 + if (profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
61 + nipc_win_shm_ctx_t *ctx = prepared->busywait;
62 + prepared->busywait = NULL;
63 + return ctx;
64 + }
65 + return NULL;
66 +}
67 +
68 +static bool server_prepare_accept_config(nipc_managed_server_t *server,
69 + uint64_t sid,
70 + nipc_np_server_config_t *cfg_out,
71 + prepared_win_shm_t *prepared)
72 +{
73 + *cfg_out = server->base_config;
74 + cfg_out->max_request_payload_bytes = server->learned_request_payload_bytes;
75 + cfg_out->max_response_payload_bytes = server->learned_response_payload_bytes;
76 + memset(prepared, 0, sizeof(*prepared));
77 +
78 + uint32_t shm_profiles = cfg_out->supported_profiles &
79 + (NIPC_WIN_SHM_PROFILE_HYBRID |
80 + NIPC_WIN_SHM_PROFILE_BUSYWAIT);
81 + if (shm_profiles == 0)
82 + return true;
83 +
84 + uint32_t request_capacity;
85 + uint32_t response_capacity;
86 + if (!nipc_service_common_header_payload_len_u32(
87 + NIPC_MAX_PAYLOAD_CAP, &request_capacity) ||
88 + !nipc_service_common_header_payload_len_u32(
89 + cfg_out->max_response_payload_bytes, &response_capacity)) {
90 + cfg_out->supported_profiles &= ~shm_profiles;
91 + cfg_out->preferred_profiles &= ~shm_profiles;
92 + return cfg_out->supported_profiles != 0;
93 + }
94 +
95 + const uint32_t profiles[] = {
96 + NIPC_WIN_SHM_PROFILE_HYBRID,
97 + NIPC_WIN_SHM_PROFILE_BUSYWAIT,
98 + };
99 + for (size_t i = 0; i < sizeof(profiles) / sizeof(profiles[0]); i++) {
100 + uint32_t profile = profiles[i];
101 + if (!(cfg_out->supported_profiles & profile))
102 + continue;
103 +
104 + nipc_win_shm_ctx_t *ctx = nipc_service_win_calloc(
105 + 1, sizeof(nipc_win_shm_ctx_t),
106 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SHM_CTX_CALLOC_INTERNAL);
107 + if (!ctx)
108 + continue;
109 +
110 + /* HELLO has not been read yet, so the request segment must cover any
111 + * client proposal the handshake may legally echo back. */
112 + nipc_win_shm_error_t serr = nipc_win_shm_server_create(
113 + server->run_dir, server->service_name,
114 + server->auth_token,
115 + sid,
116 + profile,
117 + request_capacity,
118 + response_capacity,
119 + ctx);
120 + if (serr == NIPC_WIN_SHM_OK) {
121 + if (profile == NIPC_WIN_SHM_PROFILE_HYBRID)
122 + prepared->hybrid = ctx;
123 + else
124 + prepared->busywait = ctx;
125 + continue;
126 + }
127 +
128 + free(ctx);
129 + cfg_out->supported_profiles &= ~profile;
130 + cfg_out->preferred_profiles &= ~profile;
131 + }
132 +
133 + return cfg_out->supported_profiles != 0;
134 +}
135 +
136 +static void server_wake_listener(nipc_managed_server_t *server)
137 +{
138 + if (server->listener.pipe == INVALID_HANDLE_VALUE ||
139 + server->listener.pipe == NULL ||
140 + server->listener.pipe_name[0] == L'\0')
141 + return;
142 +
143 + HANDLE wake = CreateFileW(
144 + server->listener.pipe_name,
145 + GENERIC_READ | GENERIC_WRITE,
146 + 0,
147 + NULL,
148 + OPEN_EXISTING,
149 + 0,
150 + NULL);
151 + if (wake != INVALID_HANDLE_VALUE)
152 + CloseHandle(wake);
153 +}
154 +
155 +/* Ask active session threads to leave synchronous ReadFile/WriteFile waits.
156 + * This is safer than targeting pipe handles from another thread because the
157 + * thread handle stays valid until the owner joins it. */
158 +static void server_cancel_active_session_io_locked(nipc_managed_server_t *server)
159 +{
160 + for (int i = 0; i < server->session_count; i++) {
161 + nipc_session_ctx_t *s = server->sessions[i];
162 + if (!s)
163 + continue;
164 + if (!InterlockedCompareExchange((volatile LONG *)&s->active, 0, 0))
165 + continue;
166 + if (s->thread == NULL || s->thread == INVALID_HANDLE_VALUE)
167 + continue;
168 + CancelSynchronousIo(s->thread);
169 + }
170 +}
171 +
172 +/* ------------------------------------------------------------------ */
173 +/* Public API: managed server */
174 +/* ------------------------------------------------------------------ */
175 +
176 +nipc_error_t nipc_service_platform_server_init_raw(
177 + nipc_managed_server_t *server,
178 + const char *run_dir,
179 + const char *service_name,
180 + const nipc_service_platform_server_config_t *config,
181 + int worker_count,
182 + uint16_t expected_method_code,
183 + nipc_server_handler_fn handler,
184 + void *user)
185 +{
186 + if (!server)
187 + return NIPC_ERR_BAD_LAYOUT;
188 +
189 + memset(server, 0, sizeof(*server));
190 + server->listener.pipe = INVALID_HANDLE_VALUE;
191 + InterlockedExchange(&server->running, 0);
192 +
193 + if (!config)
194 + return NIPC_ERR_BAD_LAYOUT;
195 +
196 + nipc_error_t ierr = nipc_service_common_server_init_base(
197 + server, run_dir, service_name, worker_count, expected_method_code,
198 + handler, user, config->max_request_payload_bytes,
199 + config->max_response_payload_bytes);
200 + if (ierr != NIPC_OK)
201 + return ierr;
202 + server->base_config = *config;
203 + server->auth_token = config->auth_token;
204 + ierr = nipc_service_common_server_alloc_sessions(
205 + server, nipc_service_win_calloc,
206 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSIONS_CALLOC_INTERNAL);
207 + if (ierr != NIPC_OK)
208 + return ierr;
209 + InitializeCriticalSection(&server->sessions_lock);
210 +
211 + /* Clean up stale SHM kernel objects from previous crashes (no-op on
212 + * Windows but maintains API symmetry with the POSIX transport). */
213 + nipc_win_shm_cleanup_stale(run_dir, service_name);
214 +
215 + /* Start listening via L1 */
216 + nipc_np_error_t uerr = nipc_np_listen(
217 + run_dir, service_name, config, &server->listener);
218 + if (uerr != NIPC_NP_OK) {
219 + free(server->sessions);
220 + server->sessions = NULL;
221 + DeleteCriticalSection(&server->sessions_lock);
222 + return NIPC_ERR_BAD_LAYOUT;
223 + }
224 +
225 + return NIPC_OK;
226 +}
227 +
228 +nipc_error_t nipc_server_init_raw_for_tests(nipc_managed_server_t *server,
229 + const char *run_dir,
230 + const char *service_name,
231 + const nipc_np_server_config_t *config,
232 + int worker_count,
233 + uint16_t expected_method_code,
234 + nipc_server_handler_fn handler,
235 + void *user)
236 +{
237 + return nipc_service_platform_server_init_raw(
238 + server, run_dir, service_name, config,
239 + worker_count, expected_method_code, handler, user);
240 +}
241 +
242 +void nipc_server_run(nipc_managed_server_t *server)
243 +{
244 + InterlockedExchange(&server->accept_loop_active, 1);
245 + InterlockedExchange(&server->running, 1);
246 +
247 + while (InterlockedCompareExchange(&server->running, 0, 0)) {
248 + /* Accept one client via L1 (blocking with internal timeout) */
249 + nipc_np_session_t session;
250 + memset(&session, 0, sizeof(session));
251 + session.pipe = INVALID_HANDLE_VALUE;
252 +
253 + uint64_t sid = server->next_session_id++;
254 + nipc_np_server_config_t accept_cfg;
255 + prepared_win_shm_t prepared_shm;
256 + if (!server_prepare_accept_config(server, sid, &accept_cfg, &prepared_shm)) {
257 + Sleep(10);
258 + continue;
259 + }
260 +
261 + server->listener.config = accept_cfg;
262 + nipc_np_error_t uerr = nipc_np_accept(&server->listener, sid, &session);
263 + if (uerr != NIPC_NP_OK) {
264 + server_destroy_prepared_win_shm(&prepared_shm);
265 + if (!InterlockedCompareExchange(&server->running, 0, 0))
266 + break;
267 + Sleep(10);
268 + continue;
269 + }
270 +
271 + nipc_service_common_server_note_request_capacity(
272 + server, session.max_request_payload_bytes);
273 + nipc_service_common_server_note_response_capacity(
274 + server, session.max_response_payload_bytes);
275 +
276 + /* Enforce worker_count limit: reap finished sessions, check count */
277 + EnterCriticalSection(&server->sessions_lock);
278 + nipc_service_win_server_reap_sessions_locked(server);
279 +
280 + if (server->session_count >= server->worker_count) {
281 + /* At capacity: reject this client by closing the session */
282 + LeaveCriticalSection(&server->sessions_lock);
283 + server_destroy_prepared_win_shm(&prepared_shm);
284 + nipc_np_close_session(&session);
285 + continue;
286 + }
287 +
288 + /* SHM profile guarantee: only negotiate SHM for sessions backed by
289 + * prepared per-session kernel objects for the selected profile. */
290 + nipc_win_shm_ctx_t *shm = NULL;
291 + if (session.selected_profile == NIPC_WIN_SHM_PROFILE_HYBRID ||
292 + session.selected_profile == NIPC_WIN_SHM_PROFILE_BUSYWAIT) {
293 + shm = server_take_prepared_win_shm(&prepared_shm, session.selected_profile);
294 + if (!shm) {
295 + server_destroy_prepared_win_shm(&prepared_shm);
296 + LeaveCriticalSection(&server->sessions_lock);
297 + nipc_np_close_session(&session);
298 + continue;
299 + }
300 + server_destroy_prepared_win_shm(&prepared_shm);
301 + } else {
302 + server_destroy_prepared_win_shm(&prepared_shm);
303 + }
304 +
305 + /* Create session context */
306 + nipc_session_ctx_t *sctx = nipc_service_win_calloc(
307 + 1, sizeof(nipc_session_ctx_t),
308 + NIPC_WIN_SERVICE_TEST_FAULT_SERVER_SESSION_CTX_CALLOC_INTERNAL);
309 + if (!sctx) {
310 + LeaveCriticalSection(&server->sessions_lock);
311 + if (shm) { nipc_win_shm_destroy(shm); free(shm); }
312 + nipc_np_close_session(&session);
313 + continue;
314 + }
315 +
316 + sctx->server = server;
317 + sctx->session = session;
318 + sctx->shm = shm;
319 + sctx->id = sid;
320 + InterlockedExchange((volatile LONG *)&sctx->active, 1);
321 +
322 + server->sessions[server->session_count++] = sctx;
323 + LeaveCriticalSection(&server->sessions_lock);
324 +
325 + /* Spawn handler thread for this session */
326 + unsigned tid_unused;
327 + sctx->thread = (HANDLE)nipc_service_win_beginthreadex(
328 + NULL, 0, nipc_service_win_session_handler_thread, sctx, 0, &tid_unused);
329 + if (sctx->thread == 0) {
330 + /* Thread creation failed: clean up */
331 + EnterCriticalSection(&server->sessions_lock);
332 + for (int i = 0; i < server->session_count; i++) {
333 + if (server->sessions[i] == sctx) {
334 + server->sessions[i] = server->sessions[server->session_count - 1];
335 + server->session_count--;
336 + break;
337 + }
338 + }
339 + LeaveCriticalSection(&server->sessions_lock);
340 +
341 + if (shm) { nipc_win_shm_destroy(shm); free(shm); }
342 + nipc_np_close_session(&session);
343 + free(sctx);
344 + }
345 + }
346 +
347 + InterlockedExchange(&server->accept_loop_active, 0);
348 + nipc_np_close_listener(&server->listener);
349 +}
350 +
351 +void nipc_server_stop(nipc_managed_server_t *server)
352 +{
353 + InterlockedExchange(&server->running, 0);
354 + if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
355 + server_wake_listener(server);
356 + else
357 + nipc_np_close_listener(&server->listener);
358 +
359 + if (server->sessions) {
360 + EnterCriticalSection(&server->sessions_lock);
361 + server_cancel_active_session_io_locked(server);
362 + LeaveCriticalSection(&server->sessions_lock);
363 + }
364 +}
365 +
366 +bool nipc_server_drain(nipc_managed_server_t *server, uint32_t timeout_ms)
367 +{
368 + /* 1. Stop accepting new clients */
369 + InterlockedExchange(&server->running, 0);
370 + if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
371 + server_wake_listener(server);
372 + else
373 + nipc_np_close_listener(&server->listener);
374 +
375 + /* 2. Wait for in-flight sessions to complete */
376 + bool all_drained = true;
377 + if (server->sessions) {
378 + ULONGLONG deadline = GetTickCount64() + timeout_ms;
379 +
380 + /* Poll until all sessions are inactive or timeout */
381 + while (1) {
382 + EnterCriticalSection(&server->sessions_lock);
383 + int active_count = 0;
384 + for (int i = 0; i < server->session_count; i++) {
385 + if (InterlockedCompareExchange(
386 + (volatile LONG *)&server->sessions[i]->active, 0, 0))
387 + active_count++;
388 + }
389 + LeaveCriticalSection(&server->sessions_lock);
390 +
391 + if (active_count == 0)
392 + break;
393 +
394 + if (GetTickCount64() >= deadline) {
395 + /* Timeout: cancel synchronous session I/O to unblock threads. */
396 + EnterCriticalSection(&server->sessions_lock);
397 + server_cancel_active_session_io_locked(server);
398 + LeaveCriticalSection(&server->sessions_lock);
399 + all_drained = false;
400 + break;
401 + }
402 +
403 + Sleep(5); /* 5ms poll interval */
404 + }
405 +
406 + /* 3. Join all session threads */
407 + EnterCriticalSection(&server->sessions_lock);
408 + for (int i = 0; i < server->session_count; i++) {
409 + nipc_session_ctx_t *s = server->sessions[i];
410 + LeaveCriticalSection(&server->sessions_lock);
411 + WaitForSingleObject(s->thread, INFINITE);
412 + CloseHandle(s->thread);
413 + free(s);
414 + EnterCriticalSection(&server->sessions_lock);
415 + }
416 + server->session_count = 0;
417 + LeaveCriticalSection(&server->sessions_lock);
418 +
419 + free(server->sessions);
420 + server->sessions = NULL;
421 + server->session_capacity = 0;
422 + DeleteCriticalSection(&server->sessions_lock);
423 + }
424 +
425 + server->worker_count = 0;
426 +
427 + return all_drained;
428 +}
429 +
430 +void nipc_server_destroy(nipc_managed_server_t *server)
431 +{
432 + InterlockedExchange(&server->running, 0);
433 + if (InterlockedCompareExchange(&server->accept_loop_active, 0, 0))
434 + server_wake_listener(server);
435 + else
436 + nipc_np_close_listener(&server->listener);
437 +
438 + /* Join all active session threads */
439 + if (server->sessions) {
440 + EnterCriticalSection(&server->sessions_lock);
441 + server_cancel_active_session_io_locked(server);
442 + for (int i = 0; i < server->session_count; i++) {
443 + nipc_session_ctx_t *s = server->sessions[i];
444 + LeaveCriticalSection(&server->sessions_lock);
445 + WaitForSingleObject(s->thread, INFINITE);
446 + CloseHandle(s->thread);
447 + free(s);
448 + EnterCriticalSection(&server->sessions_lock);
449 + }
450 + server->session_count = 0;
451 + LeaveCriticalSection(&server->sessions_lock);
452 +
453 + free(server->sessions);
454 + server->sessions = NULL;
455 + server->session_capacity = 0;
456 + DeleteCriticalSection(&server->sessions_lock);
457 + }
458 +
459 + server->worker_count = 0;
460 +
461 +}
462 +
463 +
464 +#endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/service/netipc_service_win_server_session.c new
+249
@@ -0,0 +1,249 @@
1 +/*
2 + * netipc_service_win_server_session.c - Windows managed server session loop.
3 + */
4 +
5 +#if defined(_WIN32) || defined(__MSYS__)
6 +
7 +#include "netipc/netipc_service.h"
8 +#include "netipc/netipc_protocol.h"
9 +#include "netipc/netipc_named_pipe.h"
10 +#include "netipc/netipc_win_shm.h"
11 +#include "netipc_service_common.h"
12 +#include "netipc_service_win_internal.h"
13 +
14 +#include <stdint.h>
15 +#include <stdlib.h>
16 +#include <string.h>
17 +#include <windows.h>
18 +
19 +static void server_handle_session(nipc_managed_server_t *server,
20 + nipc_np_session_t *session,
21 + nipc_win_shm_ctx_t *shm,
22 + uint8_t *resp_buf,
23 + size_t resp_buf_size)
24 +{
25 + /* Dynamically allocate recv buffer based on negotiated max */
26 + size_t recv_size;
27 + if (!nipc_service_common_header_payload_len(
28 + session->max_request_payload_bytes, &recv_size))
29 + return;
30 + if (recv_size < NIPC_HEADER_LEN + 1024u)
31 + recv_size = NIPC_HEADER_LEN + 1024u;
32 + uint8_t *recv_buf = nipc_service_win_malloc(
33 + recv_size, NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RECV_BUF_MALLOC_INTERNAL);
34 + if (!recv_buf)
35 + return;
36 +
37 + while (InterlockedCompareExchange(&server->running, 0, 0)) {
38 + nipc_header_t hdr;
39 + const void *payload;
40 + size_t payload_len;
41 +
42 + /* Receive request via the active transport */
43 + if (shm) {
44 + size_t msg_len;
45 + nipc_win_shm_error_t serr = nipc_win_shm_receive(shm, recv_buf, recv_size,
46 + &msg_len, SERVER_POLL_TIMEOUT_MS);
47 + if (serr == NIPC_WIN_SHM_ERR_TIMEOUT)
48 + continue;
49 + if (serr != NIPC_WIN_SHM_OK)
50 + break;
51 + if (msg_len < NIPC_HEADER_LEN)
52 + break;
53 +
54 + nipc_error_t perr = nipc_header_decode(recv_buf, msg_len, &hdr);
55 + if (perr != NIPC_OK)
56 + break;
57 +
58 + payload = recv_buf + NIPC_HEADER_LEN;
59 + payload_len = msg_len - NIPC_HEADER_LEN;
60 + } else {
61 + /* Named Pipe path: wait for readability first, then receive.
62 + * This mirrors the Go/Rust Windows server loops and avoids
63 + * relying on a blocking ReadFile wake-up for each ping-pong
64 + * request. */
65 + bool readable = false;
66 + nipc_np_error_t werr = nipc_np_wait_readable(
67 + session, SERVER_POLL_TIMEOUT_MS, &readable);
68 + if (werr == NIPC_NP_ERR_DISCONNECTED)
69 + break;
70 + if (werr != NIPC_NP_OK)
71 + break;
72 + if (!readable)
73 + continue;
74 +
75 + nipc_np_error_t uerr = nipc_np_receive(
76 + session, recv_buf, recv_size,
77 + &hdr, &payload, &payload_len);
78 + if (uerr == NIPC_NP_ERR_LIMIT_EXCEEDED) {
79 + if (hdr.kind == NIPC_KIND_REQUEST) {
80 + if (hdr.payload_len > 0)
81 + nipc_service_common_server_note_request_capacity(
82 + server, hdr.payload_len);
83 +
84 + nipc_header_t resp_hdr = {0};
85 + resp_hdr.kind = NIPC_KIND_RESPONSE;
86 + resp_hdr.code = hdr.code;
87 + resp_hdr.message_id = hdr.message_id;
88 + resp_hdr.transport_status = NIPC_STATUS_LIMIT_EXCEEDED;
89 + resp_hdr.item_count = 1;
90 + resp_hdr.flags = 0;
91 +
92 + if (nipc_np_send(session, &resp_hdr, NULL, 0) != NIPC_NP_OK)
93 + break;
94 + }
95 + break;
96 + }
97 + if (uerr != NIPC_NP_OK)
98 + break;
99 + }
100 +
101 + /* Protocol violation: unexpected message kind terminates session */
102 + if (hdr.kind != NIPC_KIND_REQUEST)
103 + break;
104 +
105 + if (hdr.code != server->expected_method_code) {
106 + nipc_header_t resp_hdr = {0};
107 + resp_hdr.kind = NIPC_KIND_RESPONSE;
108 + resp_hdr.code = hdr.code;
109 + resp_hdr.message_id = hdr.message_id;
110 + resp_hdr.transport_status = NIPC_STATUS_UNSUPPORTED;
111 + resp_hdr.item_count = 1;
112 + resp_hdr.flags = 0;
113 +
114 + if (shm) {
115 + uint8_t msg[NIPC_HEADER_LEN];
116 + resp_hdr.magic = NIPC_MAGIC_MSG;
117 + resp_hdr.version = NIPC_VERSION;
118 + resp_hdr.header_len = NIPC_HEADER_LEN;
119 + resp_hdr.payload_len = 0;
120 + nipc_header_encode(&resp_hdr, msg, sizeof(msg));
121 + if (nipc_win_shm_send(shm, msg, sizeof(msg)) != NIPC_WIN_SHM_OK)
122 + break;
123 + } else {
124 + if (nipc_np_send(session, &resp_hdr, NULL, 0) != NIPC_NP_OK)
125 + break;
126 + }
127 + continue;
128 + }
129 +
130 + if (payload_len <= UINT32_MAX)
131 + nipc_service_common_server_note_request_capacity(
132 + server, (uint32_t)payload_len);
133 +
134 + /* Dispatch: one request kind per service endpoint. */
135 + size_t response_len = 0;
136 + nipc_error_t dispatch_err = server->handler(
137 + server->handler_user,
138 + &hdr,
139 + (const uint8_t *)payload, payload_len,
140 + resp_buf, resp_buf_size,
141 + &response_len);
142 +
143 + /* Build response header */
144 + nipc_header_t resp_hdr;
145 + bool close_after_response = false;
146 + nipc_service_common_prepare_response_header(&hdr, &resp_hdr);
147 + nipc_service_common_apply_dispatch_result(
148 + server, dispatch_err, resp_buf_size,
149 + session->max_response_payload_bytes, true,
150 + &resp_hdr, &response_len, &close_after_response);
151 +
152 + /* Send response via the active transport */
153 + if (shm) {
154 + size_t msg_len;
155 + if (!nipc_service_common_header_payload_len(response_len, &msg_len))
156 + break;
157 +
158 + resp_hdr.magic = NIPC_MAGIC_MSG;
159 + resp_hdr.version = NIPC_VERSION;
160 + resp_hdr.header_len = NIPC_HEADER_LEN;
161 + resp_hdr.payload_len = (uint32_t)response_len;
162 +
163 + uint8_t stack_msg[4096];
164 + uint8_t *msg = (msg_len <= sizeof(stack_msg)) ? stack_msg : malloc(msg_len);
165 + if (!msg)
166 + break;
167 +
168 + nipc_header_encode(&resp_hdr, msg, NIPC_HEADER_LEN);
169 + if (response_len > 0)
170 + memcpy(msg + NIPC_HEADER_LEN, resp_buf, response_len);
171 +
172 + nipc_win_shm_error_t serr = nipc_win_shm_send(shm, msg, msg_len);
173 + if (msg != stack_msg)
174 + free(msg);
175 + if (serr != NIPC_WIN_SHM_OK)
176 + break;
177 + } else {
178 + nipc_np_error_t uerr = nipc_np_send(
179 + session, &resp_hdr, resp_buf, response_len);
180 + if (uerr != NIPC_NP_OK)
181 + break;
182 + }
183 +
184 + if (close_after_response)
185 + break;
186 + }
187 +
188 + free(recv_buf);
189 +}
190 +
191 +/* ------------------------------------------------------------------ */
192 +/* Internal: per-session handler thread */
193 +/* ------------------------------------------------------------------ */
194 +
195 +/* Thread function: handles one client session from accept to disconnect. */
196 +unsigned __stdcall nipc_service_win_session_handler_thread(void *arg)
197 +{
198 + nipc_session_ctx_t *sctx = (nipc_session_ctx_t *)arg;
199 + nipc_managed_server_t *server = sctx->server;
200 + /* Allocate a per-session response buffer */
201 + size_t resp_size = (size_t)sctx->session.max_response_payload_bytes;
202 + if (resp_size < 1024u)
203 + resp_size = 1024u;
204 + uint8_t *resp_buf = nipc_service_win_malloc(
205 + resp_size, NIPC_WIN_SERVICE_TEST_FAULT_SERVER_RESP_BUF_MALLOC_INTERNAL);
206 + if (resp_buf) {
207 + server_handle_session(server, &sctx->session, sctx->shm,
208 + resp_buf, resp_size);
209 + free(resp_buf);
210 + }
211 +
212 + /* Cleanup SHM and session */
213 + if (sctx->shm) {
214 + nipc_win_shm_destroy(sctx->shm);
215 + free(sctx->shm);
216 + }
217 + nipc_np_close_session(&sctx->session);
218 +
219 + /* Mark inactive; the reap/destroy path owns removal from the array */
220 + InterlockedExchange((volatile LONG *)&sctx->active, 0);
221 + return 0;
222 +}
223 +
224 +/* ------------------------------------------------------------------ */
225 +/* Internal: reap finished session threads */
226 +/* ------------------------------------------------------------------ */
227 +
228 +/* Reap all finished (inactive) session threads. Called with lock held. */
229 +void nipc_service_win_server_reap_sessions_locked(nipc_managed_server_t *server)
230 +{
231 + int i = 0;
232 + while (i < server->session_count) {
233 + nipc_session_ctx_t *s = server->sessions[i];
234 + if (!InterlockedCompareExchange((volatile LONG *)&s->active, 0, 0)) {
235 + WaitForSingleObject(s->thread, INFINITE);
236 + CloseHandle(s->thread);
237 + /* Swap with last, free */
238 + server->sessions[i] = server->sessions[server->session_count - 1];
239 + server->session_count--;
240 + free(s);
241 + } else {
242 + i++;
243 + }
244 + }
245 +
246 +}
247 +
248 +
249 +#endif /* _WIN32 || __MSYS__ */
src/libnetdata/netipc/src/transport/posix/netipc_shm.c
+149 -43
@@ -11,7 +11,6 @@
11 #include <dirent.h>
12 #include <errno.h>
13 #include <fcntl.h>
14 -#include <inttypes.h>
14 #include <signal.h>
15 #include <stdio.h>
16 #include <stdlib.h>
@@ -36,6 +35,21 @@ static inline uint32_t align64(uint32_t v)
35 return (v + (NIPC_SHM_REGION_ALIGNMENT - 1)) & ~(uint32_t)(NIPC_SHM_REGION_ALIGNMENT - 1);
36 }
37
38 +static int copy_cstr_checked(char *dst, size_t dst_size, const char *src)
39 +{
40 + if (!dst || !src || dst_size == 0)
41 + return -1;
42 +
43 + size_t len = 0;
44 + while (len < dst_size && src[len] != '\0')
45 + len++;
46 + if (len == dst_size)
47 + return -1;
48 +
49 + memcpy(dst, src, len + 1);
50 + return 0;
51 +}
52 +
53 /* Validate service_name: only [a-zA-Z0-9._-], non-empty, not "." or "..". */
54 static int validate_service_name(const char *name)
55 {
@@ -56,21 +70,61 @@ static int validate_service_name(const char *name)
70 return 0;
71 }
72
73 +static int build_shm_name(char *dst, size_t dst_len,
74 + const char *service_name,
75 + uint64_t session_id)
76 +{
77 + if (validate_service_name(service_name) < 0)
78 + return -2; /* invalid service name */
79 +
80 + int n = snprintf(dst, dst_len, "%s-%016llx.ipcshm",
81 + service_name, (unsigned long long)session_id);
82 + if (n < 0 || (size_t)n >= dst_len)
83 + return -1;
84 + return 0;
85 +}
86 +
87 /* Build per-session SHM file path: {run_dir}/{service_name}-{session_id:016x}.ipcshm */
88 static int build_shm_path(char *dst, size_t dst_len,
89 const char *run_dir, const char *service_name,
90 uint64_t session_id)
91 {
64 - if (validate_service_name(service_name) < 0)
65 - return -2; /* invalid service name */
92 + char shm_name[256];
93 + int name_rc = build_shm_name(shm_name, sizeof(shm_name), service_name,
94 + session_id);
95 + if (name_rc < 0)
96 + return name_rc;
97
67 - int n = snprintf(dst, dst_len, "%s/%s-%016" PRIx64 ".ipcshm",
68 - run_dir, service_name, session_id);
98 + int n = snprintf(dst, dst_len, "%s/%s", run_dir, shm_name);
99 if (n < 0 || (size_t)n >= dst_len)
100 return -1;
101 return 0;
102 }
103
104 +static bool dir_fd_allows_stale_unlink(int dir_fd)
105 +{
106 + struct stat st;
107 + if (dir_fd < 0 || fstat(dir_fd, &st) != 0)
108 + return false;
109 + if (!S_ISDIR(st.st_mode))
110 + return false;
111 + if (st.st_uid != geteuid())
112 + return false;
113 + return (st.st_mode & (S_IWGRP | S_IWOTH)) == 0;
114 +}
115 +
116 +static int open_run_dir_fd(const char *run_dir)
117 +{
118 + DIR *dir = opendir(run_dir);
119 + if (!dir)
120 + return -1;
121 +
122 + int raw_fd = dirfd(dir);
123 + int fd = raw_fd >= 0 ? fcntl(raw_fd, F_DUPFD_CLOEXEC, 0) : -1;
124 + closedir(dir);
125 + return fd;
126 +}
127 +
128 /* Thin wrapper around the futex syscall. */
129 static int futex_wake(uint32_t *addr, int count)
130 {
@@ -110,12 +164,6 @@ static inline void *region_ptr(const nipc_shm_ctx_t *ctx, uint32_t offset)
164 return (uint8_t *)ctx->base + offset;
165 }
166
113 -/* Pointer to the header (always at offset 0). Used for non-atomic fields. */
114 -static inline nipc_shm_region_header_t *region_hdr(const nipc_shm_ctx_t *ctx)
115 -{
116 - return (nipc_shm_region_header_t *)ctx->base;
117 -}
118 -
167 /*
168 * Byte-offset accessors for atomic fields. These avoid taking the
169 * address of a packed struct member, which GCC warns about.
@@ -148,27 +196,33 @@ static inline uint32_t *shm_u32_ptr(void *base, int offset)
196 * -1 = doesn't exist
197 * -2 = exists but undersized / invalid (treated as stale, unlinked)
198 */
151 -static int unlink_same_file(const char *path, const struct stat *expected)
199 +static int unlink_same_file(int dir_fd, const char *name, const struct stat *expected,
200 + bool allow_stale_unlink)
201 {
202 + if (!allow_stale_unlink)
203 + return -1;
204 +
205 struct stat current;
154 - if (lstat(path, &current) != 0)
206 + if (fstatat(dir_fd, name, &current, AT_SYMLINK_NOFOLLOW) != 0)
207 return (errno == ENOENT) ? 0 : -1;
208
209 if (current.st_dev != expected->st_dev || current.st_ino != expected->st_ino)
210 return -1;
211
160 - if (unlink(path) == 0 || errno == ENOENT)
212 + /* Stale recovery only unlinks same-inode entries in a private run directory. */
213 + if (unlinkat(dir_fd, name, 0) == 0 || errno == ENOENT)
214 return 0;
215
216 return -1;
217 }
218
166 -static int check_shm_stale(const char *path)
219 +static int check_shm_stale(int dir_fd, const char *name,
220 + bool allow_stale_unlink)
221 {
222 #ifdef O_NOFOLLOW
169 - int fd = open(path, O_RDONLY | O_NOFOLLOW);
223 + int fd = openat(dir_fd, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
224 #else
171 - int fd = open(path, O_RDONLY);
225 + int fd = openat(dir_fd, name, O_RDONLY | O_CLOEXEC);
226 #endif
227 if (fd < 0) {
228 if (errno == ENOENT)
@@ -192,13 +246,13 @@ static int check_shm_stale(const char *path)
246 /* Must be at least header-sized to inspect. */
247 if (st.st_size < (off_t)NIPC_SHM_HEADER_LEN) {
248 close(fd);
195 - return (unlink_same_file(path, &st) == 0) ? -2 : 1;
249 + return (unlink_same_file(dir_fd, name, &st, allow_stale_unlink) == 0) ? -2 : 1;
250 }
251
252 void *map = mmap(NULL, NIPC_SHM_HEADER_LEN, PROT_READ, MAP_SHARED, fd, 0);
253 close(fd);
254 if (map == MAP_FAILED) {
201 - return (unlink_same_file(path, &st) == 0) ? -2 : 1;
255 + return (unlink_same_file(dir_fd, name, &st, allow_stale_unlink) == 0) ? -2 : 1;
256 }
257
258 const nipc_shm_region_header_t *hdr = (const nipc_shm_region_header_t *)map;
@@ -206,7 +260,7 @@ static int check_shm_stale(const char *path)
260 /* Validate magic first. */
261 if (hdr->magic != NIPC_SHM_REGION_MAGIC) {
262 munmap(map, NIPC_SHM_HEADER_LEN);
209 - return (unlink_same_file(path, &st) == 0) ? -2 : 1;
263 + return (unlink_same_file(dir_fd, name, &st, allow_stale_unlink) == 0) ? -2 : 1;
264 }
265
266 int32_t owner = hdr->owner_pid;
@@ -218,7 +272,7 @@ static int check_shm_stale(const char *path)
272 }
273
274 /* Dead owner or zero generation (uninitialized/legacy) — stale */
221 - return (unlink_same_file(path, &st) == 0) ? 0 : 1;
275 + return (unlink_same_file(dir_fd, name, &st, allow_stale_unlink) == 0) ? 0 : 1;
276 }
277
278 /* ------------------------------------------------------------------ */
@@ -247,37 +301,61 @@ nipc_shm_error_t nipc_shm_server_create(const char *run_dir,
301 if (path_rc < 0)
302 return NIPC_SHM_ERR_PATH_TOO_LONG;
303
304 + char shm_name[256];
305 + int name_rc = build_shm_name(shm_name, sizeof(shm_name), service_name,
306 + session_id);
307 + if (name_rc == -2)
308 + return NIPC_SHM_ERR_BAD_PARAM;
309 + if (name_rc < 0)
310 + return NIPC_SHM_ERR_PATH_TOO_LONG;
311 +
312 + int dir_fd = open_run_dir_fd(run_dir);
313 + if (dir_fd < 0)
314 + return NIPC_SHM_ERR_OPEN;
315 +
316 /* Round capacities up to alignment. */
317 req_capacity = align64(req_capacity);
318 resp_capacity = align64(resp_capacity);
319
320 uint32_t req_off = align64(NIPC_SHM_HEADER_LEN);
321 /* Guard against uint32 overflow before computing resp_off */
256 - if (req_capacity > UINT32_MAX - req_off)
322 + if (req_capacity > UINT32_MAX - req_off) {
323 + close(dir_fd);
324 return NIPC_SHM_ERR_BAD_PARAM;
325 + }
326 uint32_t resp_off = align64(req_off + req_capacity);
259 - if (resp_capacity > UINT32_MAX - resp_off ||
260 - (size_t)resp_off > SIZE_MAX - (size_t)resp_capacity)
327 + if (resp_capacity > UINT32_MAX - resp_off) {
328 + close(dir_fd);
329 return NIPC_SHM_ERR_BAD_PARAM;
330 + }
331 size_t region_size = (size_t)resp_off + resp_capacity;
332
333 /* Try O_EXCL create first (fast path, no stale check needed). */
265 - int fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
334 + int fd = openat(dir_fd, shm_name,
335 + O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
336
337 /* If O_EXCL failed because file exists, do stale recovery and retry. */
338 if (fd < 0 && errno == EEXIST) {
269 - int stale = check_shm_stale(path);
270 - if (stale == 1)
339 + bool allow_stale_unlink = dir_fd_allows_stale_unlink(dir_fd);
340 +
341 + int stale = check_shm_stale(dir_fd, shm_name, allow_stale_unlink);
342 + if (stale == 1) {
343 + close(dir_fd);
344 return NIPC_SHM_ERR_ADDR_IN_USE;
345 + }
346 /* Stale file was unlinked, retry create */
273 - fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
347 + fd = openat(dir_fd, shm_name,
348 + O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
349 }
275 - if (fd < 0)
350 + if (fd < 0) {
351 + close(dir_fd);
352 return NIPC_SHM_ERR_OPEN;
353 + }
354
355 if (ftruncate(fd, (off_t)region_size) < 0) {
356 close(fd);
280 - unlink(path);
357 + unlinkat(dir_fd, shm_name, 0);
358 + close(dir_fd);
359 return NIPC_SHM_ERR_TRUNCATE;
360 }
361
@@ -285,7 +363,8 @@ nipc_shm_error_t nipc_shm_server_create(const char *run_dir,
363 MAP_SHARED, fd, 0);
364 if (map == MAP_FAILED) {
365 close(fd);
288 - unlink(path);
366 + unlinkat(dir_fd, shm_name, 0);
367 + close(dir_fd);
368 return NIPC_SHM_ERR_MMAP;
369 }
370
@@ -326,9 +405,17 @@ nipc_shm_error_t nipc_shm_server_create(const char *run_dir,
405 out->local_resp_seq = 0;
406 out->spin_tries = NIPC_SHM_DEFAULT_SPIN;
407 out->owner_generation = hdr->owner_generation;
329 - strncpy(out->path, path, sizeof(out->path) - 1);
330 - out->path[sizeof(out->path) - 1] = '\0';
408 + if (copy_cstr_checked(out->path, sizeof(out->path), path) != 0) {
409 + munmap(map, region_size);
410 + close(fd);
411 + unlinkat(dir_fd, shm_name, 0);
412 + close(dir_fd);
413 + memset(out, 0, sizeof(*out));
414 + out->fd = -1;
415 + return NIPC_SHM_ERR_PATH_TOO_LONG;
416 + }
417
418 + close(dir_fd);
419 return NIPC_SHM_OK;
420 }
421
@@ -382,8 +469,20 @@ nipc_shm_error_t nipc_shm_client_attach(const char *run_dir,
469 if (path_rc < 0)
470 return NIPC_SHM_ERR_PATH_TOO_LONG;
471
472 + char shm_name[256];
473 + int name_rc = build_shm_name(shm_name, sizeof(shm_name), service_name,
474 + session_id);
475 + if (name_rc == -2)
476 + return NIPC_SHM_ERR_BAD_PARAM;
477 + if (name_rc < 0)
478 + return NIPC_SHM_ERR_PATH_TOO_LONG;
479 +
480 /* Open the file. */
386 - int fd = open(path, O_RDWR);
481 + int dir_fd = open_run_dir_fd(run_dir);
482 + if (dir_fd < 0)
483 + return NIPC_SHM_ERR_OPEN;
484 + int fd = openat(dir_fd, shm_name, O_RDWR | O_CLOEXEC);
485 + close(dir_fd);
486 if (fd < 0)
487 return NIPC_SHM_ERR_OPEN;
488
@@ -493,8 +592,13 @@ nipc_shm_error_t nipc_shm_client_attach(const char *run_dir,
592 out->local_resp_seq = cur_resp_seq;
593 out->spin_tries = NIPC_SHM_DEFAULT_SPIN;
594 out->owner_generation = hdr->owner_generation;
496 - strncpy(out->path, path, sizeof(out->path) - 1);
497 - out->path[sizeof(out->path) - 1] = '\0';
595 + if (copy_cstr_checked(out->path, sizeof(out->path), path) != 0) {
596 + munmap(map, file_size);
597 + close(fd);
598 + memset(out, 0, sizeof(*out));
599 + out->fd = -1;
600 + return NIPC_SHM_ERR_PATH_TOO_LONG;
601 + }
602
603 return NIPC_SHM_OK;
604 }
@@ -786,6 +890,14 @@ void nipc_shm_cleanup_stale(const char *run_dir, const char *service_name)
890 if (!dir)
891 return;
892
893 + int dir_fd = dirfd(dir);
894 + if (dir_fd < 0) {
895 + closedir(dir);
896 + return;
897 + }
898 +
899 + bool allow_stale_unlink = dir_fd_allows_stale_unlink(dir_fd);
900 +
901 struct dirent *ent;
902 while ((ent = readdir(dir)) != NULL) {
903 size_t nlen = strlen(ent->d_name);
@@ -798,15 +910,9 @@ void nipc_shm_cleanup_stale(const char *run_dir, const char *service_name)
910 if (strcmp(ent->d_name + nlen - suffix_len, suffix) != 0)
911 continue;
912
801 - /* Build full path and check if stale */
802 - char path[512];
803 - int n = snprintf(path, sizeof(path), "%s/%s", run_dir, ent->d_name);
804 - if (n < 0 || (size_t)n >= sizeof(path))
805 - continue;
806 -
913 /* check_shm_stale unlinks stale files and returns:
914 * 0 = stale (unlinked), +1 = live, -1 = gone, -2 = invalid (unlinked) */
809 - check_shm_stale(path);
915 + check_shm_stale(dir_fd, ent->d_name, allow_stale_unlink);
916 }
917
918 closedir(dir);
src/libnetdata/netipc/src/transport/posix/netipc_uds.c
+19 -1055
@@ -1,189 +1,39 @@
1 /*
2 - * netipc_uds.c - L1 POSIX UDS SEQPACKET transport.
3 - *
4 - * Implements connection lifecycle, handshake with profile/limit negotiation,
5 - * and send/receive with transparent chunking over AF_UNIX SEQPACKET sockets.
2 + * netipc_uds.c - shared low-level helpers for the POSIX UDS transport.
3 */
4
8 -#include "netipc/netipc_uds.h"
9 -#include "netipc/netipc_protocol.h"
5 +#include "netipc_uds_internal.h"
6
11 -#include <errno.h>
12 -#include <fcntl.h>
13 -#include <stdlib.h>
14 -#include <stdio.h>
7 +#include <limits.h>
8 #include <string.h>
16 -#include <unistd.h>
17 -
9 #include <sys/socket.h>
19 -#include <sys/stat.h>
20 -#include <sys/un.h>
21 -
22 -/* ------------------------------------------------------------------ */
23 -/* Internal constants */
24 -/* ------------------------------------------------------------------ */
25 -
26 -#define UDS_DEFAULT_BACKLOG 16
27 -#define UDS_DEFAULT_BATCH_ITEMS 1
28 -#define UDS_INITIAL_RECV_BUF 4096
10
30 -/* ------------------------------------------------------------------ */
31 -/* Internal helpers */
32 -/* ------------------------------------------------------------------ */
33 -
34 -static bool header_payload_len(size_t payload_len, size_t *msg_len_out)
11 +bool nipc_uds_header_payload_len(size_t payload_len, size_t *msg_len_out)
12 {
36 -#if SIZE_MAX <= UINT32_MAX
13 if (payload_len > SIZE_MAX - NIPC_HEADER_LEN)
14 return false;
39 -#endif
40 -
41 - *msg_len_out = NIPC_HEADER_LEN + payload_len;
42 - return true;
43 -}
44 -
45 -/* Validate service_name: only [a-zA-Z0-9._-], non-empty, not "." or "..". */
46 -static int validate_service_name(const char *name)
47 -{
48 - if (!name || name[0] == '\0')
49 - return -1;
15
51 - /* Reject "." and ".." */
52 - if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')))
53 - return -1;
54 -
55 - for (const char *p = name; *p; p++) {
56 - char c = *p;
57 - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
58 - (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')
59 - continue;
60 - return -1;
61 - }
62 - return 0;
63 -}
64 -
65 -/* Build socket path into dst, return 0 on success, -1 if too long. */
66 -static int build_socket_path(char *dst, size_t dst_len,
67 - const char *run_dir, const char *service_name)
68 -{
69 - if (validate_service_name(service_name) < 0)
70 - return -2; /* invalid service name */
16 + size_t msg_len = NIPC_HEADER_LEN + payload_len;
17 + if (msg_len > UINT32_MAX)
18 + return false;
19
72 - int n = snprintf(dst, dst_len, "%s/%s.sock", run_dir, service_name);
73 - if (n < 0 || (size_t)n >= dst_len)
74 - return -1; /* path too long */
75 - return 0;
20 + *msg_len_out = msg_len;
21 + return true;
22 }
23
78 -/* Get the socket's send buffer size as the packet size. */
79 -static uint32_t detect_packet_size(int fd)
24 +uint32_t nipc_uds_detect_packet_size(int fd)
25 {
26 int val = 0;
27 socklen_t len = sizeof(val);
28 if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &val, &len) < 0)
84 - return 65536; /* safe default */
29 + return 65536;
30
86 - /* Linux doubles SO_SNDBUF internally; use the value as-is, it
87 - * represents the actual kernel buffer available. Clamp to u32. */
31 if (val <= 0)
32 return 65536;
33 return (uint32_t)val;
34 }
35
93 -/* Highest set bit in a bitmask (0 if empty). */
94 -static uint32_t highest_bit(uint32_t mask)
95 -{
96 - if (mask == 0)
97 - return 0;
98 -
99 - uint32_t bit = 1u << 31;
100 - while (!(mask & bit))
101 - bit >>= 1;
102 - return bit;
103 -}
104 -
105 -static inline uint32_t min_u32(uint32_t a, uint32_t b)
106 -{
107 - return a < b ? a : b;
108 -}
109 -
110 -static inline uint32_t max_u32(uint32_t a, uint32_t b)
111 -{
112 - return a > b ? a : b;
113 -}
114 -
115 -static inline uint32_t apply_default(uint32_t val, uint32_t def)
116 -{
117 - return val == 0 ? def : val;
118 -}
119 -
120 -static nipc_uds_error_t raw_send(int fd, const void *data, size_t len);
121 -
122 -static bool header_version_incompatible(const void *buf, size_t buf_len,
123 - uint16_t expected_code)
124 -{
125 - if (buf_len < NIPC_HEADER_LEN)
126 - return false;
127 -
128 - nipc_header_t hdr;
129 - memcpy(&hdr, buf, sizeof(hdr));
130 - return hdr.magic == NIPC_MAGIC_MSG &&
131 - hdr.version != NIPC_VERSION &&
132 - hdr.header_len == NIPC_HEADER_LEN &&
133 - hdr.kind == NIPC_KIND_CONTROL &&
134 - hdr.code == expected_code;
135 -}
136 -
137 -static bool hello_layout_incompatible(const void *buf, size_t buf_len)
138 -{
139 - if (buf_len < sizeof(uint16_t))
140 - return false;
141 -
142 - nipc_hello_t hello;
143 - memset(&hello, 0, sizeof(hello));
144 - memcpy(&hello, buf, sizeof(uint16_t));
145 - return hello.layout_version != 1;
146 -}
147 -
148 -static bool hello_ack_layout_incompatible(const void *buf, size_t buf_len)
149 -{
150 - if (buf_len < sizeof(uint16_t))
151 - return false;
152 -
153 - nipc_hello_ack_t ack;
154 - memset(&ack, 0, sizeof(ack));
155 - memcpy(&ack, buf, sizeof(uint16_t));
156 - return ack.layout_version != 1;
157 -}
158 -
159 -static void send_rejection_ack(int fd, uint16_t status)
160 -{
161 - nipc_hello_ack_t ack = { .layout_version = 1 };
162 - uint8_t ack_buf[48];
163 - uint8_t pkt[80];
164 - nipc_header_t ack_hdr = {
165 - .magic = NIPC_MAGIC_MSG,
166 - .version = NIPC_VERSION,
167 - .header_len = NIPC_HEADER_LEN,
168 - .kind = NIPC_KIND_CONTROL,
169 - .code = NIPC_CODE_HELLO_ACK,
170 - .transport_status = status,
171 - .payload_len = sizeof(ack_buf),
172 - .item_count = 1,
173 - };
174 -
175 - nipc_hello_ack_encode(&ack, ack_buf, sizeof(ack_buf));
176 - nipc_header_encode(&ack_hdr, pkt, sizeof(pkt));
177 - memcpy(pkt + NIPC_HEADER_LEN, ack_buf, sizeof(ack_buf));
178 - raw_send(fd, pkt, NIPC_HEADER_LEN + sizeof(ack_buf));
179 -}
180 -
181 -/* ------------------------------------------------------------------ */
182 -/* Low-level send/recv (one SEQPACKET datagram) */
183 -/* ------------------------------------------------------------------ */
184 -
185 -/* Send exactly len bytes as one SEQPACKET message. */
186 -static nipc_uds_error_t raw_send(int fd, const void *data, size_t len)
36 +nipc_uds_error_t nipc_uds_raw_send(int fd, const void *data, size_t len)
37 {
38 ssize_t n = send(fd, data, len, MSG_NOSIGNAL);
39 if (n < 0 || (size_t)n != len)
@@ -191,9 +41,8 @@ static nipc_uds_error_t raw_send(int fd, const void *data, size_t len)
41 return NIPC_UDS_OK;
42 }
43
194 -/* Send header + payload as one SEQPACKET message using sendmsg. */
195 -static nipc_uds_error_t raw_send_iov(int fd, const void *hdr, size_t hdr_len,
196 - const void *payload, size_t payload_len)
44 +nipc_uds_error_t nipc_uds_raw_send_iov(int fd, const void *hdr, size_t hdr_len,
45 + const void *payload, size_t payload_len)
46 {
47 struct iovec iov[2];
48 struct msghdr msg;
@@ -202,16 +51,16 @@ static nipc_uds_error_t raw_send_iov(int fd, const void *hdr, size_t hdr_len,
51 memset(&msg, 0, sizeof(msg));
52
53 iov[0].iov_base = (void *)hdr;
205 - iov[0].iov_len = hdr_len;
54 + iov[0].iov_len = hdr_len;
55 iovcnt = 1;
56
57 if (payload && payload_len > 0) {
58 iov[1].iov_base = (void *)payload;
210 - iov[1].iov_len = payload_len;
59 + iov[1].iov_len = payload_len;
60 iovcnt = 2;
61 }
62
214 - msg.msg_iov = iov;
63 + msg.msg_iov = iov;
64 msg.msg_iovlen = iovcnt;
65
66 size_t total = hdr_len + payload_len;
@@ -222,892 +71,7 @@ static nipc_uds_error_t raw_send_iov(int fd, const void *hdr, size_t hdr_len,
71 return NIPC_UDS_OK;
72 }
73
225 -/* Receive one SEQPACKET message into buf. Returns bytes received, 0 on
226 - * disconnect, -1 on error. */
227 -static ssize_t raw_recv(int fd, void *buf, size_t buf_len)
228 -{
229 - ssize_t n = recv(fd, buf, buf_len, 0);
230 - return n;
231 -}
232 -
233 -/* ------------------------------------------------------------------ */
234 -/* Handshake: client side */
235 -/* ------------------------------------------------------------------ */
236 -
237 -static nipc_uds_error_t client_handshake(int fd,
238 - const nipc_uds_client_config_t *cfg,
239 - nipc_uds_session_t *session)
240 -{
241 - uint8_t buf[128]; /* enough for header(32) + hello(44) = 76, and ack */
242 - nipc_uds_error_t err;
243 -
244 - /* Detect packet size if not specified */
245 - uint32_t pkt_size = cfg->packet_size;
246 - if (pkt_size == 0)
247 - pkt_size = detect_packet_size(fd);
248 -
249 - /* Build HELLO payload */
250 - nipc_hello_t hello = {
251 - .layout_version = 1,
252 - .flags = 0,
253 - .supported_profiles = cfg->supported_profiles ? cfg->supported_profiles : NIPC_PROFILE_BASELINE,
254 - .preferred_profiles = cfg->preferred_profiles,
255 - .max_request_payload_bytes = apply_default(cfg->max_request_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT),
256 - .max_request_batch_items = apply_default(cfg->max_request_batch_items, UDS_DEFAULT_BATCH_ITEMS),
257 - .max_response_payload_bytes = apply_default(cfg->max_response_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT),
258 - .max_response_batch_items = apply_default(cfg->max_response_batch_items, UDS_DEFAULT_BATCH_ITEMS),
259 - .auth_token = cfg->auth_token,
260 - .packet_size = pkt_size,
261 - };
262 -
263 - uint8_t hello_buf[44];
264 - nipc_hello_encode(&hello, hello_buf, sizeof(hello_buf));
265 -
266 - /* Build outer CONTROL header */
267 - nipc_header_t hdr = {
268 - .magic = NIPC_MAGIC_MSG,
269 - .version = NIPC_VERSION,
270 - .header_len = NIPC_HEADER_LEN,
271 - .kind = NIPC_KIND_CONTROL,
272 - .flags = 0,
273 - .code = NIPC_CODE_HELLO,
274 - .transport_status = NIPC_STATUS_OK,
275 - .payload_len = sizeof(hello_buf),
276 - .item_count = 1,
277 - .message_id = 0,
278 - };
279 -
280 - nipc_header_encode(&hdr, buf, sizeof(buf));
281 - memcpy(buf + NIPC_HEADER_LEN, hello_buf, sizeof(hello_buf));
282 -
283 - /* Send HELLO */
284 - err = raw_send(fd, buf, NIPC_HEADER_LEN + sizeof(hello_buf));
285 - if (err != NIPC_UDS_OK)
286 - return err;
287 -
288 - /* Receive HELLO_ACK */
289 - ssize_t n = raw_recv(fd, buf, sizeof(buf));
290 - if (n <= 0)
291 - return NIPC_UDS_ERR_RECV;
292 -
293 - /* Decode outer header */
294 - nipc_header_t ack_hdr;
295 - nipc_error_t perr = nipc_header_decode(buf, (size_t)n, &ack_hdr);
296 - if (perr == NIPC_ERR_BAD_VERSION)
297 - return NIPC_UDS_ERR_INCOMPATIBLE;
298 - if (perr != NIPC_OK)
299 - return NIPC_UDS_ERR_PROTOCOL;
300 -
301 - if (ack_hdr.kind != NIPC_KIND_CONTROL || ack_hdr.code != NIPC_CODE_HELLO_ACK)
302 - return NIPC_UDS_ERR_PROTOCOL;
303 -
304 - /* Check transport_status for rejection */
305 - if (ack_hdr.transport_status == NIPC_STATUS_AUTH_FAILED)
306 - return NIPC_UDS_ERR_AUTH_FAILED;
307 - if (ack_hdr.transport_status == NIPC_STATUS_UNSUPPORTED)
308 - return NIPC_UDS_ERR_NO_PROFILE;
309 - if (ack_hdr.transport_status == NIPC_STATUS_INCOMPATIBLE)
310 - return NIPC_UDS_ERR_INCOMPATIBLE;
311 - if (ack_hdr.transport_status == NIPC_STATUS_LIMIT_EXCEEDED)
312 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
313 - if (ack_hdr.transport_status != NIPC_STATUS_OK)
314 - return NIPC_UDS_ERR_HANDSHAKE;
315 -
316 - /* Decode hello-ack payload */
317 - nipc_hello_ack_t ack;
318 - perr = nipc_hello_ack_decode(buf + NIPC_HEADER_LEN,
319 - (size_t)n - NIPC_HEADER_LEN, &ack);
320 - if (perr == NIPC_ERR_BAD_LAYOUT &&
321 - hello_ack_layout_incompatible(buf + NIPC_HEADER_LEN,
322 - (size_t)n - NIPC_HEADER_LEN))
323 - return NIPC_UDS_ERR_INCOMPATIBLE;
324 - if (perr != NIPC_OK)
325 - return NIPC_UDS_ERR_PROTOCOL;
326 -
327 - /* Fill session */
328 - session->fd = fd;
329 - session->role = NIPC_UDS_ROLE_CLIENT;
330 - session->max_request_payload_bytes = ack.agreed_max_request_payload_bytes;
331 - session->max_request_batch_items = ack.agreed_max_request_batch_items;
332 - session->max_response_payload_bytes = ack.agreed_max_response_payload_bytes;
333 - session->max_response_batch_items = ack.agreed_max_response_batch_items;
334 - session->packet_size = ack.agreed_packet_size;
335 - session->selected_profile = ack.selected_profile;
336 - session->session_id = ack.session_id;
337 - session->recv_buf = NULL;
338 - session->recv_buf_size = 0;
339 -
340 - /* Sanity: reject a packet_size too small for chunking arithmetic */
341 - if (session->packet_size <= NIPC_HEADER_LEN)
342 - return NIPC_UDS_ERR_PROTOCOL;
343 -
344 - return NIPC_UDS_OK;
345 -}
346 -
347 -/* ------------------------------------------------------------------ */
348 -/* Handshake: server side */
349 -/* ------------------------------------------------------------------ */
350 -
351 -static nipc_uds_error_t server_handshake(int fd,
352 - const nipc_uds_server_config_t *cfg,
353 - uint64_t session_id,
354 - nipc_uds_session_t *session)
74 +ssize_t nipc_uds_raw_recv(int fd, void *buf, size_t buf_len)
75 {
356 - uint8_t buf[128];
357 -
358 - /* Detect server packet size */
359 - uint32_t server_pkt_size = cfg->packet_size;
360 - if (server_pkt_size == 0)
361 - server_pkt_size = detect_packet_size(fd);
362 -
363 - /* Server limits with defaults applied */
364 - uint32_t s_req_pay = apply_default(cfg->max_request_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT);
365 - uint32_t s_req_bat = apply_default(cfg->max_request_batch_items, UDS_DEFAULT_BATCH_ITEMS);
366 - uint32_t s_resp_pay = apply_default(cfg->max_response_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT);
367 - uint32_t s_resp_bat = apply_default(cfg->max_response_batch_items, UDS_DEFAULT_BATCH_ITEMS);
368 - uint32_t s_profiles = cfg->supported_profiles ? cfg->supported_profiles : NIPC_PROFILE_BASELINE;
369 - uint32_t s_preferred = cfg->preferred_profiles;
370 -
371 - /* Receive HELLO */
372 - ssize_t n = raw_recv(fd, buf, sizeof(buf));
373 - if (n <= 0)
374 - return NIPC_UDS_ERR_RECV;
375 -
376 - nipc_header_t hdr;
377 - nipc_error_t perr = nipc_header_decode(buf, (size_t)n, &hdr);
378 - if (perr == NIPC_ERR_BAD_VERSION &&
379 - header_version_incompatible(buf, (size_t)n, NIPC_CODE_HELLO)) {
380 - send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
381 - return NIPC_UDS_ERR_INCOMPATIBLE;
382 - }
383 - if (perr != NIPC_OK)
384 - return NIPC_UDS_ERR_PROTOCOL;
385 -
386 - if (hdr.kind != NIPC_KIND_CONTROL || hdr.code != NIPC_CODE_HELLO)
387 - return NIPC_UDS_ERR_PROTOCOL;
388 -
389 - nipc_hello_t hello;
390 - perr = nipc_hello_decode(buf + NIPC_HEADER_LEN,
391 - (size_t)n - NIPC_HEADER_LEN, &hello);
392 - if (perr == NIPC_ERR_BAD_LAYOUT &&
393 - hello_layout_incompatible(buf + NIPC_HEADER_LEN,
394 - (size_t)n - NIPC_HEADER_LEN)) {
395 - send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
396 - return NIPC_UDS_ERR_INCOMPATIBLE;
397 - }
398 - if (perr != NIPC_OK)
399 - return NIPC_UDS_ERR_PROTOCOL;
400 -
401 - /* Compute intersection */
402 - uint32_t intersection = hello.supported_profiles & s_profiles;
403 -
404 - /* Check intersection */
405 - if (intersection == 0) {
406 - send_rejection_ack(fd, NIPC_STATUS_UNSUPPORTED);
407 - return NIPC_UDS_ERR_NO_PROFILE;
408 - }
409 -
410 - /* Check auth */
411 - if (hello.auth_token != cfg->auth_token) {
412 - send_rejection_ack(fd, NIPC_STATUS_AUTH_FAILED);
413 - return NIPC_UDS_ERR_AUTH_FAILED;
414 - }
415 -
416 - /* Select profile: prefer preferred_intersection, then intersection */
417 - uint32_t preferred_intersection = intersection &
418 - hello.preferred_profiles & s_preferred;
419 - uint32_t selected;
420 - if (preferred_intersection != 0)
421 - selected = highest_bit(preferred_intersection);
422 - else
423 - selected = highest_bit(intersection);
424 -
425 - if (hello.max_request_payload_bytes > NIPC_MAX_PAYLOAD_CAP) {
426 - send_rejection_ack(fd, NIPC_STATUS_LIMIT_EXCEEDED);
427 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
428 - }
429 -
430 - /* Negotiate limits:
431 - * - request payload and batch size are client-proposed and echoed
432 - * - response payload is server-authoritative
433 - * - response batch size is symmetric with request batch size */
434 - uint32_t agreed_req_pay = hello.max_request_payload_bytes;
435 - uint32_t agreed_req_bat = hello.max_request_batch_items;
436 - uint32_t agreed_resp_pay = s_resp_pay;
437 - uint32_t agreed_resp_bat = agreed_req_bat;
438 - uint32_t agreed_pkt = min_u32(hello.packet_size, server_pkt_size);
439 -
440 - /* packet_size must be large enough for a usable message packet */
441 - if (agreed_pkt <= NIPC_HEADER_LEN) {
442 - send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
443 - return NIPC_UDS_ERR_INCOMPATIBLE;
444 - }
445 -
446 - /* Send HELLO_ACK (success) */
447 - nipc_hello_ack_t ack = {
448 - .layout_version = 1,
449 - .flags = 0,
450 - .server_supported_profiles = s_profiles,
451 - .intersection_profiles = intersection,
452 - .selected_profile = selected,
453 - .agreed_max_request_payload_bytes = agreed_req_pay,
454 - .agreed_max_request_batch_items = agreed_req_bat,
455 - .agreed_max_response_payload_bytes = agreed_resp_pay,
456 - .agreed_max_response_batch_items = agreed_resp_bat,
457 - .agreed_packet_size = agreed_pkt,
458 - .session_id = session_id,
459 - };
460 -
461 - uint8_t ack_buf[48];
462 - nipc_hello_ack_encode(&ack, ack_buf, sizeof(ack_buf));
463 -
464 - nipc_header_t ack_hdr = {
465 - .magic = NIPC_MAGIC_MSG,
466 - .version = NIPC_VERSION,
467 - .header_len = NIPC_HEADER_LEN,
468 - .kind = NIPC_KIND_CONTROL,
469 - .flags = 0,
470 - .code = NIPC_CODE_HELLO_ACK,
471 - .transport_status = NIPC_STATUS_OK,
472 - .payload_len = sizeof(ack_buf),
473 - .item_count = 1,
474 - .message_id = 0,
475 - };
476 -
477 - uint8_t pkt[80];
478 - nipc_header_encode(&ack_hdr, pkt, sizeof(pkt));
479 - memcpy(pkt + NIPC_HEADER_LEN, ack_buf, sizeof(ack_buf));
480 -
481 - nipc_uds_error_t send_ack_err = raw_send(fd, pkt, NIPC_HEADER_LEN + sizeof(ack_buf));
482 - if (send_ack_err != NIPC_UDS_OK)
483 - return send_ack_err;
484 -
485 - /* Fill session */
486 - session->fd = fd;
487 - session->role = NIPC_UDS_ROLE_SERVER;
488 - session->max_request_payload_bytes = agreed_req_pay;
489 - session->max_request_batch_items = agreed_req_bat;
490 - session->max_response_payload_bytes = agreed_resp_pay;
491 - session->max_response_batch_items = agreed_resp_bat;
492 - session->packet_size = agreed_pkt;
493 - session->selected_profile = selected;
494 - session->session_id = session_id;
495 - session->recv_buf = NULL;
496 - session->recv_buf_size = 0;
497 -
498 - return NIPC_UDS_OK;
499 -}
500 -
501 -/* ------------------------------------------------------------------ */
502 -/* Stale endpoint recovery */
503 -/* ------------------------------------------------------------------ */
504 -
505 -static int unlink_stale_socket_path(const char *path)
506 -{
507 - struct stat st;
508 - if (lstat(path, &st) != 0)
509 - return (errno == ENOENT) ? 0 : -1;
510 -
511 - if (!S_ISSOCK(st.st_mode))
512 - return -1;
513 -
514 - if (unlink(path) == 0 || errno == ENOENT)
515 - return 0;
516 -
517 - return -1;
518 -}
519 -
520 -/* Returns: 0 = stale (unlinked), 1 = live server, -1 = doesn't exist */
521 -static int check_and_recover_stale(const char *path)
522 -{
523 - /* Try connecting to check if a live server is there */
524 - int probe = socket(AF_UNIX, SOCK_SEQPACKET, 0);
525 - if (probe < 0)
526 - return -1;
527 -
528 - struct sockaddr_un addr;
529 - memset(&addr, 0, sizeof(addr));
530 - addr.sun_family = AF_UNIX;
531 - strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
532 -
533 - int ret;
534 - if (connect(probe, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
535 - /* Connected -> live server */
536 - close(probe);
537 - ret = 1;
538 - } else {
539 - int saved_errno = errno;
540 - close(probe);
541 - /* Only ECONNREFUSED proves a stale socket path. Other errors
542 - * (including regular files at the path) must not remove anything. */
543 - if (saved_errno == ENOENT) {
544 - ret = -1;
545 - } else if (saved_errno == ECONNREFUSED) {
546 - ret = (unlink_stale_socket_path(path) == 0) ? 0 : 1;
547 - } else {
548 - /* Can't determine ownership — treat as live to prevent overwriting */
549 - ret = 1;
550 - }
551 - }
552 - return ret;
553 -}
554 -
555 -/* ------------------------------------------------------------------ */
556 -/* Public API: listen */
557 -/* ------------------------------------------------------------------ */
558 -
559 -nipc_uds_error_t nipc_uds_listen(const char *run_dir,
560 - const char *service_name,
561 - const nipc_uds_server_config_t *config,
562 - nipc_uds_listener_t *out)
563 -{
564 - memset(out, 0, sizeof(*out));
565 - out->fd = -1;
566 -
567 - /* Build path */
568 - char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
569 - int path_rc = build_socket_path(path, sizeof(path), run_dir, service_name);
570 - if (path_rc == -2)
571 - return NIPC_UDS_ERR_BAD_PARAM;
572 - if (path_rc < 0)
573 - return NIPC_UDS_ERR_PATH_TOO_LONG;
574 -
575 - /* Stale recovery */
576 - int stale = check_and_recover_stale(path);
577 - if (stale == 1)
578 - return NIPC_UDS_ERR_ADDR_IN_USE;
579 -
580 - /* Create socket */
581 - int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
582 - if (fd < 0)
583 - return NIPC_UDS_ERR_SOCKET;
584 -
585 - struct sockaddr_un addr;
586 - memset(&addr, 0, sizeof(addr));
587 - addr.sun_family = AF_UNIX;
588 - strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
589 -
590 - if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
591 - close(fd);
592 - return NIPC_UDS_ERR_SOCKET;
593 - }
594 -
595 - int backlog = config->backlog > 0 ? config->backlog : UDS_DEFAULT_BACKLOG;
596 - if (listen(fd, backlog) < 0) {
597 - close(fd);
598 - unlink(path);
599 - return NIPC_UDS_ERR_SOCKET;
600 - }
601 -
602 - out->fd = fd;
603 - out->config = *config;
604 - strncpy(out->path, path, sizeof(out->path) - 1);
605 - out->path[sizeof(out->path) - 1] = '\0';
606 -
607 - return NIPC_UDS_OK;
608 -}
609 -
610 -/* ------------------------------------------------------------------ */
611 -/* Public API: accept */
612 -/* ------------------------------------------------------------------ */
613 -
614 -nipc_uds_error_t nipc_uds_accept(nipc_uds_listener_t *listener,
615 - uint64_t session_id,
616 - nipc_uds_session_t *out)
617 -{
618 - memset(out, 0, sizeof(*out));
619 - out->fd = -1;
620 -
621 - int client_fd = accept(listener->fd, NULL, NULL);
622 - if (client_fd < 0)
623 - return NIPC_UDS_ERR_ACCEPT;
624 -
625 - nipc_uds_error_t err = server_handshake(client_fd, &listener->config,
626 - session_id, out);
627 - if (err != NIPC_UDS_OK) {
628 - close(client_fd);
629 - out->fd = -1;
630 - return err;
631 - }
632 -
633 - return NIPC_UDS_OK;
634 -}
635 -
636 -/* ------------------------------------------------------------------ */
637 -/* Public API: connect */
638 -/* ------------------------------------------------------------------ */
639 -
640 -nipc_uds_error_t nipc_uds_connect(const char *run_dir,
641 - const char *service_name,
642 - const nipc_uds_client_config_t *config,
643 - nipc_uds_session_t *out)
644 -{
645 - memset(out, 0, sizeof(*out));
646 - out->fd = -1;
647 -
648 - /* Build path */
649 - char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
650 - int path_rc2 = build_socket_path(path, sizeof(path), run_dir, service_name);
651 - if (path_rc2 == -2)
652 - return NIPC_UDS_ERR_BAD_PARAM;
653 - if (path_rc2 < 0)
654 - return NIPC_UDS_ERR_PATH_TOO_LONG;
655 -
656 - /* Create socket */
657 - int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
658 - if (fd < 0)
659 - return NIPC_UDS_ERR_SOCKET;
660 -
661 - struct sockaddr_un addr;
662 - memset(&addr, 0, sizeof(addr));
663 - addr.sun_family = AF_UNIX;
664 - strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
665 -
666 - if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
667 - close(fd);
668 - return NIPC_UDS_ERR_CONNECT;
669 - }
670 -
671 - nipc_uds_error_t err = client_handshake(fd, config, out);
672 - if (err != NIPC_UDS_OK) {
673 - close(fd);
674 - out->fd = -1;
675 - return err;
676 - }
677 -
678 - return NIPC_UDS_OK;
679 -}
680 -
681 -/* ------------------------------------------------------------------ */
682 -/* Public API: close */
683 -/* ------------------------------------------------------------------ */
684 -
685 -void nipc_uds_close_session(nipc_uds_session_t *session)
686 -{
687 - if (!session)
688 - return;
689 -
690 - if (session->fd >= 0) {
691 - close(session->fd);
692 - session->fd = -1;
693 - }
694 -
695 - free(session->recv_buf);
696 - session->recv_buf = NULL;
697 - session->recv_buf_size = 0;
698 -
699 - free(session->inflight_ids);
700 - session->inflight_ids = NULL;
701 - session->inflight_count = 0;
702 - session->inflight_capacity = 0;
703 -}
704 -
705 -void nipc_uds_close_listener(nipc_uds_listener_t *listener)
706 -{
707 - if (!listener)
708 - return;
709 -
710 - if (listener->fd >= 0) {
711 - close(listener->fd);
712 - listener->fd = -1;
713 - }
714 -
715 - if (listener->path[0]) {
716 - unlink(listener->path);
717 - listener->path[0] = '\0';
718 - }
719 -}
720 -
721 -/* ------------------------------------------------------------------ */
722 -/* In-flight message_id tracking (client side) */
723 -/* ------------------------------------------------------------------ */
724 -
725 -/* Add message_id to in-flight set. Returns 0 on success, -1 if duplicate,
726 - * -2 on allocation failure. */
727 -static int inflight_add(nipc_uds_session_t *s, uint64_t id)
728 -{
729 - for (uint32_t i = 0; i < s->inflight_count; i++) {
730 - if (s->inflight_ids[i] == id)
731 - return -1; /* duplicate */
732 - }
733 - if (s->inflight_count >= s->inflight_capacity) {
734 - uint32_t new_cap = s->inflight_capacity ? s->inflight_capacity * 2 : 16;
735 - uint64_t *new_ids = realloc(s->inflight_ids, (size_t)new_cap * sizeof(uint64_t));
736 - if (!new_ids)
737 - return -2; /* allocation failure */
738 - s->inflight_ids = new_ids;
739 - s->inflight_capacity = new_cap;
740 - }
741 - s->inflight_ids[s->inflight_count++] = id;
742 - return 0;
743 -}
744 -
745 -/* Remove message_id from in-flight set. Returns 0 on success, -1 if
746 - * not found. */
747 -static int inflight_remove(nipc_uds_session_t *s, uint64_t id)
748 -{
749 - for (uint32_t i = 0; i < s->inflight_count; i++) {
750 - if (s->inflight_ids[i] == id) {
751 - /* Swap with last */
752 - s->inflight_ids[i] = s->inflight_ids[s->inflight_count - 1];
753 - s->inflight_count--;
754 - return 0;
755 - }
756 - }
757 - return -1; /* not found */
758 -}
759 -
760 -static void inflight_fail_all(nipc_uds_session_t *s)
761 -{
762 - if (!s || s->role != NIPC_UDS_ROLE_CLIENT)
763 - return;
764 -
765 - /* A broken session invalidates every in-flight request on it. */
766 - s->inflight_count = 0;
767 -}
768 -
769 -/* ------------------------------------------------------------------ */
770 -/* Public API: send */
771 -/* ------------------------------------------------------------------ */
772 -
773 -nipc_uds_error_t nipc_uds_send(nipc_uds_session_t *session,
774 - nipc_header_t *hdr,
775 - const void *payload,
776 - size_t payload_len)
777 -{
778 - if (!session || session->fd < 0)
779 - return NIPC_UDS_ERR_BAD_PARAM;
780 -
781 - int tracked = (session->role == NIPC_UDS_ROLE_CLIENT &&
782 - hdr->kind == NIPC_KIND_REQUEST);
783 -
784 - /* Client-side: track in-flight message_ids for requests */
785 - if (tracked) {
786 - int rc = inflight_add(session, hdr->message_id);
787 - if (rc == -1)
788 - return NIPC_UDS_ERR_DUPLICATE_MSG_ID;
789 - if (rc == -2)
790 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
791 - }
792 -
793 - uint32_t max_payload = 0;
794 - uint32_t max_batch = 0;
795 - if (session->role == NIPC_UDS_ROLE_CLIENT &&
796 - hdr->kind == NIPC_KIND_REQUEST) {
797 - max_payload = session->max_request_payload_bytes;
798 - max_batch = session->max_request_batch_items;
799 - } else if (session->role == NIPC_UDS_ROLE_SERVER &&
800 - hdr->kind == NIPC_KIND_RESPONSE) {
801 - max_payload = session->max_response_payload_bytes;
802 - max_batch = session->max_response_batch_items;
803 - }
804 - if (payload_len > UINT32_MAX ||
805 - (max_payload > 0 && payload_len > max_payload) ||
806 - (max_batch > 0 && hdr->item_count > max_batch)) {
807 - if (tracked)
808 - inflight_remove(session, hdr->message_id);
809 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
810 - }
811 -
812 - /* Fill envelope fields the caller shouldn't set */
813 - hdr->magic = NIPC_MAGIC_MSG;
814 - hdr->version = NIPC_VERSION;
815 - hdr->header_len = NIPC_HEADER_LEN;
816 - hdr->payload_len = (uint32_t)payload_len;
817 -
818 - size_t total_msg;
819 - if (!header_payload_len(payload_len, &total_msg)) {
820 - if (tracked)
821 - inflight_remove(session, hdr->message_id);
822 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
823 - }
824 -
825 - /* Does it fit in one packet? */
826 - if (total_msg <= session->packet_size) {
827 - /* Single packet: encode header then send header+payload together */
828 - uint8_t hdr_buf[NIPC_HEADER_LEN];
829 - nipc_header_encode(hdr, hdr_buf, sizeof(hdr_buf));
830 - nipc_uds_error_t send_err = raw_send_iov(session->fd, hdr_buf, NIPC_HEADER_LEN,
831 - payload, payload_len);
832 - if (send_err != NIPC_UDS_OK) {
833 - if (session->role == NIPC_UDS_ROLE_CLIENT &&
834 - hdr->kind == NIPC_KIND_REQUEST) {
835 - if (send_err == NIPC_UDS_ERR_SEND)
836 - inflight_fail_all(session);
837 - else
838 - inflight_remove(session, hdr->message_id);
839 - }
840 - }
841 - return send_err;
842 - }
843 -
844 - /* Chunked send */
845 - size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
846 - if (chunk_payload_budget == 0)
847 - return NIPC_UDS_ERR_BAD_PARAM;
848 -
849 - /* Calculate chunk count:
850 - * First chunk: header(32) + up to chunk_payload_budget payload bytes.
851 - * But the first chunk carries the outer header, so payload in first
852 - * chunk = chunk_payload_budget. */
853 - size_t remaining = payload_len;
854 - size_t first_chunk_payload = remaining < chunk_payload_budget
855 - ? remaining : chunk_payload_budget;
856 -
857 - remaining -= first_chunk_payload;
858 - uint32_t continuation_chunks = 0;
859 - if (remaining > 0) {
860 - continuation_chunks = (uint32_t)((remaining + chunk_payload_budget - 1)
861 - / chunk_payload_budget);
862 - }
863 - uint32_t chunk_count = 1 + continuation_chunks;
864 -
865 - /* Send first chunk: outer header + first part of payload */
866 - uint8_t hdr_buf[NIPC_HEADER_LEN];
867 - nipc_header_encode(hdr, hdr_buf, sizeof(hdr_buf));
868 -
869 - nipc_uds_error_t err = raw_send_iov(session->fd, hdr_buf, NIPC_HEADER_LEN,
870 - payload, first_chunk_payload);
871 - if (err != NIPC_UDS_OK) {
872 - if (session->role == NIPC_UDS_ROLE_CLIENT &&
873 - hdr->kind == NIPC_KIND_REQUEST) {
874 - if (err == NIPC_UDS_ERR_SEND)
875 - inflight_fail_all(session);
876 - else
877 - inflight_remove(session, hdr->message_id);
878 - }
879 - return err;
880 - }
881 -
882 - /* Send continuation chunks */
883 - const uint8_t *src = (const uint8_t *)payload + first_chunk_payload;
884 - remaining = payload_len - first_chunk_payload;
885 -
886 - for (uint32_t ci = 1; ci < chunk_count; ci++) {
887 - size_t this_chunk = remaining < chunk_payload_budget
888 - ? remaining : chunk_payload_budget;
889 -
890 - nipc_chunk_header_t chk = {
891 - .magic = NIPC_MAGIC_CHUNK,
892 - .version = NIPC_VERSION,
893 - .flags = 0,
894 - .message_id = hdr->message_id,
895 - .total_message_len = (uint32_t)total_msg,
896 - .chunk_index = ci,
897 - .chunk_count = chunk_count,
898 - .chunk_payload_len = (uint32_t)this_chunk,
899 - };
900 -
901 - uint8_t chk_buf[NIPC_HEADER_LEN];
902 - nipc_chunk_header_encode(&chk, chk_buf, sizeof(chk_buf));
903 -
904 - err = raw_send_iov(session->fd, chk_buf, NIPC_HEADER_LEN,
905 - src, this_chunk);
906 - if (err != NIPC_UDS_OK) {
907 - if (session->role == NIPC_UDS_ROLE_CLIENT &&
908 - hdr->kind == NIPC_KIND_REQUEST) {
909 - if (err == NIPC_UDS_ERR_SEND)
910 - inflight_fail_all(session);
911 - else
912 - inflight_remove(session, hdr->message_id);
913 - }
914 - return err;
915 - }
916 -
917 - src += this_chunk;
918 - remaining -= this_chunk;
919 - }
920 -
921 - return NIPC_UDS_OK;
922 -}
923 -
924 -/* ------------------------------------------------------------------ */
925 -/* Public API: receive */
926 -/* ------------------------------------------------------------------ */
927 -
928 -/* Ensure session recv_buf can hold at least `needed` bytes. */
929 -static nipc_uds_error_t ensure_recv_buf(nipc_uds_session_t *session,
930 - size_t needed)
931 -{
932 - if (session->recv_buf_size >= needed)
933 - return NIPC_UDS_OK;
934 -
935 - uint8_t *p = realloc(session->recv_buf, needed);
936 - if (!p)
937 - return NIPC_UDS_ERR_ALLOC;
938 -
939 - session->recv_buf = p;
940 - session->recv_buf_size = needed;
941 - return NIPC_UDS_OK;
942 -}
943 -
944 -/* Validate batch directory if the message has BATCH flag and item_count > 1.
945 - * Called after the full payload is assembled, before returning to caller. */
946 -static nipc_uds_error_t validate_batch(const nipc_header_t *hdr,
947 - const void *payload, size_t payload_len)
948 -{
949 - if (!(hdr->flags & NIPC_FLAG_BATCH) || hdr->item_count <= 1)
950 - return NIPC_UDS_OK;
951 -
952 - uint32_t dir_bytes = hdr->item_count * 8;
953 - uint32_t dir_aligned = (uint32_t)nipc_align8(dir_bytes);
954 - if (payload_len < dir_aligned)
955 - return NIPC_UDS_ERR_PROTOCOL;
956 -
957 - uint32_t packed_area_len = (uint32_t)(payload_len - dir_aligned);
958 - nipc_error_t perr = nipc_batch_dir_validate(payload, dir_bytes,
959 - hdr->item_count,
960 - packed_area_len);
961 - return (perr == NIPC_OK) ? NIPC_UDS_OK : NIPC_UDS_ERR_PROTOCOL;
962 -}
963 -
964 -nipc_uds_error_t nipc_uds_receive(nipc_uds_session_t *session,
965 - void *buf, size_t buf_size,
966 - nipc_header_t *hdr_out,
967 - const void **payload_out,
968 - size_t *payload_len_out)
969 -{
970 - if (!session || session->fd < 0)
971 - return NIPC_UDS_ERR_BAD_PARAM;
972 -
973 - /* Read first packet into the caller's buffer */
974 - ssize_t n = raw_recv(session->fd, buf, buf_size);
975 - if (n <= 0) {
976 - inflight_fail_all(session);
977 - return NIPC_UDS_ERR_RECV;
978 - }
979 -
980 - if ((size_t)n < NIPC_HEADER_LEN)
981 - return NIPC_UDS_ERR_PROTOCOL;
982 -
983 - /* Decode outer header */
984 - nipc_error_t perr = nipc_header_decode(buf, (size_t)n, hdr_out);
985 - if (perr != NIPC_OK)
986 - return NIPC_UDS_ERR_PROTOCOL;
987 -
988 - /* Validate payload_len against negotiated directional limit.
989 - * Server receives requests; client receives responses. */
990 - uint32_t max_payload = (session->role == NIPC_UDS_ROLE_SERVER)
991 - ? session->max_request_payload_bytes
992 - : session->max_response_payload_bytes;
993 - if (hdr_out->payload_len > max_payload)
994 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
995 -
996 - /* Validate item_count against negotiated directional batch limit. */
997 - uint32_t max_batch = (session->role == NIPC_UDS_ROLE_SERVER)
998 - ? session->max_request_batch_items
999 - : session->max_response_batch_items;
1000 - if (hdr_out->item_count > max_batch)
1001 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
1002 -
1003 - /* Client-side: validate response message_id is in-flight */
1004 - if (session->role == NIPC_UDS_ROLE_CLIENT &&
1005 - hdr_out->kind == NIPC_KIND_RESPONSE) {
1006 - if (inflight_remove(session, hdr_out->message_id) < 0)
1007 - return NIPC_UDS_ERR_UNKNOWN_MSG_ID;
1008 - }
1009 -
1010 - size_t total_msg;
1011 - if (!header_payload_len(hdr_out->payload_len, &total_msg))
1012 - return NIPC_UDS_ERR_LIMIT_EXCEEDED;
1013 -
1014 - /* Non-chunked: entire message arrived in one packet */
1015 - if ((size_t)n >= total_msg) {
1016 - *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
1017 - *payload_len_out = hdr_out->payload_len;
1018 -
1019 - nipc_uds_error_t berr = validate_batch(hdr_out, *payload_out, *payload_len_out);
1020 - if (berr != NIPC_UDS_OK)
1021 - return berr;
1022 -
1023 - return NIPC_UDS_OK;
1024 - }
1025 -
1026 - /* Chunked: first packet has partial payload. The total message
1027 - * size is NIPC_HEADER_LEN + payload_len from the header. */
1028 - size_t first_payload_bytes = (size_t)n - NIPC_HEADER_LEN;
1029 -
1030 - /* We need a buffer for the full payload. Use the session recv_buf. */
1031 - nipc_uds_error_t err = ensure_recv_buf(session, hdr_out->payload_len);
1032 - if (err != NIPC_UDS_OK)
1033 - return err;
1034 -
1035 - /* Copy first chunk's payload into recv_buf */
1036 - memcpy(session->recv_buf, (uint8_t *)buf + NIPC_HEADER_LEN,
1037 - first_payload_bytes);
1038 -
1039 - size_t assembled = first_payload_bytes;
1040 - size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
1041 -
1042 - /* Calculate expected chunk count */
1043 - size_t remaining_after_first = hdr_out->payload_len - first_payload_bytes;
1044 - uint32_t expected_continuations = 0;
1045 - if (remaining_after_first > 0 && chunk_payload_budget > 0) {
1046 - expected_continuations = (uint32_t)((remaining_after_first +
1047 - chunk_payload_budget - 1)
1048 - / chunk_payload_budget);
1049 - }
1050 - uint32_t expected_chunk_count = 1 + expected_continuations;
1051 -
1052 - /* A temporary buffer for reading continuation packets */
1053 - size_t pkt_buf_size = session->packet_size;
1054 - uint8_t *pkt_buf = malloc(pkt_buf_size);
1055 - if (!pkt_buf)
1056 - return NIPC_UDS_ERR_ALLOC;
1057 -
1058 - for (uint32_t ci = 1; assembled < hdr_out->payload_len; ci++) {
1059 - ssize_t cn = raw_recv(session->fd, pkt_buf, pkt_buf_size);
1060 - if (cn <= 0) {
1061 - free(pkt_buf);
1062 - inflight_fail_all(session);
1063 - return NIPC_UDS_ERR_RECV;
1064 - }
1065 -
1066 - if ((size_t)cn < NIPC_HEADER_LEN) {
1067 - free(pkt_buf);
1068 - return NIPC_UDS_ERR_CHUNK;
1069 - }
1070 -
1071 - nipc_chunk_header_t chk;
1072 - perr = nipc_chunk_header_decode(pkt_buf, (size_t)cn, &chk);
1073 - if (perr != NIPC_OK) {
1074 - free(pkt_buf);
1075 - return NIPC_UDS_ERR_CHUNK;
1076 - }
1077 -
1078 - /* Validate chunk header */
1079 - if (chk.message_id != hdr_out->message_id ||
1080 - chk.chunk_index != ci ||
1081 - chk.chunk_count != expected_chunk_count ||
1082 - chk.total_message_len != (uint32_t)total_msg) {
1083 - free(pkt_buf);
1084 - return NIPC_UDS_ERR_CHUNK;
1085 - }
1086 -
1087 - size_t chunk_data = (size_t)cn - NIPC_HEADER_LEN;
1088 - if (chunk_data != chk.chunk_payload_len) {
1089 - free(pkt_buf);
1090 - return NIPC_UDS_ERR_CHUNK;
1091 - }
1092 -
1093 - if (assembled + chunk_data > hdr_out->payload_len) {
1094 - free(pkt_buf);
1095 - return NIPC_UDS_ERR_CHUNK;
1096 - }
1097 -
1098 - memcpy(session->recv_buf + assembled,
1099 - pkt_buf + NIPC_HEADER_LEN, chunk_data);
1100 - assembled += chunk_data;
1101 - }
1102 -
1103 - free(pkt_buf);
1104 -
1105 - *payload_out = session->recv_buf;
1106 - *payload_len_out = hdr_out->payload_len;
1107 -
1108 - nipc_uds_error_t berr = validate_batch(hdr_out, *payload_out, *payload_len_out);
1109 - if (berr != NIPC_UDS_OK)
1110 - return berr;
1111 -
1112 - return NIPC_UDS_OK;
76 + return recv(fd, buf, buf_len, 0);
77 }
src/libnetdata/netipc/src/transport/posix/netipc_uds_handshake.c new
+319
@@ -0,0 +1,319 @@
1 +#include "netipc_uds_internal.h"
2 +
3 +#include <string.h>
4 +
5 +static inline uint32_t min_u32(uint32_t a, uint32_t b)
6 +{
7 + return a < b ? a : b;
8 +}
9 +
10 +static inline uint32_t apply_default(uint32_t val, uint32_t def)
11 +{
12 + return val == 0 ? def : val;
13 +}
14 +
15 +static uint32_t highest_bit(uint32_t mask)
16 +{
17 + if (mask == 0)
18 + return 0;
19 +
20 + uint32_t bit = 1u << 31;
21 + while (!(mask & bit))
22 + bit >>= 1;
23 + return bit;
24 +}
25 +
26 +static bool header_version_incompatible(const void *buf, size_t buf_len,
27 + uint16_t expected_code)
28 +{
29 + if (buf_len < NIPC_HEADER_LEN)
30 + return false;
31 +
32 + nipc_header_t hdr;
33 + memcpy(&hdr, buf, sizeof(hdr));
34 + return hdr.magic == NIPC_MAGIC_MSG &&
35 + hdr.version != NIPC_VERSION &&
36 + hdr.header_len == NIPC_HEADER_LEN &&
37 + hdr.kind == NIPC_KIND_CONTROL &&
38 + hdr.code == expected_code;
39 +}
40 +
41 +static bool hello_layout_incompatible(const void *buf, size_t buf_len)
42 +{
43 + if (buf_len < sizeof(uint16_t))
44 + return false;
45 +
46 + nipc_hello_t hello;
47 + memset(&hello, 0, sizeof(hello));
48 + memcpy(&hello, buf, sizeof(uint16_t));
49 + return hello.layout_version != 1;
50 +}
51 +
52 +static bool hello_ack_layout_incompatible(const void *buf, size_t buf_len)
53 +{
54 + if (buf_len < sizeof(uint16_t))
55 + return false;
56 +
57 + nipc_hello_ack_t ack;
58 + memset(&ack, 0, sizeof(ack));
59 + memcpy(&ack, buf, sizeof(uint16_t));
60 + return ack.layout_version != 1;
61 +}
62 +
63 +static void encode_control_header(nipc_header_t *hdr, uint16_t code,
64 + uint16_t status, uint32_t payload_len)
65 +{
66 + *hdr = (nipc_header_t){
67 + .magic = NIPC_MAGIC_MSG,
68 + .version = NIPC_VERSION,
69 + .header_len = NIPC_HEADER_LEN,
70 + .kind = NIPC_KIND_CONTROL,
71 + .code = code,
72 + .transport_status = status,
73 + .payload_len = payload_len,
74 + .item_count = 1,
75 + };
76 +}
77 +
78 +static void send_rejection_ack(int fd, uint16_t status)
79 +{
80 + nipc_hello_ack_t ack = { .layout_version = 1 };
81 + uint8_t ack_buf[48];
82 + uint8_t pkt[80];
83 + nipc_header_t ack_hdr;
84 +
85 + encode_control_header(&ack_hdr, NIPC_CODE_HELLO_ACK, status,
86 + sizeof(ack_buf));
87 + nipc_hello_ack_encode(&ack, ack_buf, sizeof(ack_buf));
88 + nipc_header_encode(&ack_hdr, pkt, sizeof(pkt));
89 + memcpy(pkt + NIPC_HEADER_LEN, ack_buf, sizeof(ack_buf));
90 + nipc_uds_raw_send(fd, pkt, NIPC_HEADER_LEN + sizeof(ack_buf));
91 +}
92 +
93 +static nipc_uds_error_t ack_status_to_error(uint16_t status)
94 +{
95 + if (status == NIPC_STATUS_OK)
96 + return NIPC_UDS_OK;
97 + if (status == NIPC_STATUS_AUTH_FAILED)
98 + return NIPC_UDS_ERR_AUTH_FAILED;
99 + if (status == NIPC_STATUS_UNSUPPORTED)
100 + return NIPC_UDS_ERR_NO_PROFILE;
101 + if (status == NIPC_STATUS_INCOMPATIBLE)
102 + return NIPC_UDS_ERR_INCOMPATIBLE;
103 + if (status == NIPC_STATUS_LIMIT_EXCEEDED)
104 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
105 + return NIPC_UDS_ERR_HANDSHAKE;
106 +}
107 +
108 +static void fill_client_session(nipc_uds_session_t *session, int fd,
109 + const nipc_hello_ack_t *ack)
110 +{
111 + session->fd = fd;
112 + session->role = NIPC_UDS_ROLE_CLIENT;
113 + session->max_request_payload_bytes = ack->agreed_max_request_payload_bytes;
114 + session->max_request_batch_items = ack->agreed_max_request_batch_items;
115 + session->max_response_payload_bytes = ack->agreed_max_response_payload_bytes;
116 + session->max_response_batch_items = ack->agreed_max_response_batch_items;
117 + session->packet_size = ack->agreed_packet_size;
118 + session->selected_profile = ack->selected_profile;
119 + session->session_id = ack->session_id;
120 + session->recv_buf = NULL;
121 + session->recv_buf_size = 0;
122 +}
123 +
124 +static void fill_server_session(nipc_uds_session_t *session, int fd,
125 + uint32_t selected,
126 + const nipc_hello_ack_t *ack)
127 +{
128 + session->fd = fd;
129 + session->role = NIPC_UDS_ROLE_SERVER;
130 + session->max_request_payload_bytes = ack->agreed_max_request_payload_bytes;
131 + session->max_request_batch_items = ack->agreed_max_request_batch_items;
132 + session->max_response_payload_bytes = ack->agreed_max_response_payload_bytes;
133 + session->max_response_batch_items = ack->agreed_max_response_batch_items;
134 + session->packet_size = ack->agreed_packet_size;
135 + session->selected_profile = selected;
136 + session->session_id = ack->session_id;
137 + session->recv_buf = NULL;
138 + session->recv_buf_size = 0;
139 +}
140 +
141 +nipc_uds_error_t nipc_uds_client_handshake(int fd,
142 + const nipc_uds_client_config_t *cfg,
143 + nipc_uds_session_t *session)
144 +{
145 + uint8_t buf[128];
146 + uint32_t pkt_size = cfg->packet_size;
147 + if (pkt_size == 0)
148 + pkt_size = nipc_uds_detect_packet_size(fd);
149 +
150 + nipc_hello_t hello = {
151 + .layout_version = 1,
152 + .supported_profiles = cfg->supported_profiles ? cfg->supported_profiles : NIPC_PROFILE_BASELINE,
153 + .preferred_profiles = cfg->preferred_profiles,
154 + .max_request_payload_bytes = apply_default(cfg->max_request_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT),
155 + .max_request_batch_items = apply_default(cfg->max_request_batch_items, UDS_DEFAULT_BATCH_ITEMS),
156 + .max_response_payload_bytes = apply_default(cfg->max_response_payload_bytes, NIPC_MAX_PAYLOAD_DEFAULT),
157 + .max_response_batch_items = apply_default(cfg->max_response_batch_items, UDS_DEFAULT_BATCH_ITEMS),
158 + .auth_token = cfg->auth_token,
159 + .packet_size = pkt_size,
160 + };
161 +
162 + uint8_t hello_buf[44];
163 + nipc_hello_encode(&hello, hello_buf, sizeof(hello_buf));
164 +
165 + nipc_header_t hdr;
166 + encode_control_header(&hdr, NIPC_CODE_HELLO, NIPC_STATUS_OK,
167 + sizeof(hello_buf));
168 + nipc_header_encode(&hdr, buf, sizeof(buf));
169 + memcpy(buf + NIPC_HEADER_LEN, hello_buf, sizeof(hello_buf));
170 +
171 + nipc_uds_error_t err = nipc_uds_raw_send(
172 + fd, buf, NIPC_HEADER_LEN + sizeof(hello_buf));
173 + if (err != NIPC_UDS_OK)
174 + return err;
175 +
176 + ssize_t n = nipc_uds_raw_recv(fd, buf, sizeof(buf));
177 + if (n <= 0)
178 + return NIPC_UDS_ERR_RECV;
179 +
180 + nipc_header_t ack_hdr;
181 + nipc_error_t perr = nipc_header_decode(buf, (size_t)n, &ack_hdr);
182 + if (perr == NIPC_ERR_BAD_VERSION)
183 + return NIPC_UDS_ERR_INCOMPATIBLE;
184 + if (perr != NIPC_OK)
185 + return NIPC_UDS_ERR_PROTOCOL;
186 +
187 + if (ack_hdr.kind != NIPC_KIND_CONTROL || ack_hdr.code != NIPC_CODE_HELLO_ACK)
188 + return NIPC_UDS_ERR_PROTOCOL;
189 +
190 + err = ack_status_to_error(ack_hdr.transport_status);
191 + if (err != NIPC_UDS_OK)
192 + return err;
193 +
194 + nipc_hello_ack_t ack;
195 + perr = nipc_hello_ack_decode(buf + NIPC_HEADER_LEN,
196 + (size_t)n - NIPC_HEADER_LEN, &ack);
197 + if (perr == NIPC_ERR_BAD_LAYOUT &&
198 + hello_ack_layout_incompatible(buf + NIPC_HEADER_LEN,
199 + (size_t)n - NIPC_HEADER_LEN))
200 + return NIPC_UDS_ERR_INCOMPATIBLE;
201 + if (perr != NIPC_OK)
202 + return NIPC_UDS_ERR_PROTOCOL;
203 +
204 + fill_client_session(session, fd, &ack);
205 + if (session->packet_size <= NIPC_HEADER_LEN)
206 + return NIPC_UDS_ERR_PROTOCOL;
207 +
208 + return NIPC_UDS_OK;
209 +}
210 +
211 +nipc_uds_error_t nipc_uds_server_handshake(int fd,
212 + const nipc_uds_server_config_t *cfg,
213 + uint64_t session_id,
214 + nipc_uds_session_t *session)
215 +{
216 + uint8_t buf[128];
217 + uint32_t server_pkt_size = cfg->packet_size;
218 + if (server_pkt_size == 0)
219 + server_pkt_size = nipc_uds_detect_packet_size(fd);
220 +
221 + uint32_t s_resp_pay = apply_default(cfg->max_response_payload_bytes,
222 + NIPC_MAX_PAYLOAD_DEFAULT);
223 + uint32_t s_profiles = cfg->supported_profiles ? cfg->supported_profiles : NIPC_PROFILE_BASELINE;
224 + uint32_t s_preferred = cfg->preferred_profiles;
225 +
226 + ssize_t n = nipc_uds_raw_recv(fd, buf, sizeof(buf));
227 + if (n <= 0)
228 + return NIPC_UDS_ERR_RECV;
229 +
230 + nipc_header_t hdr;
231 + nipc_error_t perr = nipc_header_decode(buf, (size_t)n, &hdr);
232 + if (perr == NIPC_ERR_BAD_VERSION &&
233 + header_version_incompatible(buf, (size_t)n, NIPC_CODE_HELLO)) {
234 + send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
235 + return NIPC_UDS_ERR_INCOMPATIBLE;
236 + }
237 + if (perr != NIPC_OK)
238 + return NIPC_UDS_ERR_PROTOCOL;
239 +
240 + if (hdr.kind != NIPC_KIND_CONTROL || hdr.code != NIPC_CODE_HELLO)
241 + return NIPC_UDS_ERR_PROTOCOL;
242 +
243 + nipc_hello_t hello;
244 + perr = nipc_hello_decode(buf + NIPC_HEADER_LEN,
245 + (size_t)n - NIPC_HEADER_LEN, &hello);
246 + if (perr == NIPC_ERR_BAD_LAYOUT &&
247 + hello_layout_incompatible(buf + NIPC_HEADER_LEN,
248 + (size_t)n - NIPC_HEADER_LEN)) {
249 + send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
250 + return NIPC_UDS_ERR_INCOMPATIBLE;
251 + }
252 + if (perr != NIPC_OK)
253 + return NIPC_UDS_ERR_PROTOCOL;
254 +
255 + uint32_t intersection = hello.supported_profiles & s_profiles;
256 + if (intersection == 0) {
257 + send_rejection_ack(fd, NIPC_STATUS_UNSUPPORTED);
258 + return NIPC_UDS_ERR_NO_PROFILE;
259 + }
260 +
261 + if (hello.auth_token != cfg->auth_token) {
262 + send_rejection_ack(fd, NIPC_STATUS_AUTH_FAILED);
263 + return NIPC_UDS_ERR_AUTH_FAILED;
264 + }
265 +
266 + uint32_t preferred_intersection = intersection &
267 + hello.preferred_profiles & s_preferred;
268 + uint32_t selected = preferred_intersection != 0
269 + ? highest_bit(preferred_intersection)
270 + : highest_bit(intersection);
271 +
272 + if (hello.max_request_payload_bytes > NIPC_MAX_PAYLOAD_CAP) {
273 + send_rejection_ack(fd, NIPC_STATUS_LIMIT_EXCEEDED);
274 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
275 + }
276 +
277 + uint32_t agreed_req_pay = hello.max_request_payload_bytes;
278 + uint32_t agreed_req_bat = hello.max_request_batch_items;
279 + uint32_t agreed_resp_pay = s_resp_pay;
280 + uint32_t agreed_resp_bat = agreed_req_bat;
281 + uint32_t agreed_pkt = min_u32(hello.packet_size, server_pkt_size);
282 +
283 + if (agreed_pkt <= NIPC_HEADER_LEN) {
284 + send_rejection_ack(fd, NIPC_STATUS_INCOMPATIBLE);
285 + return NIPC_UDS_ERR_INCOMPATIBLE;
286 + }
287 +
288 + nipc_hello_ack_t ack = {
289 + .layout_version = 1,
290 + .server_supported_profiles = s_profiles,
291 + .intersection_profiles = intersection,
292 + .selected_profile = selected,
293 + .agreed_max_request_payload_bytes = agreed_req_pay,
294 + .agreed_max_request_batch_items = agreed_req_bat,
295 + .agreed_max_response_payload_bytes = agreed_resp_pay,
296 + .agreed_max_response_batch_items = agreed_resp_bat,
297 + .agreed_packet_size = agreed_pkt,
298 + .session_id = session_id,
299 + };
300 +
301 + uint8_t ack_buf[48];
302 + nipc_hello_ack_encode(&ack, ack_buf, sizeof(ack_buf));
303 +
304 + nipc_header_t ack_hdr;
305 + encode_control_header(&ack_hdr, NIPC_CODE_HELLO_ACK, NIPC_STATUS_OK,
306 + sizeof(ack_buf));
307 +
308 + uint8_t pkt[80];
309 + nipc_header_encode(&ack_hdr, pkt, sizeof(pkt));
310 + memcpy(pkt + NIPC_HEADER_LEN, ack_buf, sizeof(ack_buf));
311 +
312 + nipc_uds_error_t send_ack_err = nipc_uds_raw_send(
313 + fd, pkt, NIPC_HEADER_LEN + sizeof(ack_buf));
314 + if (send_ack_err != NIPC_UDS_OK)
315 + return send_ack_err;
316 +
317 + fill_server_session(session, fd, selected, &ack);
318 + return NIPC_UDS_OK;
319 +}
src/libnetdata/netipc/src/transport/posix/netipc_uds_inflight.c new
+44
@@ -0,0 +1,44 @@
1 +#include "netipc_uds_internal.h"
2 +
3 +#include <stdlib.h>
4 +
5 +int nipc_uds_inflight_add(nipc_uds_session_t *s, uint64_t id)
6 +{
7 + for (uint32_t i = 0; i < s->inflight_count; i++) {
8 + if (s->inflight_ids[i] == id)
9 + return -1;
10 + }
11 +
12 + if (s->inflight_count >= s->inflight_capacity) {
13 + uint32_t new_cap = s->inflight_capacity ? s->inflight_capacity * 2 : 16;
14 + uint64_t *new_ids = realloc(s->inflight_ids,
15 + (size_t)new_cap * sizeof(uint64_t));
16 + if (!new_ids)
17 + return -2;
18 + s->inflight_ids = new_ids;
19 + s->inflight_capacity = new_cap;
20 + }
21 +
22 + s->inflight_ids[s->inflight_count++] = id;
23 + return 0;
24 +}
25 +
26 +int nipc_uds_inflight_remove(nipc_uds_session_t *s, uint64_t id)
27 +{
28 + for (uint32_t i = 0; i < s->inflight_count; i++) {
29 + if (s->inflight_ids[i] == id) {
30 + s->inflight_ids[i] = s->inflight_ids[s->inflight_count - 1];
31 + s->inflight_count--;
32 + return 0;
33 + }
34 + }
35 + return -1;
36 +}
37 +
38 +void nipc_uds_inflight_fail_all(nipc_uds_session_t *s)
39 +{
40 + if (!s || s->role != NIPC_UDS_ROLE_CLIENT)
41 + return;
42 +
43 + s->inflight_count = 0;
44 +}
src/libnetdata/netipc/src/transport/posix/netipc_uds_internal.h new
+46
@@ -0,0 +1,46 @@
1 +#ifndef NETIPC_UDS_INTERNAL_H
2 +#define NETIPC_UDS_INTERNAL_H
3 +
4 +#include "netipc/netipc_protocol.h"
5 +#include "netipc/netipc_uds.h"
6 +
7 +#include <stdbool.h>
8 +#include <stddef.h>
9 +#include <stdint.h>
10 +#include <sys/types.h>
11 +
12 +#define UDS_DEFAULT_BACKLOG 16
13 +#define UDS_DEFAULT_BATCH_ITEMS 1
14 +
15 +bool nipc_uds_header_payload_len(size_t payload_len, size_t *msg_len_out);
16 +uint32_t nipc_uds_detect_packet_size(int fd);
17 +
18 +nipc_uds_error_t nipc_uds_raw_send(int fd, const void *data, size_t len);
19 +nipc_uds_error_t nipc_uds_raw_send_iov(int fd, const void *hdr, size_t hdr_len,
20 + const void *payload, size_t payload_len);
21 +ssize_t nipc_uds_raw_recv(int fd, void *buf, size_t buf_len);
22 +
23 +int nipc_uds_build_socket_name(char *dst, size_t dst_len,
24 + const char *service_name);
25 +int nipc_uds_build_socket_path(char *dst, size_t dst_len,
26 + const char *run_dir,
27 + const char *service_name);
28 +bool nipc_uds_run_dir_allows_stale_unlink(const char *run_dir);
29 +int nipc_uds_check_and_recover_stale(const char *run_dir,
30 + const char *socket_name,
31 + const char *path,
32 + bool allow_stale_unlink);
33 +
34 +nipc_uds_error_t nipc_uds_client_handshake(int fd,
35 + const nipc_uds_client_config_t *cfg,
36 + nipc_uds_session_t *session);
37 +nipc_uds_error_t nipc_uds_server_handshake(int fd,
38 + const nipc_uds_server_config_t *cfg,
39 + uint64_t session_id,
40 + nipc_uds_session_t *session);
41 +
42 +int nipc_uds_inflight_add(nipc_uds_session_t *s, uint64_t id);
43 +int nipc_uds_inflight_remove(nipc_uds_session_t *s, uint64_t id);
44 +void nipc_uds_inflight_fail_all(nipc_uds_session_t *s);
45 +
46 +#endif /* NETIPC_UDS_INTERNAL_H */
src/libnetdata/netipc/src/transport/posix/netipc_uds_lifecycle.c new
+329
@@ -0,0 +1,329 @@
1 +#include "netipc_uds_internal.h"
2 +
3 +#include <dirent.h>
4 +#include <errno.h>
5 +#include <fcntl.h>
6 +#include <stdio.h>
7 +#include <stdlib.h>
8 +#include <string.h>
9 +#include <unistd.h>
10 +
11 +#include <sys/socket.h>
12 +#include <sys/stat.h>
13 +#include <sys/un.h>
14 +
15 +static int copy_cstr_checked(char *dst, size_t dst_size, const char *src)
16 +{
17 + if (!dst || !src || dst_size == 0)
18 + return -1;
19 +
20 + size_t len = 0;
21 + while (len < dst_size && src[len] != '\0')
22 + len++;
23 + if (len == dst_size)
24 + return -1;
25 +
26 + memcpy(dst, src, len + 1);
27 + return 0;
28 +}
29 +
30 +static int fill_sockaddr_path(struct sockaddr_un *addr, const char *path)
31 +{
32 + memset(addr, 0, sizeof(*addr));
33 + addr->sun_family = AF_UNIX;
34 + return copy_cstr_checked(addr->sun_path, sizeof(addr->sun_path), path);
35 +}
36 +
37 +static int validate_service_name(const char *name)
38 +{
39 + if (!name || name[0] == '\0')
40 + return -1;
41 +
42 + if (name[0] == '.' && (name[1] == '\0' ||
43 + (name[1] == '.' && name[2] == '\0')))
44 + return -1;
45 +
46 + for (const char *p = name; *p; p++) {
47 + char c = *p;
48 + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
49 + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')
50 + continue;
51 + return -1;
52 + }
53 + return 0;
54 +}
55 +
56 +int nipc_uds_build_socket_name(char *dst, size_t dst_len,
57 + const char *service_name)
58 +{
59 + if (validate_service_name(service_name) < 0)
60 + return -2;
61 +
62 + int n = snprintf(dst, dst_len, "%s.sock", service_name);
63 + if (n < 0 || (size_t)n >= dst_len)
64 + return -1;
65 + return 0;
66 +}
67 +
68 +int nipc_uds_build_socket_path(char *dst, size_t dst_len,
69 + const char *run_dir,
70 + const char *service_name)
71 +{
72 + char socket_name[sizeof(((struct sockaddr_un *)0)->sun_path)];
73 + int name_rc = nipc_uds_build_socket_name(socket_name, sizeof(socket_name),
74 + service_name);
75 + if (name_rc < 0)
76 + return name_rc;
77 +
78 + int n = snprintf(dst, dst_len, "%s/%s", run_dir, socket_name);
79 + if (n < 0 || (size_t)n >= dst_len)
80 + return -1;
81 + return 0;
82 +}
83 +
84 +bool nipc_uds_run_dir_allows_stale_unlink(const char *run_dir)
85 +{
86 + struct stat st;
87 + if (stat(run_dir, &st) != 0)
88 + return false;
89 + if (!S_ISDIR(st.st_mode))
90 + return false;
91 + if (st.st_uid != geteuid())
92 + return false;
93 + return (st.st_mode & (S_IWGRP | S_IWOTH)) == 0;
94 +}
95 +
96 +static int unlink_stale_socket_path(const char *run_dir,
97 + const char *socket_name,
98 + bool allow_stale_unlink)
99 +{
100 + if (!allow_stale_unlink)
101 + return -1;
102 +
103 + DIR *dir = opendir(run_dir);
104 + if (!dir)
105 + return -1;
106 +
107 + int dir_fd = dirfd(dir);
108 + if (dir_fd < 0) {
109 + closedir(dir);
110 + return -1;
111 + }
112 +
113 + struct stat st;
114 + if (fstatat(dir_fd, socket_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
115 + int ret = (errno == ENOENT) ? 0 : -1;
116 + closedir(dir);
117 + return ret;
118 + }
119 +
120 + if (!S_ISSOCK(st.st_mode)) {
121 + closedir(dir);
122 + return -1;
123 + }
124 +
125 + int ret = -1;
126 + if (unlinkat(dir_fd, socket_name, 0) == 0 || errno == ENOENT)
127 + ret = 0;
128 +
129 + closedir(dir);
130 + return ret;
131 +}
132 +
133 +int nipc_uds_check_and_recover_stale(const char *run_dir,
134 + const char *socket_name,
135 + const char *path,
136 + bool allow_stale_unlink)
137 +{
138 + int probe = socket(AF_UNIX, SOCK_SEQPACKET, 0);
139 + if (probe < 0)
140 + return -1;
141 +
142 + struct sockaddr_un addr;
143 + if (fill_sockaddr_path(&addr, path) != 0) {
144 + close(probe);
145 + return -1;
146 + }
147 +
148 + int ret;
149 + if (connect(probe, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
150 + close(probe);
151 + ret = 1;
152 + } else {
153 + int saved_errno = errno;
154 + close(probe);
155 + if (saved_errno == ENOENT) {
156 + ret = -1;
157 + } else if (saved_errno == ECONNREFUSED) {
158 + ret = (unlink_stale_socket_path(run_dir, socket_name,
159 + allow_stale_unlink) == 0) ? 0 : 1;
160 + } else {
161 + ret = 1;
162 + }
163 + }
164 + return ret;
165 +}
166 +
167 +nipc_uds_error_t nipc_uds_listen(const char *run_dir,
168 + const char *service_name,
169 + const nipc_uds_server_config_t *config,
170 + nipc_uds_listener_t *out)
171 +{
172 + memset(out, 0, sizeof(*out));
173 + out->fd = -1;
174 +
175 + char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
176 + int path_rc = nipc_uds_build_socket_path(path, sizeof(path), run_dir,
177 + service_name);
178 + if (path_rc == -2)
179 + return NIPC_UDS_ERR_BAD_PARAM;
180 + if (path_rc < 0)
181 + return NIPC_UDS_ERR_PATH_TOO_LONG;
182 +
183 + char socket_name[sizeof(((struct sockaddr_un *)0)->sun_path)];
184 + int name_rc = nipc_uds_build_socket_name(socket_name, sizeof(socket_name),
185 + service_name);
186 + if (name_rc == -2)
187 + return NIPC_UDS_ERR_BAD_PARAM;
188 + if (name_rc < 0)
189 + return NIPC_UDS_ERR_PATH_TOO_LONG;
190 +
191 + bool allow_stale_unlink = nipc_uds_run_dir_allows_stale_unlink(run_dir);
192 + int stale = nipc_uds_check_and_recover_stale(run_dir, socket_name, path,
193 + allow_stale_unlink);
194 + if (stale == 1)
195 + return NIPC_UDS_ERR_ADDR_IN_USE;
196 +
197 + int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
198 + if (fd < 0)
199 + return NIPC_UDS_ERR_SOCKET;
200 +
201 + struct sockaddr_un addr;
202 + if (fill_sockaddr_path(&addr, path) != 0) {
203 + close(fd);
204 + return NIPC_UDS_ERR_PATH_TOO_LONG;
205 + }
206 +
207 + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
208 + close(fd);
209 + return NIPC_UDS_ERR_SOCKET;
210 + }
211 +
212 + int backlog = config->backlog > 0 ? config->backlog : UDS_DEFAULT_BACKLOG;
213 + if (listen(fd, backlog) < 0) {
214 + close(fd);
215 + unlink(path);
216 + return NIPC_UDS_ERR_SOCKET;
217 + }
218 +
219 + out->fd = fd;
220 + out->config = *config;
221 + if (copy_cstr_checked(out->path, sizeof(out->path), path) != 0) {
222 + close(fd);
223 + unlink(path);
224 + memset(out, 0, sizeof(*out));
225 + out->fd = -1;
226 + return NIPC_UDS_ERR_PATH_TOO_LONG;
227 + }
228 +
229 + return NIPC_UDS_OK;
230 +}
231 +
232 +nipc_uds_error_t nipc_uds_accept(nipc_uds_listener_t *listener,
233 + uint64_t session_id,
234 + nipc_uds_session_t *out)
235 +{
236 + memset(out, 0, sizeof(*out));
237 + out->fd = -1;
238 +
239 + int client_fd = accept(listener->fd, NULL, NULL);
240 + if (client_fd < 0)
241 + return NIPC_UDS_ERR_ACCEPT;
242 +
243 + nipc_uds_error_t err = nipc_uds_server_handshake(
244 + client_fd, &listener->config, session_id, out);
245 + if (err != NIPC_UDS_OK) {
246 + close(client_fd);
247 + out->fd = -1;
248 + return err;
249 + }
250 +
251 + return NIPC_UDS_OK;
252 +}
253 +
254 +nipc_uds_error_t nipc_uds_connect(const char *run_dir,
255 + const char *service_name,
256 + const nipc_uds_client_config_t *config,
257 + nipc_uds_session_t *out)
258 +{
259 + memset(out, 0, sizeof(*out));
260 + out->fd = -1;
261 +
262 + char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
263 + int path_rc = nipc_uds_build_socket_path(path, sizeof(path), run_dir,
264 + service_name);
265 + if (path_rc == -2)
266 + return NIPC_UDS_ERR_BAD_PARAM;
267 + if (path_rc < 0)
268 + return NIPC_UDS_ERR_PATH_TOO_LONG;
269 +
270 + int fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
271 + if (fd < 0)
272 + return NIPC_UDS_ERR_SOCKET;
273 +
274 + struct sockaddr_un addr;
275 + if (fill_sockaddr_path(&addr, path) != 0) {
276 + close(fd);
277 + return NIPC_UDS_ERR_PATH_TOO_LONG;
278 + }
279 +
280 + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
281 + close(fd);
282 + return NIPC_UDS_ERR_CONNECT;
283 + }
284 +
285 + nipc_uds_error_t err = nipc_uds_client_handshake(fd, config, out);
286 + if (err != NIPC_UDS_OK) {
287 + close(fd);
288 + out->fd = -1;
289 + return err;
290 + }
291 +
292 + return NIPC_UDS_OK;
293 +}
294 +
295 +void nipc_uds_close_session(nipc_uds_session_t *session)
296 +{
297 + if (!session)
298 + return;
299 +
300 + if (session->fd >= 0) {
301 + close(session->fd);
302 + session->fd = -1;
303 + }
304 +
305 + free(session->recv_buf);
306 + session->recv_buf = NULL;
307 + session->recv_buf_size = 0;
308 +
309 + free(session->inflight_ids);
310 + session->inflight_ids = NULL;
311 + session->inflight_count = 0;
312 + session->inflight_capacity = 0;
313 +}
314 +
315 +void nipc_uds_close_listener(nipc_uds_listener_t *listener)
316 +{
317 + if (!listener)
318 + return;
319 +
320 + if (listener->fd >= 0) {
321 + close(listener->fd);
322 + listener->fd = -1;
323 + }
324 +
325 + if (listener->path[0]) {
326 + unlink(listener->path);
327 + listener->path[0] = '\0';
328 + }
329 +}
src/libnetdata/netipc/src/transport/posix/netipc_uds_receive.c new
+257
@@ -0,0 +1,257 @@
1 +#include "netipc_uds_internal.h"
2 +
3 +#include <stdlib.h>
4 +#include <string.h>
5 +
6 +static nipc_uds_error_t ensure_recv_buf(nipc_uds_session_t *session,
7 + size_t needed)
8 +{
9 + if (session->recv_buf_size >= needed)
10 + return NIPC_UDS_OK;
11 +
12 + uint8_t *p = realloc(session->recv_buf, needed);
13 + if (!p)
14 + return NIPC_UDS_ERR_ALLOC;
15 +
16 + session->recv_buf = p;
17 + session->recv_buf_size = needed;
18 + return NIPC_UDS_OK;
19 +}
20 +
21 +static nipc_uds_error_t validate_batch(const nipc_header_t *hdr,
22 + const void *payload,
23 + size_t payload_len)
24 +{
25 + if (!(hdr->flags & NIPC_FLAG_BATCH) || hdr->item_count <= 1)
26 + return NIPC_UDS_OK;
27 +
28 + uint32_t dir_bytes = hdr->item_count * 8;
29 + uint32_t dir_aligned = (uint32_t)nipc_align8(dir_bytes);
30 + if (payload_len < dir_aligned)
31 + return NIPC_UDS_ERR_PROTOCOL;
32 +
33 + uint32_t packed_area_len = (uint32_t)(payload_len - dir_aligned);
34 + nipc_error_t perr = nipc_batch_dir_validate(payload, dir_bytes,
35 + hdr->item_count,
36 + packed_area_len);
37 + return (perr == NIPC_OK) ? NIPC_UDS_OK : NIPC_UDS_ERR_PROTOCOL;
38 +}
39 +
40 +static nipc_uds_error_t validate_inbound_limits(
41 + const nipc_uds_session_t *session,
42 + const nipc_header_t *hdr)
43 +{
44 + uint32_t max_payload = (session->role == NIPC_UDS_ROLE_SERVER)
45 + ? session->max_request_payload_bytes
46 + : session->max_response_payload_bytes;
47 + if (hdr->payload_len > max_payload)
48 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
49 +
50 + uint32_t max_batch = (session->role == NIPC_UDS_ROLE_SERVER)
51 + ? session->max_request_batch_items
52 + : session->max_response_batch_items;
53 + if (hdr->item_count > max_batch)
54 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
55 +
56 + return NIPC_UDS_OK;
57 +}
58 +
59 +static nipc_uds_error_t track_inbound_response(nipc_uds_session_t *session,
60 + const nipc_header_t *hdr)
61 +{
62 + if (session->role != NIPC_UDS_ROLE_CLIENT ||
63 + hdr->kind != NIPC_KIND_RESPONSE)
64 + return NIPC_UDS_OK;
65 +
66 + if (nipc_uds_inflight_remove(session, hdr->message_id) < 0)
67 + return NIPC_UDS_ERR_UNKNOWN_MSG_ID;
68 + return NIPC_UDS_OK;
69 +}
70 +
71 +static nipc_uds_error_t receive_first_packet(nipc_uds_session_t *session,
72 + void *buf,
73 + size_t buf_size,
74 + nipc_header_t *hdr_out,
75 + ssize_t *received_out)
76 +{
77 + ssize_t n = nipc_uds_raw_recv(session->fd, buf, buf_size);
78 + if (n <= 0) {
79 + nipc_uds_inflight_fail_all(session);
80 + return NIPC_UDS_ERR_RECV;
81 + }
82 +
83 + if ((size_t)n < NIPC_HEADER_LEN)
84 + return NIPC_UDS_ERR_PROTOCOL;
85 +
86 + nipc_error_t perr = nipc_header_decode(buf, (size_t)n, hdr_out);
87 + if (perr != NIPC_OK)
88 + return NIPC_UDS_ERR_PROTOCOL;
89 +
90 + *received_out = n;
91 + return NIPC_UDS_OK;
92 +}
93 +
94 +static nipc_uds_error_t return_complete_packet(void *buf,
95 + const nipc_header_t *hdr,
96 + const void **payload_out,
97 + size_t *payload_len_out)
98 +{
99 + *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
100 + *payload_len_out = hdr->payload_len;
101 + return validate_batch(hdr, *payload_out, *payload_len_out);
102 +}
103 +
104 +static uint32_t expected_chunk_count(size_t payload_len,
105 + size_t first_payload_bytes,
106 + size_t chunk_payload_budget)
107 +{
108 + size_t remaining_after_first = payload_len - first_payload_bytes;
109 + uint32_t expected_continuations = 0;
110 + if (remaining_after_first > 0 && chunk_payload_budget > 0) {
111 + expected_continuations = (uint32_t)(1 + ((remaining_after_first - 1)
112 + / chunk_payload_budget));
113 + }
114 + return 1 + expected_continuations;
115 +}
116 +
117 +static nipc_uds_error_t validate_chunk_header(const nipc_chunk_header_t *chk,
118 + const nipc_header_t *hdr,
119 + uint32_t chunk_index,
120 + uint32_t chunk_count,
121 + size_t total_msg)
122 +{
123 + if (chk->message_id != hdr->message_id ||
124 + chk->chunk_index != chunk_index ||
125 + chk->chunk_count != chunk_count ||
126 + chk->total_message_len != (uint32_t)total_msg)
127 + return NIPC_UDS_ERR_CHUNK;
128 + return NIPC_UDS_OK;
129 +}
130 +
131 +static nipc_uds_error_t receive_one_chunk(nipc_uds_session_t *session,
132 + uint8_t *pkt_buf,
133 + size_t pkt_buf_size,
134 + const nipc_header_t *hdr,
135 + uint32_t chunk_index,
136 + uint32_t chunk_count,
137 + size_t total_msg,
138 + size_t *assembled)
139 +{
140 + ssize_t cn = nipc_uds_raw_recv(session->fd, pkt_buf, pkt_buf_size);
141 + if (cn <= 0) {
142 + nipc_uds_inflight_fail_all(session);
143 + return NIPC_UDS_ERR_RECV;
144 + }
145 +
146 + if ((size_t)cn < NIPC_HEADER_LEN)
147 + return NIPC_UDS_ERR_CHUNK;
148 + if ((size_t)cn > pkt_buf_size)
149 + return NIPC_UDS_ERR_CHUNK;
150 +
151 + nipc_chunk_header_t chk;
152 + nipc_error_t perr = nipc_chunk_header_decode(pkt_buf, (size_t)cn, &chk);
153 + if (perr != NIPC_OK)
154 + return NIPC_UDS_ERR_CHUNK;
155 +
156 + nipc_uds_error_t err = validate_chunk_header(
157 + &chk, hdr, chunk_index, chunk_count, total_msg);
158 + if (err != NIPC_UDS_OK)
159 + return err;
160 +
161 + size_t chunk_data = (size_t)cn - NIPC_HEADER_LEN;
162 + if (chunk_data != chk.chunk_payload_len)
163 + return NIPC_UDS_ERR_CHUNK;
164 +
165 + if (*assembled > hdr->payload_len ||
166 + chunk_data > hdr->payload_len - *assembled)
167 + return NIPC_UDS_ERR_CHUNK;
168 +
169 + memcpy(&session->recv_buf[*assembled], &pkt_buf[NIPC_HEADER_LEN],
170 + chunk_data);
171 + *assembled += chunk_data;
172 + return NIPC_UDS_OK;
173 +}
174 +
175 +static nipc_uds_error_t receive_chunked_payload(nipc_uds_session_t *session,
176 + void *buf,
177 + ssize_t first_packet_len,
178 + const nipc_header_t *hdr,
179 + size_t total_msg,
180 + const void **payload_out,
181 + size_t *payload_len_out)
182 +{
183 + size_t first_payload_bytes = (size_t)first_packet_len - NIPC_HEADER_LEN;
184 + if (first_payload_bytes > hdr->payload_len)
185 + return NIPC_UDS_ERR_CHUNK;
186 +
187 + nipc_uds_error_t err = ensure_recv_buf(session, hdr->payload_len);
188 + if (err != NIPC_UDS_OK)
189 + return err;
190 +
191 + memcpy(session->recv_buf, (uint8_t *)buf + NIPC_HEADER_LEN,
192 + first_payload_bytes);
193 +
194 + size_t assembled = first_payload_bytes;
195 + size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
196 + uint32_t chunk_count = expected_chunk_count(
197 + hdr->payload_len, first_payload_bytes, chunk_payload_budget);
198 +
199 + size_t pkt_buf_size = session->packet_size;
200 + uint8_t *pkt_buf = malloc(pkt_buf_size);
201 + if (!pkt_buf)
202 + return NIPC_UDS_ERR_ALLOC;
203 +
204 + for (uint32_t ci = 1; assembled < hdr->payload_len; ci++) {
205 + err = receive_one_chunk(session, pkt_buf, pkt_buf_size, hdr, ci,
206 + chunk_count, total_msg, &assembled);
207 + if (err != NIPC_UDS_OK) {
208 + free(pkt_buf);
209 + return err;
210 + }
211 + }
212 +
213 + free(pkt_buf);
214 +
215 + *payload_out = session->recv_buf;
216 + *payload_len_out = hdr->payload_len;
217 + return validate_batch(hdr, *payload_out, *payload_len_out);
218 +}
219 +
220 +nipc_uds_error_t nipc_uds_receive(nipc_uds_session_t *session,
221 + void *buf, size_t buf_size,
222 + nipc_header_t *hdr_out,
223 + const void **payload_out,
224 + size_t *payload_len_out)
225 +{
226 + if (!session || session->fd < 0)
227 + return NIPC_UDS_ERR_BAD_PARAM;
228 +
229 + ssize_t n;
230 + nipc_uds_error_t err = receive_first_packet(session, buf, buf_size,
231 + hdr_out, &n);
232 + if (err != NIPC_UDS_OK)
233 + return err;
234 +
235 + err = validate_inbound_limits(session, hdr_out);
236 + if (err != NIPC_UDS_OK)
237 + return err;
238 +
239 + err = track_inbound_response(session, hdr_out);
240 + if (err != NIPC_UDS_OK)
241 + return err;
242 +
243 + size_t total_msg;
244 + if (!nipc_uds_header_payload_len(hdr_out->payload_len, &total_msg))
245 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
246 +
247 + if ((size_t)n > total_msg)
248 + return NIPC_UDS_ERR_PROTOCOL;
249 +
250 + if ((size_t)n == total_msg) {
251 + return return_complete_packet(buf, hdr_out, payload_out,
252 + payload_len_out);
253 + }
254 +
255 + return receive_chunked_payload(session, buf, n, hdr_out, total_msg,
256 + payload_out, payload_len_out);
257 +}
src/libnetdata/netipc/src/transport/posix/netipc_uds_send.c new
+244
@@ -0,0 +1,244 @@
1 +#include "netipc_uds_internal.h"
2 +
3 +#include <assert.h>
4 +#include <string.h>
5 +
6 +static size_t min_of_size_t(size_t a, size_t b)
7 +{
8 + return a < b ? a : b;
9 +}
10 +
11 +static bool tracks_client_request(const nipc_uds_session_t *session,
12 + const nipc_header_t *hdr)
13 +{
14 + return session->role == NIPC_UDS_ROLE_CLIENT &&
15 + hdr->kind == NIPC_KIND_REQUEST;
16 +}
17 +
18 +static void update_inflight_after_send(nipc_uds_session_t *session,
19 + const nipc_header_t *hdr,
20 + bool tracked,
21 + nipc_uds_error_t err)
22 +{
23 + if (!tracked)
24 + return;
25 +
26 + if (err == NIPC_UDS_OK)
27 + return;
28 +
29 + if (err == NIPC_UDS_ERR_SEND)
30 + nipc_uds_inflight_fail_all(session);
31 + else
32 + nipc_uds_inflight_remove(session, hdr->message_id);
33 +}
34 +
35 +static nipc_uds_error_t track_outbound_request(nipc_uds_session_t *session,
36 + const nipc_header_t *hdr,
37 + bool tracked)
38 +{
39 + if (!tracked)
40 + return NIPC_UDS_OK;
41 +
42 + int rc = nipc_uds_inflight_add(session, hdr->message_id);
43 + if (rc == -1)
44 + return NIPC_UDS_ERR_DUPLICATE_MSG_ID;
45 + if (rc == -2)
46 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
47 + return NIPC_UDS_OK;
48 +}
49 +
50 +static void outbound_limits(const nipc_uds_session_t *session,
51 + const nipc_header_t *hdr,
52 + uint32_t *max_payload,
53 + uint32_t *max_batch)
54 +{
55 + *max_payload = 0;
56 + *max_batch = 0;
57 +
58 + if (session->role == NIPC_UDS_ROLE_CLIENT &&
59 + hdr->kind == NIPC_KIND_REQUEST) {
60 + *max_payload = session->max_request_payload_bytes;
61 + *max_batch = session->max_request_batch_items;
62 + } else if (session->role == NIPC_UDS_ROLE_SERVER &&
63 + hdr->kind == NIPC_KIND_RESPONSE) {
64 + *max_payload = session->max_response_payload_bytes;
65 + *max_batch = session->max_response_batch_items;
66 + }
67 +}
68 +
69 +static nipc_uds_error_t validate_outbound_limits(nipc_uds_session_t *session,
70 + const nipc_header_t *hdr,
71 + size_t payload_len,
72 + bool tracked)
73 +{
74 + uint32_t max_payload;
75 + uint32_t max_batch;
76 + outbound_limits(session, hdr, &max_payload, &max_batch);
77 +
78 + bool payload_fits_u32 = payload_len <= UINT32_MAX;
79 + bool payload_within_limit = max_payload == 0 || payload_len <= max_payload;
80 + bool batch_within_limit = max_batch == 0 || hdr->item_count <= max_batch;
81 +
82 + if (payload_fits_u32 && payload_within_limit && batch_within_limit)
83 + return NIPC_UDS_OK;
84 +
85 + if (tracked)
86 + nipc_uds_inflight_remove(session, hdr->message_id);
87 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
88 +}
89 +
90 +static void fill_envelope(nipc_header_t *hdr, size_t payload_len)
91 +{
92 + hdr->magic = NIPC_MAGIC_MSG;
93 + hdr->version = NIPC_VERSION;
94 + hdr->header_len = NIPC_HEADER_LEN;
95 + /* validate_outbound_limits() rejects payloads that cannot fit the header. */
96 + assert(payload_len <= UINT32_MAX);
97 + hdr->payload_len = (uint32_t)payload_len;
98 +}
99 +
100 +static nipc_uds_error_t send_single_packet(nipc_uds_session_t *session,
101 + nipc_header_t *hdr,
102 + const void *payload,
103 + size_t payload_len,
104 + bool tracked)
105 +{
106 + uint8_t hdr_buf[NIPC_HEADER_LEN];
107 + nipc_header_encode(hdr, hdr_buf, sizeof(hdr_buf));
108 +
109 + nipc_uds_error_t err = nipc_uds_raw_send_iov(
110 + session->fd, hdr_buf, NIPC_HEADER_LEN, payload, payload_len);
111 + update_inflight_after_send(session, hdr, tracked, err);
112 + return err;
113 +}
114 +
115 +static nipc_uds_error_t send_first_chunk(nipc_uds_session_t *session,
116 + nipc_header_t *hdr,
117 + const void *payload,
118 + size_t first_chunk_payload,
119 + bool tracked)
120 +{
121 + uint8_t hdr_buf[NIPC_HEADER_LEN];
122 + nipc_header_encode(hdr, hdr_buf, sizeof(hdr_buf));
123 +
124 + nipc_uds_error_t err = nipc_uds_raw_send_iov(
125 + session->fd, hdr_buf, NIPC_HEADER_LEN, payload, first_chunk_payload);
126 + update_inflight_after_send(session, hdr, tracked, err);
127 + return err;
128 +}
129 +
130 +static nipc_uds_error_t send_continuation_chunk(nipc_uds_session_t *session,
131 + nipc_header_t *hdr,
132 + const uint8_t *src,
133 + size_t this_chunk,
134 + uint32_t chunk_index,
135 + uint32_t chunk_count,
136 + uint32_t total_msg,
137 + bool tracked)
138 +{
139 + /* packet_size is negotiated as uint32_t, so continuation chunks fit u32. */
140 + assert(this_chunk <= UINT32_MAX);
141 +
142 + nipc_chunk_header_t chk = {
143 + .magic = NIPC_MAGIC_CHUNK,
144 + .version = NIPC_VERSION,
145 + .message_id = hdr->message_id,
146 + .total_message_len = total_msg,
147 + .chunk_index = chunk_index,
148 + .chunk_count = chunk_count,
149 + .chunk_payload_len = (uint32_t)this_chunk,
150 + };
151 +
152 + uint8_t chk_buf[NIPC_HEADER_LEN];
153 + nipc_chunk_header_encode(&chk, chk_buf, sizeof(chk_buf));
154 +
155 + nipc_uds_error_t err = nipc_uds_raw_send_iov(
156 + session->fd, chk_buf, NIPC_HEADER_LEN, src, this_chunk);
157 + update_inflight_after_send(session, hdr, tracked, err);
158 + return err;
159 +}
160 +
161 +static nipc_uds_error_t send_chunked(nipc_uds_session_t *session,
162 + nipc_header_t *hdr,
163 + const void *payload,
164 + size_t payload_len,
165 + size_t total_msg,
166 + bool tracked)
167 +{
168 + if (session->packet_size <= NIPC_HEADER_LEN)
169 + return NIPC_UDS_ERR_BAD_PARAM;
170 +
171 + /* nipc_uds_header_payload_len() rejects totals wider than the wire field. */
172 + assert(total_msg <= UINT32_MAX);
173 + uint32_t total_msg_u32 = (uint32_t)total_msg;
174 +
175 + size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
176 + size_t remaining = payload_len;
177 + size_t first_chunk_payload = min_of_size_t(remaining, chunk_payload_budget);
178 + remaining -= first_chunk_payload;
179 +
180 + uint32_t continuation_chunks = 0;
181 + if (remaining > 0) {
182 + continuation_chunks = (uint32_t)(1 + ((remaining - 1)
183 + / chunk_payload_budget));
184 + }
185 + uint32_t chunk_count = 1 + continuation_chunks;
186 +
187 + nipc_uds_error_t err = send_first_chunk(session, hdr, payload,
188 + first_chunk_payload, tracked);
189 + if (err != NIPC_UDS_OK)
190 + return err;
191 +
192 + const uint8_t *src = (const uint8_t *)payload + first_chunk_payload;
193 + remaining = payload_len - first_chunk_payload;
194 +
195 + for (uint32_t ci = 1; ci < chunk_count; ci++) {
196 + size_t this_chunk = min_of_size_t(remaining, chunk_payload_budget);
197 +
198 + err = send_continuation_chunk(session, hdr, src, this_chunk, ci,
199 + chunk_count, total_msg_u32,
200 + tracked);
201 + if (err != NIPC_UDS_OK)
202 + return err;
203 +
204 + src += this_chunk;
205 + remaining -= this_chunk;
206 + }
207 +
208 + return NIPC_UDS_OK;
209 +}
210 +
211 +nipc_uds_error_t nipc_uds_send(nipc_uds_session_t *session,
212 + nipc_header_t *hdr,
213 + const void *payload,
214 + size_t payload_len)
215 +{
216 + if (!session || session->fd < 0)
217 + return NIPC_UDS_ERR_BAD_PARAM;
218 +
219 + bool tracked = tracks_client_request(session, hdr);
220 + nipc_uds_error_t err = track_outbound_request(session, hdr, tracked);
221 + if (err != NIPC_UDS_OK)
222 + return err;
223 +
224 + err = validate_outbound_limits(session, hdr, payload_len, tracked);
225 + if (err != NIPC_UDS_OK)
226 + return err;
227 +
228 + fill_envelope(hdr, payload_len);
229 +
230 + size_t total_msg;
231 + if (!nipc_uds_header_payload_len(payload_len, &total_msg)) {
232 + if (tracked)
233 + nipc_uds_inflight_remove(session, hdr->message_id);
234 + return NIPC_UDS_ERR_LIMIT_EXCEEDED;
235 + }
236 +
237 + if (total_msg <= session->packet_size) {
238 + return send_single_packet(session, hdr, payload, payload_len,
239 + tracked);
240 + }
241 +
242 + return send_chunked(session, hdr, payload, payload_len, total_msg,
243 + tracked);
244 +}
src/libnetdata/netipc/src/transport/windows/netipc_named_pipe.c
+25 -12
@@ -11,6 +11,7 @@
11 #include "netipc/netipc_named_pipe.h"
12 #include "netipc/netipc_protocol.h"
13
14 +#include <assert.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
@@ -50,12 +51,14 @@ static inline uint32_t apply_default(uint32_t val, uint32_t def)
51
52 static bool header_payload_len(size_t payload_len, size_t *msg_len_out)
53 {
53 -#if SIZE_MAX <= UINT32_MAX
54 if (payload_len > SIZE_MAX - NIPC_HEADER_LEN)
55 return false;
56 -#endif
56
58 - *msg_len_out = NIPC_HEADER_LEN + payload_len;
57 + size_t msg_len = NIPC_HEADER_LEN + payload_len;
58 + if (msg_len > UINT32_MAX)
59 + return false;
60 +
61 + *msg_len_out = msg_len;
62 return true;
63 }
64
@@ -936,6 +939,8 @@ nipc_np_error_t nipc_np_send(nipc_np_session_t *session,
939 hdr->magic = NIPC_MAGIC_MSG;
940 hdr->version = NIPC_VERSION;
941 hdr->header_len = NIPC_HEADER_LEN;
942 + /* validate_outbound_limits() rejects payloads that cannot fit the header. */
943 + assert(payload_len <= UINT32_MAX);
944 hdr->payload_len = (uint32_t)payload_len;
945
946 size_t total_msg;
@@ -961,12 +966,16 @@ nipc_np_error_t nipc_np_send(nipc_np_session_t *session,
966 }
967
968 /* Chunked send */
964 - size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
965 - if (chunk_payload_budget == 0) {
969 + if (session->packet_size <= NIPC_HEADER_LEN) {
970 if (tracked) inflight_remove(session, hdr->message_id);
971 return NIPC_NP_ERR_BAD_PARAM;
972 }
973
974 + /* header_payload_len() rejects totals wider than the wire field. */
975 + assert(total_msg <= UINT32_MAX);
976 + uint32_t total_msg_u32 = (uint32_t)total_msg;
977 +
978 + size_t chunk_payload_budget = session->packet_size - NIPC_HEADER_LEN;
979 size_t remaining = payload_len;
980 size_t first_chunk_payload = remaining < chunk_payload_budget
981 ? remaining : chunk_payload_budget;
@@ -974,8 +983,8 @@ nipc_np_error_t nipc_np_send(nipc_np_session_t *session,
983 remaining -= first_chunk_payload;
984 uint32_t continuation_chunks = 0;
985 if (remaining > 0) {
977 - continuation_chunks = (uint32_t)((remaining + chunk_payload_budget - 1)
978 - / chunk_payload_budget);
986 + continuation_chunks = (uint32_t)(1 + ((remaining - 1)
987 + / chunk_payload_budget));
988 }
989 uint32_t chunk_count = 1 + continuation_chunks;
990
@@ -1002,13 +1011,15 @@ nipc_np_error_t nipc_np_send(nipc_np_session_t *session,
1011 for (uint32_t ci = 1; ci < chunk_count; ci++) {
1012 size_t this_chunk = remaining < chunk_payload_budget
1013 ? remaining : chunk_payload_budget;
1014 + /* packet_size is negotiated as uint32_t, so continuation chunks fit u32. */
1015 + assert(this_chunk <= UINT32_MAX);
1016
1017 nipc_chunk_header_t chk = {
1018 .magic = NIPC_MAGIC_CHUNK,
1019 .version = NIPC_VERSION,
1020 .flags = 0,
1021 .message_id = hdr->message_id,
1011 - .total_message_len = (uint32_t)total_msg,
1022 + .total_message_len = total_msg_u32,
1023 .chunk_index = ci,
1024 .chunk_count = chunk_count,
1025 .chunk_payload_len = (uint32_t)this_chunk,
@@ -1126,8 +1137,11 @@ nipc_np_error_t nipc_np_receive(nipc_np_session_t *session,
1137 if (!header_payload_len(hdr_out->payload_len, &total_msg))
1138 return NIPC_NP_ERR_LIMIT_EXCEEDED;
1139
1140 + if (n > total_msg)
1141 + return NIPC_NP_ERR_PROTOCOL;
1142 +
1143 /* Non-chunked: entire message in one read */
1130 - if (n >= total_msg) {
1144 + if (n == total_msg) {
1145 *payload_out = (const uint8_t *)buf + NIPC_HEADER_LEN;
1146 *payload_len_out = hdr_out->payload_len;
1147
@@ -1155,9 +1169,8 @@ nipc_np_error_t nipc_np_receive(nipc_np_session_t *session,
1169 size_t remaining_after_first = hdr_out->payload_len - first_payload_bytes;
1170 uint32_t expected_continuations = 0;
1171 if (remaining_after_first > 0 && chunk_payload_budget > 0) {
1158 - expected_continuations = (uint32_t)((remaining_after_first +
1159 - chunk_payload_budget - 1)
1160 - / chunk_payload_budget);
1172 + expected_continuations = (uint32_t)(1 + ((remaining_after_first - 1)
1173 + / chunk_payload_budget));
1174 }
1175 uint32_t expected_chunk_count = 1 + expected_continuations;
1176