• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Alan-Jowett / sonde / 23937929947

03 Apr 2026 03:17AM UTC coverage: 85.585% (+0.1%) from 85.489%
23937929947

push

github

web-flow
fix: BPF send/send_recv helpers now use AEAD when enabled (#624)

* fix: BPF send/send_recv helpers now use AEAD when enabled

Fixes #622

The BPF helper functions \helper_send\ and \helper_send_recv\ were
calling the HMAC variants (\send_app_data\, \send_recv_app_data\)
unconditionally. With \es-gcm-codec\ enabled by default (PR #615),
the gateway decrypts with AES-256-GCM and silently discards the
HMAC-framed APP_DATA.

Changes:
- \pf_dispatch.rs\: Add AEAD provider storage to \DispatchContext\.
  New \install_aead()\ function passes \AeadProvider\ + \Sha256Provider\.
  Helpers use AEAD when providers are installed, fall back to HMAC
  when not (backward compatible with HMAC-only wake cycle).
- \wake_cycle.rs\: AEAD wake cycle calls \install_aead()\ with the
  \ead\ and \sha\ providers from its arguments. Added \'static\
  bounds on \A: AeadProvider\ and \H: Sha256Provider\ for trait
  object coercion.
- \ead_codec.rs\: Relaxed \Sized\ bounds on \ncode_frame_aead\,
  \open_frame\, and \uild_gcm_nonce\ to accept \?Sized\ providers
  (enables \&dyn AeadProvider\ usage from dispatch context).
- \send_app_data_aead\ / \send_recv_app_data_aead\: Relaxed \?Sized\
  bounds on provider parameters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alan Jowett <alan.jowett@microsoft.com>

* feat: enable debug/trace logging in gateway release builds

Updates GW-1304 to remove compile-time stripping of debug/trace call
sites in release builds. Operators can now enable debug logging on
production gateway binaries via \RUST_LOG=sonde_gateway=debug\ without
recompilation.

The runtime default remains \sonde_gateway=warn\ for release builds.
Node firmware is unaffected (retains \elease_max_level_warn\).

Changes:
- \sonde-gateway/Cargo.toml\: Remove \elease_max_level_info\ from
  tracing features.
- \docs/gateway-requirements.md\: Update GW-1304 AC-2/AC-5 and
  GW-1306 AC-3 to reflect TRACE availability in release.
- \docs... (continued)

245 of 248 new or added lines in 3 files covered. (98.79%)

2 existing lines in 1 file now uncovered.

27133 of 31703 relevant lines covered (85.58%)

135.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.93
/crates/sonde-node/src/bpf_dispatch.rs
1
// SPDX-License-Identifier: MIT
2
// Copyright (c) 2026 sonde contributors
3

4
//! Thread-local BPF helper dispatch.
5
//!
6
//! BPF helpers are registered as bare `fn` pointers
7
//! ([`crate::bpf_runtime::HelperFn`]) with the interpreter, so they
8
//! cannot capture state. This module bridges that gap by stashing
9
//! mutable references into a thread-local [`DispatchContext`] that is
10
//! installed at the start of BPF execution and cleared at the end.
11
//!
12
//! **Lifetime contract:** the context is valid only while
13
//! [`crate::wake_cycle::run_wake_cycle`] is executing BPF. No helper
14
//! may be invoked outside that window. The owning function holds all
15
//! referenced objects on its stack, guaranteeing pointer validity.
16
//!
17
//! See node-design.md §8.2 which explicitly endorses thread-local
18
//! dispatch for wiring helpers to platform state.
19

20
use std::cell::RefCell;
21

22
use sonde_protocol::HmacProvider;
23
#[cfg(feature = "aes-gcm-codec")]
24
use sonde_protocol::{AeadProvider, Sha256Provider};
25

26
use crate::bpf_helpers::ProgramClass;
27
use crate::hal::Hal;
28
use crate::key_store::NodeIdentity;
29
use crate::map_storage::MapStorage;
30
use crate::sleep::SleepManager;
31
use crate::traits::{Clock, Transport};
32

33
/// Default response timeout for `send_recv` helper (ms). Matches the protocol
34
/// spec (node-requirements.md ND-0702).
35
const SEND_RECV_TIMEOUT_MS: u32 = 50;
36

37
/// Maximum buffer length for bus helper operations (I2C/SPI).
38
/// Defence-in-depth cap to prevent oversized stack/heap access.
39
const MAX_BUS_TRANSFER_LEN: usize = 4096;
40

41
/// Maximum allowed timeout for `send_recv` helper (ms).
42
const MAX_SEND_RECV_TIMEOUT_MS: u32 = 5000;
43

44
/// Maximum delay allowed by `delay_us` helper (1 second).
45
const MAX_DELAY_US: u32 = 1_000_000;
46

47
/// Upper bound on the number of BPF maps supported per program.
48
/// Typical usage is 1–4 maps (bounded by RTC SRAM budget).
49
pub const MAX_MAPS: usize = 16;
50

51
// ---------------------------------------------------------------------------
52
// Map pointer index
53
// ---------------------------------------------------------------------------
54

55
/// Error returned by [`MapPtrIndex::insert`].
56
#[derive(Debug, PartialEq)]
57
enum MapPtrInsertError {
58
    /// The index is already at capacity (`MAX_MAPS`).
59
    Overflow,
60
    /// The pointer already exists in the index.
61
    Duplicate,
62
}
63

64
/// Fixed-size flat array mapping relocated map pointers to map indices.
65
///
66
/// Replaces `HashMap<u64, usize>` for zero heap allocation and faster
67
/// lookup over the small (1–4 entry) typical map counts.
68
///
69
/// Map pointers originate from `Vec::as_ptr()` in [`MapStorage`], which
70
/// guarantees non-null for non-zero-capacity vectors. The sentinel value `0`
71
/// therefore never collides with a valid map pointer.
72
struct MapPtrIndex {
73
    entries: [(u64, usize); MAX_MAPS],
74
    len: usize,
75
}
76

77
impl MapPtrIndex {
78
    fn new() -> Self {
72✔
79
        Self {
72✔
80
            entries: [(0, 0); MAX_MAPS],
72✔
81
            len: 0,
72✔
82
        }
72✔
83
    }
72✔
84

85
    /// Insert a map pointer → index mapping. Returns an error if the
86
    /// index is full or if the pointer is a duplicate (which would cause
87
    /// `get()` to resolve the wrong map).
88
    fn insert(&mut self, ptr: u64, idx: usize) -> Result<(), MapPtrInsertError> {
33✔
89
        if self.len >= MAX_MAPS {
33✔
90
            return Err(MapPtrInsertError::Overflow);
1✔
91
        }
32✔
92
        // Reject duplicates in all builds — not just debug. Duplicate
93
        // pointers can arise from zero-sized maps (empty Vec returns a
94
        // dangling non-null pointer that may collide).
95
        if self.entries[..self.len].iter().any(|(p, _)| *p == ptr) {
126✔
96
            return Err(MapPtrInsertError::Duplicate);
1✔
97
        }
31✔
98
        self.entries[self.len] = (ptr, idx);
31✔
99
        self.len += 1;
31✔
100
        Ok(())
31✔
101
    }
33✔
102

103
    fn get(&self, ptr: u64) -> Option<usize> {
9✔
104
        self.entries[..self.len]
9✔
105
            .iter()
9✔
106
            .find(|(p, _)| *p == ptr)
12✔
107
            .map(|(_, idx)| *idx)
9✔
108
    }
9✔
109
}
110

111
// ---------------------------------------------------------------------------
112
// Dispatch context
113
// ---------------------------------------------------------------------------
114

115
/// Raw-pointer bundle installed in the thread-local before BPF runs.
116
///
117
/// Every pointer is valid for the duration of a single
118
/// `interpreter.execute()` call. The owning `run_wake_cycle` holds
119
/// all objects on its stack.
120
struct DispatchContext {
121
    hal: *mut dyn Hal,
122
    transport: *mut dyn Transport,
123
    map_storage: *mut MapStorage,
124
    sleep_mgr: *mut SleepManager,
125
    clock: *const dyn Clock,
126
    /// HMAC provider — used for APP_DATA framing when `aes-gcm-codec` is disabled.
127
    /// Retained (but unused) when AEAD is active so the HMAC wake cycle still compiles.
128
    #[cfg_attr(feature = "aes-gcm-codec", allow(dead_code))]
129
    hmac: *const dyn HmacProvider,
130
    identity: *const NodeIdentity,
131
    current_seq: *mut u64,
132
    program_class: ProgramClass,
133
    trace_log: *mut Vec<String>,
134
    gateway_timestamp_ms: u64,
135
    command_received_at_ms: u64,
136
    battery_mv: u32,
137
    /// Relocated map pointer → index mapping (linear scan, bounded by MAX_MAPS).
138
    map_ptr_index: MapPtrIndex,
139
    /// AEAD + SHA providers for AES-256-GCM frame encoding.
140
    /// `None` when running under the HMAC-only wake cycle (no AEAD providers installed).
141
    #[cfg(feature = "aes-gcm-codec")]
142
    aead: Option<(*const dyn AeadProvider, *const dyn Sha256Provider)>,
143
}
144

145
thread_local! {
146
    static CTX: RefCell<Option<DispatchContext>> = const { RefCell::new(None) };
147
}
148

149
/// Maximum number of trace entries kept per BPF execution.
150
const MAX_TRACE_ENTRIES: usize = 64;
151

152
/// Borrow the context mutably and run `f` inside the borrow.
153
/// Returns `None` if no context is installed (helper called outside BPF
154
/// execution). Callers map `None` to the appropriate error sentinel.
155
fn with_ctx<R>(f: impl FnOnce(&mut DispatchContext) -> R) -> Option<R> {
52✔
156
    CTX.with(|cell| {
52✔
157
        let mut borrow = cell.borrow_mut();
52✔
158
        borrow.as_mut().map(f)
52✔
159
    })
52✔
160
}
52✔
161

162
// ---------------------------------------------------------------------------
163
// Lifecycle (called by run_wake_cycle)
164
// ---------------------------------------------------------------------------
165

166
/// Install the dispatch context for the current thread.
167
///
168
/// # Safety
169
///
170
/// All pointers must remain valid until [`clear`] is called.
171
/// The caller (`run_wake_cycle`) guarantees this by holding ownership
172
/// of every referenced object on its stack.
173
#[allow(clippy::too_many_arguments)]
174
pub unsafe fn install(
64✔
175
    hal: *mut dyn Hal,
64✔
176
    transport: *mut dyn Transport,
64✔
177
    map_storage: *mut MapStorage,
64✔
178
    sleep_mgr: *mut SleepManager,
64✔
179
    clock: *const dyn Clock,
64✔
180
    hmac: *const dyn HmacProvider,
64✔
181
    identity: *const NodeIdentity,
64✔
182
    current_seq: *mut u64,
64✔
183
    program_class: ProgramClass,
64✔
184
    trace_log: *mut Vec<String>,
64✔
185
    gateway_timestamp_ms: u64,
64✔
186
    command_received_at_ms: u64,
64✔
187
    battery_mv: u32,
64✔
188
) {
64✔
189
    install_core(
64✔
190
        hal,
64✔
191
        transport,
64✔
192
        map_storage,
64✔
193
        sleep_mgr,
64✔
194
        clock,
64✔
195
        hmac,
64✔
196
        identity,
64✔
197
        current_seq,
64✔
198
        program_class,
64✔
199
        trace_log,
64✔
200
        gateway_timestamp_ms,
64✔
201
        command_received_at_ms,
64✔
202
        battery_mv,
64✔
203
        #[cfg(feature = "aes-gcm-codec")]
204
        None,
64✔
205
    );
206
}
64✔
207

208
/// Install with AEAD providers for AES-256-GCM frame encoding.
209
///
210
/// # Safety
211
///
212
/// Same contract as [`install`], plus `aead` and `sha` must remain valid.
213
#[cfg(feature = "aes-gcm-codec")]
214
#[allow(clippy::too_many_arguments)]
215
pub unsafe fn install_aead(
4✔
216
    hal: *mut dyn Hal,
4✔
217
    transport: *mut dyn Transport,
4✔
218
    map_storage: *mut MapStorage,
4✔
219
    sleep_mgr: *mut SleepManager,
4✔
220
    clock: *const dyn Clock,
4✔
221
    hmac: *const dyn HmacProvider,
4✔
222
    identity: *const NodeIdentity,
4✔
223
    current_seq: *mut u64,
4✔
224
    program_class: ProgramClass,
4✔
225
    trace_log: *mut Vec<String>,
4✔
226
    gateway_timestamp_ms: u64,
4✔
227
    command_received_at_ms: u64,
4✔
228
    battery_mv: u32,
4✔
229
    aead: *const dyn AeadProvider,
4✔
230
    sha: *const dyn Sha256Provider,
4✔
231
) {
4✔
232
    install_core(
4✔
233
        hal,
4✔
234
        transport,
4✔
235
        map_storage,
4✔
236
        sleep_mgr,
4✔
237
        clock,
4✔
238
        hmac,
4✔
239
        identity,
4✔
240
        current_seq,
4✔
241
        program_class,
4✔
242
        trace_log,
4✔
243
        gateway_timestamp_ms,
4✔
244
        command_received_at_ms,
4✔
245
        battery_mv,
4✔
246
        Some((aead, sha)),
4✔
247
    );
248
}
4✔
249

250
#[allow(clippy::too_many_arguments)]
251
unsafe fn install_core(
68✔
252
    hal: *mut dyn Hal,
68✔
253
    transport: *mut dyn Transport,
68✔
254
    map_storage: *mut MapStorage,
68✔
255
    sleep_mgr: *mut SleepManager,
68✔
256
    clock: *const dyn Clock,
68✔
257
    hmac: *const dyn HmacProvider,
68✔
258
    identity: *const NodeIdentity,
68✔
259
    current_seq: *mut u64,
68✔
260
    program_class: ProgramClass,
68✔
261
    trace_log: *mut Vec<String>,
68✔
262
    gateway_timestamp_ms: u64,
68✔
263
    command_received_at_ms: u64,
68✔
264
    battery_mv: u32,
68✔
265
    #[cfg(feature = "aes-gcm-codec")] aead_sha: Option<(
68✔
266
        *const dyn AeadProvider,
68✔
267
        *const dyn Sha256Provider,
68✔
268
    )>,
68✔
269
) {
68✔
270
    CTX.with(|cell| {
68✔
271
        let mut borrow = cell.borrow_mut();
68✔
272
        assert!(borrow.is_none(), "BPF dispatch context already installed");
68✔
273
        // Build pointer→index map for fast lookup in map helpers.
274
        let map_ptr_index = {
68✔
275
            // SAFETY: caller guarantees map_storage is valid until clear().
276
            let ms = unsafe { &*map_storage };
68✔
277
            let mut index = MapPtrIndex::new();
68✔
278
            let mut ok = true;
68✔
279
            for (i, &p) in ms.map_pointers().iter().enumerate() {
68✔
280
                match index.insert(p, i) {
9✔
281
                    Ok(()) => {}
9✔
282
                    Err(MapPtrInsertError::Overflow) => {
283
                        log::error!(
×
284
                            "map pointer index overflow at map {i} \
285
                             (capacity {MAX_MAPS}) — \
286
                             all map helpers will return errors this cycle"
287
                        );
288
                        ok = false;
×
289
                        break;
×
290
                    }
291
                    Err(MapPtrInsertError::Duplicate) => {
292
                        log::error!(
×
293
                            "duplicate map pointer at map {i} — \
294
                             all map helpers will return errors this cycle"
295
                        );
296
                        ok = false;
×
297
                        break;
×
298
                    }
299
                }
300
            }
301
            // If any insert failed, use an empty index so all map
302
            // operations fail consistently rather than having a partial
303
            // index where some maps work and others silently don't.
304
            if !ok {
68✔
305
                MapPtrIndex::new()
×
306
            } else {
307
                index
68✔
308
            }
309
        };
310
        *borrow = Some(DispatchContext {
68✔
311
            hal,
68✔
312
            transport,
68✔
313
            map_storage,
68✔
314
            sleep_mgr,
68✔
315
            clock,
68✔
316
            hmac,
68✔
317
            identity,
68✔
318
            current_seq,
68✔
319
            program_class,
68✔
320
            trace_log,
68✔
321
            gateway_timestamp_ms,
68✔
322
            command_received_at_ms,
68✔
323
            battery_mv,
68✔
324
            map_ptr_index,
68✔
325
            #[cfg(feature = "aes-gcm-codec")]
68✔
326
            aead: aead_sha,
68✔
327
        });
68✔
328
    });
68✔
329
}
68✔
330

331
/// Clear the dispatch context after BPF execution completes.
332
pub fn clear() {
68✔
333
    CTX.with(|cell| {
68✔
334
        cell.borrow_mut().take();
68✔
335
    });
68✔
336
}
68✔
337

338
/// RAII guard that clears the dispatch context on drop.
339
pub struct DispatchGuard;
340

341
impl Drop for DispatchGuard {
342
    fn drop(&mut self) {
68✔
343
        clear();
68✔
344
    }
68✔
345
}
346

347
// ---------------------------------------------------------------------------
348
// Helper implementations (bare fn pointers for BpfInterpreter)
349
// ---------------------------------------------------------------------------
350
//
351
// # Safety contract for all helpers
352
//
353
// Each helper dereferences raw pointers from two sources:
354
//
355
// 1. **Dispatch context pointers** (`ctx.hal`, `ctx.transport`, etc.) —
356
//    guaranteed valid by `run_wake_cycle`, which holds all objects on
357
//    its stack for the duration of BPF execution.
358
//
359
// 2. **BPF register values** (`r1`–`r5` used as buffer pointers) —
360
//    guaranteed valid by the Prevail static verifier + interpreter
361
//    sandboxing. The verifier ensures all memory accesses fall within
362
//    the program's stack, context, or map regions. Null checks and
363
//    length caps are defence-in-depth against verifier bypass.
364

365
/// Helper 1: I2C read.
366
/// Args: r1=handle, r2=buf_ptr, r3=buf_len.
367
/// Returns: 0 on success, negative on error.
368
pub fn helper_i2c_read(r1: u64, r2: u64, r3: u64, _r4: u64, _r5: u64) -> u64 {
5✔
369
    let result = with_ctx(|ctx| {
5✔
370
        let handle = r1 as u32;
5✔
371
        let buf_ptr = r2 as *mut u8;
5✔
372
        let buf_len = r3 as usize;
5✔
373
        if buf_ptr.is_null() || buf_len == 0 || buf_len > MAX_BUS_TRANSFER_LEN {
5✔
374
            return (-1i64) as u64;
×
375
        }
5✔
376
        // SAFETY: buf_ptr and buf_len are verified by the BPF verifier
377
        // to point within the program's accessible memory regions.
378
        unsafe {
379
            let buf = core::slice::from_raw_parts_mut(buf_ptr, buf_len);
5✔
380
            (*ctx.hal).i2c_read(handle, buf) as i64 as u64
5✔
381
        }
382
    })
5✔
383
    .unwrap_or((-1i64) as u64);
5✔
384
    log::debug!("bpf helper i2c_read: result={}", result as i64);
5✔
385
    result
5✔
386
}
5✔
387

388
/// Helper 2: I2C write.
389
/// Args: r1=handle, r2=data_ptr, r3=data_len.
390
pub fn helper_i2c_write(r1: u64, r2: u64, r3: u64, _r4: u64, _r5: u64) -> u64 {
1✔
391
    let result = with_ctx(|ctx| {
1✔
392
        let handle = r1 as u32;
1✔
393
        let data_ptr = r2 as *const u8;
1✔
394
        let data_len = r3 as usize;
1✔
395
        if data_ptr.is_null() || data_len == 0 || data_len > MAX_BUS_TRANSFER_LEN {
1✔
396
            return (-1i64) as u64;
×
397
        }
1✔
398
        unsafe {
399
            let data = core::slice::from_raw_parts(data_ptr, data_len);
1✔
400
            (*ctx.hal).i2c_write(handle, data) as i64 as u64
1✔
401
        }
402
    })
1✔
403
    .unwrap_or((-1i64) as u64);
1✔
404
    log::debug!("bpf helper i2c_write: result={}", result as i64);
1✔
405
    result
1✔
406
}
1✔
407

408
/// Helper 3: I2C write-then-read.
409
/// Args: r1=handle, r2=write_ptr, r3=write_len, r4=read_ptr, r5=read_len.
410
pub fn helper_i2c_write_read(r1: u64, r2: u64, r3: u64, r4: u64, r5: u64) -> u64 {
3✔
411
    let result = with_ctx(|ctx| {
3✔
412
        let handle = r1 as u32;
3✔
413
        let write_ptr = r2 as *const u8;
3✔
414
        let write_len = r3 as usize;
3✔
415
        let read_ptr = r4 as *mut u8;
3✔
416
        let read_len = r5 as usize;
3✔
417
        if write_ptr.is_null()
3✔
418
            || read_ptr.is_null()
3✔
419
            || write_len == 0
3✔
420
            || read_len == 0
2✔
421
            || write_len > MAX_BUS_TRANSFER_LEN
1✔
422
            || read_len > MAX_BUS_TRANSFER_LEN
1✔
423
        {
424
            return (-1i64) as u64;
2✔
425
        }
1✔
426
        unsafe {
427
            let write_data = core::slice::from_raw_parts(write_ptr, write_len);
1✔
428
            let read_buf = core::slice::from_raw_parts_mut(read_ptr, read_len);
1✔
429
            (*ctx.hal).i2c_write_read(handle, write_data, read_buf) as i64 as u64
1✔
430
        }
431
    })
3✔
432
    .unwrap_or((-1i64) as u64);
3✔
433
    log::debug!("bpf helper i2c_write_read: result={}", result as i64);
3✔
434
    result
3✔
435
}
3✔
436

437
/// Helper 4: SPI full-duplex transfer.
438
/// Args: r1=handle, r2=tx_ptr (0=none), r3=rx_ptr (0=none), r4=len.
439
pub fn helper_spi_transfer(r1: u64, r2: u64, r3: u64, r4: u64, _r5: u64) -> u64 {
3✔
440
    let result = with_ctx(|ctx| {
3✔
441
        let handle = r1 as u32;
3✔
442
        let tx_ptr = r2 as *const u8;
3✔
443
        let rx_ptr = r3 as *mut u8;
3✔
444
        let len = r4 as usize;
3✔
445
        if len == 0 || len > MAX_BUS_TRANSFER_LEN {
3✔
446
            return (-1i64) as u64;
×
447
        }
3✔
448
        unsafe {
449
            let tx = if tx_ptr.is_null() {
3✔
450
                None
×
451
            } else {
452
                Some(core::slice::from_raw_parts(tx_ptr, len))
3✔
453
            };
454
            let rx = if rx_ptr.is_null() {
3✔
455
                None
×
456
            } else {
457
                Some(core::slice::from_raw_parts_mut(rx_ptr, len))
3✔
458
            };
459
            (*ctx.hal).spi_transfer(handle, tx, rx, len) as i64 as u64
3✔
460
        }
461
    })
3✔
462
    .unwrap_or((-1i64) as u64);
3✔
463
    log::debug!("bpf helper spi_transfer: result={}", result as i64);
3✔
464
    result
3✔
465
}
3✔
466

467
/// Helper 5: GPIO read.
468
/// Args: r1=pin.
469
pub fn helper_gpio_read(r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
4✔
470
    let result = with_ctx(|ctx| {
4✔
471
        let pin = r1 as u32;
4✔
472
        unsafe { (*ctx.hal).gpio_read(pin) as i64 as u64 }
4✔
473
    })
4✔
474
    .unwrap_or((-1i64) as u64);
4✔
475
    log::debug!("bpf helper gpio_read: result={}", result as i64);
4✔
476
    result
4✔
477
}
4✔
478

479
/// Helper 6: GPIO write.
480
/// Args: r1=pin, r2=value.
481
pub fn helper_gpio_write(r1: u64, r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
1✔
482
    let result = with_ctx(|ctx| {
1✔
483
        let pin = r1 as u32;
1✔
484
        let value = r2 as u32;
1✔
485
        unsafe { (*ctx.hal).gpio_write(pin, value) as i64 as u64 }
1✔
486
    })
1✔
487
    .unwrap_or((-1i64) as u64);
1✔
488
    log::debug!("bpf helper gpio_write: result={}", result as i64);
1✔
489
    result
1✔
490
}
1✔
491

492
/// Helper 7: ADC read.
493
/// Args: r1=channel.
494
/// Returns: raw ADC reading on success, negative on error (invalid channel).
495
pub fn helper_adc_read(r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
3✔
496
    let result = with_ctx(|ctx| {
3✔
497
        let channel = r1 as u32;
3✔
498
        unsafe { (*ctx.hal).adc_read(channel) as i64 as u64 }
3✔
499
    })
3✔
500
    .unwrap_or((-1i64) as u64);
3✔
501
    log::debug!("bpf helper adc_read: result={}", result as i64);
3✔
502
    result
3✔
503
}
3✔
504

505
/// Helper 8: send (fire-and-forget APP_DATA).
506
/// Args: r1=blob_ptr, r2=blob_len.
507
/// Returns: 0 on success, negative on error.
508
pub fn helper_send(r1: u64, r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
5✔
509
    let result = with_ctx(|ctx| {
5✔
510
        let blob_ptr = r1 as *const u8;
5✔
511
        let blob_len = r2 as usize;
5✔
512
        #[cfg(feature = "aes-gcm-codec")]
513
        let max_payload = if ctx.aead.is_some() {
5✔
514
            sonde_protocol::MAX_PAYLOAD_SIZE_AEAD
2✔
515
        } else {
516
            sonde_protocol::MAX_PAYLOAD_SIZE
3✔
517
        };
518
        #[cfg(not(feature = "aes-gcm-codec"))]
519
        let max_payload = sonde_protocol::MAX_PAYLOAD_SIZE;
520
        if blob_ptr.is_null() || blob_len > max_payload {
5✔
UNCOV
521
            return (-1i64) as u64;
×
522
        }
5✔
523

524
        unsafe {
525
            let blob = core::slice::from_raw_parts(blob_ptr, blob_len);
5✔
526
            let identity = &*ctx.identity;
5✔
527
            let transport = &mut *ctx.transport;
5✔
528
            let seq = &mut *ctx.current_seq;
5✔
529

530
            #[cfg(feature = "aes-gcm-codec")]
531
            {
532
                if let Some((aead_ptr, sha_ptr)) = ctx.aead {
5✔
533
                    let aead = &*aead_ptr;
2✔
534
                    let sha = &*sha_ptr;
2✔
535
                    match crate::wake_cycle::send_app_data_aead(
2✔
536
                        transport, identity, seq, blob, aead, sha,
2✔
537
                    ) {
2✔
538
                        Ok(()) => 0,
2✔
NEW
539
                        Err(_) => (-1i64) as u64,
×
540
                    }
541
                } else {
542
                    let hmac = &*ctx.hmac;
3✔
543
                    match crate::wake_cycle::send_app_data(transport, identity, seq, blob, hmac) {
3✔
544
                        Ok(()) => 0,
3✔
NEW
545
                        Err(_) => (-1i64) as u64,
×
546
                    }
547
                }
548
            }
549
            #[cfg(not(feature = "aes-gcm-codec"))]
550
            {
551
                let hmac = &*ctx.hmac;
552
                match crate::wake_cycle::send_app_data(transport, identity, seq, blob, hmac) {
553
                    Ok(()) => 0,
554
                    Err(_) => (-1i64) as u64,
555
                }
556
            }
557
        }
558
    })
5✔
559
    .unwrap_or((-1i64) as u64);
5✔
560
    log::debug!("bpf helper send: result={}", result as i64);
5✔
561
    result
5✔
562
}
5✔
563

564
/// Helper 9: send_recv (APP_DATA + wait for APP_DATA_REPLY).
565
/// Args: r1=blob_ptr, r2=blob_len, r3=reply_ptr, r4=reply_cap, r5=timeout_ms (0=default).
566
/// Returns: reply length on success, negative on error.
567
pub fn helper_send_recv(r1: u64, r2: u64, r3: u64, r4: u64, r5: u64) -> u64 {
4✔
568
    let result = with_ctx(|ctx| {
4✔
569
        let blob_ptr = r1 as *const u8;
4✔
570
        let blob_len = r2 as usize;
4✔
571
        let reply_ptr = r3 as *mut u8;
4✔
572
        let reply_cap = r4 as usize;
4✔
573
        #[cfg(feature = "aes-gcm-codec")]
574
        let max_payload = if ctx.aead.is_some() {
4✔
575
            sonde_protocol::MAX_PAYLOAD_SIZE_AEAD
1✔
576
        } else {
577
            sonde_protocol::MAX_PAYLOAD_SIZE
3✔
578
        };
579
        #[cfg(not(feature = "aes-gcm-codec"))]
580
        let max_payload = sonde_protocol::MAX_PAYLOAD_SIZE;
581
        if blob_ptr.is_null() || blob_len > max_payload || reply_ptr.is_null() || reply_cap == 0 {
4✔
UNCOV
582
            return (-1i64) as u64;
×
583
        }
4✔
584

585
        let timeout_ms = if r5 == 0 {
4✔
586
            SEND_RECV_TIMEOUT_MS
2✔
587
        } else {
588
            (r5 as u32).min(MAX_SEND_RECV_TIMEOUT_MS)
2✔
589
        };
590

591
        unsafe {
592
            let blob = core::slice::from_raw_parts(blob_ptr, blob_len);
4✔
593
            let identity = &*ctx.identity;
4✔
594
            let transport = &mut *ctx.transport;
4✔
595
            let clock = &*ctx.clock;
4✔
596
            let seq = &mut *ctx.current_seq;
4✔
597

598
            #[cfg(feature = "aes-gcm-codec")]
599
            let send_result = if let Some((aead_ptr, sha_ptr)) = ctx.aead {
4✔
600
                let aead = &*aead_ptr;
1✔
601
                let sha = &*sha_ptr;
1✔
602
                crate::wake_cycle::send_recv_app_data_aead(
1✔
603
                    transport, identity, seq, blob, timeout_ms, clock, aead, sha,
1✔
604
                )
605
            } else {
606
                let hmac = &*ctx.hmac;
3✔
607
                crate::wake_cycle::send_recv_app_data(
3✔
608
                    transport, identity, seq, blob, timeout_ms, clock, hmac,
3✔
609
                )
610
            };
611
            #[cfg(not(feature = "aes-gcm-codec"))]
612
            let send_result = {
613
                let hmac = &*ctx.hmac;
614
                crate::wake_cycle::send_recv_app_data(
615
                    transport, identity, seq, blob, timeout_ms, clock, hmac,
616
                )
617
            };
618

619
            match send_result {
4✔
620
                Ok(reply_blob) => {
3✔
621
                    if reply_blob.len() > reply_cap {
3✔
622
                        return (-1i64) as u64;
×
623
                    }
3✔
624
                    let copy_len = reply_blob.len();
3✔
625
                    let reply_buf = core::slice::from_raw_parts_mut(reply_ptr, copy_len);
3✔
626
                    reply_buf.copy_from_slice(&reply_blob);
3✔
627
                    copy_len as u64
3✔
628
                }
629
                Err(_) => (-1i64) as u64,
1✔
630
            }
631
        }
632
    })
4✔
633
    .unwrap_or((-1i64) as u64);
4✔
634
    log::debug!("bpf helper send_recv: result={}", result as i64);
4✔
635
    result
4✔
636
}
4✔
637

638
/// Helper 10: map_lookup_elem.
639
/// Args: r1=relocated map pointer, r2=key_ptr.
640
/// Returns: pointer to value, or 0 (NULL) on error/not-found.
641
pub fn helper_map_lookup_elem(r1: u64, r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
3✔
642
    with_ctx(|ctx| {
3✔
643
        let key_ptr = r2 as *const u32;
3✔
644
        if key_ptr.is_null() {
3✔
645
            return 0;
×
646
        }
3✔
647
        unsafe {
648
            let key = core::ptr::read_unaligned(key_ptr);
3✔
649
            let map_idx = match ctx.map_ptr_index.get(r1) {
3✔
650
                Some(idx) => idx,
3✔
651
                None => return 0,
×
652
            };
653
            let maps = &*ctx.map_storage;
3✔
654
            match maps.get(map_idx) {
3✔
655
                Some(map) => match map.lookup(key) {
3✔
656
                    Some(value) => value.as_ptr() as u64,
2✔
657
                    None => 0,
1✔
658
                },
659
                None => 0,
×
660
            }
661
        }
662
    })
3✔
663
    .unwrap_or(0)
3✔
664
}
3✔
665

666
/// Helper 11: map_update_elem (blocked for ephemeral programs).
667
/// Args: r1=relocated map pointer, r2=key_ptr, r3=value_ptr.
668
/// Returns: 0 on success, negative on error.
669
pub fn helper_map_update_elem(r1: u64, r2: u64, r3: u64, _r4: u64, _r5: u64) -> u64 {
3✔
670
    with_ctx(|ctx| {
3✔
671
        if ctx.program_class == ProgramClass::Ephemeral {
3✔
672
            return (-1i64) as u64;
2✔
673
        }
1✔
674

675
        let key_ptr = r2 as *const u32;
1✔
676
        let value_ptr = r3 as *const u8;
1✔
677
        if key_ptr.is_null() || value_ptr.is_null() {
1✔
678
            return (-1i64) as u64;
×
679
        }
1✔
680
        unsafe {
681
            let key = core::ptr::read_unaligned(key_ptr);
1✔
682
            let map_idx = match ctx.map_ptr_index.get(r1) {
1✔
683
                Some(idx) => idx,
1✔
684
                None => return (-1i64) as u64,
×
685
            };
686
            let maps = &mut *ctx.map_storage;
1✔
687
            match maps.get_mut(map_idx) {
1✔
688
                Some(map) => {
1✔
689
                    let value_size = map.def.value_size as usize;
1✔
690
                    let value = core::slice::from_raw_parts(value_ptr, value_size);
1✔
691
                    match map.update(key, value) {
1✔
692
                        Ok(()) => 0,
1✔
693
                        Err(_) => (-1i64) as u64,
×
694
                    }
695
                }
696
                None => (-1i64) as u64,
×
697
            }
698
        }
699
    })
3✔
700
    .unwrap_or((-1i64) as u64)
3✔
701
}
3✔
702

703
/// Helper 12: get_time.
704
/// Returns: estimated epoch time in milliseconds.
705
pub fn helper_get_time(_r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
3✔
706
    with_ctx(|ctx| unsafe {
3✔
707
        let elapsed = (*ctx.clock)
3✔
708
            .elapsed_ms()
3✔
709
            .saturating_sub(ctx.command_received_at_ms);
3✔
710
        ctx.gateway_timestamp_ms.saturating_add(elapsed)
3✔
711
    })
3✔
712
    .unwrap_or(0)
3✔
713
}
3✔
714

715
/// Helper 13: get_battery_mv.
716
/// Returns: battery voltage in millivolts, clamped to u16 to match
717
/// the BPF execution context `ctx->battery_mv` field.
718
pub fn helper_get_battery_mv(_r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
2✔
719
    with_ctx(|ctx| {
2✔
720
        if ctx.battery_mv > u16::MAX as u32 {
2✔
721
            u16::MAX as u64
×
722
        } else {
723
            ctx.battery_mv as u64
2✔
724
        }
725
    })
2✔
726
    .unwrap_or(0)
2✔
727
}
2✔
728

729
/// Helper 14: delay_us.
730
/// Args: r1=microseconds (max 1 second).
731
/// Returns: 0 on success, negative if delay exceeds maximum.
732
pub fn helper_delay_us(r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
12✔
733
    if r1 > MAX_DELAY_US as u64 {
12✔
734
        return (-1i64) as u64;
6✔
735
    }
6✔
736
    let us = r1 as u32;
6✔
737
    with_ctx(|ctx| {
6✔
738
        if us > 0 {
6✔
739
            unsafe {
5✔
740
                (*ctx.clock).delay_us(us);
5✔
741
            }
5✔
742
        }
1✔
743
        0
6✔
744
    })
6✔
745
    .unwrap_or((-1i64) as u64)
6✔
746
}
12✔
747

748
/// Helper 15: set_next_wake (blocked for ephemeral programs).
749
/// Args: r1=seconds.
750
pub fn helper_set_next_wake(r1: u64, _r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
5✔
751
    with_ctx(|ctx| {
5✔
752
        if ctx.program_class == ProgramClass::Ephemeral {
5✔
753
            return (-1i64) as u64;
2✔
754
        }
3✔
755
        let seconds = match u32::try_from(r1) {
3✔
756
            Ok(s) => s,
3✔
757
            Err(_) => return (-1i64) as u64,
×
758
        };
759
        unsafe {
3✔
760
            (*ctx.sleep_mgr).set_next_wake(seconds);
3✔
761
        }
3✔
762
        0
3✔
763
    })
5✔
764
    .unwrap_or((-1i64) as u64)
5✔
765
}
5✔
766

767
/// Helper 16: bpf_trace_printk.
768
/// Args: r1=fmt_ptr, r2=fmt_len.
769
pub fn helper_bpf_trace_printk(r1: u64, r2: u64, _r3: u64, _r4: u64, _r5: u64) -> u64 {
1✔
770
    with_ctx(|ctx| {
1✔
771
        let fmt_ptr = r1 as *const u8;
1✔
772
        let fmt_len = r2 as usize;
1✔
773
        if fmt_ptr.is_null() || fmt_len == 0 || fmt_len > 256 {
1✔
774
            return (-1i64) as u64;
×
775
        }
1✔
776
        unsafe {
777
            let bytes = core::slice::from_raw_parts(fmt_ptr, fmt_len);
1✔
778
            match core::str::from_utf8(bytes) {
1✔
779
                Ok(s) => {
1✔
780
                    let log = &mut *ctx.trace_log;
1✔
781
                    if log.len() < MAX_TRACE_ENTRIES {
1✔
782
                        log.push(s.to_string());
1✔
783
                    }
1✔
784
                    0
1✔
785
                }
786
                Err(_) => (-1i64) as u64,
×
787
            }
788
        }
789
    })
1✔
790
    .unwrap_or((-1i64) as u64)
1✔
791
}
1✔
792

793
/// Register all 16 helpers with the interpreter.
794
pub fn register_all(
37✔
795
    interpreter: &mut impl crate::bpf_runtime::BpfInterpreter,
37✔
796
) -> Result<(), crate::bpf_runtime::BpfError> {
37✔
797
    use crate::bpf_helpers::helper_ids::*;
798
    interpreter.register_helper(I2C_READ, helper_i2c_read)?;
37✔
799
    interpreter.register_helper(I2C_WRITE, helper_i2c_write)?;
37✔
800
    interpreter.register_helper(I2C_WRITE_READ, helper_i2c_write_read)?;
37✔
801
    interpreter.register_helper(SPI_TRANSFER, helper_spi_transfer)?;
37✔
802
    interpreter.register_helper(GPIO_READ, helper_gpio_read)?;
37✔
803
    interpreter.register_helper(GPIO_WRITE, helper_gpio_write)?;
37✔
804
    interpreter.register_helper(ADC_READ, helper_adc_read)?;
37✔
805
    interpreter.register_helper(SEND, helper_send)?;
37✔
806
    interpreter.register_helper(SEND_RECV, helper_send_recv)?;
37✔
807
    interpreter.register_helper(MAP_LOOKUP_ELEM, helper_map_lookup_elem)?;
37✔
808
    interpreter.register_helper(MAP_UPDATE_ELEM, helper_map_update_elem)?;
37✔
809
    interpreter.register_helper(GET_TIME, helper_get_time)?;
37✔
810
    interpreter.register_helper(GET_BATTERY_MV, helper_get_battery_mv)?;
37✔
811
    interpreter.register_helper(DELAY_US, helper_delay_us)?;
37✔
812
    interpreter.register_helper(SET_NEXT_WAKE, helper_set_next_wake)?;
37✔
813
    interpreter.register_helper(BPF_TRACE_PRINTK, helper_bpf_trace_printk)?;
37✔
814
    Ok(())
37✔
815
}
37✔
816

817
// ---------------------------------------------------------------------------
818
// Tests
819
// ---------------------------------------------------------------------------
820

821
#[cfg(test)]
822
mod tests {
823
    use super::*;
824
    use crate::error::NodeResult;
825
    use crate::hal::Hal;
826
    use crate::map_storage::MapStorage;
827
    use crate::sleep::{SleepManager, WakeReason};
828
    use crate::traits::{Clock, Transport};
829
    use sonde_protocol::{
830
        decode_frame, encode_frame, verify_frame, FrameHeader, GatewayMessage, HmacProvider,
831
        MapDef, NodeMessage, MSG_APP_DATA, MSG_APP_DATA_REPLY,
832
    };
833

834
    // -- Test mocks ---------------------------------------------------------
835

836
    struct TestHal {
837
        /// Data returned by i2c_read.
838
        i2c_read_data: Vec<u8>,
839
        /// Return code for i2c operations (-1 = NACK).
840
        i2c_return: i32,
841
        gpio_states: [i32; 32],
842
        adc_values: [i32; 8],
843
        spi_echo: bool,
844
    }
845

846
    impl TestHal {
847
        fn new() -> Self {
30✔
848
            Self {
30✔
849
                i2c_read_data: vec![0x1A, 0x2B],
30✔
850
                i2c_return: 0,
30✔
851
                gpio_states: [0; 32],
30✔
852
                adc_values: [0; 8],
30✔
853
                spi_echo: true,
30✔
854
            }
30✔
855
        }
30✔
856
    }
857

858
    impl Hal for TestHal {
859
        fn i2c_read(&mut self, _handle: u32, buf: &mut [u8]) -> i32 {
5✔
860
            if self.i2c_return != 0 {
5✔
861
                return self.i2c_return;
1✔
862
            }
4✔
863
            let copy_len = buf.len().min(self.i2c_read_data.len());
4✔
864
            buf[..copy_len].copy_from_slice(&self.i2c_read_data[..copy_len]);
4✔
865
            0
4✔
866
        }
5✔
867
        fn i2c_write(&mut self, _handle: u32, _data: &[u8]) -> i32 {
1✔
868
            self.i2c_return
1✔
869
        }
1✔
870
        fn i2c_write_read(&mut self, _handle: u32, _w: &[u8], buf: &mut [u8]) -> i32 {
1✔
871
            if self.i2c_return != 0 {
1✔
872
                return self.i2c_return;
×
873
            }
1✔
874
            let copy_len = buf.len().min(self.i2c_read_data.len());
1✔
875
            buf[..copy_len].copy_from_slice(&self.i2c_read_data[..copy_len]);
1✔
876
            0
1✔
877
        }
1✔
878
        fn spi_transfer(
3✔
879
            &mut self,
3✔
880
            _handle: u32,
3✔
881
            tx: Option<&[u8]>,
3✔
882
            rx: Option<&mut [u8]>,
3✔
883
            _len: usize,
3✔
884
        ) -> i32 {
3✔
885
            if self.spi_echo {
3✔
886
                if let (Some(tx_data), Some(rx_buf)) = (tx, rx) {
3✔
887
                    let n = tx_data.len().min(rx_buf.len());
3✔
888
                    rx_buf[..n].copy_from_slice(&tx_data[..n]);
3✔
889
                }
3✔
890
            }
×
891
            0
3✔
892
        }
3✔
893
        fn gpio_read(&self, pin: u32) -> i32 {
4✔
894
            self.gpio_states.get(pin as usize).copied().unwrap_or(-1)
4✔
895
        }
4✔
896
        fn gpio_write(&mut self, pin: u32, value: u32) -> i32 {
1✔
897
            if let Some(slot) = self.gpio_states.get_mut(pin as usize) {
1✔
898
                *slot = value as i32;
1✔
899
                0
1✔
900
            } else {
901
                -1
×
902
            }
903
        }
1✔
904
        fn adc_read(&mut self, channel: u32) -> i32 {
3✔
905
            self.adc_values.get(channel as usize).copied().unwrap_or(-1)
3✔
906
        }
3✔
907
    }
908

909
    struct TestTransport {
910
        outbound: Vec<Vec<u8>>,
911
        inbound: std::collections::VecDeque<Option<Vec<u8>>>,
912
    }
913
    impl TestTransport {
914
        fn new() -> Self {
30✔
915
            Self {
30✔
916
                outbound: Vec::new(),
30✔
917
                inbound: std::collections::VecDeque::new(),
30✔
918
            }
30✔
919
        }
30✔
920
    }
921
    impl Transport for TestTransport {
922
        fn send(&mut self, frame: &[u8]) -> NodeResult<()> {
5✔
923
            self.outbound.push(frame.to_vec());
5✔
924
            Ok(())
5✔
925
        }
5✔
926
        fn recv(&mut self, _timeout_ms: u32) -> NodeResult<Option<Vec<u8>>> {
3✔
927
            Ok(self.inbound.pop_front().flatten())
3✔
928
        }
3✔
929
    }
930

931
    struct TestClock(u64);
932
    impl Clock for TestClock {
933
        fn elapsed_ms(&self) -> u64 {
9✔
934
            self.0
9✔
935
        }
9✔
936
        fn delay_ms(&self, _ms: u32) {}
4✔
937
    }
938

939
    struct TestHmac;
940
    impl HmacProvider for TestHmac {
941
        fn compute(&self, key: &[u8], data: &[u8]) -> [u8; 32] {
6✔
942
            use hmac::Mac;
943
            let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(key).expect("HMAC key");
6✔
944
            mac.update(data);
6✔
945
            mac.finalize().into_bytes().into()
6✔
946
        }
6✔
947
        fn verify(&self, key: &[u8], data: &[u8], expected: &[u8; 32]) -> bool {
2✔
948
            self.compute(key, data) == *expected
2✔
949
        }
2✔
950
    }
951

952
    /// Install the dispatch context for the test, run `f`, then clear.
953
    #[allow(clippy::too_many_arguments)]
954
    fn with_test_context<F, R>(
27✔
955
        hal: &mut TestHal,
27✔
956
        transport: &mut TestTransport,
27✔
957
        map_storage: &mut MapStorage,
27✔
958
        sleep_mgr: &mut SleepManager,
27✔
959
        clock: &TestClock,
27✔
960
        hmac: &TestHmac,
27✔
961
        identity: &NodeIdentity,
27✔
962
        seq: &mut u64,
27✔
963
        program_class: ProgramClass,
27✔
964
        trace_log: &mut Vec<String>,
27✔
965
        f: F,
27✔
966
    ) -> R
27✔
967
    where
27✔
968
        F: FnOnce() -> R,
27✔
969
    {
970
        unsafe {
27✔
971
            install(
27✔
972
                hal as *mut TestHal as *mut dyn Hal,
27✔
973
                transport as *mut TestTransport as *mut dyn Transport,
27✔
974
                map_storage as *mut MapStorage,
27✔
975
                sleep_mgr as *mut SleepManager,
27✔
976
                clock as *const TestClock as *const dyn Clock,
27✔
977
                hmac as *const TestHmac as *const dyn HmacProvider,
27✔
978
                identity as *const NodeIdentity,
27✔
979
                seq as *mut u64,
27✔
980
                program_class,
27✔
981
                trace_log as *mut Vec<String>,
27✔
982
                1_710_000_000_000,
27✔
983
                100,
27✔
984
                3300,
27✔
985
            );
27✔
986
        }
27✔
987
        let _guard = DispatchGuard;
27✔
988
        f()
27✔
989
    }
27✔
990

991
    fn default_identity() -> NodeIdentity {
30✔
992
        NodeIdentity {
30✔
993
            key_hint: 1,
30✔
994
            psk: [0xAA; 32],
30✔
995
        }
30✔
996
    }
30✔
997

998
    // -- Tests --------------------------------------------------------------
999

1000
    #[test]
1001
    fn test_context_lifecycle() {
1✔
1002
        // Verify install sets context and clear removes it.
1003
        let mut hal = TestHal::new();
1✔
1004
        let mut transport = TestTransport::new();
1✔
1005
        let mut maps = MapStorage::new(4096);
1✔
1006
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1007
        let clock = TestClock(1000);
1✔
1008
        let hmac = TestHmac;
1✔
1009
        let identity = default_identity();
1✔
1010
        let mut seq = 0u64;
1✔
1011
        let mut trace = Vec::new();
1✔
1012

1013
        with_test_context(
1✔
1014
            &mut hal,
1✔
1015
            &mut transport,
1✔
1016
            &mut maps,
1✔
1017
            &mut sleep,
1✔
1018
            &clock,
1✔
1019
            &hmac,
1✔
1020
            &identity,
1✔
1021
            &mut seq,
1✔
1022
            ProgramClass::Resident,
1✔
1023
            &mut trace,
1✔
1024
            || {
1✔
1025
                // Should not panic — context is installed
1026
                // get_time: 1_710_000_000_000 + (1000 - 100) = 1_710_000_000_900
1027
                let result = helper_get_time(0, 0, 0, 0, 0);
1✔
1028
                assert_eq!(result, 1_710_000_000_900);
1✔
1029
            },
1✔
1030
        );
1031
    }
1✔
1032

1033
    #[test]
1034
    fn test_helper_i2c_read() {
1✔
1035
        // T-N600: I2C read returns data from mock device.
1036
        let mut hal = TestHal::new();
1✔
1037
        hal.i2c_read_data = vec![0x1A, 0x2B];
1✔
1038
        let mut transport = TestTransport::new();
1✔
1039
        let mut maps = MapStorage::new(4096);
1✔
1040
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1041
        let clock = TestClock(0);
1✔
1042
        let hmac = TestHmac;
1✔
1043
        let identity = default_identity();
1✔
1044
        let mut seq = 0u64;
1✔
1045
        let mut trace = Vec::new();
1✔
1046
        let mut buf = [0u8; 2];
1✔
1047

1048
        with_test_context(
1✔
1049
            &mut hal,
1✔
1050
            &mut transport,
1✔
1051
            &mut maps,
1✔
1052
            &mut sleep,
1✔
1053
            &clock,
1✔
1054
            &hmac,
1✔
1055
            &identity,
1✔
1056
            &mut seq,
1✔
1057
            ProgramClass::Resident,
1✔
1058
            &mut trace,
1✔
1059
            || {
1✔
1060
                let handle = crate::hal::i2c_handle(0, 0x48);
1✔
1061
                let result = helper_i2c_read(
1✔
1062
                    handle as u64,
1✔
1063
                    buf.as_mut_ptr() as u64,
1✔
1064
                    buf.len() as u64,
1✔
1065
                    0,
1066
                    0,
1067
                );
1068
                assert_eq!(result, 0);
1✔
1069
            },
1✔
1070
        );
1071
        assert_eq!(buf, [0x1A, 0x2B]);
1✔
1072
    }
1✔
1073

1074
    #[test]
1075
    fn test_helper_i2c_error() {
1✔
1076
        // T-N601: I2C NACK → helper returns negative.
1077
        let mut hal = TestHal::new();
1✔
1078
        hal.i2c_return = -1;
1✔
1079
        let mut transport = TestTransport::new();
1✔
1080
        let mut maps = MapStorage::new(4096);
1✔
1081
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1082
        let clock = TestClock(0);
1✔
1083
        let hmac = TestHmac;
1✔
1084
        let identity = default_identity();
1✔
1085
        let mut seq = 0u64;
1✔
1086
        let mut trace = Vec::new();
1✔
1087
        let mut buf = [0u8; 2];
1✔
1088

1089
        with_test_context(
1✔
1090
            &mut hal,
1✔
1091
            &mut transport,
1✔
1092
            &mut maps,
1✔
1093
            &mut sleep,
1✔
1094
            &clock,
1✔
1095
            &hmac,
1✔
1096
            &identity,
1✔
1097
            &mut seq,
1✔
1098
            ProgramClass::Resident,
1✔
1099
            &mut trace,
1✔
1100
            || {
1✔
1101
                let result =
1✔
1102
                    helper_i2c_read(0x0048, buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0);
1✔
1103
                assert_eq!(result as i64, -1);
1✔
1104
            },
1✔
1105
        );
1106
    }
1✔
1107

1108
    #[test]
1109
    fn test_helper_i2c_write_read_rejects_zero_length() {
1✔
1110
        // Zero-length write_len or read_len must return -1.
1111
        let mut hal = TestHal::new();
1✔
1112
        let mut transport = TestTransport::new();
1✔
1113
        let mut maps = MapStorage::new(4096);
1✔
1114
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1115
        let clock = TestClock(0);
1✔
1116
        let hmac = TestHmac;
1✔
1117
        let identity = default_identity();
1✔
1118
        let mut seq = 0u64;
1✔
1119
        let mut trace = Vec::new();
1✔
1120
        let write_buf = [0x42u8; 2];
1✔
1121
        let mut read_buf = [0u8; 2];
1✔
1122

1123
        with_test_context(
1✔
1124
            &mut hal,
1✔
1125
            &mut transport,
1✔
1126
            &mut maps,
1✔
1127
            &mut sleep,
1✔
1128
            &clock,
1✔
1129
            &hmac,
1✔
1130
            &identity,
1✔
1131
            &mut seq,
1✔
1132
            ProgramClass::Resident,
1✔
1133
            &mut trace,
1✔
1134
            || {
1✔
1135
                let handle = crate::hal::i2c_handle(0, 0x48) as u64;
1✔
1136

1137
                // Zero write_len → -1
1138
                let result = helper_i2c_write_read(
1✔
1139
                    handle,
1✔
1140
                    write_buf.as_ptr() as u64,
1✔
1141
                    0, // write_len = 0
1142
                    read_buf.as_mut_ptr() as u64,
1✔
1143
                    read_buf.len() as u64,
1✔
1144
                );
1145
                assert_eq!(result as i64, -1, "zero write_len should be rejected");
1✔
1146

1147
                // Zero read_len → -1
1148
                let result = helper_i2c_write_read(
1✔
1149
                    handle,
1✔
1150
                    write_buf.as_ptr() as u64,
1✔
1151
                    write_buf.len() as u64,
1✔
1152
                    read_buf.as_mut_ptr() as u64,
1✔
1153
                    0, // read_len = 0
1154
                );
1155
                assert_eq!(result as i64, -1, "zero read_len should be rejected");
1✔
1156
            },
1✔
1157
        );
1158
    }
1✔
1159

1160
    #[test]
1161
    fn test_helper_spi_transfer() {
1✔
1162
        // T-N602: SPI echo — rx matches tx.
1163
        let mut hal = TestHal::new();
1✔
1164
        let mut transport = TestTransport::new();
1✔
1165
        let mut maps = MapStorage::new(4096);
1✔
1166
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1167
        let clock = TestClock(0);
1✔
1168
        let hmac = TestHmac;
1✔
1169
        let identity = default_identity();
1✔
1170
        let mut seq = 0u64;
1✔
1171
        let mut trace = Vec::new();
1✔
1172
        let tx = [0xDE, 0xAD, 0xBE, 0xEF];
1✔
1173
        let mut rx = [0u8; 4];
1✔
1174

1175
        with_test_context(
1✔
1176
            &mut hal,
1✔
1177
            &mut transport,
1✔
1178
            &mut maps,
1✔
1179
            &mut sleep,
1✔
1180
            &clock,
1✔
1181
            &hmac,
1✔
1182
            &identity,
1✔
1183
            &mut seq,
1✔
1184
            ProgramClass::Resident,
1✔
1185
            &mut trace,
1✔
1186
            || {
1✔
1187
                let handle = crate::hal::spi_handle(0);
1✔
1188
                let result = helper_spi_transfer(
1✔
1189
                    handle as u64,
1✔
1190
                    tx.as_ptr() as u64,
1✔
1191
                    rx.as_mut_ptr() as u64,
1✔
1192
                    tx.len() as u64,
1✔
1193
                    0,
1194
                );
1195
                assert_eq!(result, 0);
1✔
1196
            },
1✔
1197
        );
1198
        assert_eq!(rx, tx);
1✔
1199
    }
1✔
1200

1201
    #[test]
1202
    fn test_helper_gpio_and_adc() {
1✔
1203
        // T-N603: GPIO pin 5=HIGH, ADC channel 0=2048.
1204
        let mut hal = TestHal::new();
1✔
1205
        hal.gpio_states[5] = 1;
1✔
1206
        hal.adc_values[0] = 2048;
1✔
1207
        let mut transport = TestTransport::new();
1✔
1208
        let mut maps = MapStorage::new(4096);
1✔
1209
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1210
        let clock = TestClock(0);
1✔
1211
        let hmac = TestHmac;
1✔
1212
        let identity = default_identity();
1✔
1213
        let mut seq = 0u64;
1✔
1214
        let mut trace = Vec::new();
1✔
1215

1216
        with_test_context(
1✔
1217
            &mut hal,
1✔
1218
            &mut transport,
1✔
1219
            &mut maps,
1✔
1220
            &mut sleep,
1✔
1221
            &clock,
1✔
1222
            &hmac,
1✔
1223
            &identity,
1✔
1224
            &mut seq,
1✔
1225
            ProgramClass::Resident,
1✔
1226
            &mut trace,
1✔
1227
            || {
1✔
1228
                assert_eq!(helper_gpio_read(5, 0, 0, 0, 0), 1);
1✔
1229
                assert_eq!(helper_adc_read(0, 0, 0, 0, 0), 2048);
1✔
1230
            },
1✔
1231
        );
1232
    }
1✔
1233

1234
    #[test]
1235
    fn test_helper_map_lookup_update() {
1✔
1236
        // T-N607: Write 42 to key 0, read it back.
1237
        let mut hal = TestHal::new();
1✔
1238
        let mut transport = TestTransport::new();
1✔
1239
        let mut maps = MapStorage::new(4096);
1✔
1240
        maps.allocate(&[MapDef {
1✔
1241
            map_type: 1,
1✔
1242
            key_size: 4,
1✔
1243
            value_size: 4,
1✔
1244
            max_entries: 4,
1✔
1245
        }])
1✔
1246
        .unwrap();
1✔
1247
        let map_ptr = maps.map_pointers()[0];
1✔
1248
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1249
        let clock = TestClock(0);
1✔
1250
        let hmac = TestHmac;
1✔
1251
        let identity = default_identity();
1✔
1252
        let mut seq = 0u64;
1✔
1253
        let mut trace = Vec::new();
1✔
1254

1255
        with_test_context(
1✔
1256
            &mut hal,
1✔
1257
            &mut transport,
1✔
1258
            &mut maps,
1✔
1259
            &mut sleep,
1✔
1260
            &clock,
1✔
1261
            &hmac,
1✔
1262
            &identity,
1✔
1263
            &mut seq,
1✔
1264
            ProgramClass::Resident,
1✔
1265
            &mut trace,
1✔
1266
            || {
1✔
1267
                let key: u32 = 0;
1✔
1268
                let value: [u8; 4] = 42u32.to_ne_bytes();
1✔
1269

1270
                // Update
1271
                let result = helper_map_update_elem(
1✔
1272
                    map_ptr,
1✔
1273
                    &key as *const u32 as u64,
1✔
1274
                    value.as_ptr() as u64,
1✔
1275
                    0,
1276
                    0,
1277
                );
1278
                assert_eq!(result, 0);
1✔
1279

1280
                // Lookup
1281
                let ptr = helper_map_lookup_elem(map_ptr, &key as *const u32 as u64, 0, 0, 0);
1✔
1282
                assert_ne!(ptr, 0, "lookup should return non-null pointer");
1✔
1283

1284
                let read_value = unsafe { core::ptr::read_unaligned(ptr as *const u32) };
1✔
1285
                assert_eq!(read_value, 42);
1✔
1286
            },
1✔
1287
        );
1288
    }
1✔
1289

1290
    #[test]
1291
    fn test_helper_map_update_ephemeral_rejected() {
1✔
1292
        // T-N609: Ephemeral program cannot write maps.
1293
        let mut hal = TestHal::new();
1✔
1294
        let mut transport = TestTransport::new();
1✔
1295
        let mut maps = MapStorage::new(4096);
1✔
1296
        maps.allocate(&[MapDef {
1✔
1297
            map_type: 1,
1✔
1298
            key_size: 4,
1✔
1299
            value_size: 4,
1✔
1300
            max_entries: 4,
1✔
1301
        }])
1✔
1302
        .unwrap();
1✔
1303
        let map_ptr = maps.map_pointers()[0];
1✔
1304
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1305
        let clock = TestClock(0);
1✔
1306
        let hmac = TestHmac;
1✔
1307
        let identity = default_identity();
1✔
1308
        let mut seq = 0u64;
1✔
1309
        let mut trace = Vec::new();
1✔
1310

1311
        with_test_context(
1✔
1312
            &mut hal,
1✔
1313
            &mut transport,
1✔
1314
            &mut maps,
1✔
1315
            &mut sleep,
1✔
1316
            &clock,
1✔
1317
            &hmac,
1✔
1318
            &identity,
1✔
1319
            &mut seq,
1✔
1320
            ProgramClass::Ephemeral, // ← ephemeral
1✔
1321
            &mut trace,
1✔
1322
            || {
1✔
1323
                let key: u32 = 0;
1✔
1324
                let value: [u8; 4] = 99u32.to_ne_bytes();
1✔
1325
                let result = helper_map_update_elem(
1✔
1326
                    map_ptr,
1✔
1327
                    &key as *const u32 as u64,
1✔
1328
                    value.as_ptr() as u64,
1✔
1329
                    0,
1330
                    0,
1331
                );
1332
                assert_eq!(result as i64, -1, "ephemeral should be rejected");
1✔
1333
            },
1✔
1334
        );
1335

1336
        // Verify map unchanged (still zero)
1337
        let val = maps.get(0).unwrap().lookup(0).unwrap();
1✔
1338
        assert!(val.iter().all(|&b| b == 0));
4✔
1339
    }
1✔
1340

1341
    #[test]
1342
    fn test_helper_get_time_and_battery() {
1✔
1343
        // T-N610: get_time returns epoch estimate, get_battery_mv returns captured value.
1344
        let mut hal = TestHal::new();
1✔
1345
        let mut transport = TestTransport::new();
1✔
1346
        let mut maps = MapStorage::new(4096);
1✔
1347
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1348
        let clock = TestClock(200);
1✔
1349
        let hmac = TestHmac;
1✔
1350
        let identity = default_identity();
1✔
1351
        let mut seq = 0u64;
1✔
1352
        let mut trace = Vec::new();
1✔
1353

1354
        // Override defaults: gateway_timestamp_ms=1_710_000_000_000,
1355
        // command_received_at_ms=100, battery_mv=3300
1356
        // Expected get_time: 1_710_000_000_000 + (200 - 100) = 1_710_000_000_100
1357
        unsafe {
1✔
1358
            install(
1✔
1359
                &mut hal as *mut TestHal as *mut dyn Hal,
1✔
1360
                &mut transport as *mut TestTransport as *mut dyn Transport,
1✔
1361
                &mut maps as *mut MapStorage,
1✔
1362
                &mut sleep as *mut SleepManager,
1✔
1363
                &clock as *const TestClock as *const dyn Clock,
1✔
1364
                &hmac as *const TestHmac as *const dyn HmacProvider,
1✔
1365
                &identity as *const NodeIdentity,
1✔
1366
                &mut seq as *mut u64,
1✔
1367
                ProgramClass::Resident,
1✔
1368
                &mut trace as *mut Vec<String>,
1✔
1369
                1_710_000_000_000,
1✔
1370
                100,
1✔
1371
                3300,
1✔
1372
            );
1✔
1373
        }
1✔
1374
        let _guard = DispatchGuard;
1✔
1375
        assert_eq!(helper_get_time(0, 0, 0, 0, 0), 1_710_000_000_100);
1✔
1376
        assert_eq!(helper_get_battery_mv(0, 0, 0, 0, 0), 3300);
1✔
1377
    }
1✔
1378

1379
    #[test]
1380
    fn test_helper_delay_us() {
1✔
1381
        // T-N611: delay_us does not crash and returns 0.
1382
        let mut hal = TestHal::new();
1✔
1383
        let mut transport = TestTransport::new();
1✔
1384
        let mut maps = MapStorage::new(4096);
1✔
1385
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1386
        let clock = TestClock(0);
1✔
1387
        let hmac = TestHmac;
1✔
1388
        let identity = default_identity();
1✔
1389
        let mut seq = 0u64;
1✔
1390
        let mut trace = Vec::new();
1✔
1391

1392
        with_test_context(
1✔
1393
            &mut hal,
1✔
1394
            &mut transport,
1✔
1395
            &mut maps,
1✔
1396
            &mut sleep,
1✔
1397
            &clock,
1✔
1398
            &hmac,
1✔
1399
            &identity,
1✔
1400
            &mut seq,
1✔
1401
            ProgramClass::Resident,
1✔
1402
            &mut trace,
1✔
1403
            || {
1✔
1404
                assert_eq!(helper_delay_us(1000, 0, 0, 0, 0), 0);
1✔
1405
                assert_eq!(helper_delay_us(0, 0, 0, 0, 0), 0);
1✔
1406
            },
1✔
1407
        );
1408
    }
1✔
1409

1410
    #[test]
1411
    fn test_helper_delay_us_max_enforcement() {
1✔
1412
        // ND-0604 AC3: delay_us with value exceeding MAX_DELAY_US (1 s)
1413
        // must return an error (-1) and must NOT busy-wait. A tracking
1414
        // clock verifies that delay_ms is only called for accepted values.
1415
        use std::cell::Cell;
1416

1417
        struct TrackingClock {
1418
            delay_calls: Cell<u32>,
1419
        }
1420
        impl Clock for TrackingClock {
1421
            fn elapsed_ms(&self) -> u64 {
×
1422
                0
×
1423
            }
×
1424
            fn delay_ms(&self, _ms: u32) {
1✔
1425
                self.delay_calls.set(self.delay_calls.get() + 1);
1✔
1426
            }
1✔
1427
        }
1428

1429
        let mut hal = TestHal::new();
1✔
1430
        let mut transport = TestTransport::new();
1✔
1431
        let mut maps = MapStorage::new(4096);
1✔
1432
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1433
        let clock = TrackingClock {
1✔
1434
            delay_calls: Cell::new(0),
1✔
1435
        };
1✔
1436
        let hmac = TestHmac;
1✔
1437
        let identity = default_identity();
1✔
1438
        let mut seq = 0u64;
1✔
1439
        let mut trace = Vec::new();
1✔
1440

1441
        unsafe {
1✔
1442
            install(
1✔
1443
                &mut hal as *mut TestHal as *mut dyn Hal,
1✔
1444
                &mut transport as *mut TestTransport as *mut dyn Transport,
1✔
1445
                &mut maps as *mut MapStorage,
1✔
1446
                &mut sleep as *mut SleepManager,
1✔
1447
                &clock as *const TrackingClock as *const dyn Clock,
1✔
1448
                &hmac as *const TestHmac as *const dyn HmacProvider,
1✔
1449
                &identity as *const NodeIdentity,
1✔
1450
                &mut seq as *mut u64,
1✔
1451
                ProgramClass::Resident,
1✔
1452
                &mut trace as *mut Vec<String>,
1✔
1453
                1_710_000_000_000,
1✔
1454
                100,
1✔
1455
                3300,
1✔
1456
            );
1✔
1457
        }
1✔
1458
        let _guard = DispatchGuard;
1✔
1459

1460
        // Exactly at the limit — must succeed and invoke delay
1461
        assert_eq!(helper_delay_us(1_000_000, 0, 0, 0, 0), 0);
1✔
1462
        assert!(
1✔
1463
            clock.delay_calls.get() > 0,
1✔
1464
            "accepted delay must invoke clock"
1465
        );
1466

1467
        clock.delay_calls.set(0);
1✔
1468

1469
        // One over the limit — must reject WITHOUT calling delay
1470
        assert_eq!(
1✔
1471
            helper_delay_us(1_000_001, 0, 0, 0, 0),
1✔
1472
            (-1i64) as u64,
1473
            "delay exceeding MAX_DELAY_US must return error"
1474
        );
1475
        assert_eq!(
1✔
1476
            clock.delay_calls.get(),
1✔
1477
            0,
1478
            "rejected delay must not invoke clock"
1479
        );
1480

1481
        // Far above the limit
1482
        assert_eq!(
1✔
1483
            helper_delay_us(u64::MAX, 0, 0, 0, 0),
1✔
1484
            (-1i64) as u64,
1485
            "extremely large delay must return error"
1486
        );
1487
        assert_eq!(
1✔
1488
            clock.delay_calls.get(),
1✔
1489
            0,
1490
            "rejected delay must not invoke clock"
1491
        );
1492
    }
1✔
1493

1494
    #[test]
1495
    fn test_helper_set_next_wake() {
1✔
1496
        // set_next_wake for resident program succeeds.
1497
        let mut hal = TestHal::new();
1✔
1498
        let mut transport = TestTransport::new();
1✔
1499
        let mut maps = MapStorage::new(4096);
1✔
1500
        let mut sleep = SleepManager::new(300, WakeReason::Scheduled);
1✔
1501
        let clock = TestClock(0);
1✔
1502
        let hmac = TestHmac;
1✔
1503
        let identity = default_identity();
1✔
1504
        let mut seq = 0u64;
1✔
1505
        let mut trace = Vec::new();
1✔
1506

1507
        with_test_context(
1✔
1508
            &mut hal,
1✔
1509
            &mut transport,
1✔
1510
            &mut maps,
1✔
1511
            &mut sleep,
1✔
1512
            &clock,
1✔
1513
            &hmac,
1✔
1514
            &identity,
1✔
1515
            &mut seq,
1✔
1516
            ProgramClass::Resident,
1✔
1517
            &mut trace,
1✔
1518
            || {
1✔
1519
                let result = helper_set_next_wake(10, 0, 0, 0, 0);
1✔
1520
                assert_eq!(result, 0);
1✔
1521
            },
1✔
1522
        );
1523
        assert_eq!(sleep.effective_sleep_s(), 10);
1✔
1524
    }
1✔
1525

1526
    #[test]
1527
    fn test_helper_set_next_wake_ephemeral_rejected() {
1✔
1528
        // T-N612: Ephemeral cannot call set_next_wake.
1529
        let mut hal = TestHal::new();
1✔
1530
        let mut transport = TestTransport::new();
1✔
1531
        let mut maps = MapStorage::new(4096);
1✔
1532
        let mut sleep = SleepManager::new(300, WakeReason::Scheduled);
1✔
1533
        let clock = TestClock(0);
1✔
1534
        let hmac = TestHmac;
1✔
1535
        let identity = default_identity();
1✔
1536
        let mut seq = 0u64;
1✔
1537
        let mut trace = Vec::new();
1✔
1538

1539
        with_test_context(
1✔
1540
            &mut hal,
1✔
1541
            &mut transport,
1✔
1542
            &mut maps,
1✔
1543
            &mut sleep,
1✔
1544
            &clock,
1✔
1545
            &hmac,
1✔
1546
            &identity,
1✔
1547
            &mut seq,
1✔
1548
            ProgramClass::Ephemeral,
1✔
1549
            &mut trace,
1✔
1550
            || {
1✔
1551
                let result = helper_set_next_wake(10, 0, 0, 0, 0);
1✔
1552
                assert_eq!(result as i64, -1);
1✔
1553
            },
1✔
1554
        );
1555
        assert_eq!(sleep.effective_sleep_s(), 300);
1✔
1556
    }
1✔
1557

1558
    #[test]
1559
    fn test_helper_bpf_trace_printk() {
1✔
1560
        // T-N613: trace_printk captures string.
1561
        let mut hal = TestHal::new();
1✔
1562
        let mut transport = TestTransport::new();
1✔
1563
        let mut maps = MapStorage::new(4096);
1✔
1564
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1565
        let clock = TestClock(0);
1✔
1566
        let hmac = TestHmac;
1✔
1567
        let identity = default_identity();
1✔
1568
        let mut seq = 0u64;
1✔
1569
        let mut trace = Vec::new();
1✔
1570

1571
        with_test_context(
1✔
1572
            &mut hal,
1✔
1573
            &mut transport,
1✔
1574
            &mut maps,
1✔
1575
            &mut sleep,
1✔
1576
            &clock,
1✔
1577
            &hmac,
1✔
1578
            &identity,
1✔
1579
            &mut seq,
1✔
1580
            ProgramClass::Resident,
1✔
1581
            &mut trace,
1✔
1582
            || {
1✔
1583
                let msg = b"hello";
1✔
1584
                let result =
1✔
1585
                    helper_bpf_trace_printk(msg.as_ptr() as u64, msg.len() as u64, 0, 0, 0);
1✔
1586
                assert_eq!(result, 0);
1✔
1587
            },
1✔
1588
        );
1589
        assert_eq!(trace, vec!["hello".to_string()]);
1✔
1590
    }
1✔
1591

1592
    #[test]
1593
    fn test_helper_send() {
1✔
1594
        // T-N604: send() produces an APP_DATA frame on the transport.
1595
        let mut hal = TestHal::new();
1✔
1596
        let mut transport = TestTransport::new();
1✔
1597
        let mut maps = MapStorage::new(4096);
1✔
1598
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1599
        let clock = TestClock(0);
1✔
1600
        let hmac = TestHmac;
1✔
1601
        let identity = default_identity();
1✔
1602
        let mut seq = 100u64;
1✔
1603
        let mut trace = Vec::new();
1✔
1604
        let blob: Vec<u8> = vec![0xAA, 0xBB];
1✔
1605

1606
        with_test_context(
1✔
1607
            &mut hal,
1✔
1608
            &mut transport,
1✔
1609
            &mut maps,
1✔
1610
            &mut sleep,
1✔
1611
            &clock,
1✔
1612
            &hmac,
1✔
1613
            &identity,
1✔
1614
            &mut seq,
1✔
1615
            ProgramClass::Resident,
1✔
1616
            &mut trace,
1✔
1617
            || {
1✔
1618
                let result = helper_send(blob.as_ptr() as u64, blob.len() as u64, 0, 0, 0);
1✔
1619
                assert_eq!(result, 0);
1✔
1620
            },
1✔
1621
        );
1622

1623
        assert_eq!(seq, 101);
1✔
1624
        assert_eq!(transport.outbound.len(), 1);
1✔
1625

1626
        // Decode and verify it's a valid APP_DATA frame
1627
        let decoded = decode_frame(&transport.outbound[0]).unwrap();
1✔
1628
        assert!(verify_frame(&decoded, &identity.psk, &TestHmac));
1✔
1629
        assert_eq!(decoded.header.msg_type, MSG_APP_DATA);
1✔
1630
        let msg = NodeMessage::decode(decoded.header.msg_type, &decoded.payload).unwrap();
1✔
1631
        match msg {
1✔
1632
            NodeMessage::AppData { blob: received } => assert_eq!(received, vec![0xAA, 0xBB]),
1✔
1633
            _ => panic!("expected AppData"),
×
1634
        }
1635
    }
1✔
1636

1637
    #[test]
1638
    fn test_helper_send_recv() {
1✔
1639
        // T-N605: send_recv sends APP_DATA and receives APP_DATA_REPLY.
1640
        let mut hal = TestHal::new();
1✔
1641
        let mut transport = TestTransport::new();
1✔
1642

1643
        // Pre-queue a valid reply
1644
        let identity = default_identity();
1✔
1645
        let reply_msg = GatewayMessage::AppDataReply {
1✔
1646
            blob: vec![0xCC, 0xDD],
1✔
1647
        };
1✔
1648
        let reply_cbor = reply_msg.encode().unwrap();
1✔
1649
        let reply_header = FrameHeader {
1✔
1650
            key_hint: identity.key_hint,
1✔
1651
            msg_type: MSG_APP_DATA_REPLY,
1✔
1652
            nonce: 100, // must match the seq we'll send with
1✔
1653
        };
1✔
1654
        let reply_frame =
1✔
1655
            encode_frame(&reply_header, &reply_cbor, &identity.psk, &TestHmac).unwrap();
1✔
1656
        transport.inbound.push_back(Some(reply_frame));
1✔
1657

1658
        let mut maps = MapStorage::new(4096);
1✔
1659
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1660
        let clock = TestClock(0);
1✔
1661
        let hmac = TestHmac;
1✔
1662
        let mut seq = 100u64;
1✔
1663
        let mut trace = Vec::new();
1✔
1664
        let blob = [0x01, 0x02];
1✔
1665
        let mut reply_buf = [0u8; 16];
1✔
1666

1667
        with_test_context(
1✔
1668
            &mut hal,
1✔
1669
            &mut transport,
1✔
1670
            &mut maps,
1✔
1671
            &mut sleep,
1✔
1672
            &clock,
1✔
1673
            &hmac,
1✔
1674
            &identity,
1✔
1675
            &mut seq,
1✔
1676
            ProgramClass::Resident,
1✔
1677
            &mut trace,
1✔
1678
            || {
1✔
1679
                let result = helper_send_recv(
1✔
1680
                    blob.as_ptr() as u64,
1✔
1681
                    blob.len() as u64,
1✔
1682
                    reply_buf.as_mut_ptr() as u64,
1✔
1683
                    reply_buf.len() as u64,
1✔
1684
                    0,
1685
                );
1686
                assert_eq!(result, 2); // 2 bytes received
1✔
1687
            },
1✔
1688
        );
1689

1690
        assert_eq!(&reply_buf[..2], &[0xCC, 0xDD]);
1✔
1691
        assert_eq!(seq, 101);
1✔
1692
    }
1✔
1693

1694
    #[test]
1695
    fn test_helper_send_recv_timeout() {
1✔
1696
        // T-N606: send_recv with no reply → negative return.
1697
        let mut hal = TestHal::new();
1✔
1698
        let mut transport = TestTransport::new();
1✔
1699
        transport.inbound.push_back(None); // timeout
1✔
1700

1701
        let mut maps = MapStorage::new(4096);
1✔
1702
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1703
        let clock = TestClock(0);
1✔
1704
        let hmac = TestHmac;
1✔
1705
        let identity = default_identity();
1✔
1706
        let mut seq = 50u64;
1✔
1707
        let mut trace = Vec::new();
1✔
1708
        let blob = [0x01];
1✔
1709
        let mut reply_buf = [0u8; 16];
1✔
1710

1711
        with_test_context(
1✔
1712
            &mut hal,
1✔
1713
            &mut transport,
1✔
1714
            &mut maps,
1✔
1715
            &mut sleep,
1✔
1716
            &clock,
1✔
1717
            &hmac,
1✔
1718
            &identity,
1✔
1719
            &mut seq,
1✔
1720
            ProgramClass::Resident,
1✔
1721
            &mut trace,
1✔
1722
            || {
1✔
1723
                let result = helper_send_recv(
1✔
1724
                    blob.as_ptr() as u64,
1✔
1725
                    blob.len() as u64,
1✔
1726
                    reply_buf.as_mut_ptr() as u64,
1✔
1727
                    reply_buf.len() as u64,
1✔
1728
                    0,
1729
                );
1730
                assert_eq!(result as i64, -1);
1✔
1731
            },
1✔
1732
        );
1733
    }
1✔
1734

1735
    // -- MapPtrIndex unit tests ---------------------------------------------
1736

1737
    #[test]
1738
    fn test_map_ptr_index_basic_insert_and_get() {
1✔
1739
        let mut idx = MapPtrIndex::new();
1✔
1740
        assert!(idx.insert(0x1000, 0).is_ok());
1✔
1741
        assert!(idx.insert(0x2000, 1).is_ok());
1✔
1742
        assert_eq!(idx.get(0x1000), Some(0));
1✔
1743
        assert_eq!(idx.get(0x2000), Some(1));
1✔
1744
        assert_eq!(idx.get(0x3000), None);
1✔
1745
    }
1✔
1746

1747
    #[test]
1748
    fn test_map_ptr_index_overflow_returns_error() {
1✔
1749
        let mut idx = MapPtrIndex::new();
1✔
1750
        for i in 0..MAX_MAPS {
16✔
1751
            assert!(
16✔
1752
                idx.insert(0x1000 + i as u64, i).is_ok(),
16✔
1753
                "insert {i} should succeed"
1754
            );
1755
        }
1756
        // MAX_MAPS+1 should fail with overflow
1757
        assert_eq!(
1✔
1758
            idx.insert(0xFFFF, MAX_MAPS),
1✔
1759
            Err(MapPtrInsertError::Overflow)
1760
        );
1761
    }
1✔
1762

1763
    #[test]
1764
    fn test_map_ptr_index_duplicate_returns_error() {
1✔
1765
        let mut idx = MapPtrIndex::new();
1✔
1766
        assert!(idx.insert(0x1000, 0).is_ok());
1✔
1767
        assert_eq!(idx.insert(0x1000, 1), Err(MapPtrInsertError::Duplicate),);
1✔
1768
        // Original mapping should be unchanged
1769
        assert_eq!(idx.get(0x1000), Some(0));
1✔
1770
    }
1✔
1771

1772
    #[test]
1773
    fn test_map_ptr_index_get_returns_first_match() {
1✔
1774
        let mut idx = MapPtrIndex::new();
1✔
1775
        assert!(idx.insert(0x1000, 0).is_ok());
1✔
1776
        assert!(idx.insert(0x2000, 1).is_ok());
1✔
1777
        assert!(idx.insert(0x3000, 2).is_ok());
1✔
1778
        assert_eq!(idx.get(0x2000), Some(1));
1✔
1779
    }
1✔
1780

1781
    // ===================================================================
1782
    // Gap 8 (ND-0601): Bus helpers available to ephemeral programs
1783
    // ===================================================================
1784

1785
    #[test]
1786
    fn test_bus_helpers_available_to_ephemeral() {
1✔
1787
        // ND-0601 AC3: Helpers are available to both resident and ephemeral
1788
        // programs. All existing bus tests use resident programs only.
1789
        let mut hal = TestHal::new();
1✔
1790
        hal.i2c_read_data = vec![0xAA, 0xBB];
1✔
1791
        hal.gpio_states[3] = 1;
1✔
1792
        hal.adc_values[1] = 1024;
1✔
1793
        let mut transport = TestTransport::new();
1✔
1794
        let mut maps = MapStorage::new(4096);
1✔
1795
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1796
        let clock = TestClock(0);
1✔
1797
        let hmac = TestHmac;
1✔
1798
        let identity = default_identity();
1✔
1799
        let mut seq = 0u64;
1✔
1800
        let mut trace = Vec::new();
1✔
1801
        let mut buf = [0u8; 2];
1✔
1802

1803
        with_test_context(
1✔
1804
            &mut hal,
1✔
1805
            &mut transport,
1✔
1806
            &mut maps,
1✔
1807
            &mut sleep,
1✔
1808
            &clock,
1✔
1809
            &hmac,
1✔
1810
            &identity,
1✔
1811
            &mut seq,
1✔
1812
            ProgramClass::Ephemeral, // ← ephemeral
1✔
1813
            &mut trace,
1✔
1814
            || {
1✔
1815
                // I2C read
1816
                let handle = crate::hal::i2c_handle(0, 0x48);
1✔
1817
                let result = helper_i2c_read(
1✔
1818
                    handle as u64,
1✔
1819
                    buf.as_mut_ptr() as u64,
1✔
1820
                    buf.len() as u64,
1✔
1821
                    0,
1822
                    0,
1823
                );
1824
                assert_eq!(result, 0, "i2c_read must work for ephemeral programs");
1✔
1825

1826
                // GPIO read
1827
                assert_eq!(
1✔
1828
                    helper_gpio_read(3, 0, 0, 0, 0),
1✔
1829
                    1,
1830
                    "gpio_read must work for ephemeral programs"
1831
                );
1832

1833
                // ADC read
1834
                assert_eq!(
1✔
1835
                    helper_adc_read(1, 0, 0, 0, 0),
1✔
1836
                    1024,
1837
                    "adc_read must work for ephemeral programs"
1838
                );
1839
            },
1✔
1840
        );
1841
        assert_eq!(buf, [0xAA, 0xBB]);
1✔
1842
    }
1✔
1843

1844
    #[test]
1845
    fn test_spi_transfer_available_to_ephemeral() {
1✔
1846
        // ND-0601 AC3: SPI helper also available to ephemeral programs.
1847
        let mut hal = TestHal::new();
1✔
1848
        let mut transport = TestTransport::new();
1✔
1849
        let mut maps = MapStorage::new(4096);
1✔
1850
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1851
        let clock = TestClock(0);
1✔
1852
        let hmac = TestHmac;
1✔
1853
        let identity = default_identity();
1✔
1854
        let mut seq = 0u64;
1✔
1855
        let mut trace = Vec::new();
1✔
1856
        let tx = [0xCA, 0xFE];
1✔
1857
        let mut rx = [0u8; 2];
1✔
1858

1859
        with_test_context(
1✔
1860
            &mut hal,
1✔
1861
            &mut transport,
1✔
1862
            &mut maps,
1✔
1863
            &mut sleep,
1✔
1864
            &clock,
1✔
1865
            &hmac,
1✔
1866
            &identity,
1✔
1867
            &mut seq,
1✔
1868
            ProgramClass::Ephemeral,
1✔
1869
            &mut trace,
1✔
1870
            || {
1✔
1871
                let handle = crate::hal::spi_handle(0);
1✔
1872
                let result = helper_spi_transfer(
1✔
1873
                    handle as u64,
1✔
1874
                    tx.as_ptr() as u64,
1✔
1875
                    rx.as_mut_ptr() as u64,
1✔
1876
                    tx.len() as u64,
1✔
1877
                    0,
1878
                );
1879
                assert_eq!(result, 0, "spi_transfer must work for ephemeral programs");
1✔
1880
            },
1✔
1881
        );
1882
        assert_eq!(rx, tx);
1✔
1883
    }
1✔
1884

1885
    // ===================================================================
1886
    // Gap 9 (ND-0604): delay_us max value enforcement
1887
    // ===================================================================
1888

1889
    #[test]
1890
    fn test_delay_us_max_value_rejected() {
1✔
1891
        // ND-0604 AC3: The firmware enforces a maximum delay value.
1892
        // No existing test calls delay_us with an excessive value.
1893
        let mut hal = TestHal::new();
1✔
1894
        let mut transport = TestTransport::new();
1✔
1895
        let mut maps = MapStorage::new(4096);
1✔
1896
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1897
        let clock = TestClock(0);
1✔
1898
        let hmac = TestHmac;
1✔
1899
        let identity = default_identity();
1✔
1900
        let mut seq = 0u64;
1✔
1901
        let mut trace = Vec::new();
1✔
1902

1903
        with_test_context(
1✔
1904
            &mut hal,
1✔
1905
            &mut transport,
1✔
1906
            &mut maps,
1✔
1907
            &mut sleep,
1✔
1908
            &clock,
1✔
1909
            &hmac,
1✔
1910
            &identity,
1✔
1911
            &mut seq,
1✔
1912
            ProgramClass::Resident,
1✔
1913
            &mut trace,
1✔
1914
            || {
1✔
1915
                // At the limit (1 second) — should succeed
1916
                assert_eq!(
1✔
1917
                    helper_delay_us(MAX_DELAY_US as u64, 0, 0, 0, 0),
1✔
1918
                    0,
1919
                    "delay_us at max value must succeed"
1920
                );
1921

1922
                // Exceeds max (1 second + 1 microsecond) — must return error
1923
                assert_eq!(
1✔
1924
                    helper_delay_us(MAX_DELAY_US as u64 + 1, 0, 0, 0, 0) as i64,
1✔
1925
                    -1,
1926
                    "delay_us exceeding max must return -1"
1927
                );
1928

1929
                // Way over max — must return error
1930
                assert_eq!(
1✔
1931
                    helper_delay_us(10_000_000, 0, 0, 0, 0) as i64,
1✔
1932
                    -1,
1933
                    "delay_us(10s) must return -1"
1934
                );
1935
            },
1✔
1936
        );
1937
    }
1✔
1938

1939
    // -- Gap 1: ND-0604 — delay_us maximum enforcement ----------------------
1940

1941
    #[test]
1942
    fn test_helper_delay_us_exceeds_max_rejected() {
1✔
1943
        // ND-0604: delay_us() values above the 1,000,000 µs cap must
1944
        // return -1. Without this, an uncapped delay hangs the node.
1945
        let mut hal = TestHal::new();
1✔
1946
        let mut transport = TestTransport::new();
1✔
1947
        let mut maps = MapStorage::new(4096);
1✔
1948
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1949
        let clock = TestClock(0);
1✔
1950
        let hmac = TestHmac;
1✔
1951
        let identity = default_identity();
1✔
1952
        let mut seq = 0u64;
1✔
1953
        let mut trace = Vec::new();
1✔
1954

1955
        with_test_context(
1✔
1956
            &mut hal,
1✔
1957
            &mut transport,
1✔
1958
            &mut maps,
1✔
1959
            &mut sleep,
1✔
1960
            &clock,
1✔
1961
            &hmac,
1✔
1962
            &identity,
1✔
1963
            &mut seq,
1✔
1964
            ProgramClass::Resident,
1✔
1965
            &mut trace,
1✔
1966
            || {
1✔
1967
                // Just above the 1-second cap → must fail.
1968
                assert_eq!(helper_delay_us(1_000_001, 0, 0, 0, 0), (-1i64) as u64);
1✔
1969
                // Way above → must fail.
1970
                assert_eq!(helper_delay_us(u64::MAX, 0, 0, 0, 0), (-1i64) as u64);
1✔
1971
            },
1✔
1972
        );
1973
    }
1✔
1974

1975
    #[test]
1976
    fn test_helper_delay_us_exact_max_succeeds() {
1✔
1977
        // ND-0604 boundary: exactly 1,000,000 µs must succeed.
1978
        let mut hal = TestHal::new();
1✔
1979
        let mut transport = TestTransport::new();
1✔
1980
        let mut maps = MapStorage::new(4096);
1✔
1981
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
1982
        let clock = TestClock(0);
1✔
1983
        let hmac = TestHmac;
1✔
1984
        let identity = default_identity();
1✔
1985
        let mut seq = 0u64;
1✔
1986
        let mut trace = Vec::new();
1✔
1987

1988
        with_test_context(
1✔
1989
            &mut hal,
1✔
1990
            &mut transport,
1✔
1991
            &mut maps,
1✔
1992
            &mut sleep,
1✔
1993
            &clock,
1✔
1994
            &hmac,
1✔
1995
            &identity,
1✔
1996
            &mut seq,
1✔
1997
            ProgramClass::Resident,
1✔
1998
            &mut trace,
1✔
1999
            || {
1✔
2000
                assert_eq!(helper_delay_us(1_000_000, 0, 0, 0, 0), 0);
1✔
2001
            },
1✔
2002
        );
2003
    }
1✔
2004

2005
    // -- Gap 2: ND-0601 — Bus helpers in ephemeral programs ------------------
2006

2007
    #[test]
2008
    fn test_helper_bus_helpers_ephemeral_succeed() {
1✔
2009
        // T-N931: All bus helper calls (I2C, SPI, GPIO, ADC) must succeed
2010
        // from an ephemeral program — behaviour identical to resident.
2011
        let mut hal = TestHal::new();
1✔
2012
        hal.i2c_read_data = vec![0x1A, 0x2B];
1✔
2013
        hal.gpio_states[5] = 1;
1✔
2014
        hal.adc_values[0] = 2048;
1✔
2015
        let mut transport = TestTransport::new();
1✔
2016
        let mut maps = MapStorage::new(4096);
1✔
2017
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2018
        let clock = TestClock(0);
1✔
2019
        let hmac = TestHmac;
1✔
2020
        let identity = default_identity();
1✔
2021
        let mut seq = 0u64;
1✔
2022
        let mut trace = Vec::new();
1✔
2023

2024
        let mut read_buf = [0u8; 2];
1✔
2025
        let write_data = [0x55u8; 2];
1✔
2026
        let mut wr_buf = [0u8; 2];
1✔
2027
        let mut spi_rx = [0u8; 2];
1✔
2028
        let spi_tx = [0xAA, 0xBB];
1✔
2029

2030
        with_test_context(
1✔
2031
            &mut hal,
1✔
2032
            &mut transport,
1✔
2033
            &mut maps,
1✔
2034
            &mut sleep,
1✔
2035
            &clock,
1✔
2036
            &hmac,
1✔
2037
            &identity,
1✔
2038
            &mut seq,
1✔
2039
            ProgramClass::Ephemeral, // ← ephemeral context
1✔
2040
            &mut trace,
1✔
2041
            || {
1✔
2042
                let handle = crate::hal::i2c_handle(0, 0x48);
1✔
2043

2044
                // I2C read — verify data populated
2045
                let r = helper_i2c_read(
1✔
2046
                    handle as u64,
1✔
2047
                    read_buf.as_mut_ptr() as u64,
1✔
2048
                    read_buf.len() as u64,
1✔
2049
                    0,
2050
                    0,
2051
                );
2052
                assert_eq!(r, 0, "i2c_read should succeed for ephemeral");
1✔
2053
                assert_eq!(read_buf, [0x1A, 0x2B], "i2c_read must populate buffer");
1✔
2054

2055
                // I2C write
2056
                let r = helper_i2c_write(
1✔
2057
                    handle as u64,
1✔
2058
                    write_data.as_ptr() as u64,
1✔
2059
                    write_data.len() as u64,
1✔
2060
                    0,
2061
                    0,
2062
                );
2063
                assert_eq!(r, 0, "i2c_write should succeed for ephemeral");
1✔
2064

2065
                // I2C write-read — verify read buffer populated
2066
                let w = [0x01u8];
1✔
2067
                let r = helper_i2c_write_read(
1✔
2068
                    handle as u64,
1✔
2069
                    w.as_ptr() as u64,
1✔
2070
                    w.len() as u64,
1✔
2071
                    wr_buf.as_mut_ptr() as u64,
1✔
2072
                    wr_buf.len() as u64,
1✔
2073
                );
2074
                assert_eq!(r, 0, "i2c_write_read should succeed for ephemeral");
1✔
2075
                assert_eq!(wr_buf, [0x1A, 0x2B], "i2c_write_read must fill read buf");
1✔
2076

2077
                // SPI transfer — verify echo (rx = tx)
2078
                let spi_h = crate::hal::spi_handle(0) as u64;
1✔
2079
                let r = helper_spi_transfer(
1✔
2080
                    spi_h,
1✔
2081
                    spi_tx.as_ptr() as u64,
1✔
2082
                    spi_rx.as_mut_ptr() as u64,
1✔
2083
                    spi_tx.len() as u64,
1✔
2084
                    0,
2085
                );
2086
                assert_eq!(r, 0, "spi_transfer should succeed for ephemeral");
1✔
2087
                assert_eq!(spi_rx, spi_tx, "spi_transfer must echo tx into rx");
1✔
2088

2089
                // GPIO read — verify pin state returned
2090
                let r = helper_gpio_read(5, 0, 0, 0, 0);
1✔
2091
                assert_eq!(
1✔
2092
                    r as i64, 1,
1✔
2093
                    "gpio_read should return pin state for ephemeral"
2094
                );
2095

2096
                // GPIO write — verify pin state changed
2097
                let r = helper_gpio_write(5, 0, 0, 0, 0);
1✔
2098
                assert_eq!(r, 0, "gpio_write should succeed for ephemeral");
1✔
2099
            },
1✔
2100
        );
2101

2102
        // Verify GPIO side effect persisted
2103
        assert_eq!(hal.gpio_states[5], 0, "gpio_write(5, 0) must clear pin");
1✔
2104

2105
        // ADC read in a separate context to confirm independence
2106
        let mut trace2 = Vec::new();
1✔
2107
        with_test_context(
1✔
2108
            &mut hal,
1✔
2109
            &mut transport,
1✔
2110
            &mut maps,
1✔
2111
            &mut sleep,
1✔
2112
            &clock,
1✔
2113
            &hmac,
1✔
2114
            &identity,
1✔
2115
            &mut seq,
1✔
2116
            ProgramClass::Ephemeral,
1✔
2117
            &mut trace2,
1✔
2118
            || {
1✔
2119
                let r = helper_adc_read(0, 0, 0, 0, 0);
1✔
2120
                assert_eq!(r as i64, 2048, "adc_read should return value for ephemeral");
1✔
2121
            },
1✔
2122
        );
2123
    }
1✔
2124

2125
    // -- Gap 3: ND-0603 — map_lookup_elem returns NULL on out-of-range key ---
2126

2127
    #[test]
2128
    fn test_helper_map_lookup_out_of_range_key_returns_null() {
1✔
2129
        // T-N932: map_lookup_elem on an out-of-range key (>= max_entries)
2130
        // must return 0 (NULL). BPF programs rely on this for NULL checks.
2131
        // Uses key = 4, which is the first out-of-range index for max_entries = 4.
2132
        let mut hal = TestHal::new();
1✔
2133
        let mut transport = TestTransport::new();
1✔
2134
        let mut maps = MapStorage::new(4096);
1✔
2135
        maps.allocate(&[MapDef {
1✔
2136
            map_type: 1,
1✔
2137
            key_size: 4,
1✔
2138
            value_size: 4,
1✔
2139
            max_entries: 4,
1✔
2140
        }])
1✔
2141
        .unwrap();
1✔
2142
        let map_ptr = maps.map_pointers()[0];
1✔
2143
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2144
        let clock = TestClock(0);
1✔
2145
        let hmac = TestHmac;
1✔
2146
        let identity = default_identity();
1✔
2147
        let mut seq = 0u64;
1✔
2148
        let mut trace = Vec::new();
1✔
2149

2150
        with_test_context(
1✔
2151
            &mut hal,
1✔
2152
            &mut transport,
1✔
2153
            &mut maps,
1✔
2154
            &mut sleep,
1✔
2155
            &clock,
1✔
2156
            &hmac,
1✔
2157
            &identity,
1✔
2158
            &mut seq,
1✔
2159
            ProgramClass::Resident,
1✔
2160
            &mut trace,
1✔
2161
            || {
1✔
2162
                // In-range key that was never explicitly written: for
2163
                // BPF_MAP_TYPE_ARRAY, entries are zero-initialized and
2164
                // always present, so lookup must return a non-NULL pointer.
2165
                let in_range_key: u32 = 0;
1✔
2166
                let in_range_ptr =
1✔
2167
                    helper_map_lookup_elem(map_ptr, &in_range_key as *const u32 as u64, 0, 0, 0);
1✔
2168
                assert_ne!(
1✔
2169
                    in_range_ptr, 0,
2170
                    "lookup of in-range never-updated key must return non-NULL (zero-initialized)"
2171
                );
2172

2173
                // Key 4 is the first out-of-range index (max_entries = 4,
2174
                // valid indices are 0..3) — lookup must return NULL (0).
2175
                let key: u32 = 4;
1✔
2176
                let ptr = helper_map_lookup_elem(map_ptr, &key as *const u32 as u64, 0, 0, 0);
1✔
2177
                assert_eq!(ptr, 0, "lookup of out-of-range key must return NULL");
1✔
2178
            },
1✔
2179
        );
2180
    }
1✔
2181

2182
    // -- Gap 6: set_next_wake min() semantics via helper dispatch -------------
2183

2184
    #[test]
2185
    fn test_helper_set_next_wake_clamped_to_base_interval() {
1✔
2186
        // bpf-env §6.4: set_next_wake cannot extend beyond the
2187
        // gateway-configured base interval. min(600, 300) == 300.
2188
        let mut hal = TestHal::new();
1✔
2189
        let mut transport = TestTransport::new();
1✔
2190
        let mut maps = MapStorage::new(4096);
1✔
2191
        let mut sleep = SleepManager::new(300, WakeReason::Scheduled);
1✔
2192
        let clock = TestClock(0);
1✔
2193
        let hmac = TestHmac;
1✔
2194
        let identity = default_identity();
1✔
2195
        let mut seq = 0u64;
1✔
2196
        let mut trace = Vec::new();
1✔
2197

2198
        with_test_context(
1✔
2199
            &mut hal,
1✔
2200
            &mut transport,
1✔
2201
            &mut maps,
1✔
2202
            &mut sleep,
1✔
2203
            &clock,
1✔
2204
            &hmac,
1✔
2205
            &identity,
1✔
2206
            &mut seq,
1✔
2207
            ProgramClass::Resident,
1✔
2208
            &mut trace,
1✔
2209
            || {
1✔
2210
                // Request 600 s when base is 300 s — helper should succeed
2211
                // but effective sleep must be clamped to base interval.
2212
                let result = helper_set_next_wake(600, 0, 0, 0, 0);
1✔
2213
                assert_eq!(result, 0, "set_next_wake should return success");
1✔
2214
            },
1✔
2215
        );
2216
        assert_eq!(
1✔
2217
            sleep.effective_sleep_s(),
1✔
2218
            300,
2219
            "effective sleep must be min(600, 300) = 300"
2220
        );
2221
        assert!(
1✔
2222
            !sleep.will_wake_early(),
1✔
2223
            "requesting longer than base should not count as early wake"
2224
        );
2225
    }
1✔
2226

2227
    // -- Log-level tests (ND-1010 / T-N1015) ----------------------------------
2228

2229
    #[cfg(debug_assertions)]
2230
    use crate::test_log_capture;
2231

2232
    // Skipped in release builds — `debug!()` is stripped at compile time by
2233
    // `release_max_level_warn`.
2234
    #[cfg(debug_assertions)]
2235
    #[test]
2236
    fn test_helper_i2c_read_emits_debug_log() {
1✔
2237
        // ND-1010 / T-N1015: I/O helpers emit DEBUG-level logs.
2238
        test_log_capture::init();
1✔
2239
        test_log_capture::drain_log_records(); // discard prior records
1✔
2240

2241
        let mut hal = TestHal::new();
1✔
2242
        let mut transport = TestTransport::new();
1✔
2243
        let mut maps = MapStorage::new(4096);
1✔
2244
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2245
        let clock = TestClock(0);
1✔
2246
        let hmac = TestHmac;
1✔
2247
        let identity = default_identity();
1✔
2248
        let mut seq = 0u64;
1✔
2249
        let mut trace = Vec::new();
1✔
2250
        let mut buf = [0u8; 2];
1✔
2251

2252
        with_test_context(
1✔
2253
            &mut hal,
1✔
2254
            &mut transport,
1✔
2255
            &mut maps,
1✔
2256
            &mut sleep,
1✔
2257
            &clock,
1✔
2258
            &hmac,
1✔
2259
            &identity,
1✔
2260
            &mut seq,
1✔
2261
            ProgramClass::Resident,
1✔
2262
            &mut trace,
1✔
2263
            || {
1✔
2264
                let handle = crate::hal::i2c_handle(0, 0x48);
1✔
2265
                helper_i2c_read(
1✔
2266
                    handle as u64,
1✔
2267
                    buf.as_mut_ptr() as u64,
1✔
2268
                    buf.len() as u64,
1✔
2269
                    0,
2270
                    0,
2271
                );
2272
            },
1✔
2273
        );
2274

2275
        let records = test_log_capture::drain_log_records();
1✔
2276
        assert!(
1✔
2277
            records
1✔
2278
                .iter()
1✔
2279
                .any(|(level, msg)| *level == log::Level::Debug
1✔
2280
                    && msg.contains("bpf helper i2c_read")
1✔
2281
                    && msg.contains("result=")),
1✔
2282
            "expected DEBUG log for i2c_read helper, got: {:?}",
2283
            records
2284
        );
2285
    }
1✔
2286

2287
    #[cfg(debug_assertions)]
2288
    #[test]
2289
    fn test_helper_gpio_read_emits_debug_log() {
1✔
2290
        // ND-1010 / T-N1015: gpio_read emits a DEBUG log with result.
2291
        test_log_capture::init();
1✔
2292
        test_log_capture::drain_log_records();
1✔
2293

2294
        let mut hal = TestHal::new();
1✔
2295
        hal.gpio_states[3] = 1;
1✔
2296
        let mut transport = TestTransport::new();
1✔
2297
        let mut maps = MapStorage::new(4096);
1✔
2298
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2299
        let clock = TestClock(0);
1✔
2300
        let hmac = TestHmac;
1✔
2301
        let identity = default_identity();
1✔
2302
        let mut seq = 0u64;
1✔
2303
        let mut trace = Vec::new();
1✔
2304

2305
        with_test_context(
1✔
2306
            &mut hal,
1✔
2307
            &mut transport,
1✔
2308
            &mut maps,
1✔
2309
            &mut sleep,
1✔
2310
            &clock,
1✔
2311
            &hmac,
1✔
2312
            &identity,
1✔
2313
            &mut seq,
1✔
2314
            ProgramClass::Resident,
1✔
2315
            &mut trace,
1✔
2316
            || {
1✔
2317
                helper_gpio_read(3, 0, 0, 0, 0);
1✔
2318
            },
1✔
2319
        );
2320

2321
        let records = test_log_capture::drain_log_records();
1✔
2322
        assert!(
1✔
2323
            records
1✔
2324
                .iter()
1✔
2325
                .any(|(level, msg)| *level == log::Level::Debug
1✔
2326
                    && msg.contains("bpf helper gpio_read")
1✔
2327
                    && msg.contains("result=")),
1✔
2328
            "expected DEBUG log for gpio_read helper, got: {:?}",
2329
            records
2330
        );
2331
    }
1✔
2332

2333
    #[cfg(debug_assertions)]
2334
    #[test]
2335
    fn test_non_io_helper_does_not_emit_debug_log() {
1✔
2336
        // ND-1010 AC #2: non-I/O helpers must NOT emit DEBUG logs.
2337
        test_log_capture::init();
1✔
2338
        test_log_capture::drain_log_records();
1✔
2339

2340
        let mut hal = TestHal::new();
1✔
2341
        let mut transport = TestTransport::new();
1✔
2342
        let mut maps = MapStorage::new(4096);
1✔
2343
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2344
        let clock = TestClock(1_000);
1✔
2345
        let hmac = TestHmac;
1✔
2346
        let identity = default_identity();
1✔
2347
        let mut seq = 0u64;
1✔
2348
        let mut trace = Vec::new();
1✔
2349

2350
        with_test_context(
1✔
2351
            &mut hal,
1✔
2352
            &mut transport,
1✔
2353
            &mut maps,
1✔
2354
            &mut sleep,
1✔
2355
            &clock,
1✔
2356
            &hmac,
1✔
2357
            &identity,
1✔
2358
            &mut seq,
1✔
2359
            ProgramClass::Resident,
1✔
2360
            &mut trace,
1✔
2361
            || {
1✔
2362
                // Call several non-I/O helpers.
2363
                helper_get_time(0, 0, 0, 0, 0);
1✔
2364
                helper_get_battery_mv(0, 0, 0, 0, 0);
1✔
2365
                helper_delay_us(10, 0, 0, 0, 0);
1✔
2366
            },
1✔
2367
        );
2368

2369
        let records = test_log_capture::drain_log_records();
1✔
2370
        let has_debug_helper_log = records
1✔
2371
            .iter()
1✔
2372
            .any(|(level, msg)| *level == log::Level::Debug && msg.contains("bpf helper"));
1✔
2373
        assert!(
1✔
2374
            !has_debug_helper_log,
1✔
2375
            "non-I/O helpers must not emit DEBUG 'bpf helper' logs, got: {:?}",
2376
            records
2377
        );
2378
    }
1✔
2379

2380
    // -- AEAD-path tests (feature-gated) ------------------------------------
2381

2382
    #[cfg(feature = "aes-gcm-codec")]
2383
    #[allow(clippy::too_many_arguments)]
2384
    fn with_test_context_aead<F, R>(
2✔
2385
        hal: &mut TestHal,
2✔
2386
        transport: &mut TestTransport,
2✔
2387
        map_storage: &mut MapStorage,
2✔
2388
        sleep_mgr: &mut SleepManager,
2✔
2389
        clock: &TestClock,
2✔
2390
        hmac: &TestHmac,
2✔
2391
        identity: &NodeIdentity,
2✔
2392
        seq: &mut u64,
2✔
2393
        program_class: ProgramClass,
2✔
2394
        trace_log: &mut Vec<String>,
2✔
2395
        aead: &(dyn sonde_protocol::AeadProvider + 'static),
2✔
2396
        sha: &(dyn sonde_protocol::Sha256Provider + 'static),
2✔
2397
        f: F,
2✔
2398
    ) -> R
2✔
2399
    where
2✔
2400
        F: FnOnce() -> R,
2✔
2401
    {
2402
        unsafe {
2✔
2403
            install_aead(
2✔
2404
                hal as *mut TestHal as *mut dyn Hal,
2✔
2405
                transport as *mut TestTransport as *mut dyn Transport,
2✔
2406
                map_storage as *mut MapStorage,
2✔
2407
                sleep_mgr as *mut SleepManager,
2✔
2408
                clock as *const TestClock as *const dyn Clock,
2✔
2409
                hmac as *const TestHmac as *const dyn HmacProvider,
2✔
2410
                identity as *const NodeIdentity,
2✔
2411
                seq as *mut u64,
2✔
2412
                program_class,
2✔
2413
                trace_log as *mut Vec<String>,
2✔
2414
                1_710_000_000_000,
2✔
2415
                100,
2✔
2416
                3300,
2✔
2417
                aead,
2✔
2418
                sha,
2✔
2419
            );
2✔
2420
        }
2✔
2421
        let _guard = DispatchGuard;
2✔
2422
        f()
2✔
2423
    }
2✔
2424

2425
    #[test]
2426
    #[cfg(feature = "aes-gcm-codec")]
2427
    fn test_helper_send_aead() {
1✔
2428
        use sonde_protocol::{decode_frame_aead, open_frame};
2429

2430
        let mut hal = TestHal::new();
1✔
2431
        let mut transport = TestTransport::new();
1✔
2432
        let mut maps = MapStorage::new(4096);
1✔
2433
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2434
        let clock = TestClock(0);
1✔
2435
        let hmac = TestHmac;
1✔
2436
        let identity = default_identity();
1✔
2437
        let mut seq = 100u64;
1✔
2438
        let mut trace = Vec::new();
1✔
2439
        let blob: Vec<u8> = vec![0xAA, 0xBB];
1✔
2440

2441
        let aead = crate::node_aead::NodeAead;
1✔
2442
        let sha = crate::crypto::SoftwareSha256;
1✔
2443

2444
        with_test_context_aead(
1✔
2445
            &mut hal,
1✔
2446
            &mut transport,
1✔
2447
            &mut maps,
1✔
2448
            &mut sleep,
1✔
2449
            &clock,
1✔
2450
            &hmac,
1✔
2451
            &identity,
1✔
2452
            &mut seq,
1✔
2453
            ProgramClass::Resident,
1✔
2454
            &mut trace,
1✔
2455
            &aead,
1✔
2456
            &sha,
1✔
2457
            || {
1✔
2458
                let result = helper_send(blob.as_ptr() as u64, blob.len() as u64, 0, 0, 0);
1✔
2459
                assert_eq!(result, 0, "helper_send should succeed");
1✔
2460
            },
1✔
2461
        );
2462

2463
        assert_eq!(seq, 101, "sequence should advance");
1✔
2464
        assert_eq!(transport.outbound.len(), 1);
1✔
2465

2466
        // Verify the frame is AEAD-authenticated (not HMAC)
2467
        let frame = &transport.outbound[0];
1✔
2468
        let decoded = decode_frame_aead(frame).expect("should be valid AEAD frame");
1✔
2469
        assert_eq!(decoded.header.msg_type, MSG_APP_DATA);
1✔
2470
        let plaintext =
1✔
2471
            open_frame(&decoded, &identity.psk, &aead, &sha).expect("AEAD decrypt should succeed");
1✔
2472
        let msg = NodeMessage::decode(MSG_APP_DATA, &plaintext).unwrap();
1✔
2473
        match msg {
1✔
2474
            NodeMessage::AppData { blob: received } => assert_eq!(received, vec![0xAA, 0xBB]),
1✔
NEW
2475
            _ => panic!("expected AppData"),
×
2476
        }
2477
    }
1✔
2478

2479
    #[test]
2480
    #[cfg(feature = "aes-gcm-codec")]
2481
    fn test_helper_send_recv_aead() {
1✔
2482
        use sonde_protocol::{encode_frame_aead, FrameHeader, GatewayMessage, MSG_APP_DATA_REPLY};
2483

2484
        let mut hal = TestHal::new();
1✔
2485
        let mut transport = TestTransport::new();
1✔
2486
        let mut maps = MapStorage::new(4096);
1✔
2487
        let mut sleep = SleepManager::new(60, WakeReason::Scheduled);
1✔
2488
        let clock = TestClock(0);
1✔
2489
        let hmac = TestHmac;
1✔
2490
        let identity = default_identity();
1✔
2491
        let mut seq = 100u64;
1✔
2492
        let mut trace = Vec::new();
1✔
2493

2494
        let aead = crate::node_aead::NodeAead;
1✔
2495
        let sha = crate::crypto::SoftwareSha256;
1✔
2496

2497
        // Pre-queue an AEAD APP_DATA_REPLY for the transport to return.
2498
        let reply_msg = GatewayMessage::AppDataReply {
1✔
2499
            blob: vec![0xCC, 0xDD],
1✔
2500
        };
1✔
2501
        let reply_cbor = reply_msg.encode().unwrap();
1✔
2502
        let reply_header = FrameHeader {
1✔
2503
            key_hint: identity.key_hint,
1✔
2504
            msg_type: MSG_APP_DATA_REPLY,
1✔
2505
            nonce: 100, // must match the seq used for the outbound APP_DATA
1✔
2506
        };
1✔
2507
        let reply_frame =
1✔
2508
            encode_frame_aead(&reply_header, &reply_cbor, &identity.psk, &aead, &sha).unwrap();
1✔
2509
        transport.inbound.push_back(Some(reply_frame));
1✔
2510

2511
        let blob: Vec<u8> = vec![0xAA, 0xBB];
1✔
2512
        let mut reply_buf = [0u8; 64];
1✔
2513

2514
        with_test_context_aead(
1✔
2515
            &mut hal,
1✔
2516
            &mut transport,
1✔
2517
            &mut maps,
1✔
2518
            &mut sleep,
1✔
2519
            &clock,
1✔
2520
            &hmac,
1✔
2521
            &identity,
1✔
2522
            &mut seq,
1✔
2523
            ProgramClass::Resident,
1✔
2524
            &mut trace,
1✔
2525
            &aead,
1✔
2526
            &sha,
1✔
2527
            || {
1✔
2528
                let result = helper_send_recv(
1✔
2529
                    blob.as_ptr() as u64,
1✔
2530
                    blob.len() as u64,
1✔
2531
                    reply_buf.as_mut_ptr() as u64,
1✔
2532
                    reply_buf.len() as u64,
1✔
2533
                    1000,
2534
                );
2535
                assert_eq!(result, 2, "should return 2 bytes of reply");
1✔
2536
            },
1✔
2537
        );
2538

2539
        assert_eq!(seq, 101);
1✔
2540
        assert_eq!(&reply_buf[..2], &[0xCC, 0xDD]);
1✔
2541
    }
1✔
2542
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc