Repository navigation
Expand file tree
/
Copy pathtask_loader.rs
More file actions
2808 lines (2598 loc) · 120 KB
/
Copy pathtask_loader.rs
File metadata and controls
2808 lines (2598 loc) · 120 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
//! Task loader — embedded raw-flat userspace image → [`LoadedImage`]
//! metadata.
//!
//! Per [ADR-0029][adr-0029] (raw-flat format choice) + [T-019][t-019]
//! (loader implementation). This module owns the public types
//! [`LoadedImage`] (success descriptor) and [`LoadError`] (failure
//! taxonomy) and the [`load_image`] function that consumes an embedded
//! raw-flat userspace blob and produces a populated address space
//! described by a `LoadedImage`.
//!
//! ## Pipeline (§Simulation rows of T-019)
//!
//! 1. Argument preflight ([`LoadError::InvalidImage`] /
//! [`LoadError::InvalidStackSize`] /
//! [`LoadError::MisalignedImageBaseVa`]).
//! 2. Cap preflight: lookup + [`CapKind::AddressSpace`] kind check
//! ([`LoadError::InvalidParentCap`]). The DERIVE-rights check is
//! *delegated* to [`cap_create_address_space`]'s step 2a and
//! surfaces in step 4 as
//! [`LoadError::AddressSpaceCreationFailed`] wrapping
//! [`CapError::InsufficientRights`].
//! 3. Frame-budget preflight: `1 + image_pages + stack_pages +
//! intermediate_frame_count(...) <= pmm.stats().free_frames`
//! (see [`intermediate_frame_count`]) — returns
//! [`LoadError::FrameBudgetExceeded`] on miss.
//! 4. Image-PA-overlap preflight: reject if the `image` slice's PA
//! range overlaps a frame [`Pmm::alloc_frame`] could yield
//! ([`LoadError::ImageOverlapsAllocatableMemory`]). Discharges
//! UNSAFE-2026-0027's non-overlap invariant at runtime instead of
//! relying on BSP-layout discipline.
//! 5. Create the AS via [`cap_create_address_space`]; on failure no
//! state was committed (T-018's preflight discipline)
//! ([`LoadError::AddressSpaceCreationFailed`]).
//! 6. Image-page loop: `pmm.alloc_frame` → `copy_nonoverlapping` byte
//! copy → [`cap_map`] under `USER | EXECUTE` per page. Tail-zeroing
//! on the partial last page is automatic via the PMM's zero-init
//! contract.
//! 7. Stack-page loop: `pmm.alloc_frame` → [`cap_map`] under
//! `USER | WRITE` per page.
//! 8. Construct [`LoadedImage`] and return.
//!
//! Steps 6+7 are fallible mid-loop; the rollback contract
//! ([`cap_unmap`] + [`Pmm::free_frame`] for the committed pages, plus
//! [`Pmm::free_frame`] for the failing iteration's leaf frame, plus
//! [`CapabilityTable::cap_drop`][crate::cap::CapabilityTable::cap_drop]
//! for the AS cap) is documented canonically in T-019 §Approach
//! §"Rollback contract (explicit)".
//!
//! ## Scope boundary (load-complete, not B5/B6-runnable)
//!
//! [`LoadedImage`] is intentionally **not** a
//! `CapHandle{CapObject::Task(...)}` — it describes a populated
//! address space but does not mint a runnable task. The current
//! [`Task`][super::Task] struct carries no PC/SP context register
//! file; the loader's new AS holds only image + stack mappings (no
//! kernel mappings, so an EL1 exception while the AS is active would
//! translation-fault on the vector fetch). The `task_create_from_image`
//! wrapper that turns a [`LoadedImage`] into a runnable task cap lands
//! with B5 (syscall ABI — placeholder slot ADR-0030; not yet authored)
//! and B6 (first userspace "hello") per
//! [phase-b §B4 §Revision-notes][phase-b-b4-rider].
//!
//! [adr-0029]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0029-initial-userspace-image-format.md
//! [t-019]: https://github.com/HodeTech/Tyrne/blob/main/docs/analysis/tasks/phase-b/T-019-task-loader.md
//! [phase-b-b4-rider]: https://github.com/HodeTech/Tyrne/blob/main/docs/roadmap/phases/phase-b.md#milestone-b4--task-loader
use super::task::{create_task, destroy_task, Task, TaskArena};
use crate::cap::{CapError, CapHandle, CapKind, CapObject, CapRights, Capability, CapabilityTable};
use crate::mm::{
cap_create_address_space, cap_map, cap_unmap, AddressSpaceArena, AddressSpaceError, Pmm,
};
use tyrne_hal::{MappingFlags, Mmu, VirtAddr, PAGE_SIZE};
/// Compute the exact number of intermediate page-table frames
/// (L1 + L2 + L3 tables) `cap_map` will allocate for the loader's
/// image + stack mappings under `VMSAv8` 4-level translation.
///
/// Walks the `VMSAv8` 4 KiB granule index decomposition:
///
/// - **L3** (4 KiB page) table covers 2 MiB (`VA[20:12]` index, 9 bits).
/// - **L2** (2 MiB block) table covers 1 GiB (`VA[29:21]` index).
/// - **L1** (1 GiB block) table covers 512 GiB (`VA[38:30]` index).
/// - **L0** root covers the full 48-bit VA range — already allocated
/// by [`crate::mm::cap_create_address_space`], **not** counted here.
///
/// Image and stack are contiguous by construction (`stack_base =
/// image_base_va + image_pages * PAGE_SIZE`), so their combined VA
/// span is closed; the distinct-slot count at each level is exactly
/// `(last >> shift) - (first >> shift) + 1`.
///
/// The returned count is **exact** for a BSP that lazy-allocates
/// table descriptors only when the parent entry is invalid (v1's
/// [`bsp-qemu-virt/src/mmu.rs`](../../../../bsp-qemu-virt/src/mmu.rs)
/// `walk_or_alloc_table` behaviour); a BSP that pre-allocates more
/// aggressively would observe this count as a lower bound. v1 has
/// the single aarch64 BSP, so the count is exact in practice.
///
/// FORWARD (C4-004): this is a kernel-core module encoding both a
/// page-table-format constant (the 21/30/39 shifts) **and** a BSP
/// allocation-policy assumption (lazy allocation). A second BSP — Pi 4 /
/// Jetson, or a RISC-V `Sv39` port — that pre-allocates more, or uses a
/// different index decomposition, would make this an **undercount**: the
/// [`FrameBudgetExceeded`][LoadError::FrameBudgetExceeded] preflight would
/// pass, then `cap_map` would fail mid-loop with
/// [`MapFailed`][LoadError::MapFailed]`(MmuMapError(OutOfFrames))` — the
/// exact path the new MR-018 test pins — turning the preflight (whose whole
/// purpose is to make that mid-loop failure structurally unreachable)
/// ineffective. Before a second BSP / translation regime lands, move the
/// exact-budget contract to a HAL method (`Mmu::intermediate_frames_for_span`)
/// or re-derive it per format.
///
/// Returns 0 for an empty span (defensive; row 1 preflight rejects
/// zero-page requests before this helper is reached).
///
/// ## Why not a closed-form constant
///
/// Earlier T-019 commits used a hard-coded constant `6` (3 L0/L1/L2
/// intermediates per range × 2 contiguous ranges = 6). PR #31
/// review-round 3 (Finding 1) observed this is **wrong** for image
/// spans that cross more than one L2 slot — every additional 2 MiB
/// boundary the image crosses adds one L3 table. Example: an 8 MiB
/// image at VA `0x0080_0000` covers L2 slots 4–7 + the stack's slot
/// 8 = 5 distinct L2 slots, requiring 1 L1 + 1 L2 + 5 L3 = **7**
/// intermediates, not 6. The exact computation below tracks the true
/// count and satisfies the "safe upper bound" contract T-019
/// §Acceptance criteria promises.
///
/// ## `VMSAv8`-specificity
///
/// The `21`/`30`/`39` bit-shift constants encode `VMSAv8`'s 4 KiB
/// granule + 9-bit per-level index decomposition per
/// [ADR-0009 §Decision drivers][adr-0009] and
/// [ADR-0027 §Decision outcome (a)][adr-0027]. Future BSPs targeting
/// a different page-table format (e.g. RISC-V `Sv39`) would either
/// re-derive these constants per their format or surface an
/// `Mmu::intermediate_frames_for_span` HAL method. v1's single
/// aarch64 BSP keeps the constants inline.
///
/// [adr-0009]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0009-mmu-trait.md
/// [adr-0027]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0027-kernel-virtual-memory-layout.md
#[must_use]
pub fn intermediate_frame_count(
image_base_va: VirtAddr,
image_pages: usize,
stack_pages: usize,
) -> usize {
let total_pages = image_pages.saturating_add(stack_pages);
if total_pages == 0 {
return 0;
}
let span_bytes = total_pages.saturating_mul(PAGE_SIZE);
let span_start = image_base_va.0;
// Last addressed byte. `total_pages > 0` guarantees `span_bytes
// >= PAGE_SIZE > 0`, so the `saturating_sub` does not underflow.
let last_byte = span_start.saturating_add(span_bytes).saturating_sub(1);
// L3 (4 KiB page) table: each covers 2 MiB → shift 21.
let l3_count = (last_byte >> 21)
.saturating_sub(span_start >> 21)
.saturating_add(1);
// L2 (2 MiB block) table: each covers 1 GiB → shift 30.
let l2_count = (last_byte >> 30)
.saturating_sub(span_start >> 30)
.saturating_add(1);
// L1 (1 GiB block) table: each covers 512 GiB → shift 39.
let l1_count = (last_byte >> 39)
.saturating_sub(span_start >> 39)
.saturating_add(1);
l1_count.saturating_add(l2_count).saturating_add(l3_count)
}
/// One-past-the-highest legal userspace virtual address.
///
/// Per [ADR-0027 §Decision outcome (a)][adr-0027], `TCR_EL1.T0SZ = 16`
/// commits the kernel to 48-bit `TTBR0_EL1` virtual addresses; the
/// architected userspace VA range is therefore `[0, USERSPACE_VA_LIMIT)`
/// with `USERSPACE_VA_LIMIT = 1 << 48 = 0x1_0000_0000_0000`. The
/// loader's mapped span `[image_base_va, stack_top_va)` (half-open)
/// must lie within this range — a span that crosses or exceeds it
/// could not be addressed by the hardware page-walker even after the
/// future ADR-0033 high-half migration opens (which moves *kernel*
/// VAs into `TTBR1_EL1`'s range, leaving the userspace `TTBR0_EL1`
/// bound unchanged).
///
/// The 0 bound on the low side is not enforced separately: a
/// `image_base_va = 0` is legal (page 0 mapped under `USER|EXECUTE`
/// is unusual but not architecturally forbidden); the loader does
/// not impose a stylistic "no-null-page" policy in v1.
///
/// [adr-0027]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0027-kernel-virtual-memory-layout.md
pub const USERSPACE_VA_LIMIT: usize = 1usize << 48;
/// Metadata describing a freshly populated address space produced by
/// [`load_image`].
///
/// `LoadedImage` is a *descriptor of loaded state*, not a runnable-task
/// capability — see the module-level §"Scope boundary" note. Returned
/// by-value (it is [`Copy`]); the caller owns the metadata after
/// `load_image` returns.
///
/// Fields are all `pub` per the public-struct-literal convention chosen
/// for T-019: callers construct a `LoadedImage` directly when they need
/// one for tests, and the loader's success path writes a literal at the
/// end of its sequence. There is no hidden invariant a constructor
/// would protect — every field is independently derived from the
/// loader's arguments + the cap-table / arena state.
///
/// # Field invariants
///
/// - `as_cap` is a freshly-minted leaf `CapHandle` wrapping
/// `CapObject::AddressSpace(AddressSpaceHandle)`, valid against the
/// `CapabilityTable` the loader was passed.
/// - `entry_va == image_base_va` (raw-flat: offset 0 of the embedded
/// blob is the userspace entry instruction).
/// - `stack_top_va` is **one-past-the-highest** mapped VA of the stack
/// region. The stack mapped range is `[stack_base, stack_top_va)`
/// half-open; `sp = stack_top_va` at task-creation initialisation
/// is correct because the first userspace push (e.g. `sp -= 16`)
/// lands inside the mapped range. Matches the AAPCS64 convention.
/// - `image_bytes == image.len()` from the loader's `image: &[u8]`
/// argument. May be smaller than `image_pages * PAGE_SIZE` because
/// tail-zeroing happens on the partial last page (the loader copies
/// only `image.len()` bytes; the remainder of the last page stays
/// zero per [UNSAFE-2026-0026][unsafe-26]'s PMM zero-init contract).
/// - `stack_bytes == stack_size_pages * PAGE_SIZE` (always a multiple
/// of `PAGE_SIZE`).
///
/// [unsafe-26]: https://github.com/HodeTech/Tyrne/blob/main/docs/audits/unsafe-log.md
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct LoadedImage {
/// Cap handle for the newly-minted address space. Backed by a
/// `CapObject::AddressSpace(AddressSpaceHandle)` cap-table entry
/// minted via
/// [`cap_create_address_space`][crate::mm::cap_create_address_space]
/// during `load_image` step 4.
pub as_cap: CapHandle,
/// Userspace entry-point VA — equals `image_base_va` (raw-flat
/// format: offset 0 of the embedded blob is the entry instruction).
pub entry_va: VirtAddr,
/// One-past-the-highest mapped VA of the stack region (half-open
/// `[stack_base, stack_top_va)` convention; see struct-level
/// invariants).
pub stack_top_va: VirtAddr,
/// Byte-count of the image as loaded into the AS (may be smaller
/// than `image_pages * PAGE_SIZE` due to tail-zeroing on the
/// partial last page).
pub image_bytes: usize,
/// Stack region size in bytes (always a multiple of `PAGE_SIZE`).
pub stack_bytes: usize,
}
/// Error taxonomy for [`load_image`].
///
/// Variants are split per the explicit rollback contract documented in
/// [T-019 §Approach §"Rollback contract (explicit)"][t-019-rollback].
/// Each variant's doc-comment names whether rollback is required and
/// what the v1 baseline leaks on the rollback path. The
/// [T-019 §"Rollback contract"][t-019-rollback] section remains the
/// canonical reference; this enum's doc-comments are summaries.
///
/// `#[non_exhaustive]` because future-state variants are foreseeable
/// — e.g. a per-section permission failure that lands with ADR-0034
/// (placeholder; B5+).
///
/// [t-019-rollback]: https://github.com/HodeTech/Tyrne/blob/main/docs/analysis/tasks/phase-b/T-019-task-loader.md#rollback-contract-explicit
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LoadError {
/// `image.is_empty()`. Pre-PMM preflight; no state change.
InvalidImage,
/// `stack_size_pages == 0`. Pre-PMM preflight; no state change.
InvalidStackSize,
/// `image_base_va` is not `PAGE_SIZE`-aligned. Pre-PMM preflight
/// rejects the request before any state change. Without this
/// check, the misalignment would surface only at the first
/// `cap_map` call inside the image-page loop — by which point
/// `cap_create_address_space` has already allocated the root L0
/// frame, which then leaks via the v1 baseline rollback. Per
/// PR #31 review-round 4 P2: the leak is preventable by argument
/// validation, so this variant takes precedence over the
/// `MapFailed(MmuMapError(MisalignedAddress))` path the loader
/// would otherwise return. The wrapped `VirtAddr` names the
/// offending base for diagnostics.
MisalignedImageBaseVa(VirtAddr),
/// The requested VA span `[image_base_va, stack_top_va)` exceeds
/// the architected userspace VA range
/// `[0, USERSPACE_VA_LIMIT)`. Per
/// [ADR-0027 §Decision outcome (a)][adr-0027], `TCR_EL1.T0SZ = 16`
/// commits userspace VAs to 48 bits; an `image_base_va` plus
/// `image_pages + stack_pages` worth of bytes that lands past
/// `1 << 48` could not be addressed by the hardware page-walker.
/// The variant fires either when `image_base_va` itself is past
/// the limit, when the saturated-add of the span end exceeds it,
/// or when the byte-count multiplication overflows `usize`
/// (saturated to `usize::MAX`). Pre-PMM preflight; no state
/// change.
///
/// `end == VirtAddr(usize::MAX)` is the sentinel for the overflow
/// path; non-sentinel `end` values name the offending
/// saturated-add result for diagnostics.
///
/// [adr-0027]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0027-kernel-virtual-memory-layout.md
InvalidImageBaseVa {
/// The caller-supplied `image_base_va`.
base: VirtAddr,
/// The computed one-past-the-highest VA the loader would map.
/// Sentinel `VirtAddr(usize::MAX)` indicates the byte-count or
/// VA-end arithmetic saturated.
end: VirtAddr,
},
/// Parent AS cap lookup or `CapKind::AddressSpace` kind check
/// failed. Wraps the underlying [`CapError`]. **DERIVE-rights
/// enforcement is *not* in this variant** — the DERIVE check is
/// delegated to `cap_create_address_space`'s step 2a and surfaces
/// as [`AddressSpaceCreationFailed`][LoadError::AddressSpaceCreationFailed]
/// wrapping `CapError::InsufficientRights`.
InvalidParentCap(CapError),
/// Frame-budget preflight: `1 + image_pages + stack_pages +
/// intermediate_frame_count(...)` exceeds `pmm.stats().free_frames`.
/// The leading `1` accounts for the root L0 frame
/// `cap_create_address_space` will allocate;
/// [`intermediate_frame_count`] returns the **exact** distinct
/// L1/L2/L3 page-table-frame count for the requested VA span
/// (per `VMSAv8` 4-level index decomposition — *not* a hard-coded
/// constant, since the previous `INTERMEDIATE_FRAME_BUDGET = 6`
/// undercounted for image spans crossing more than one 2 MiB L2
/// slot per PR #31 review-round 3 Finding 1). Pre-PMM preflight;
/// no state change.
FrameBudgetExceeded {
/// Frames the loader would commit (root + image + stack +
/// exact intermediate count).
needed: usize,
/// Frames currently available per `pmm.stats().free_frames`.
available: usize,
},
/// `cap_create_address_space` returned `Err`. Covers
/// `CapError::InsufficientRights` if `parent_as_cap` lacks DERIVE,
/// `CapError::WidenedRights` if `new_rights ⊄ parent_cap.rights`
/// (the no-widening rule enforced by `cap_create_address_space` step
/// 2b — asking for rights the parent does not hold is rejected here,
/// not in [`InvalidParentCap`][LoadError::InvalidParentCap]), plus the
/// T-018-guarded `CapsExhausted` / `DerivationTooDeep` / `ArenaFull`
/// paths. No rollback needed at this layer (T-018's preflight ensures
/// no committed state on failure).
AddressSpaceCreationFailed(AddressSpaceError),
/// The `image` byte slice's PA range overlaps a frame
/// [`Pmm::alloc_frame`] could yield. If accepted, the loader's
/// `core::ptr::copy_nonoverlapping` would alias source and
/// destination — undefined behaviour per the Rust safety contract.
/// The check is a runtime preflight on
/// [UNSAFE-2026-0027][unsafe-27]'s "source and destination do not
/// overlap" invariant, replacing the BSP-layout-documented form
/// (ADR-0027 + ADR-0035) with a mechanically enforced rejection.
/// Pre-PMM preflight; no state change.
///
/// Practically unreachable under correct BSP wiring (`.rodata`-
/// resident images are in PMM-reserved memory by ADR-0035), but
/// retained as a defensive variant so a misconfigured BSP fails
/// fast with a typed error instead of UB.
///
/// [unsafe-27]: https://github.com/HodeTech/Tyrne/blob/main/docs/audits/unsafe-log.md
ImageOverlapsAllocatableMemory,
/// `pmm.alloc_frame()` (the loader's *own* leaf/root alloc) returned
/// `None` mid-image-or-stack-loop. This is distinct from a mid-walk
/// `Mmu::map` intermediate-table failure, which surfaces as
/// [`MapFailed`][LoadError::MapFailed]`(MmuMapError(OutOfFrames))`.
/// Structurally unreachable post-[`FrameBudgetExceeded`][LoadError::FrameBudgetExceeded]
/// preflight under v1's single-thread cooperative model; retained
/// as a defensive variant for budget-calculation bugs and future-
/// concurrency scenarios. **Rollback required**, per the
/// [T-019 §"Rollback contract"][t-019-rollback] (leaf frames +
/// `cap_unmap` undo + `cap_drop(loaded_as_cap)`).
///
/// Exercised in tests via `Pmm::force_alloc_failure_after` (the loader's
/// leaf-alloc path); the sibling mid-walk `MapFailed(OutOfFrames)` clause
/// is exercised via `tyrne_test_hal::OutOfFramesMmu`.
///
/// [t-019-rollback]: https://github.com/HodeTech/Tyrne/blob/main/docs/analysis/tasks/phase-b/T-019-task-loader.md#rollback-contract-explicit
OutOfFrames,
/// `cap_map` returned `Err` mid-loop. Wraps the underlying
/// [`AddressSpaceError`] — typically
/// `MmuMapError(MmuError::OutOfFrames)` if the intermediate-frame
/// budget was underestimated, `MmuMapError(MmuError::AlreadyMapped)`
/// on a VA-range collision, or `MmuMapError(MmuError::BlockMapped)`
/// if the VA falls inside a block descriptor. **Rollback required**,
/// same shape as [`OutOfFrames`][LoadError::OutOfFrames].
MapFailed(AddressSpaceError),
}
/// Load an embedded raw-flat userspace image into a fresh address
/// space and return a [`LoadedImage`] descriptor.
///
/// See the module-level pipeline summary + [T-019][t-019] for the full
/// state-machine specification. The sequence of fallible steps is:
///
/// 1. Argument preflight ([`LoadError::InvalidImage`] /
/// [`LoadError::InvalidStackSize`] /
/// [`LoadError::MisalignedImageBaseVa`]).
/// 2. Cap preflight: lookup + [`CapKind::AddressSpace`] kind check
/// ([`LoadError::InvalidParentCap`]).
/// 3. VA-range preflight: the mapped span end
/// `image_base_va + (image_pages + stack_pages) * PAGE_SIZE` must
/// be `<= USERSPACE_VA_LIMIT` (= `1 << 48` per ADR-0027 §Decision
/// outcome (a)). Returns [`LoadError::InvalidImageBaseVa`] on
/// miss. Frame-budget preflight: `1 + image_pages + stack_pages +
/// intermediate_frame_count(...) <= pmm.stats().free_frames` (see
/// [`intermediate_frame_count`]). Returns
/// [`LoadError::FrameBudgetExceeded`] on miss. VA-range runs
/// first so an arithmetic-saturated request surfaces the
/// documented [`LoadError::InvalidImageBaseVa`] sentinel instead
/// of being masked by [`LoadError::FrameBudgetExceeded`].
/// 4. Image-PA-overlap preflight: reject if the `image` slice's PA
/// range overlaps a frame [`Pmm::alloc_frame`] could yield
/// ([`LoadError::ImageOverlapsAllocatableMemory`]). Discharges
/// [UNSAFE-2026-0027][unsafe-27]'s non-overlap invariant at runtime.
/// 5. [`cap_create_address_space`]: mint the AS cap
/// ([`LoadError::AddressSpaceCreationFailed`] — no rollback,
/// T-018's preflight guarantees no committed state on failure).
/// 6. Image-page loop under `USER | EXECUTE`
/// ([`LoadError::OutOfFrames`] / [`LoadError::MapFailed`] — rollback
/// discipline below).
/// 7. Stack-page loop under `USER | WRITE` (same).
/// 8. Construct and return [`LoadedImage`].
///
/// [unsafe-27]: https://github.com/HodeTech/Tyrne/blob/main/docs/audits/unsafe-log.md
///
/// # Arguments
///
/// - `image`: the embedded raw-flat blob. Offset 0 is the userspace
/// entry instruction (per [ADR-0029][adr-0029]).
/// - `pmm`: the kernel PMM. Direct concrete type — *not*
/// `&mut dyn FrameProvider` — because the rollback path needs
/// [`Pmm::free_frame`] which is not on the trait surface.
/// - `mmu`: the BSP MMU instance.
/// - `table`: the cap table the new AS cap will be minted into.
/// - `as_arena`: the address-space arena slot pool.
/// - `parent_as_cap`: cap authorising the mint via
/// [`cap_create_address_space`]. Must be `CapKind::AddressSpace`;
/// must hold `CapRights::DERIVE` (delegated check).
/// - `new_rights`: rights set the new AS cap will carry.
/// - `image_base_va`: VA at which the image's offset 0 lands. The
/// caller's userspace linker script dictates this. Must be
/// `PAGE_SIZE`-aligned — verified in row 1's argument preflight;
/// misaligned bases return [`LoadError::MisalignedImageBaseVa`]
/// before any state change.
/// - `stack_size_pages`: stack-region size in `PAGE_SIZE`-multiples;
/// minimum 1.
///
/// # Errors
///
/// Every variant of [`LoadError`]. See per-variant doc-comments for
/// when each fires and whether rollback runs.
///
/// # Rollback discipline
///
/// On any `Err` from step 6 or 7 the function unwinds *every*
/// committed mapping (via [`cap_unmap`] + [`Pmm::free_frame`] in
/// reverse order), frees the failing iteration's already-allocated
/// leaf frame, and drops the AS cap via
/// [`CapabilityTable::cap_drop`][crate::cap::CapabilityTable::cap_drop].
/// The v1 baseline leaks the root L0 frame + the intermediate
/// L1/L2/L3 frames `cap_map` allocated + the AS arena slot itself —
/// per [T-019 §"Rollback contract"][t-019-rollback]; full reclaim
/// arrives with the future `MemoryRegionCap` + per-AS destroy ADR
/// (B5+).
///
/// `cap_drop` (not `cap_revoke`) is used because the AS cap is a leaf
/// in the derivation tree by construction (the loader does not
/// derive children from it) and `cap_revoke(src)` would walk
/// `src`'s *descendants* while leaving `src` itself valid; it also
/// requires `CapRights::REVOKE` which `new_rights` may omit.
/// `cap_drop` `free_slot`s the leaf directly, is rights-agnostic, and
/// fails only with `HasChildren` (impossible here).
///
/// [adr-0029]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0029-initial-userspace-image-format.md
/// [t-019]: https://github.com/HodeTech/Tyrne/blob/main/docs/analysis/tasks/phase-b/T-019-task-loader.md
/// [t-019-rollback]: https://github.com/HodeTech/Tyrne/blob/main/docs/analysis/tasks/phase-b/T-019-task-loader.md#rollback-contract-explicit
#[allow(
clippy::too_many_arguments,
reason = "load_image threads the full kernel-state surface (pmm + mmu + \
table + arena + parent cap + rights + VA + stack size) through \
by reference per the no-ambient-authority discipline; bundling \
would obscure the data-flow without reducing argument count at \
the call site (same pattern as cap_create_address_space)"
)]
#[allow(
clippy::too_many_lines,
reason = "the function is a linear state-machine matching T-019 §Simulation \
rows 1–7 one-for-one; splitting into helpers (e.g. one per row) \
would obscure the row-to-code mapping that reviewers verify \
against the §Simulation table"
)]
pub fn load_image<M: Mmu, const N: usize, const R: usize>(
image: &[u8],
pmm: &mut Pmm<N, R>,
mmu: &M,
table: &mut CapabilityTable,
as_arena: &mut AddressSpaceArena<M>,
parent_as_cap: CapHandle,
new_rights: CapRights,
image_base_va: VirtAddr,
stack_size_pages: usize,
) -> Result<LoadedImage, LoadError> {
// §Simulation row 1: argument preflight. No state change.
if image.is_empty() {
return Err(LoadError::InvalidImage);
}
if stack_size_pages == 0 {
return Err(LoadError::InvalidStackSize);
}
// PR #31 review-round 4 P2: an unaligned `image_base_va` would
// surface as `MmuError::MisalignedAddress` from the first cap_map
// call inside the image-page loop, but only *after*
// `cap_create_address_space` has allocated the root L0 frame
// (which then leaks via the v1 baseline rollback). Catch it at
// argument-preflight time to keep PMM byte-stable on rejection.
if !image_base_va.0.is_multiple_of(PAGE_SIZE) {
return Err(LoadError::MisalignedImageBaseVa(image_base_va));
}
// §Simulation row 2: cap preflight — lookup + kind check. The
// DERIVE-rights check is delegated to cap_create_address_space
// step 2a; surfaces as AddressSpaceCreationFailed below.
let parent_cap = table
.lookup(parent_as_cap)
.map_err(LoadError::InvalidParentCap)?;
if parent_cap.kind() != CapKind::AddressSpace {
return Err(LoadError::InvalidParentCap(CapError::WrongKind));
}
// §Simulation row 3: VA-range preflight (first) + frame-budget
// preflight (second). VA-range runs first so an arithmetic-
// saturated request (e.g. `stack_size_pages = usize::MAX`)
// surfaces the documented `InvalidImageBaseVa` sentinel instead
// of being masked by the saturated-then-overshoot frame-budget
// comparison — addresses PR #31 review-round 3 Finding 2.
let image_pages = image.len().div_ceil(PAGE_SIZE);
let stack_pages = stack_size_pages;
// VA-range bound: the mapped span `[image_base_va, stack_top_va)`
// (half-open) must lie within `[0, USERSPACE_VA_LIMIT)` per
// ADR-0027 §Decision outcome (a). saturating_* primitives cap
// overflow at usize::MAX which is far past USERSPACE_VA_LIMIT
// (`1 << 48`), so the single `> USERSPACE_VA_LIMIT` check below
// catches both "base too high" and "span overflowed". The end
// value `usize::MAX` is the documented sentinel for the saturated
// path; non-sentinel ends name the offending VA for diagnostics.
let total_va_bytes = image_pages
.saturating_add(stack_pages)
.saturating_mul(PAGE_SIZE);
let span_end = image_base_va.0.saturating_add(total_va_bytes);
if span_end > USERSPACE_VA_LIMIT {
return Err(LoadError::InvalidImageBaseVa {
base: image_base_va,
end: VirtAddr(span_end),
});
}
// Frame-budget bound. The intermediate count is *exact* (not a
// safe upper bound) per `intermediate_frame_count`'s docstring;
// PR #31 review-round 3 Finding 1 replaced the previous
// hard-coded `6` (which under-counted when the image span crossed
// more than one 2 MiB L2 slot — every additional boundary adds
// one L3 table).
let intermediate_budget = intermediate_frame_count(image_base_va, image_pages, stack_pages);
let needed = 1usize
.saturating_add(image_pages)
.saturating_add(stack_pages)
.saturating_add(intermediate_budget);
let available = pmm.stats().free_frames;
if needed > available {
return Err(LoadError::FrameBudgetExceeded { needed, available });
}
// §Simulation row 4: image-PA-overlap preflight. Discharges
// UNSAFE-2026-0027 invariant "source and destination do not
// overlap" at runtime. Since the high-half migration (ADR-0033 /
// T-022) the kernel runs in the high half, so `image.as_ptr()`
// resolves to the image's high-half VA — `kernel_va_to_phys`
// converts it back to the PA the PMM extent is expressed in (on host
// test builds the offset is 0, so this is the identity). If any byte
// of the image's PA range could be returned by `pmm.alloc_frame()`,
// `copy_nonoverlapping` in the image-page loop below would alias
// source and destination — undefined behaviour per Rust's
// `core::ptr::copy_nonoverlapping` safety contract. The check is
// practically unreachable under correct BSP wiring (`.rodata`-
// resident images live in PMM-reserved memory by ADR-0035) but
// defensive against BSP misconfiguration. Pre-state-change; no
// rollback needed.
let image_pa_start = tyrne_hal::kernel_va_to_phys(image.as_ptr() as usize);
let image_pa_end = image_pa_start.saturating_add(image.len());
if pmm.could_yield_pa_overlapping(image_pa_start..image_pa_end) {
return Err(LoadError::ImageOverlapsAllocatableMemory);
}
// §Simulation row 5: mint the new AS cap. T-018's preflight
// discipline guarantees no PMM / arena / cap-table state was
// committed on failure → no rollback at this layer.
let loaded_as_cap =
cap_create_address_space(table, parent_as_cap, new_rights, mmu, pmm, as_arena)
.map_err(LoadError::AddressSpaceCreationFailed)?;
// Stack base = first VA above the image region.
let stack_base_va = VirtAddr(
image_base_va
.0
.saturating_add(image_pages.saturating_mul(PAGE_SIZE)),
);
// §Simulation row 6: image-page loop.
let mut image_pages_mapped: usize = 0;
for (i, chunk) in image.chunks(PAGE_SIZE).enumerate() {
// Defensive: alloc_frame returning None here is structurally
// unreachable post-budget preflight in v1's single-thread
// cooperative model; the OutOfFrames variant is retained
// (with rollback) for forward-concurrency scenarios.
let Some(frame) = pmm.alloc_frame() else {
rollback(
table,
pmm,
mmu,
as_arena,
loaded_as_cap,
image_base_va,
stack_base_va,
image_pages_mapped,
0,
);
return Err(LoadError::OutOfFrames);
};
// Byte-copy from .rodata-resident image bytes into the freshly
// PMM-allocated frame. Tail-zeroing on the partial last page
// happens automatically: the chunk is at most PAGE_SIZE bytes;
// bytes (chunk.len()..PAGE_SIZE) stay zero from the PMM's
// zero-init contract (UNSAFE-2026-0026). Audit:
// UNSAFE-2026-0027.
//
// SAFETY:
// **Why unsafe is needed.** `core::ptr::copy_nonoverlapping`
// requires raw pointers; the destination is a PA obtained from
// `pmm.alloc_frame()` (a `PhysFrame` whose payload is not a
// Rust-owned slice), so the materialisation step into a
// writable pointer is itself the operation we're auditing.
//
// **Invariants upheld.** (1) `chunk.as_ptr()` is a valid pointer
// to at least `chunk.len()` initialised bytes inside `image`'s
// backing storage (slice invariant). (2) The destination
// pointer is materialised via
// [`crate::mm::phys_frame_kernel_ptr`], which centralises v1's
// identity-mapping invariant per
// [ADR-0027 §Decision outcome (a)][adr-0027] so the future
// high-half migration ([ADR-0033 placeholder][adr-0027])
// updates one helper instead of every call site. The returned
// pointer is page-aligned (the `PhysFrame` type enforces
// alignment via `from_aligned`) and points at 4 KiB of PMM-
// owned, zero-initialised RAM exclusively owned by this stack
// frame until `cap_map` moves it into the AS (per the PMM's
// single-thread cooperative ownership discipline +
// [UNSAFE-2026-0026]'s zero-fill contract). (3) `chunk.len()`
// is at most `PAGE_SIZE`, so the write is in-bounds for the
// destination frame. (4) Source and destination are
// non-overlapping — runtime-enforced by the row-4 preflight
// (`Pmm::could_yield_pa_overlapping(image_pa_range)`); the
// BSP-layout discipline (`.rodata` ⊆ PMM-reserved range per
// ADR-0027 + ADR-0035) remains the production reality.
//
// **Why safer alternatives were rejected.** Per
// [UNSAFE-2026-0027][audit]: `write_volatile` would falsely
// imply MMIO ordering for a plain RAM-to-RAM copy; a
// `slice::from_raw_parts_mut(...).copy_from_slice(...)` form
// would push the same unsafety into the slice construction
// step; `Mmu::copy_into_frame`-style HAL relocation just moves
// the audit point without removing it.
//
// [UNSAFE-2026-0026]: https://github.com/HodeTech/Tyrne/blob/main/docs/audits/unsafe-log.md
// [adr-0027]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0027-kernel-virtual-memory-layout.md
// [audit]: https://github.com/HodeTech/Tyrne/blob/main/docs/audits/unsafe-log.md
unsafe {
let src = chunk.as_ptr();
let dst = crate::mm::phys_frame_kernel_ptr(frame);
core::ptr::copy_nonoverlapping(src, dst, chunk.len());
}
// cap_map: install the mapping under USER|EXECUTE.
let va = VirtAddr(image_base_va.0.saturating_add(i.saturating_mul(PAGE_SIZE)));
if let Err(e) = cap_map(
table,
loaded_as_cap,
mmu,
pmm,
as_arena,
va,
frame,
MappingFlags::USER | MappingFlags::EXECUTE,
) {
// Per [`tyrne_hal::Mmu::map`]'s failure-semantics contract
// (clause 2: "`pa` is not consumed on Err"), the leaf
// frame `frame` is still owned by this stack frame after
// the failed `cap_map`. Free it directly so it doesn't
// leak. Previously-mapped pages roll back via the helper
// below. **If a future BSP `Mmu::map` impl violates clause
// 2**, this `pmm.free_frame` would alias a still-mapped
// frame — the trait contract is the load-bearing safety
// argument here.
let _ = pmm.free_frame(frame);
rollback(
table,
pmm,
mmu,
as_arena,
loaded_as_cap,
image_base_va,
stack_base_va,
image_pages_mapped,
0,
);
return Err(LoadError::MapFailed(e));
}
image_pages_mapped = image_pages_mapped.saturating_add(1);
}
// §Simulation row 7: stack-page loop. Same shape, USER|WRITE.
let mut stack_pages_mapped: usize = 0;
for i in 0..stack_pages {
let Some(frame) = pmm.alloc_frame() else {
rollback(
table,
pmm,
mmu,
as_arena,
loaded_as_cap,
image_base_va,
stack_base_va,
image_pages_mapped,
stack_pages_mapped,
);
return Err(LoadError::OutOfFrames);
};
let va = VirtAddr(stack_base_va.0.saturating_add(i.saturating_mul(PAGE_SIZE)));
if let Err(e) = cap_map(
table,
loaded_as_cap,
mmu,
pmm,
as_arena,
va,
frame,
MappingFlags::USER | MappingFlags::WRITE,
) {
// See image-loop equivalent above: `pa` is not consumed
// on Err per `tyrne_hal::Mmu::map`'s failure-semantics
// contract (clause 2). Free the leaf frame; previous
// mappings unwind via `rollback` below.
let _ = pmm.free_frame(frame);
rollback(
table,
pmm,
mmu,
as_arena,
loaded_as_cap,
image_base_va,
stack_base_va,
image_pages_mapped,
stack_pages_mapped,
);
return Err(LoadError::MapFailed(e));
}
stack_pages_mapped = stack_pages_mapped.saturating_add(1);
}
// §Simulation row 8: construct + return. `stack_top_va` is
// one-past-the-highest mapped address (half-open `[stack_base,
// stack_top_va)` convention; matches AAPCS64 sp init).
let stack_top_va = VirtAddr(
stack_base_va
.0
.saturating_add(stack_pages.saturating_mul(PAGE_SIZE)),
);
Ok(LoadedImage {
as_cap: loaded_as_cap,
entry_va: image_base_va,
stack_top_va,
image_bytes: image.len(),
stack_bytes: stack_pages.saturating_mul(PAGE_SIZE),
})
}
#[allow(
clippy::too_many_arguments,
reason = "rollback mirrors load_image's argument surface (every kernel-state \
handle the forward-direction touches must be reachable for \
undo); bundling would obscure the symmetry"
)]
/// Roll back a partial `load_image` commit.
///
/// Reverses the committed mappings (stack pages first, then image
/// pages, reverse-order within each range) via [`cap_unmap`] + the
/// returned frame's [`Pmm::free_frame`], then drops the AS cap via
/// [`CapabilityTable::cap_drop`][crate::cap::CapabilityTable::cap_drop].
///
/// Errors during rollback are intentionally swallowed (the rollback
/// path runs only after a primary failure; surfacing a secondary
/// rollback error would mask the first one and provide no actionable
/// information). The leaks documented in T-019 §"Rollback contract"
/// (root L0, intermediate L1/L2/L3, AS arena slot) are unavoidable in
/// v1; full reclaim arrives with the future `MemoryRegionCap` +
/// per-AS destroy ADR.
fn rollback<M: Mmu, const N: usize, const R: usize>(
table: &mut CapabilityTable,
pmm: &mut Pmm<N, R>,
mmu: &M,
as_arena: &mut AddressSpaceArena<M>,
loaded_as_cap: CapHandle,
image_base_va: VirtAddr,
stack_base_va: VirtAddr,
image_pages_mapped: usize,
stack_pages_mapped: usize,
) {
// Unmap stack pages first (reverse install order).
for i in (0..stack_pages_mapped).rev() {
let va = VirtAddr(stack_base_va.0.saturating_add(i.saturating_mul(PAGE_SIZE)));
if let Ok(frame) = cap_unmap(table, loaded_as_cap, mmu, as_arena, va) {
let _ = pmm.free_frame(frame);
}
}
// Then image pages (reverse install order).
for i in (0..image_pages_mapped).rev() {
let va = VirtAddr(image_base_va.0.saturating_add(i.saturating_mul(PAGE_SIZE)));
if let Ok(frame) = cap_unmap(table, loaded_as_cap, mmu, as_arena, va) {
let _ = pmm.free_frame(frame);
}
}
// Cap-side cleanup. cap_drop (not cap_revoke) because the AS cap
// is a leaf by construction (no descendants derived from it).
let _ = table.cap_drop(loaded_as_cap);
}
/// Failure taxonomy for [`task_create_from_image`].
///
/// `#[non_exhaustive]` because future-state variants are foreseeable — e.g. a
/// per-operation Task-rights check if/when that ADR lands.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TaskCreateError {
/// `loaded.as_cap` did not resolve to a live `CapKind::AddressSpace`
/// capability in the supplied table — wraps [`CapError::InvalidHandle`]
/// (stale / freed handle) or [`CapError::WrongKind`] (wrong-kind cap). No
/// task object was minted.
InvalidAddressSpaceCap(CapError),
/// The task arena had no free slot. No task object was minted and no
/// capability inserted.
TaskArenaFull,
/// The capability table could not hold the Task capability (wraps
/// [`CapError::CapsExhausted`]). The just-minted task object is **rolled
/// back** (its arena slot freed) before returning, so the failure leaves
/// no orphaned task.
CapTableExhausted(CapError),
}
/// Turn a [`LoadedImage`] into a runnable `CapObject::Task` capability — the
/// deferred [B4 §3 / B6-step-3][phase-b-b6] bridge.
///
/// Resolves the loaded address space from `loaded.as_cap`, mints a [`Task`]
/// kernel object bound to it, and installs a **root** `CapKind::Task`
/// capability naming that task into `table`, returning its [`CapHandle`].
///
/// **Mints, does not schedule.** This produces a runnable task capability; it
/// does *not* run the task. The B6 wire-up runs it — calling
/// [`Scheduler::add_user_task`][crate::sched::Scheduler::add_user_task] with
/// `loaded.entry_va` / `loaded.stack_top_va` (the EL0 entry context lives in
/// the cooperative task context per [ADR-0037], not on the `Task` object, so
/// `task_create_from_image` does not read them) — and **not** before the
/// T-021 carry-forward gates (per-task `console_write` window + per-page
/// user-VA translation; current-task capability table) close.
///
/// **v1 cap-rights model (kind-only).** The minted `CapKind::Task` cap carries
/// `task_rights`, which govern *capability management* (duplicate / derive /
/// transfer / revoke the Task cap). There is **no Task-*operation* right** in
/// v1 — this mirrors the `CapKind::AddressSpace` kind-only model (see
/// [`cap_map`], where the cap *kind* grants the authority). Callers should
/// therefore pass only cap-management rights; object-operation rights
/// (`SEND` / `RECV` / `NOTIFY` / `CONSOLE_WRITE`) are **inert** on a Task cap —
/// no operation gates on them — and are deliberately **not** masked here (the
/// caller chooses the rights at mint, matching the cap model's convention; a
/// uniform Task-cap rights policy, if ever wanted, is an ADR, not a silent
/// per-site mask). Per-operation Task rights (a future run / suspend / kill
/// authority) are deferred to a later ADR.
///
/// # Errors
///
/// - [`TaskCreateError::InvalidAddressSpaceCap`] if `loaded.as_cap` is stale
/// or is not a `CapKind::AddressSpace` cap when resolved against `table`.
/// - [`TaskCreateError::TaskArenaFull`] if `task_arena` has no free slot (no
/// task minted, no cap inserted).
/// - [`TaskCreateError::CapTableExhausted`] if `table` cannot hold the Task
/// cap; the freshly-minted task object is rolled back (arena slot freed)
/// before returning, so the failure leaves no leaked task.
///
/// [ADR-0037]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0037-el0-entry-context.md
/// [phase-b-b6]: https://github.com/HodeTech/Tyrne/blob/main/docs/roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello
pub fn task_create_from_image(
loaded: &LoadedImage,
table: &mut CapabilityTable,
task_arena: &mut TaskArena,
id: u32,
task_rights: CapRights,
) -> Result<CapHandle, TaskCreateError> {
// 1 — resolve the loaded AS cap to its handle (lookup + kind-check),
// mirroring `resolve_address_space_cap`'s kind-only discipline. The
// immutable borrow of `table` ends with this block, before the
// `insert_root` mutable borrow below.
let as_handle = {
let cap = table
.lookup(loaded.as_cap)
.map_err(TaskCreateError::InvalidAddressSpaceCap)?;
match cap.object() {
CapObject::AddressSpace(h) => h,
_ => return Err(TaskCreateError::InvalidAddressSpaceCap(CapError::WrongKind)),
}
};
// 2 — mint the Task kernel object bound to that AS. `create_task` only ever
// returns `ObjError::ArenaFull` (its body is `arena.allocate(..).ok_or(
// ObjError::ArenaFull)`), so collapsing its error to `TaskArenaFull` is
// exhaustive by that contract — not a lossy catch-all.
let task_handle = create_task(task_arena, Task::new(id, as_handle))
.map_err(|_| TaskCreateError::TaskArenaFull)?;
// 3 — install a root Task capability.
let cap = Capability::new(task_rights, CapObject::Task(task_handle));
match table.insert_root(cap) {
Ok(task_cap) => Ok(task_cap),
Err(e) => {
// `insert_root` only ever returns `CapsExhausted` (cap/table.rs);
// pin that so a future widening of its contract is caught here in
// debug rather than silently mislabelled as `CapTableExhausted`.
debug_assert!(
matches!(e, CapError::CapsExhausted),
"insert_root returned a non-CapsExhausted error: {e:?}",
);
// Roll the task object back so a full cap-table leaks no arena slot.
// Called UNCONDITIONALLY (deliberately NOT inside `debug_assert!`,
// which would strip the call in release builds and leak the slot);
// the handle was just created by `create_task`, so `destroy_task`
// cannot fail (it deallocates a live, generation-matched slot and
// performs no reachability check — task.rs).
let _ = destroy_task(task_arena, task_handle);
Err(TaskCreateError::CapTableExhausted(e))
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
reason = "tests may use pragmas forbidden in production kernel code"
)]
mod tests {
use super::{
create_task, intermediate_frame_count, load_image, rollback, task_create_from_image,
LoadError, LoadedImage, TaskCreateError,
};
use crate::cap::{
CapError, CapHandle, CapKind, CapObject, CapRights, Capability, CapabilityTable,
};
use crate::mm::{AddressSpaceArena, AddressSpaceError, PhysFrameRange, Pmm};
use crate::obj::{EndpointHandle, Task, TaskArena};
use std::sync::Mutex;
use std::vec;
use std::vec::Vec;
use tyrne_hal::{
FrameProvider, MapperFlush, MappingFlags, Mmu, MmuError, PhysAddr, PhysFrame, VirtAddr,
PAGE_SIZE,
};
use tyrne_test_hal::{BlockMappedMmu, FakeAddressSpace, FakeMmu, OutOfFramesMmu};
// ── Pmm-over-backing helper (mirrors kernel/src/mm/pmm.rs::tests) ─────────
//
// Allocates a host-side `Vec<u8>` aligned to PAGE_SIZE, constructs a Pmm
// whose extent points at the Vec's payload, and returns both — the Vec
// kept alive by the caller, the Pmm wrapped over its physical-storage