Skip to content

Commit 9acc2db

Browse files
authored
refactor(bytecode): make module loading the trust boundary (#612)
1 parent bd88631 commit 9acc2db

25 files changed

Lines changed: 291 additions & 706 deletions

File tree

‎crates/plotnik-lib/src/bytecode/module/load.rs‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
//! Loading and structural validation of compiler-emitted bytecode.
1+
//! Loading and validation of raw bytecode.
22
//!
3-
//! This loader is crate-private and accepts only compiler output. Its checks catch
4-
//! compiler bugs before the VM trusts the bytecode. Malformed bytes
5-
//! return [`ModuleError`]; they never become a [`Module`] or reach execution.
3+
//! The in-process compiler is currently the only producer, but this boundary
4+
//! treats its bytes as untrusted. Malformed bytes return [`ModuleError`]; they
5+
//! never become a [`Module`] or reach execution.
66
77
use super::super::effects::{Effect, EffectKind};
88
use super::super::instructions::{
@@ -70,6 +70,8 @@ pub enum ModuleError {
7070
EffectStackBudget(CodeAddr),
7171
#[error("cursor depth imbalance at instruction address {0}")]
7272
DepthImbalance(CodeAddr),
73+
#[error("cursor read on an empty path at instruction address {0}")]
74+
EmptyPathCursorRead(CodeAddr),
7375
#[error("invalid span entry at index {0}")]
7476
InvalidSpanEntry(usize),
7577
#[error("span effect payload out of range at instruction address {0}")]
@@ -167,7 +169,7 @@ impl Module {
167169
Ok(module)
168170
}
169171

170-
/// Validate compiler output so later *view* accesses cannot panic and
172+
/// Validate raw bytecode so later *view* accesses cannot panic and
171173
/// accidental corruption of the body is detected.
172174
///
173175
/// Section bounds are checked earlier, in [`validate_section_bounds`], before
@@ -182,7 +184,7 @@ impl Module {
182184
/// construction. It is not a substitute for structural validation, so a
183185
/// test buffer with a recomputed checksum must still fail the checks below;
184186
/// [`Self::validate_instructions`] therefore re-verifies the lazily-decoded
185-
/// instruction stream structurally. The shared matcher verifier then proves
187+
/// instruction stream structurally. The matcher verifier then proves
186188
/// return routing, cursor depth, and materializer-stack safety, so validated
187189
/// bytecode never panics on view/decode/VM access even when compiler output
188190
/// is malformed.
@@ -214,8 +216,8 @@ impl Module {
214216
self.validate_symbol_ids()?;
215217
let is_start = self.validate_instructions()?;
216218
self.validate_entry_points(&is_start)?;
217-
// Structural validity is now established, so the target-neutral matcher
218-
// verifier can safely project the typed instruction stream.
219+
// Structural validity is now established, so the matcher verifier can
220+
// safely project the typed instruction stream.
219221
super::verify::validate(self)?;
220222
Ok((regex_dfas, is_start))
221223
}

crates/plotnik-lib/src/matcher_verify.rs renamed to crates/plotnik-lib/src/bytecode/module/matcher_verify.rs

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
//! Target-neutral verification of matcher control flow and result effects.
1+
//! Validation of decoded bytecode control flow and result effects.
22
//!
3-
//! Both the semantic NFA and validated bytecode project into this small program
4-
//! shape. Keeping the abstract interpreter here prevents their two trust
5-
//! boundaries from maintaining parallel implementations of recursive call
6-
//! summaries, materializer-stack safety, return routing, and cursor depth.
7-
//!
8-
//! Representation adapters resolve their own metadata before constructing a
9-
//! [`Program`], so this layer needs only normalized instructions and body
10-
//! contracts. It proves every control-flow path, not just every instruction:
3+
//! The bytecode loader projects structurally decoded instructions into this
4+
//! small program shape before the VM can observe the module. The abstract
5+
//! interpreter proves every control-flow path, not just every instruction:
116
//! deduplicated alternative tails may legitimately reach one address with
127
//! different builder stacks, so effect analysis keeps a set of abstract states
138
//! per address.
@@ -32,14 +27,14 @@ use crate::bytecode::{
3227
const STATE_BUDGET: usize = 1 << 18;
3328

3429
#[derive(Clone, Copy, Debug)]
35-
pub(crate) struct Effect {
30+
pub(super) struct Effect {
3631
kind: EffectKind,
3732
payload: usize,
3833
variant_has_no_payload: Option<bool>,
3934
}
4035

4136
impl Effect {
42-
pub(crate) fn new(kind: EffectKind, payload: usize) -> Self {
37+
pub(super) fn new(kind: EffectKind, payload: usize) -> Self {
4338
assert_ne!(
4439
kind,
4540
EffectKind::VariantOpen,
@@ -52,7 +47,7 @@ impl Effect {
5247
}
5348
}
5449

55-
pub(crate) fn variant_open(payload: usize, has_no_payload: bool) -> Self {
50+
pub(super) fn variant_open(payload: usize, has_no_payload: bool) -> Self {
5651
Self {
5752
kind: EffectKind::VariantOpen,
5853
payload,
@@ -62,14 +57,14 @@ impl Effect {
6257
}
6358

6459
#[derive(Clone, Debug)]
65-
pub(crate) struct Match<A> {
60+
pub(super) struct Match<A> {
6661
nav: Nav,
6762
effects: Box<[Effect]>,
6863
successors: Box<[A]>,
6964
}
7065

7166
impl<A> Match<A> {
72-
pub(crate) fn new(
67+
pub(super) fn new(
7368
nav: Nav,
7469
effects: impl Into<Box<[Effect]>>,
7570
successors: impl Into<Box<[A]>>,
@@ -83,7 +78,7 @@ impl<A> Match<A> {
8378
}
8479

8580
#[derive(Clone, Debug)]
86-
pub(crate) struct Call<A> {
81+
pub(super) struct Call<A> {
8782
nav: Nav,
8883
contract: CalleeContract,
8984
target: A,
@@ -92,7 +87,7 @@ pub(crate) struct Call<A> {
9287
}
9388

9489
impl<A> Call<A> {
95-
pub(crate) fn new(
90+
pub(super) fn new(
9691
nav: Nav,
9792
contract: CalleeContract,
9893
target: A,
@@ -118,45 +113,45 @@ impl<A> Call<A> {
118113
}
119114

120115
#[derive(Clone, Copy, Debug)]
121-
pub(crate) struct Return {
116+
pub(super) struct Return {
122117
port: PortId,
123118
contract: CalleeContract,
124119
}
125120

126121
impl Return {
127-
pub(crate) fn new(port: PortId, contract: CalleeContract) -> Self {
122+
pub(super) fn new(port: PortId, contract: CalleeContract) -> Self {
128123
Self { port, contract }
129124
}
130125
}
131126

132127
#[derive(Clone, Debug)]
133-
pub(crate) enum Instruction<A> {
128+
pub(super) enum Instruction<A> {
134129
Match(Match<A>),
135130
Call(Call<A>),
136131
Return(Return),
137132
}
138133

139134
#[derive(Clone, Copy, Debug)]
140-
pub(crate) struct Entry<A> {
135+
pub(super) struct Entry<A> {
141136
target: A,
142137
boundary: EntryBoundary,
143138
}
144139

145140
impl<A> Entry<A> {
146-
pub(crate) fn new(target: A, boundary: EntryBoundary) -> Self {
141+
pub(super) fn new(target: A, boundary: EntryBoundary) -> Self {
147142
Self { target, boundary }
148143
}
149144
}
150145

151146
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152-
pub(crate) struct BodyContract {
147+
struct BodyContract {
153148
contract: CalleeContract,
154149
arity: u8,
155150
consumed_mask: u8,
156151
}
157152

158153
impl BodyContract {
159-
pub(crate) fn new(contract: CalleeContract, arity: usize, consumed_mask: u8) -> Self {
154+
fn new(contract: CalleeContract, arity: usize, consumed_mask: u8) -> Self {
160155
Self {
161156
contract,
162157
arity: u8::try_from(arity).expect("matcher call arity fits u8"),
@@ -205,7 +200,7 @@ impl BodyContract {
205200
}
206201
}
207202

208-
pub(crate) struct Program<A> {
203+
pub(super) struct Program<A> {
209204
instructions: HashMap<A, Instruction<A>>,
210205
entries: Vec<Entry<A>>,
211206
roots: HashMap<A, BodyContract>,
@@ -215,10 +210,9 @@ impl<A> Program<A>
215210
where
216211
A: Copy + Eq + Hash + Debug,
217212
{
218-
pub(crate) fn new(
213+
pub(super) fn new(
219214
instructions: impl IntoIterator<Item = (A, Instruction<A>)>,
220215
entries: Vec<Entry<A>>,
221-
declared_roots: impl IntoIterator<Item = (A, BodyContract)>,
222216
) -> Result<Self, VerifyError<A>> {
223217
let mut instruction_map = HashMap::new();
224218
for (address, instruction) in instructions {
@@ -231,9 +225,6 @@ where
231225
}
232226

233227
let mut roots = HashMap::new();
234-
for (address, contract) in declared_roots {
235-
insert_root(&mut roots, address, contract)?;
236-
}
237228
for entry in &entries {
238229
insert_root(&mut roots, entry.target, BodyContract::entry_point())?;
239230
}
@@ -322,7 +313,7 @@ where
322313
}
323314

324315
#[derive(Clone, Debug, PartialEq, Eq)]
325-
pub(crate) enum VerifyError<A> {
316+
pub(super) enum VerifyError<A> {
326317
Malformed { at: Option<A>, detail: String },
327318
EffectStack(A),
328319
SpanStack(A),
@@ -341,28 +332,17 @@ impl<A> VerifyError<A> {
341332
}
342333

343334
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
344-
pub(crate) struct VerifyStats {
345-
pub(crate) body_analyses: usize,
335+
pub(super) struct VerifyStats {
336+
pub(super) body_analyses: usize,
346337
}
347338

348-
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349-
pub(crate) enum EmptyPathCheck {
350-
Verify,
351-
Skip,
352-
}
353-
354-
pub(crate) fn verify<A>(
355-
program: &Program<A>,
356-
empty_paths: EmptyPathCheck,
357-
) -> Result<VerifyStats, VerifyError<A>>
339+
pub(super) fn verify<A>(program: &Program<A>) -> Result<VerifyStats, VerifyError<A>>
358340
where
359341
A: Copy + Eq + Hash + Debug,
360342
{
361343
verify_return_routes(program)?;
362344
verify_cursor_depth(program)?;
363-
if empty_paths == EmptyPathCheck::Verify {
364-
verify_empty_paths(program)?;
365-
}
345+
verify_empty_paths(program)?;
366346
verify_effects(program)
367347
}
368348

‎crates/plotnik-lib/src/bytecode/module/mod.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use plotnik_rt::RegexDfas;
2121

2222
mod decoded;
2323
mod load;
24+
mod matcher_verify;
2425
mod verify;
2526

2627
pub(crate) use decoded::{
@@ -117,10 +118,10 @@ pub struct Module {
117118
}
118119

119120
impl Module {
120-
/// Load compiler output into the VM after running every boundary check.
121+
/// Validate raw bytecode and construct the module accepted by the VM.
121122
///
122-
/// Crate-private visibility keeps this loader on the compiler-to-VM boundary.
123-
pub(crate) fn load_compiler_output(bytes: &[u8]) -> Result<Self, ModuleError> {
123+
/// Crate-private visibility prevents bypassing the validation boundary.
124+
pub(crate) fn validate_and_load(bytes: &[u8]) -> Result<Self, ModuleError> {
124125
Self::load_storage(ByteStorage::from_emitted_bytes(bytes))
125126
}
126127

0 commit comments

Comments
 (0)