Skip to main content

truce_loader/
canary.rs

1//! ABI canary — runtime verification that shell and dylib have
2//! compatible type layouts and vtable ordering.
3
4use truce_core::buffer::AudioBuffer;
5use truce_core::events::{Event, EventBody, EventList, TransportInfo as Transport};
6use truce_core::process::{ProcessContext, ProcessStatus};
7use truce_gui::interaction::WidgetRegion;
8use truce_gui::layout::GridLayout;
9use truce_gui::render::RenderBackend;
10use truce_gui::theme::{Color, Theme};
11
12use crate::traits::PluginLogic;
13
14/// ABI fingerprint. Compared between shell and dylib before loading.
15///
16/// This is the ONE `#[repr(C)]` type in the system — it's the
17/// bootstrap verification struct that makes everything else safe.
18#[repr(C)]
19pub struct AbiCanary {
20    pub trait_object_size: usize,
21    pub audio_buffer_size: usize,
22    pub process_context_size: usize,
23    pub process_status_size: usize,
24    pub event_size: usize,
25    pub event_body_size: usize,
26    pub transport_size: usize,
27    pub widget_region_size: usize,
28    pub theme_size: usize,
29    pub plugin_layout_size: usize,
30    pub color_size: usize,
31    pub vec_u8_size: usize,
32    pub option_usize_size: usize,
33    pub audio_buffer_align: usize,
34    pub process_status_align: usize,
35    pub result_normal_disc: u8,
36    pub result_tail_disc: u8,
37    pub result_keepalive_disc: u8,
38    pub rustc_version_hash: u64,
39}
40
41impl AbiCanary {
42    pub fn current() -> Self {
43        Self {
44            trait_object_size: std::mem::size_of::<*const dyn PluginLogic>() * 2,
45            audio_buffer_size: std::mem::size_of::<AudioBuffer>(),
46            process_context_size: std::mem::size_of::<ProcessContext>(),
47            process_status_size: std::mem::size_of::<ProcessStatus>(),
48            event_size: std::mem::size_of::<Event>(),
49            event_body_size: std::mem::size_of::<EventBody>(),
50            transport_size: std::mem::size_of::<Transport>(),
51            widget_region_size: std::mem::size_of::<WidgetRegion>(),
52            theme_size: std::mem::size_of::<Theme>(),
53            plugin_layout_size: std::mem::size_of::<GridLayout>(),
54            color_size: std::mem::size_of::<Color>(),
55            vec_u8_size: std::mem::size_of::<Vec<u8>>(),
56            option_usize_size: std::mem::size_of::<Option<usize>>(),
57            audio_buffer_align: std::mem::align_of::<AudioBuffer>(),
58            process_status_align: std::mem::align_of::<ProcessStatus>(),
59            result_normal_disc: discriminant_byte(&ProcessStatus::Normal),
60            result_tail_disc: discriminant_byte(&ProcessStatus::Tail(0)),
61            result_keepalive_disc: discriminant_byte(&ProcessStatus::KeepAlive),
62            rustc_version_hash: rustc_hash(),
63        }
64    }
65
66    pub fn matches(&self, other: &Self) -> bool {
67        self.trait_object_size == other.trait_object_size
68            && self.audio_buffer_size == other.audio_buffer_size
69            && self.process_context_size == other.process_context_size
70            && self.process_status_size == other.process_status_size
71            && self.event_size == other.event_size
72            && self.event_body_size == other.event_body_size
73            && self.transport_size == other.transport_size
74            && self.widget_region_size == other.widget_region_size
75            && self.theme_size == other.theme_size
76            && self.plugin_layout_size == other.plugin_layout_size
77            && self.color_size == other.color_size
78            && self.vec_u8_size == other.vec_u8_size
79            && self.option_usize_size == other.option_usize_size
80            && self.audio_buffer_align == other.audio_buffer_align
81            && self.process_status_align == other.process_status_align
82            && self.result_normal_disc == other.result_normal_disc
83            && self.result_tail_disc == other.result_tail_disc
84            && self.result_keepalive_disc == other.result_keepalive_disc
85            && self.rustc_version_hash == other.rustc_version_hash
86    }
87
88    pub fn diff_report(&self, other: &Self) -> String {
89        let mut diffs = Vec::new();
90        macro_rules! check {
91            ($field:ident) => {
92                if self.$field != other.$field {
93                    diffs.push(format!(
94                        "  {}: shell={}, dylib={}",
95                        stringify!($field), self.$field, other.$field
96                    ));
97                }
98            };
99        }
100        check!(trait_object_size);
101        check!(audio_buffer_size);
102        check!(process_context_size);
103        check!(process_status_size);
104        check!(event_size);
105        check!(event_body_size);
106        check!(transport_size);
107        check!(widget_region_size);
108        check!(theme_size);
109        check!(plugin_layout_size);
110        check!(color_size);
111        check!(vec_u8_size);
112        check!(option_usize_size);
113        check!(audio_buffer_align);
114        check!(process_status_align);
115        check!(result_normal_disc);
116        check!(result_tail_disc);
117        check!(result_keepalive_disc);
118        check!(rustc_version_hash);
119        if diffs.is_empty() {
120            "no differences".into()
121        } else {
122            format!("ABI mismatches:\n{}", diffs.join("\n"))
123        }
124    }
125}
126
127fn discriminant_byte<T>(value: &T) -> u8 {
128    unsafe { *(value as *const T as *const u8) }
129}
130
131fn rustc_hash() -> u64 {
132    env!("TRUCE_RUSTC_HASH").parse().unwrap_or(0)
133}
134
135// ---------------------------------------------------------------------------
136// Vtable probe
137// ---------------------------------------------------------------------------
138
139/// A plugin with known return values for vtable verification.
140///
141/// The shell creates this via `truce_vtable_probe()`, calls every
142/// method, and checks the results. If any method returns the wrong
143/// value, the vtable is reordered and the dylib is rejected.
144pub struct ProbePlugin;
145
146impl PluginLogic for ProbePlugin {
147    fn new() -> Self { Self }
148    fn reset(&mut self, _sr: f64, _bs: usize) {}
149
150    fn process(
151        &mut self,
152        _buffer: &mut AudioBuffer,
153        _events: &EventList,
154        _context: &mut ProcessContext,
155    ) -> ProcessStatus {
156        ProcessStatus::Normal
157    }
158
159    fn render(&self, _backend: &mut dyn RenderBackend) {}
160
161    fn layout(&self) -> truce_gui::layout::GridLayout {
162        let mut gl = truce_gui::layout::GridLayout::build("", "", 1, 80.0, vec![], vec![]);
163        gl.width = 0xDEAD;
164        gl.height = 0xBEEF;
165        gl
166    }
167
168    fn hit_test(&self, _w: &[WidgetRegion], _x: f32, _y: f32) -> Option<usize> {
169        Some(42)
170    }
171
172    fn save_state(&self) -> Vec<u8> { vec![0xCA, 0xFE] }
173    fn load_state(&mut self, _data: &[u8]) {}
174    fn latency(&self) -> u32 { 0xAAAA }
175    fn tail(&self) -> u32 { 0xBBBB }
176}
177
178/// Verify a probe plugin returns the expected values.
179pub fn verify_probe(probe: &dyn PluginLogic) -> Result<(), String> {
180    if probe.latency() != 0xAAAA {
181        return Err(format!("latency: expected 0xAAAA, got 0x{:X}", probe.latency()));
182    }
183    if probe.tail() != 0xBBBB {
184        return Err(format!("tail: expected 0xBBBB, got 0x{:X}", probe.tail()));
185    }
186    let layout = probe.layout();
187    if layout.width != 0xDEAD || layout.height != 0xBEEF {
188        return Err(format!(
189            "layout: expected 0xDEAD×0xBEEF, got 0x{:X}×0x{:X}",
190            layout.width, layout.height
191        ));
192    }
193    if probe.hit_test(&[], 0.0, 0.0) != Some(42) {
194        return Err("hit_test: expected Some(42)".into());
195    }
196    if probe.save_state() != vec![0xCA, 0xFE] {
197        return Err("save_state: expected [0xCA, 0xFE]".into());
198    }
199    Ok(())
200}