Repository navigation
Expand file tree
/
Copy pathdispatch.rs
More file actions
1289 lines (1232 loc) · 54.4 KB
/
Copy pathdispatch.rs
File metadata and controls
1289 lines (1232 loc) · 54.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! The panic-free syscall dispatcher.
//!
//! [`dispatch`] decodes the `x8` syscall number, validates the caller's
//! capabilities, performs the operation through an existing kernel primitive,
//! and produces a [`SyscallEffect`] for the BSP trampoline to apply. It is the
//! single most security-sensitive control-flow join in the kernel, so it is
//! held to one hard rule from [ADR-0030][adr-0030] / B0's hardening pattern:
//!
//! > **No path may `panic!` / `unwrap` / `expect` on any register-supplied
//! > input.** Every failure — bad number, missing/stale/wrong-kind capability,
//! > out-of-bounds pointer — returns a typed [`SyscallError`] as a value.
//!
//! The dispatcher operates on the **data plane** (capabilities, IPC, console,
//! user memory) and is pure, host-testable Rust over kernel state references.
//! The two **control-plane** syscalls (`task_yield` / `task_exit`) return a
//! [`SyscallEffect`] directive instead of touching the scheduler directly,
//! because the scheduler is raw-pointer-wired per [ADR-0021][adr-0021] and
//! generic over the BSP CPU — keeping them as directives keeps `dispatch`
//! testable without a live scheduler and matches [ADR-0031][adr-0031]'s "B5
//! lands the dispatch; real EL0 yield/termination is B6" split.
//!
//! Every object-naming syscall performs a capability check before any effect
//! ([P1 / P4][principles]): `send` / `recv` validate the endpoint capability
//! inside [`ipc_send`] / [`ipc_recv`]; `console_write` validates a debug-console
//! capability here before a single byte is emitted. `task_yield` / `task_exit`
//! act only on the trusted current-task identity (no object-capability argument).
//!
//! [adr-0030]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0030-syscall-abi.md
//! [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md
//! [adr-0021]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0021-raw-pointer-scheduler-ipc-bridge.md
//! [principles]: https://github.com/HodeTech/Tyrne/blob/main/docs/standards/architectural-principles.md
use tyrne_hal::{Console, Mmu};
use crate::cap::{CapError, CapHandle, CapKind, CapRights, CapabilityTable};
use crate::ipc::{ipc_recv, ipc_send, IpcQueues};
use crate::obj::EndpointArena;
use super::abi::{
decode_required_cap_handle, decode_send_message, encode_recv_outcome, encode_send_outcome,
SyscallArgs, SyscallEffect, SyscallNumber, SyscallReturn,
};
use super::error::SyscallError;
use super::user_access::{copy_from_user, probe_user_pages, UserAccessWindow};
/// Maximum bytes `console_write` stages through its kernel stack buffer per
/// chunk. The handler validates the whole `[ptr, ptr + len)` range up front,
/// then copies and emits in chunks of this size — bounding the kernel-stack
/// footprint regardless of the userspace length argument.
const CONSOLE_WRITE_CHUNK: usize = 256;
/// The kernel state a single syscall dispatch reads and mutates.
///
/// The BSP trampoline builds this from its statics (the active task's IPC
/// arena / queues, the caller's capability table, the console, and the active
/// address space's user-access window) and hands it to [`dispatch`] for the
/// duration of one syscall. The borrows live only across the dispatch call —
/// they never cross a context switch, honouring the [ADR-0021][adr-0021]
/// no-`&mut`-across-switch discipline (the data-plane syscalls do not switch;
/// the control-plane ones return a directive *before* any switch happens).
///
/// `caller_table` is the **current task's** capability table. In B5 the only
/// `SVC` comes from an EL1 kernel-stub, so the BSP passes a dedicated stub
/// table; B6 wires the scheduler's current-task table once a real EL0 task
/// exists (gate #3 / T-026). Either way the dispatcher never lets a syscall
/// name a capability outside this one table — the per-subject unforgeability
/// [ADR-0014][adr-0014] guarantees.
///
/// `mmu` + `task_as` are the translation surface `console_write`'s copy-from-user
/// resolves user pointers through (gate #1 / [ADR-0038][adr-0038]): every user
/// page is translated through the running task's own address space and checked
/// for `USER` access before a byte is read. Generic over `M: Mmu` so the
/// dispatcher stays architecture-agnostic and host-testable against `FakeMmu`.
///
/// [adr-0021]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0021-raw-pointer-scheduler-ipc-bridge.md
/// [adr-0014]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0014-capability-representation.md
/// [adr-0038]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0038-mmu-translate-and-user-access.md
pub struct SyscallContext<'a, M: Mmu> {
/// The endpoint kernel-object arena `send` / `recv` resolve handles against.
pub ep_arena: &'a mut EndpointArena,
/// The IPC waiter-state queues `send` / `recv` advance.
pub queues: &'a mut IpcQueues,
/// The calling task's capability table — the only table any syscall in this
/// dispatch may name a capability in.
pub caller_table: &'a mut CapabilityTable,
/// The debug console `console_write` emits to after its capability check.
pub console: &'a dyn Console,
/// The active address space's user-accessible window — the cheap range
/// first-gate `console_write`'s copy-from-user validates against.
pub user_window: UserAccessWindow,
/// The MMU used to translate user pointers per page (gate #1).
pub mmu: &'a M,
/// The running task's address space — the translation regime user pointers
/// are resolved through. Sourced from the scheduler's current task in B6
/// (gate #3 / T-026); in B5 it is the EL1 stub's bootstrap AS.
pub task_as: &'a M::AddressSpace,
/// Whether a running EL0 task is current (gate #3 / T-026). The BSP sets it
/// `true` only when the running task's capability table, user-access window,
/// and (generation-checked) address space **all** resolve from the scheduler
/// — the same all-or-nothing unit as the data-plane context; any incomplete
/// binding yields `false`. The **control-plane**
/// syscalls (`task_yield` / `task_exit`) act on the trusted current-task
/// identity ([ADR-0031][adr-0031]) and consult **no** capability, so the
/// empty fail-closed `caller_table` cannot guard them — the dispatcher
/// instead rejects them with `InvalidHandle` when this is `false` (H2). A
/// data-plane syscall with no current task fails closed via the empty table
/// regardless of this flag.
///
/// [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md
pub has_current_task: bool,
}
/// Decode and execute one syscall, returning the trampoline's next action.
///
/// **Panic-free on every syscall input.** An unrecognised number (including `0`
/// and the debug-gated `console_write` in release) yields
/// [`SyscallError::BadSyscallNumber`]; every capability / pointer failure yields
/// a typed [`SyscallError`] as a value. No register-supplied value can drive
/// this function to `panic!` / `unwrap` / `expect`.
///
/// Scope: this property is **syscall-input-scoped** — it covers the
/// dispatcher's response to the register frame of an `SVC`. It is distinct from
/// EL0 **execution-time faults**: a non-`SVC` synchronous fault (illegal
/// instruction, unmapped deref by running EL0 code) is *not* handled here and in
/// v1 routes to the kernel panic handler — a denial-of-self (the faulting task
/// harms only itself), not an escalation. Containing such faults is the deferred
/// K3-4 item.
#[must_use]
pub fn dispatch<M: Mmu>(ctx: &mut SyscallContext<'_, M>, args: SyscallArgs) -> SyscallEffect {
let Some(number) = SyscallNumber::decode(args.number) else {
// Number 0 (reserved-invalid), out-of-range, or console_write in a
// non-debug build (the release debug-gate): no capability is touched.
return SyscallEffect::Resume(SyscallReturn::error(SyscallError::BadSyscallNumber));
};
match number {
SyscallNumber::Send => SyscallEffect::Resume(sys_send(ctx, args.args)),
SyscallNumber::Recv => SyscallEffect::Resume(sys_recv(ctx, args.args)),
// Control-plane: act on the caller's own task. These consult no
// capability, so the empty fail-closed `caller_table` does not guard
// them — gate #3 (T-026, H2) rejects them here when no EL0 task is
// current (nothing to yield / exit). With a current task they return
// the directive the BSP applies (real `yield_now` / termination is the
// B6 wire-up).
SyscallNumber::TaskYield => {
if ctx.has_current_task {
SyscallEffect::Reschedule
} else {
SyscallEffect::Resume(SyscallReturn::error(SyscallError::from(
CapError::InvalidHandle,
)))
}
}
SyscallNumber::TaskExit => {
if ctx.has_current_task {
SyscallEffect::Terminate(args.args[0])
} else {
SyscallEffect::Resume(SyscallReturn::error(SyscallError::from(
CapError::InvalidHandle,
)))
}
}
SyscallNumber::ConsoleWrite => SyscallEffect::Resume(sys_console_write(ctx, args.args)),
}
}
/// `send` (number `1`): `ipc_send` on the endpoint capability in `x0`.
///
/// `x0` = endpoint cap handle, `x1` = `msg.label`, `x2`–`x4` = `msg.params`,
/// `x5` = transfer cap handle (or the null sentinel). The endpoint capability
/// check (`SEND` right, right kind, live object) happens inside [`ipc_send`];
/// its [`IpcError`][crate::ipc::IpcError] composes into [`SyscallError::Ipc`].
fn sys_send<M: Mmu>(ctx: &mut SyscallContext<'_, M>, args: [u64; 6]) -> SyscallReturn {
let ep_cap = decode_required_cap_handle(args[0]);
let msg = decode_send_message(args);
let transfer = super::abi::decode_cap_handle(args[5]);
// Disjoint field reborrows: ep_arena / queues / caller_table are distinct
// fields, so the three `&mut` reborrows do not alias.
match ipc_send(
&mut *ctx.ep_arena,
&mut *ctx.queues,
ep_cap,
&mut *ctx.caller_table,
msg,
transfer,
) {
Ok(outcome) => SyscallReturn::ok().with_payload::<0>(encode_send_outcome(outcome)),
Err(e) => SyscallReturn::error(SyscallError::from(e)),
}
}
/// `recv` (number `2`): `ipc_recv` on the endpoint capability in `x0`.
///
/// `x0` = endpoint cap handle. On success the message + optional transferred
/// capability pack into `x1`–`x6` per [`encode_recv_outcome`]. The endpoint
/// capability check (`RECV` right, right kind, live object) happens inside
/// [`ipc_recv`]; its error composes into [`SyscallError::Ipc`].
fn sys_recv<M: Mmu>(ctx: &mut SyscallContext<'_, M>, args: [u64; 6]) -> SyscallReturn {
let ep_cap = decode_required_cap_handle(args[0]);
match ipc_recv(
&mut *ctx.ep_arena,
&mut *ctx.queues,
ep_cap,
&mut *ctx.caller_table,
) {
Ok(outcome) => encode_recv_outcome(outcome),
Err(e) => SyscallReturn::error(SyscallError::from(e)),
}
}
/// `console_write` (number `5`, debug-gated): emit a user buffer to the console.
///
/// `x0` = debug-console cap handle, `x1` = user VA of the buffer, `x2` = length.
/// Two independent gates, in order: (1) the **capability gate** — the cap must
/// resolve, be [`CapKind::DebugConsole`], and carry [`CapRights::CONSOLE_WRITE`]
/// (all builds; the [P1 / P4][principles] authority check); (2) the **debug
/// gate** — number `5` only reaches here in a debug build ([`SyscallNumber::decode`]
/// returns `None` for it in release). Only after the capability check passes is
/// the buffer range validated and copied through [`copy_from_user`]; the raw
/// user pointer is never dereferenced before both the cap check and the range
/// check succeed.
///
/// [principles]: https://github.com/HodeTech/Tyrne/blob/main/docs/standards/architectural-principles.md
#[allow(
clippy::cast_possible_truncation,
reason = "Tyrne targets are 64-bit (aarch64 kernel / x86-64 host tests); \
usize == u64, so the u64 register words → usize casts are lossless"
)]
fn sys_console_write<M: Mmu>(ctx: &mut SyscallContext<'_, M>, args: [u64; 6]) -> SyscallReturn {
let cons_cap = decode_required_cap_handle(args[0]);
let ptr = args[1] as usize;
let len = args[2] as usize;
// Gate 1 — capability (authority). Validate before any output or any read
// of the user buffer; a stale / wrong-kind / no-CONSOLE_WRITE cap returns a
// typed Cap-family error with the console untouched.
if let Err(e) = validate_debug_console_cap(&*ctx.caller_table, cons_cap) {
return SyscallReturn::error(SyscallError::from(e));
}
// Gate 2 — range (cheap first gate). Validate the WHOLE range up front.
if let Err(e) = ctx.user_window.validate(ptr, len) {
return SyscallReturn::error(e);
}
// Gate 3 — per-page translation (gate #1, ADR-0038). PROBE every page the
// whole buffer spans (translate through the task's AS + require USER) BEFORE
// emitting any chunk, so a faulting buffer — including an in-window kernel
// (non-USER) VA — emits *nothing* (the confused-deputy defence + all-or-
// nothing, §Simulation rows 1/4). The per-chunk `copy_from_user` below
// re-validates + re-probes its own chunk for self-containment; this
// whole-range probe is what bounds the multi-chunk *emit*.
if len > 0 {
if let Err(e) = probe_user_pages(
ctx.mmu,
ctx.task_as,
ptr,
len,
/* require_write */ false,
) {
return SyscallReturn::error(e);
}
}
// Copy + emit in bounded chunks through a kernel stack buffer.
//
// Unreachability of the three defensive error arms below (kept for totality,
// never hit on this path): the up-front `validate(ptr, len)` proved that
// `ptr + len` does not overflow `usize` AND that `[ptr, ptr + len)` is wholly
// inside the active window. The loop invariant `0 <= offset < len` then gives
// `ptr + offset <= ptr + len` (no overflow → `checked_add` is `Some`) and
// `[chunk_ptr, chunk_ptr + chunk) ⊆ [ptr, ptr + len)` (a sub-range of the
// validated whole → `copy_from_user`'s re-validation is `Ok`), and
// `offset + chunk <= len` (no overflow → `checked_add` is `Some`). The arms
// exist only so the function is total even if a future refactor weakens the
// up-front check.
let mut buf = [0u8; CONSOLE_WRITE_CHUNK];
let mut offset: usize = 0;
while offset < len {
// `wrapping_sub` cannot wrap (offset < len) and satisfies the
// arithmetic-side-effects lint; `min` bounds the chunk to the buffer.
let remaining = len.wrapping_sub(offset);
let chunk = core::cmp::min(remaining, CONSOLE_WRITE_CHUNK);
let Some(chunk_ptr) = ptr.checked_add(offset) else {
return SyscallReturn::error(SyscallError::FaultAddress);
};
if let Err(e) = copy_from_user(
ctx.mmu,
ctx.task_as,
&ctx.user_window,
chunk_ptr,
&mut buf[..chunk],
) {
return SyscallReturn::error(e);
}
ctx.console.write_bytes(&buf[..chunk]);
let Some(next) = offset.checked_add(chunk) else {
return SyscallReturn::error(SyscallError::FaultAddress);
};
offset = next;
}
// x1 = bytes written (all of them, on success).
SyscallReturn::ok().with_payload::<0>(len as u64)
}
/// Validate that `cons_cap` authorises a console write: it must resolve in the
/// caller's table, name the [`CapKind::DebugConsole`] singleton, and carry the
/// [`CapRights::CONSOLE_WRITE`] right.
///
/// Returns the in-kernel [`CapError`] so the caller composes it into
/// [`SyscallError::Cap`]; the resolve → kind → rights order mirrors the IPC
/// capability-validation order ([ADR-0030][adr-0030]'s taxonomy).
///
/// # Errors
///
/// - [`CapError::InvalidHandle`] — the handle did not resolve (stale / absent).
/// - [`CapError::WrongKind`] — the capability is not a debug-console capability.
/// - [`CapError::InsufficientRights`] — it lacks [`CapRights::CONSOLE_WRITE`].
///
/// [adr-0030]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0030-syscall-abi.md
fn validate_debug_console_cap(
table: &CapabilityTable,
cons_cap: CapHandle,
) -> Result<(), CapError> {
let cap = table.lookup(cons_cap)?;
if cap.kind() != CapKind::DebugConsole {
return Err(CapError::WrongKind);
}
if !cap.rights().contains(CapRights::CONSOLE_WRITE) {
return Err(CapError::InsufficientRights);
}
Ok(())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::too_many_arguments,
reason = "tests may use pragmas forbidden in production kernel code; \
run_console_write threads the SyscallContext pieces"
)]
mod tests {
#[cfg(debug_assertions)]
use super::CONSOLE_WRITE_CHUNK;
use super::{dispatch, SyscallContext};
use crate::cap::{CapHandle, CapObject, CapRights, Capability, CapabilityTable};
use crate::ipc::IpcError;
use crate::ipc::IpcQueues;
use crate::obj::endpoint::{create_endpoint, Endpoint, EndpointArena};
use crate::syscall::abi::{
decode_cap_handle, encode_cap_handle, SyscallArgs, SyscallEffect, SyscallNumber,
NULL_CAP_HANDLE, RECV_OUTCOME_PENDING, RECV_OUTCOME_RECEIVED, SEND_OUTCOME_ENQUEUED,
};
use crate::syscall::error::SyscallError;
use crate::syscall::user_access::UserAccessWindow;
#[cfg(debug_assertions)]
use tyrne_hal::MappingFlags;
use tyrne_hal::{Mmu, PhysAddr, PhysFrame};
#[cfg(debug_assertions)]
use tyrne_test_hal::FakeUserMem;
use tyrne_test_hal::{FakeAddressSpace, FakeConsole, FakeMmu};
// ── fixtures ─────────────────────────────────────────────────────────────
/// Mint a debug-console capability with `CONSOLE_WRITE` in a fresh table,
/// returning `(table, handle)`.
fn table_with_console_cap() -> (CapabilityTable, CapHandle) {
let mut table = CapabilityTable::new();
let h = table
.insert_root(Capability::new(
CapRights::CONSOLE_WRITE,
CapObject::DebugConsole,
))
.unwrap();
(table, h)
}
/// A `SyscallArgs` whose number is `n` and whose arg words are `args`.
fn call(n: SyscallNumber, args: [u64; 6]) -> SyscallArgs {
SyscallArgs {
number: n.as_u64(),
args,
}
}
/// A `FakeMmu` + empty address space for syscalls that never copy from user
/// (everything except `console_write`). Both returned values must outlive
/// the `SyscallContext` that borrows them.
fn empty_mmu_as() -> (FakeMmu, <FakeMmu as Mmu>::AddressSpace) {
let mmu = FakeMmu::new();
// SAFETY: FakeMmu::create_address_space stores the root, never derefs it.
let as_ =
unsafe { mmu.create_address_space(PhysFrame::from_aligned(PhysAddr(0x1000)).unwrap()) };
(mmu, as_)
}
/// Dispatch a `console_write` of `[ptr, len)` (cap word `cap_word`) against
/// the given translation surface + window, returning the effect. Folds the
/// per-test `ep_arena` / `queues` (unused by `console_write`) + the context
/// construction so each console test reads as a one-liner.
fn run_console_write(
table: &mut CapabilityTable,
console: &FakeConsole,
mmu: &FakeMmu,
task_as: &FakeAddressSpace,
window: UserAccessWindow,
cap_word: u64,
ptr: u64,
len: u64,
) -> SyscallEffect {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: table,
console,
user_window: window,
mmu,
task_as,
has_current_task: true,
};
dispatch(
&mut ctx,
call(SyscallNumber::ConsoleWrite, [cap_word, ptr, len, 0, 0, 0]),
)
}
// ── number decode ────────────────────────────────────────────────────────
#[test]
fn bad_number_zero_returns_bad_syscall_number_touching_nothing() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let effect = dispatch(
&mut ctx,
SyscallArgs {
number: 0,
args: [0; 6],
},
);
match effect {
SyscallEffect::Resume(r) => {
assert_eq!(r.status, SyscallError::BadSyscallNumber.as_status());
}
other => panic!("expected Resume(BadSyscallNumber), got {other:?}"),
}
assert!(console.captured().is_empty());
}
#[test]
fn out_of_range_number_returns_bad_syscall_number() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let effect = dispatch(
&mut ctx,
SyscallArgs {
number: 99,
args: [0; 6],
},
);
assert_eq!(
effect,
SyscallEffect::Resume(super::SyscallReturn::error(SyscallError::BadSyscallNumber))
);
}
// ── control-plane routing ────────────────────────────────────────────────
#[test]
fn task_yield_routes_to_reschedule() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
assert_eq!(
dispatch(&mut ctx, call(SyscallNumber::TaskYield, [0; 6])),
SyscallEffect::Reschedule
);
}
#[test]
fn task_exit_routes_to_terminate_with_code() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
assert_eq!(
dispatch(
&mut ctx,
call(SyscallNumber::TaskExit, [0x2A, 0, 0, 0, 0, 0])
),
SyscallEffect::Terminate(0x2A)
);
}
// ── control-plane fail-closed (gate #3 / T-026, H2) ──────────────────────
#[test]
fn task_yield_with_no_current_task_fails_closed() {
// Control-plane consults no capability, so the empty fail-closed table
// cannot guard it; the dispatcher rejects task_yield with InvalidHandle
// when no EL0 task is current (nothing to yield) — not Reschedule.
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: false,
};
let effect = dispatch(&mut ctx, call(SyscallNumber::TaskYield, [0; 6]));
match effect {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Cap(crate::cap::CapError::InvalidHandle).as_status()
),
other => panic!("expected Resume(InvalidHandle), not Reschedule, got {other:?}"),
}
}
#[test]
fn task_exit_with_no_current_task_fails_closed() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: false,
};
let effect = dispatch(
&mut ctx,
call(SyscallNumber::TaskExit, [0x2A, 0, 0, 0, 0, 0]),
);
match effect {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Cap(crate::cap::CapError::InvalidHandle).as_status()
),
other => panic!("expected Resume(InvalidHandle), not Terminate, got {other:?}"),
}
}
#[test]
#[cfg(debug_assertions)] // exercises console_write (number 5), which is debug-gated
fn incomplete_binding_context_fails_closed_on_both_planes() {
// Models the context the BSP `syscall_entry` builds on an INCOMPLETE
// running-task binding (any of table / window / generation-checked AS
// missing or stale): the empty `FAILCLOSED_TABLE` + an empty window +
// `has_current_task = false`. The BSP match that assembles it is
// no_std / no_main and not host-testable directly; this pins the
// dispatcher's handling of that exact context — **both** planes must
// fail closed in the *same* context: the data-plane `console_write` via
// the empty table (InvalidHandle, no output) and the control-plane
// `task_yield` via the `has_current_task` gate (InvalidHandle, not
// Reschedule). Closes the incomplete-context coverage gate #3's BSP
// fallback arm cannot unit-test itself (T-026 review-round).
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new(); // empty: the FAILCLOSED_TABLE analog
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: false,
};
// Data-plane: console_write fails closed via the empty table (the cap
// gate rejects before the window / translate is ever consulted).
let bogus = encode_cap_handle(Some(CapHandle::from_raw(0, 0)));
match dispatch(
&mut ctx,
call(SyscallNumber::ConsoleWrite, [bogus, 0x40_0000, 5, 0, 0, 0]),
) {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Cap(crate::cap::CapError::InvalidHandle).as_status(),
"data-plane console_write must fail closed on an incomplete binding"
),
other => panic!("expected Resume(InvalidHandle), got {other:?}"),
}
assert!(
console.captured().is_empty(),
"no byte may be emitted from the incomplete-binding fallback context"
);
// Control-plane: task_yield fails closed via the has_current_task gate
// (the empty table cannot guard it — it consults no capability).
match dispatch(&mut ctx, call(SyscallNumber::TaskYield, [0; 6])) {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Cap(crate::cap::CapError::InvalidHandle).as_status(),
"control-plane task_yield must fail closed on an incomplete binding"
),
other => panic!("expected Resume(InvalidHandle), not Reschedule, got {other:?}"),
}
}
// ── send / recv ──────────────────────────────────────────────────────────
#[test]
fn send_with_no_receiver_enqueues_and_returns_ok() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let ep = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
let ep_cap = table
.insert_root(Capability::new(
CapRights::SEND | CapRights::RECV,
CapObject::Endpoint(ep),
))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let cap_word = encode_cap_handle(Some(ep_cap));
let effect = dispatch(
&mut ctx,
call(
SyscallNumber::Send,
[cap_word, 0xAB, 1, 2, 3, encode_cap_handle(None)],
),
);
match effect {
SyscallEffect::Resume(r) => {
assert_eq!(r.status, 0, "send must succeed");
assert_eq!(r.payload[0], SEND_OUTCOME_ENQUEUED);
}
other => panic!("expected Resume, got {other:?}"),
}
}
#[test]
fn send_without_send_right_returns_typed_ipc_missing_right() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let ep = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
// RECV only — no SEND. ipc_send → MissingRight → SyscallError::Ipc.
let ep_cap = table
.insert_root(Capability::new(CapRights::RECV, CapObject::Endpoint(ep)))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let cap_word = encode_cap_handle(Some(ep_cap));
let effect = dispatch(
&mut ctx,
call(
SyscallNumber::Send,
[cap_word, 0, 0, 0, 0, encode_cap_handle(None)],
),
);
match effect {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Ipc(crate::ipc::IpcError::MissingRight).as_status()
),
other => panic!("expected Resume(error), got {other:?}"),
}
}
#[test]
fn recv_of_enqueued_message_unpacks_into_registers() {
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let ep = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
let ep_cap = table
.insert_root(Capability::new(
CapRights::SEND | CapRights::RECV,
CapObject::Endpoint(ep),
))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let cap_word = encode_cap_handle(Some(ep_cap));
// Enqueue a message via the send syscall.
let _ = dispatch(
&mut ctx,
call(
SyscallNumber::Send,
[cap_word, 0x55, 7, 8, 9, encode_cap_handle(None)],
),
);
// Receive it back.
let effect = dispatch(
&mut ctx,
call(SyscallNumber::Recv, [cap_word, 0, 0, 0, 0, 0]),
);
match effect {
SyscallEffect::Resume(r) => {
assert_eq!(r.status, 0);
assert_eq!(r.payload[0], RECV_OUTCOME_RECEIVED); // x1
assert_eq!(r.payload[1], 0x55); // x2 label
assert_eq!(r.payload[2], 7); // x3 param0
assert_eq!(r.payload[3], 8); // x4 param1
assert_eq!(r.payload[4], 9); // x5 param2
}
other => panic!("expected Resume(Received), got {other:?}"),
}
}
// ── review-round follow-up: end-to-end transfer + Pending + chunk boundary ─
//
// These close the dispatch-level coverage gaps the T-021 review-round
// surfaced: the transfer-cap wiring (x5 decode → ipc_send cap_take, and
// ipc_recv install → x6 pack) had no through-dispatch test, nor did the
// RecvOutcome::Pending register packing or the exact one-chunk boundary.
#[test]
fn send_with_transfer_cap_then_recv_returns_cap_in_x6() {
// Exercises the x5 transfer-handle decode → `ipc_send` `cap_take` AND the
// `ipc_recv` → `encode_recv_outcome` x6 cap-pack, end-to-end through
// `dispatch` — wiring no existing dispatch test covered (every other send
// test passes the null sentinel; the recv test never asserts x6).
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
// ep1: the comm endpoint (SEND|RECV). ep2: the object whose cap we move.
let ep1 = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
let ep2 = create_endpoint(&mut ep_arena, Endpoint::new(1)).unwrap();
let ep_cap = table
.insert_root(Capability::new(
CapRights::SEND | CapRights::RECV,
CapObject::Endpoint(ep1),
))
.unwrap();
// The transferred cap needs the TRANSFER right (ipc_send enforces it).
let xfer_cap = table
.insert_root(Capability::new(
CapRights::TRANSFER,
CapObject::Endpoint(ep2),
))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let ep_word = encode_cap_handle(Some(ep_cap));
// send with the transfer handle in x5.
let send_effect = dispatch(
&mut ctx,
call(
SyscallNumber::Send,
[ep_word, 0x77, 0, 0, 0, encode_cap_handle(Some(xfer_cap))],
),
);
assert!(
matches!(send_effect, SyscallEffect::Resume(r) if r.status == 0),
"send-with-transfer must succeed, got {send_effect:?}"
);
// The transferred cap left the sender's table (cap_take).
assert!(
ctx.caller_table.lookup(xfer_cap).is_err(),
"transferred cap must be taken out of the sender's table"
);
// recv collects the message AND the transferred cap (x6).
let recv_effect = dispatch(
&mut ctx,
call(SyscallNumber::Recv, [ep_word, 0, 0, 0, 0, 0]),
);
let SyscallEffect::Resume(r) = recv_effect else {
panic!("expected Resume from recv, got {recv_effect:?}");
};
assert_eq!(r.status, 0);
assert_eq!(r.payload[0], RECV_OUTCOME_RECEIVED); // x1
assert_eq!(r.payload[1], 0x77); // x2 label
// x6 carries the transferred cap as a non-null handle that resolves.
assert_ne!(r.payload[5], NULL_CAP_HANDLE, "x6 must carry a real handle");
let received = decode_cap_handle(r.payload[5]).expect("x6 decodes to Some(handle)");
assert!(
ctx.caller_table.lookup(received).is_ok(),
"the transferred cap must be installed in the receiver's table"
);
}
#[test]
fn send_with_stale_transfer_handle_returns_invalid_transfer_cap() {
// A valid endpoint cap but a transfer handle (x5) that resolves to
// nothing: `ipc_send`'s transfer pre-flight returns `InvalidTransferCap`,
// composing into `SyscallError::Ipc(InvalidTransferCap)` (status 0x205).
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let ep = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
let ep_cap = table
.insert_root(Capability::new(CapRights::SEND, CapObject::Endpoint(ep)))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
// x5 = a handle naming no live slot (index far past CAP_TABLE_CAPACITY).
let stale_xfer = encode_cap_handle(Some(CapHandle::from_raw(50, 7)));
let effect = dispatch(
&mut ctx,
call(
SyscallNumber::Send,
[encode_cap_handle(Some(ep_cap)), 0, 0, 0, 0, stale_xfer],
),
);
match effect {
SyscallEffect::Resume(r) => assert_eq!(
r.status,
SyscallError::Ipc(IpcError::InvalidTransferCap).as_status()
),
other => panic!("expected Resume(InvalidTransferCap), got {other:?}"),
}
}
#[test]
fn recv_with_no_sender_returns_pending_packing() {
// recv on an endpoint with no waiting sender returns RecvOutcome::Pending:
// status Ok, x1 = pending code, x2..x7 zeroed (deterministic).
let mut ep_arena = EndpointArena::default();
let mut queues = IpcQueues::new();
let mut table = CapabilityTable::new();
let ep = create_endpoint(&mut ep_arena, Endpoint::new(0)).unwrap();
let ep_cap = table
.insert_root(Capability::new(CapRights::RECV, CapObject::Endpoint(ep)))
.unwrap();
let console = FakeConsole::new();
let (mmu, task_as) = empty_mmu_as();
let mut ctx = SyscallContext {
ep_arena: &mut ep_arena,
queues: &mut queues,
caller_table: &mut table,
console: &console,
user_window: UserAccessWindow::empty(),
mmu: &mmu,
task_as: &task_as,
has_current_task: true,
};
let effect = dispatch(
&mut ctx,
call(
SyscallNumber::Recv,
[encode_cap_handle(Some(ep_cap)), 0, 0, 0, 0, 0],
),
);
let SyscallEffect::Resume(r) = effect else {
panic!("expected Resume(Pending), got {effect:?}");
};
assert_eq!(r.status, 0);
assert_eq!(r.payload[0], RECV_OUTCOME_PENDING); // x1
assert_eq!(
&r.payload[1..],
&[0, 0, 0, 0, 0, 0],
"x2..x7 must be zeroed on Pending"
);
}
#[test]
#[cfg(debug_assertions)] // console_write number 5 is debug-gated
fn console_write_exactly_one_chunk_emits_all_bytes() {
// Boundary: len == CONSOLE_WRITE_CHUNK exercises the `offset < len` loop
// termination exactly — one chunk, then offset == len.
let (mut table, cons_cap) = table_with_console_cap();
let console = FakeConsole::new();
let payload: Vec<u8> = (0..CONSOLE_WRITE_CHUNK).map(|i| (i % 251) as u8).collect();
let mem = FakeUserMem::new(0x40_0000, 1, MappingFlags::USER | MappingFlags::WRITE);
mem.write(0, &payload);
let effect = run_console_write(
&mut table,
&console,
mem.mmu(),
mem.address_space(),
UserAccessWindow::new(mem.base_va(), mem.region_len()),
encode_cap_handle(Some(cons_cap)),
mem.base_va() as u64,
payload.len() as u64,
);
match effect {
SyscallEffect::Resume(r) => assert_eq!(r.payload[0], CONSOLE_WRITE_CHUNK as u64),
other => panic!("expected Resume(ok), got {other:?}"),
}
assert_eq!(console.captured(), payload);
}
// ── console_write: capability gate ───────────────────────────────────────