Skip to content

Latest commit

 

History

History
412 lines (383 loc) · 26.6 KB

File metadata and controls

412 lines (383 loc) · 26.6 KB

Rust Subsystems

Audience: Kernel hackers adding or maintaining Rust subsystem crates.

Execution context: Kernel build tooling and kernel-linked Rust crates.

Maturity: Stable foundation; twenty-six production Rust subsystems live in the kernel tree.

Production: DuetFS, USB HID, USB class config, DHCP / DNS / TCP-options / IPv4-header byte-walkers, USB MSC SCSI responses, PNG / BMP / TGA / JPEG header validators, ELF / PE-image validators, NTFS metadata walker, exFAT metadata walker, ext4 metadata walker, ACPI table walker, ACPI AML namespace walker, IEEE 802.11 management-frame walker, Bluetooth HCI walker, SMBIOS table walker, PCI / PCIe capability list walkers, Multiboot2 info-structure walker, TLS 1.2 record + handshake walker, VT/ANSI escape parser, NVIDIA GSP firmware-image (nvfw_bin_hdr) parser, AMD GFX9+ microcode-image (gfx_firmware_header_v1_0) parser, Intel iwlwifi TLV firmware parser, Realtek rtlwifi/rtw88/rtw89 firmware-header parser, and Broadcom b43 firmware-record-stream parser.

All twenty-six crates have a current C++ caller; there are no skeleton crates left in this slice.

Overview

Rust in DuetOS is a kernel subsystem tool, not a second application framework. A Rust crate is appropriate when the subsystem owns a clear boundary with attacker-controlled structured bytes or lifetime-heavy state, such as DuetFS, USB class descriptors, or the TCP/IP stack. C++ remains the orchestration language for the kernel image.

"Attacker-controlled bytes" means bytes that cross a trust boundary before the kernel has parsed them: USB descriptors returned by a plugged-in device, network packets from the wire, filesystem metadata from removable media, and PE/ELF metadata from an executable image. The attacker is not assumed to have code execution yet; the risk is that a malicious device or image can choose lengths, offsets, counts, nesting depth, and tag ordering that stress every parser edge case.

Rust is not magic sandboxing and it does not make an unsafe FFI boundary safe by itself. It is useful here because the byte walkers can be written as bounds-checked slice traversal with checked/saturating arithmetic, no ambient aliasing, and no unchecked pointer increments in the parser core. In C++ the same parser can be made correct, but every ptr + len, cast, packed-struct view, and manual lifetime convention must stay correct forever; one missed bounds check can become a kernel read/write primitive. DuetOS therefore keeps C++ as the owning/orchestration layer and uses Rust selectively for narrow parsers where memory-safety bugs are the main risk.

Candidate priority

The first Rust slice covers DuetFS plus USB descriptor parsers, but it does not exhaust the high-risk surface. Prioritize future Rust crates where the subsystem is mostly byte parsing or state-machine validation and can expose a small C ABI back to the C++ owner.

Priority Candidate Why it is high risk Rust boundary shape
P0 Network packet parsers (Ethernet / ARP / IPv4 / ICMP / UDP / TCP options, DHCP / DNS where applicable) Remote peers control packet lengths, header offsets, fragmentation state, option lists, and checksummed payload shape. A parser bug is remotely reachable before auth. Parse borrowed RX buffers into validated header/value structs; C++ keeps NIC rings, routing, timers, and socket ownership.
P0 PE/COFF and ELF metadata readers Executable images control section tables, data-directory RVAs, imports, relocations, TLS records, resources, and symbol/export tables. This sits directly on the project pillar of running PE binaries. Rust validates image metadata and returns a relocation/import/load plan; C++ owns address-space mapping, capability checks, and process creation.
P0 Read-only disk-format parsers not already in Rust (NTFS / exFAT / FAT32 / ext4 metadata walkers) Removable or dual-boot disks control superblocks, directory entries, extents, runlists, timestamps, and string encodings. The kernel must reject malformed metadata without trusting lengths or offsets. Rust turns blocks into validated directory / inode / extent records; C++ VFS owns handles, caching policy, block devices, and permissions.
P1 Font and image decoders used by UI or boot assets (TTF, PNG, BMP/TGA where non-trivial) Asset files can be supplied by themes, apps, or downloaded content and tend to contain nested tables, compressed streams, and attacker-chosen dimensions. Rust decodes/validates metadata and bounded spans; C++ renderer owns surfaces, glyph cache, and GPU upload.
P1 Protocol control-plane parsers (USB RNDIS/CDC control messages, Bluetooth HCI events, Wi-Fi management frames/EAPOL) Devices or nearby radios control variable-length protocol records and state transitions. Bugs can be reached from hardware, radio, or network-adjacent inputs. Rust parses envelopes and validates state-machine messages; C++ owns driver rings, DMA, IRQs, and kernel object lifetimes.
P2 ACPI/SMBIOS/PCI capability-table readers Firmware controls table lengths, offsets, checksums, and nested structures before the kernel has a normal trust base. Bugs are usually local/firmware-level, but they run very early. Rust validates table walks into plain records; C++ owns boot sequencing, MMIO mapping, and architecture effects.

Do not move code to Rust just because it is important. Scheduler paths, address-space mutation, IRQ dispatch, DMA ring programming, and GPU command submission are high-consequence but not automatically good Rust candidates: they are dominated by hardware side effects, lock ordering, and existing C++ kernel ownership. Rust is the best fit when the risky part can be isolated as "bytes/state in, validated plan out."

The repository now has one shared Rust foundation and actual Rust subsystem code:

  • /rust-toolchain.toml pins the nightly toolchain and bare-metal target.
  • /Cargo.toml is the workspace root and owns the profiles for every Rust crate linked into the kernel.
  • /Cargo.lock is tracked so dependency resolution is reproducible; CMake invokes cargo with --locked. This freezes package selection but is not an offline or hermetic dependency source: a clean Cargo home may still fetch the registry index and crates. Use a previously populated cache for offline builds; a separately reviewed vendored-source policy is still required before the Rust build can be called network-independent.
  • /.cargo/config.toml selects x86_64-unknown-none and the build-std knobs needed by freestanding crates.
  • /kernel/rust/ is the single Rust staticlib link unit. Subsystem crates are rlibs; the aggregate staticlib pulls in core / alloc / panic runtime once so the C++ kernel link does not get duplicate Rust runtime objects.
  • /kernel/fs/duetfs/ is the native filesystem Rust subsystem.
  • /kernel/drivers/usb/hid_rust/ is the USB HID report-descriptor parser Rust subsystem; C++ HID APIs are wrappers over this parser.
  • /kernel/drivers/usb/class_rust/ parses USB configuration/interface/endpoint descriptor streams for MSC, hub, UVC, and Bluetooth class-driver binding.
  • /kernel/net/parsers_rust/ (duetos_net_parsers) wraps the DHCPv4 option walker and the DNSv1 name skipper. C++ callers in kernel/net/stack.cpp delegate DhcpFindOption and DnsSkipName through this crate.
  • /kernel/drivers/usb/msc_scsi_rust/ (duetos_usb_msc_scsi) parses USB MSC SCSI INQUIRY / READ CAPACITY(10) / GET CONFIGURATION header / READ TOC header / READ DISC INFORMATION responses. The C++ MSC driver (kernel/drivers/usb/msc_scsi.cpp) delegates its parse functions through this crate.
  • /kernel/util/img_meta_rust/ (duetos_img_meta) validates PNG, BMP, TGA, and JPEG / JFIF / EXIF image headers. kernel/util/png.cpp::PngParseHeader, kernel/util/bmp.cpp::BmpParseHeader, kernel/util/tga.cpp::TgaParseHeader, and kernel/util/jpeg.cpp::JpegParseHeader all delegate to this crate; the C++ side keeps zlib inflate, scanline filter unwind, and pixel-copy. The JPEG validator walks segments from the SOI marker until the first Start-of-Frame (SOF) and extracts dimensions + precision + component count without entering the entropy-coded scan data. No decoder lives in the tree yet; the validator is the toolkit a future viewer / thumbnail cache / wallpaper-extension consumer will sit on.
  • /kernel/loader/exec_meta_rust/ (duetos_exec_meta) validates ELF64 files (header + every PT_LOAD segment) and PE/COFF images (DOS stub + e_lfanew bounds + PE signature + AMD64 machine check + optional-header magic / section / file alignment + image-base low-half bound + section-table bounds + per-section raw extent fit). kernel/loader/elf_loader.cpp::ElfValidate and the body of kernel/loader/pe_loader.cpp::ParseHeaders (up to but not including the data-directory walks) delegate to this crate; the C++ side keeps data-directory checks, address-space mapping, capability checks, and process creation.
  • /kernel/fs/ntfs_rust/ (duetos_ntfs) parses NTFS boot sectors, MFT record headers, resident $FILE_NAME attributes, and runlist (mapping-pairs) entries. kernel/fs/ntfs.cpp delegates byte parsing to this crate; UTF-16 → ASCII translation stays in C++.
  • /kernel/fs/exfat_rust/ (duetos_exfat) parses exFAT VBRs, derives cluster geometry, walks the FAT chain (4-byte LE per cluster), and decodes dirent sets (File 0x85 + Stream-Extension 0xC0 + FileName 0xC1 tuples). kernel/fs/exfat.cpp delegates byte parsing to this crate.
  • /kernel/fs/ext4_rust/ (duetos_ext4) parses ext2/3/4 superblocks, group descriptors, inode records, extent headers, extent leaves / index nodes, and linux_dirent records. kernel/fs/ext4.cpp delegates byte parsing to this crate; block I/O, scratch management, and the depth>0 extent-tree DFS stay in C++.
  • /kernel/acpi/acpi_rust/ (duetos_acpi) parses RSDP v1 / v2, ACPI table headers, MADT entry headers, FADT body fields, MCFG entries, HPET descriptors, and SRAT memory-affinity entries. kernel/acpi/acpi.cpp::AcpiInit delegates the RSDP signature + checksum validation to the crate; ParseFadt cross-validates its packed-struct overlay against the Rust decoder.
  • /kernel/acpi/aml_rust/ (duetos_aml) is the recursive AML TermList walker over the DSDT / SSDT bytecode: PkgLength / NameString decode, Scope / Device / Method / Name / OperationRegion / Mutex / Event / Alias / External / Processor / ThermalZone / PowerResource records, and the constant-bound NamedField index. kernel/acpi/aml.cpp::WalkTable delegates one table's byte parse to the crate's duetos_aml_walk_table, which appends named-object records straight into the kernel's namespace / region / field tables (layout-asserted FFI mirrors). C++ keeps the table storage, accessors, and the offset slicers (AmlMethodBody / AmlNameValue / AmlReadS5); the AML evaluator (aml_eval.cpp, hardware FieldUnit I/O) stays C++. Continuously fuzzed by tests/fuzz/fuzz_aml.
  • /kernel/net/wifi80211_rust/ (duetos_wifi80211) parses 802.11 frame headers, Beacon / Probe Response body prefixes, the IE (Information Element) list, and EAPOL-Key (4-way handshake) descriptors. kernel/net/wireless/beacon.cpp::BeaconParse delegates the frame header, body, and IE walks to the crate.
  • /kernel/net/hci_rust/ (duetos_hci) parses Bluetooth HCI event packets and the Command Complete, Command Status, Disconnection Complete, LE Meta, Read_Local_Version, and Read_BD_ADDR bodies. kernel/net/bluetooth/hci.cpp delegates the Read_Local_Version + Read_BD_ADDR rparam decoders to the crate.
  • /kernel/drivers/net/iwlwifi_fw_rust/ (duetos_iwlwifi_fw) walks the Intel iwlwifi TLV firmware blob: 88-byte preamble (zero + magic + 64-byte name + ver + build + 8 ignored) + stream of (u32 type, u32 length, payload[length], pad-to-4) TLV records. Recognises INST / DATA / INIT / INIT_DATA / SEC_RT / FLAGS / NUM_OF_CPU / FW_VERSION / PHY_SKU / HW_TYPE; unknown TLVs bump a counter without failing the parse. Every off + 8 + length arithmetic uses checked_add so a hostile TLV length can't wrap into a smaller "fits the blob" value. kernel/drivers/net/iwlwifi_fw.cpp delegates byte parsing to this crate; C++ side keeps the sanitize-for-serial-print pass on the human-readable name.
  • /kernel/drivers/net/rtl88xx_fw_rust/ (duetos_rtl88xx_fw) walks the Realtek rtlwifi/rtw88/rtw89 32-byte fixed firmware header. Classifies the generation by signature (rtl8192/8723/8821/8812/8814 → rtlwifi, 0x88B0 → rtw88, 0x8852 → rtw89). Rejects unknown signatures + short blobs. kernel/drivers/net/rtl88xx_fw.cpp delegates.
  • /kernel/drivers/net/bcm43xx_fw_rust/ (duetos_bcm43xx_fw) walks the Broadcom b43 8-byte-big-endian record stream (type / version / reserved / be32 size / payload). Up to 8 records per blob (configurable cap); convenience indices for the first ucode / pcm / iv record. Truncated-on-overflow semantics — earlier-records still report cleanly when a later record's declared size overflows the blob. The C++ wrapper converts the Rust-side indices back to in-place convenience pointers in the caller-owned struct.
  • /kernel/drivers/gpu/nvidia_gsp_fw_rust/ (duetos_nvidia_gsp_fw) parses NVIDIA Turing+ GSP firmware containers (gsp_tu10x.bin / ga10x.bin / ad10x.bin). The 24-byte outer nvfw_bin_hdr + per-arch inner descriptor (76 bytes Turing/GA100, 84 bytes GA102+) + ELF64 RISC-V payload are all attacker-controllable when the install media is hostile. Checked arithmetic on data_offset + data_size; rejects bad magic, bad version, descriptor-too-small, data-bounds, and oversize images. kernel/drivers/gpu/nvidia_gsp_fw.cpp delegates the byte parse to this crate; the C++ side keeps the public NvidiaGspFwParse API + boot self-test.
  • /kernel/drivers/gpu/amd_gfx_fw_rust/ (duetos_amd_gfx_fw) parses AMD GFX9+ microcode images (linux-firmware blobs). 32-byte common_firmware_header + optional 12-byte gfx_firmware_header_v1_0 tail (feature version + jump-table offset/size) + ucode payload. Validates header_size_bytes vs blob, ucode_array_offset+size bound, ucode multiple-of-4, and jump-table fits inside the payload — every check via checked arithmetic so a hostile peer can't drive a length field into wrap-around. kernel/drivers/gpu/amd_gfx_fw.cpp delegates to this crate.
  • /kernel/util/vt_parser_rust/ (duetos_vt) implements the DEC ANSI / xterm escape parser. State machine + UTF-8 decoder + CSI parameter accumulator + OSC string buffer over a &mut DuetosVtParser (the C++ side allocates the struct; Rust operates on it). Four callbacks (print/execute/csi/osc) cross the FFI wall via repr(C) function pointers; the extern "C" invocations are otherwise plain Rust calls so the parser core stays unsafe-free outside the three init/reset/feed entry points. Untrusted PTY bytes from user processes feed kernel/util/vt_parser.cpp, which delegates every operation to this crate. The compile-time static_assert on sizeof(Parser) == sizeof(DuetosVtParser) + sizeof(Callbacks) == sizeof(DuetosVtCallbacks) pins the binary equivalence so a future drift on either side can't silently desync.
  • /kernel/net/tls_rust/ (duetos_tls) parses TLS 1.2 record
    • handshake byte streams: the 5-byte record header, the 4-byte handshake header, the ServerHello body (version + random + session-id + cipher + compression + optional extensions), the Certificate-message body (3-byte total list + per-cert length prefix + leaf DER slice), and the zero-byte ServerHelloDone. Remote peer controls every length prefix; the Rust core uses checked arithmetic to reject u32 length overflows that would otherwise wrap under attacker control. kernel/net/tls.cpp delegates the five TlsPeek* / TlsParse* entry points to this crate; the C++ side keeps AES-GCM record crypto, RSA pre-master encryption, the PRF, the transcript hash, and the connection lifecycle.
  • /kernel/arch/x86_64/smbios_rust/ (duetos_smbios) decodes the 2.x (_SM_ + _DMI_) and 3.x (_SM3_) entry-point anchors (signature + length + 8-bit checksum), then walks the variable-length structure table — each call returns the bounded (formatted_offset, strings_offset, end_offset) triple a C++ caller needs to advance to the next record. The trailing-strings walker enforces a 1 KiB per-string cap so a firmware that omits a NUL terminator can't make the walker run past the structure-table slice. kernel/arch/x86_64/smbios.cpp keeps the legacy-BIOS scan window (PhysToVirt(0xF0000) + 16-byte stride), single-init guarding, the BIOS / system / chassis / processor field extraction, and the boot-log line.
  • /kernel/drivers/pci/caps_rust/ (duetos_pci_caps) walks both the standard capability list (8-bit "next" pointers, head at config-space offset 0x34) and the PCIe extended capability list (12-bit "next" pointers, head at ECAM offset 0x100). Each chain hop is bounded; self-loops, out-of-range pointers, unaligned next-offsets, and the all-zero "no ext caps" sentinel are clamped to end-of-list. kernel/drivers/pci/pci.cpp materialises the device's standard config into a 256-byte buffer and routes PciFindCapability through the crate. The new PciFindExtCapability entry point is ready for the MMCONFIG-routed read primitive that a future PCIe driver needing AER / SR-IOV / ATS will add.
  • /kernel/mm/multiboot2_rust/ (duetos_multiboot2) validates the Multiboot2 info-structure header and walks the tag list + the mmap entry array. The bootloader-controlled total_size is capped at 64 MiB; each tag's size field is validated to fit in the remaining slice; mmap-entry base+length overflow is rejected. kernel/mm/frame_allocator.cpp::ForEachMmapEntry delegates every cursor advance to the crate.
  • /cmake/DuetOSRust.cmake exposes duetos_add_rust_staticlib(...), used by /kernel/rust/CMakeLists.txt to build the aggregate Rust link unit. Its input list is derived from the root workspace by tools/test/check-rust-ffi.py; a missing aggregate dependency or non-member dependency stops configuration.

Lint + format policy

The workspace pins one [workspace.lints] block in /Cargo.toml; every member crate inherits via [lints] workspace = true. The deny-set is intentionally small (unsafe_op_in_unsafe_fn, unused_must_use, non_ascii_idents, clippy::todo, clippy::unimplemented, clippy::dbg_macro); undocumented_unsafe_blocks is documented as an aspirational lint pending a SAFETY-comment backfill on the v0 crates.

Style follows idiomatic Rust (K&R braces, default control flow); the C++ Allman convention does not bleed in. The pin lives in /rustfmt.toml; the local CI preflight (tools/dev/check-local.sh) runs cargo fmt --check, cargo clippy -- -D warnings, and a host unit-test smoke (tools/dev/cargo-host-test.sh) against every crate that ships #[cfg(test)] modules.

python tools/test/check-rust-ffi.py is the separate build-truth and FFI boundary audit. It inventories every explicit workspace member and checks four contracts:

  1. The aggregate crate has one local dependency and one pub use for every other workspace member, with no extras.
  2. Rust sources, hand-written headers, manifests, literal data includes, the canonical repository-root Cargo config, the lockfile, and the pinned toolchain are all visible to the CMake archive dependency graph. Kernel crates use the conventional in-member source layout: custom target paths, build.rs, source include!, manifest patch/replace tables, and unlisted local path dependencies fail closed. Cargo config is exact-schema validated so paths, source replacement, compiler/wrapper overrides, and nested configs cannot redirect the audited graph. The root toolchain file is likewise validated against the exact dated channel, component, target, and profile; no other rustup override is permitted in the tree.
  3. Every raw-pointer export is an unsafe extern fn using the single supported C ABI; a safe export is accepted only by exact name in the checker's scalar-only allowlist, and a signature change away from C scalars invalidates that entry. Exported function using C-unwind, system, or any other extern ABI is a hard finding (FFI014). The canonical parity gate enforces the same C-only rule.
  4. Rust exports and each crate's hand-written C declarations agree by symbol, calling convention, return type, arity, scalar width, pointer depth, and pointee constness. The canonical parity gate is tools/test/check-rust-ffi-signatures.py; any parser error or mismatch is a fail-closed FFI013 build-truth error. FFI003 also applies a conservative lexical check for direct helper signatures that return an unconstrained generic or 'static borrow from a raw pointer.

The normal audit exits nonzero for either build errors or FFI findings. CMake uses the path-only emit mode: it fails closed on workspace/build-graph errors and on canonical signature-parity failures, while other FFI findings remain an explicit hardening backlog rather than being silently grandfathered into the safe-export allowlist. Inventory is a single streaming traversal per member, prunes member-root .git, target, and __pycache__ output only (nested Rust modules with those identifiers remain audited), rejects symlinks and name-surrogate reparse boundaries, permits contained nonredirecting OneDrive cloud-filter tags, and enforces global entry, byte, record, diagnostic, and emitted-output caps before retaining more data. Emitted paths reject CMake-list and line-protocol delimiters. Existing inputs and every in-repository Cargo config/toolchain candidate from kernel/rust through the repository root are configure dependencies, so content edits and newly introduced overrides rerun the audit before Cargo. An always-run build target rechecks the graph before every requested Rust/kernel build. CMake consumes the bounded exact input list rather than recursively globbing member trees, and Cargo is restricted to --lib, so a newly compiled module requires a tracked source or manifest change. The parity checker itself is returned as a CMake build input.

Cargo/rustup configuration above the repository root is not a trusted project input. CMake runs Cargo from a controlled OS-temporary working directory outside the repository, recreates its empty Cargo home, and makes the always-run audit reject any Cargo config on either effective ancestor search path. It also pins RUSTUP_TOOLCHAIN and clears compiler/wrapper/flag override variables. Release jobs explicitly provision that exact dated toolchain with rust-src and the bare-metal target before building. The source audit guarantees the complete in-repository portion of the configuration search path even though Cargo receives the validated target/build-std policy through explicit command flags.

FFI003 is a bounded source-signature heuristic, not a Rust borrow/lifetime proof. It deliberately catches the current unconstrained helper pattern and may need extension for macro-generated or type-aliased signatures; every FFI wall still requires independent unsafe-code review.

Host unit tests

Workspace .cargo/config.toml forces target = x86_64-unknown-none + unstable.build-std, which makes cargo test unusable directly (the test harness needs std). tools/dev/cargo-host-test.sh works around this by calling rustc --test directly against each crate's src/lib.rs, building a hosted binary with the system libcore + libstd. New crates that ship #[cfg(test)] modules add themselves to the HOST_TEST_CRATES list at the top of the script.

Contract for a new crate

  1. Add the crate directory to the root [workspace].members list.
  2. Keep the crate standalone: C++ may call Rust through a narrow C FFI, but do not create C++ → Rust → C++ → Rust chains.
  3. Expose a hand-written C header in the crate's include/ directory. Bindgen and cbindgen are intentionally not part of the kernel build.
  4. Keep unsafe at the FFI wall. Convert raw C pointers into Rust references or slices once, validate null/empty cases first, then keep the parser core in safe Rust. Any internal unsafe block needs a one-line comment naming the kernel invariant that makes it sound.
  5. Add the crate as a dependency of /kernel/rust/Cargo.toml. Do not link subsystem crates as independent staticlibs; multiple build-std staticlibs duplicate core / alloc symbols.
  6. Add the hand-written header path to kernel/CMakeLists.txt if C++ code needs to include it directly, then expose C++ wrappers through the owning subsystem directory. Do not add a second source/header list to /kernel/rust/CMakeLists.txt; CMake derives those inputs from the workspace.

CMake shape

Only /kernel/rust/CMakeLists.txt calls duetos_add_rust_staticlib(...). It builds the aggregate duetos_kernel_rust staticlib. Before defining the custom command, duetos_collect_rust_workspace_depends(...) validates the explicit workspace/aggregate relationship and derives all current member source, header, manifest, and build inputs. CONFIGURE_DEPENDS globs are rooted only at those derived member directories so adding or removing a matching input regenerates the dependency list without a hand-maintained crate table. A separate FFI validation stamp depends on every derived input and runs the normal audit before Cargo, so editing an existing Rust export, header declaration, or checker cannot reuse an earlier parity result. kernel/CMakeLists.txt links the resulting one .a into both kernel ELF stages and includes each subsystem's hand-written C header directory for C++ wrappers.

The Cargo custom command owns a completion stamp and declares the archive as a byproduct. Header- or checker-only edits first refresh the validation stamp; Cargo may then legitimately reuse an unchanged archive, while touching its own completion stamp records the successful dependency edge without rewriting the archive or leaving Ninja permanently dirty.

Profiles

Profiles live only at the workspace root so every kernel-linked crate has the same panic, LTO, optimization, and overflow-check behavior. Crate-local profile sections are ignored by cargo once a workspace root exists, so do not add them back to member crates. The panic handler lives in /kernel/rust/src/panic.rs; subsystem rlibs must not define their own #[panic_handler].

CMake accepts only DUETOS_RUST_PROFILE=release (Cargo output directory release) or DUETOS_RUST_PROFILE=dev (Cargo's special output directory debug). Any other cache value fails configuration instead of naming an output archive Cargo may never create.