1use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::vec::Vec;
5use core::fmt;
6
7use crate::compile::hash_plan;
8use crate::model::{CompiledPlan, Effect, ExecutionRealm, InputBinding, OutputBinding, PlanId};
9
10const MAGIC: &[u8; 4] = b"BGP3";
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct PlanLimits {
14 pub max_bytes: usize,
15 pub max_nodes: usize,
16 pub max_buffers: usize,
17 pub max_contract_entries: usize,
18 pub max_peak_bytes: u64,
19 pub max_persistent_state_bytes: u64,
20 pub max_feedback_edges: usize,
21 pub max_subgraph_depth: usize,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct PlanAuthorization {
26 pub expected_realm: ExecutionRealm,
27 pub expected_plan_id: PlanId,
28}
29
30impl Default for PlanLimits {
31 fn default() -> Self {
32 Self {
33 max_bytes: 1024 * 1024,
34 max_nodes: 65_536,
35 max_buffers: 262_144,
36 max_contract_entries: 65_536,
37 max_peak_bytes: 64 * 1024 * 1024,
38 max_persistent_state_bytes: 64 * 1024 * 1024,
39 max_feedback_edges: 65_536,
40 max_subgraph_depth: 16,
41 }
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum PlanDecodeError {
47 TooLarge,
48 BadMagic,
49 Malformed,
50 UnsupportedSchema(u32),
51 LimitExceeded,
52 ResourceLimitExceeded,
53 IdentityMismatch,
54 InvalidBuffer,
55 InvalidPlan,
56 RealmMismatch,
57 UnknownToken,
60 UnauthorizedPlan,
61}
62
63impl fmt::Display for PlanDecodeError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 write!(f, "{self:?}")
66 }
67}
68
69#[cfg(feature = "std")]
70impl std::error::Error for PlanDecodeError {}
71
72fn valid_compiled_port(port: &crate::CompiledPortContract) -> bool {
73 let descriptor = crate::PortDescriptor {
74 name: port.name.clone(),
75 semantic_type: port.semantic_type.clone(),
76 optional: port.optional,
77 layouts: alloc::vec![port.layout],
78 max_bytes: port.max_bytes,
79 domain: port.domain.clone(),
80 proof: port.proof.clone(),
81 policy: port.policy.clone(),
82 fidelity: port.fidelity.clone(),
83 extent: port.extent.clone(),
84 lease: port.lease.clone(),
85 };
86 crate::compile::valid_port_contract(&descriptor)
87 && port.proof.requires.windows(2).all(|pair| pair[0] < pair[1])
88 && port.proof.provides.windows(2).all(|pair| pair[0] < pair[1])
89 && port
90 .proof
91 .invalidates
92 .windows(2)
93 .all(|pair| pair[0] < pair[1])
94 && port
95 .policy
96 .requires
97 .windows(2)
98 .all(|pair| pair[0] < pair[1])
99 && port.policy.adds.windows(2).all(|pair| pair[0] < pair[1])
100}
101
102fn contract_entry_count(port: &crate::CompiledPortContract) -> Option<usize> {
103 0usize
104 .checked_add(port.proof.requires.len())?
105 .checked_add(port.proof.provides.len())?
106 .checked_add(port.proof.invalidates.len())?
107 .checked_add(port.policy.requires.len())?
108 .checked_add(port.policy.adds.len())?
109 .checked_add(port.extent.maximum_shape.len())
110}
111
112impl CompiledPlan {
113 pub fn to_aot_bytes(&self) -> Result<Vec<u8>, PlanDecodeError> {
116 let mut bytes = Vec::from(MAGIC.as_slice());
117 bytes.extend(postcard::to_allocvec(self).map_err(|_| PlanDecodeError::Malformed)?);
118 Ok(bytes)
119 }
120
121 pub fn from_aot_bytes(bytes: &[u8], limits: PlanLimits) -> Result<Self, PlanDecodeError> {
126 if bytes.len() > limits.max_bytes {
127 return Err(PlanDecodeError::TooLarge);
128 }
129 let body = bytes.strip_prefix(MAGIC).ok_or(PlanDecodeError::BadMagic)?;
130 let (plan, remainder): (Self, &[u8]) =
131 postcard::take_from_bytes(body).map_err(|_| PlanDecodeError::Malformed)?;
132 if !remainder.is_empty() {
133 return Err(PlanDecodeError::Malformed);
134 }
135 if plan.schema_version != 3 {
136 return Err(PlanDecodeError::UnsupportedSchema(plan.schema_version));
137 }
138 if !plan.realm.is_known() {
145 return Err(PlanDecodeError::UnknownToken);
146 }
147 if plan.buffers.iter().any(|buffer| !buffer.layout.is_known()) {
148 return Err(PlanDecodeError::UnknownToken);
149 }
150 if plan.nodes.iter().any(|node| {
151 node.input_contracts
152 .iter()
153 .chain(node.output_contracts.iter())
154 .any(|contract| !contract.layout.is_known())
155 || node.conversion.as_ref().is_some_and(|conversion| {
156 !conversion.from.is_known() || !conversion.to.is_known()
157 })
158 }) {
159 return Err(PlanDecodeError::UnknownToken);
160 }
161 if plan.nodes.len() > limits.max_nodes
162 || plan.order.len() > limits.max_nodes
163 || plan.buffers.len() > limits.max_buffers
164 || plan.invocation_ports.len() > limits.max_contract_entries
165 || plan.propagated_proofs.len() > limits.max_contract_entries
166 || plan.propagated_policy.len() > limits.max_contract_entries
167 || plan.feedback.len() > limits.max_feedback_edges
168 {
169 return Err(PlanDecodeError::LimitExceeded);
170 }
171 if plan
172 .invocation_ports
173 .windows(2)
174 .any(|pair| pair[0] >= pair[1])
175 || plan
176 .propagated_proofs
177 .windows(2)
178 .any(|pair| pair[0] >= pair[1])
179 || plan
180 .propagated_policy
181 .windows(2)
182 .any(|pair| pair[0] >= pair[1])
183 || plan.propagated_proofs.iter().any(|entry| entry.is_empty())
184 || plan.propagated_policy.iter().any(|entry| entry.is_empty())
185 {
186 return Err(PlanDecodeError::InvalidPlan);
187 }
188 if plan.peak_bytes > limits.max_peak_bytes
189 || plan.persistent_state_bytes > limits.max_persistent_state_bytes
190 {
191 return Err(PlanDecodeError::ResourceLimitExceeded);
192 }
193 if plan.order.is_empty() || plan.nodes.is_empty() {
194 return Err(PlanDecodeError::InvalidPlan);
195 }
196 let semantic_positions: BTreeMap<_, _> = plan
197 .order
198 .iter()
199 .enumerate()
200 .map(|(index, node)| (*node, index))
201 .collect();
202 if semantic_positions.len() != plan.order.len()
203 || plan
204 .nodes
205 .iter()
206 .flat_map(|node| node.semantic_nodes.iter())
207 .copied()
208 .ne(plan.order.iter().copied())
209 {
210 return Err(PlanDecodeError::InvalidPlan);
211 }
212 let mut buffer_bytes = 0u64;
213 for (index, buffer) in plan.buffers.iter().enumerate() {
214 if buffer.id.0 as usize != index
217 || buffer.capacity_bytes == 0
218 || buffer.consumers.is_empty()
219 || !buffer.consumers.contains(&buffer.last_consumer)
220 || buffer.producer.0 as usize >= plan.nodes.len()
221 || buffer
222 .consumers
223 .iter()
224 .any(|consumer| consumer.0 as usize >= plan.nodes.len())
225 || buffer.consumers.iter().collect::<BTreeSet<_>>().len() != buffer.consumers.len()
226 || buffer.consumers.windows(2).any(|pair| pair[0] >= pair[1])
227 || buffer.consumers.iter().max() != Some(&buffer.last_consumer)
228 || buffer
229 .consumers
230 .iter()
231 .any(|consumer| *consumer <= buffer.producer)
232 || !plan.nodes[buffer.producer.0 as usize]
233 .output_bindings
234 .contains(&OutputBinding::Buffer(buffer.id))
235 || buffer.consumers.iter().any(|consumer| {
236 !plan.nodes[consumer.0 as usize]
237 .input_bindings
238 .contains(&InputBinding::Buffer(buffer.id))
239 })
240 || buffer.aliases.is_some_and(|alias| {
241 alias.0 >= buffer.id.0
242 || plan.buffers[alias.0 as usize].aliases.is_some()
243 || plan.buffers[alias.0 as usize].layout != buffer.layout
244 || plan.buffers[alias.0 as usize].capacity_bytes < buffer.capacity_bytes
245 })
246 {
247 return Err(PlanDecodeError::InvalidBuffer);
248 }
249 if buffer.aliases.is_none() {
250 buffer_bytes = buffer_bytes
251 .checked_add(buffer.capacity_bytes)
252 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
253 }
254 }
255 let mut alias_intervals: BTreeMap<_, Vec<_>> = BTreeMap::new();
256 for buffer in &plan.buffers {
257 let root = buffer.aliases.unwrap_or(buffer.id);
258 alias_intervals
259 .entry(root)
260 .or_default()
261 .push((buffer.producer, buffer.last_consumer));
262 }
263 for intervals in alias_intervals.values_mut() {
264 intervals.sort_unstable();
265 if intervals.windows(2).any(|pair| pair[0].1 >= pair[1].0) {
266 return Err(PlanDecodeError::InvalidBuffer);
267 }
268 }
269 let mut workspace_bytes = 0u64;
270 let mut failure_entries = 0usize;
271 let mut used_invocations = BTreeSet::new();
272 let mut used_feedback = BTreeSet::new();
273 let mut state_bytes = 0u64;
274 for (index, node) in plan.nodes.iter().enumerate() {
275 failure_entries = failure_entries
276 .checked_add(node.failure.domains.len())
277 .ok_or(PlanDecodeError::LimitExceeded)?;
278 failure_entries = node
279 .semantic_configs
280 .iter()
281 .try_fold(failure_entries, |count, config| {
282 count.checked_add(config.len())
283 })
284 .ok_or(PlanDecodeError::LimitExceeded)?;
285 failure_entries = node
286 .input_contracts
287 .iter()
288 .chain(&node.output_contracts)
289 .try_fold(failure_entries, |count, port| {
290 count.checked_add(contract_entry_count(port)?)
291 })
292 .and_then(|count| count.checked_add(node.subgraph_path.len()))
293 .ok_or(PlanDecodeError::LimitExceeded)?;
294 let conversion = node.conversion.is_some();
295 for invocation in node
296 .input_bindings
297 .iter()
298 .filter_map(|binding| match binding {
299 InputBinding::Invocation(invocation) => Some(*invocation),
300 _ => None,
301 })
302 {
303 if !used_invocations.insert(invocation) {
304 return Err(PlanDecodeError::InvalidPlan);
305 }
306 }
307 for feedback in node
308 .input_bindings
309 .iter()
310 .filter_map(|binding| match binding {
311 InputBinding::Feedback(feedback) => Some(*feedback),
312 _ => None,
313 })
314 {
315 if !used_feedback.insert(feedback) {
316 return Err(PlanDecodeError::InvalidPlan);
317 }
318 }
319 if node.id.0 as usize != index
320 || conversion != node.semantic_nodes.is_empty()
321 || node.semantic_nodes.len() != node.semantic_types.len()
322 || node.semantic_nodes.len() != node.semantic_configs.len()
323 || node.input_ports.len() != node.input_bindings.len()
324 || node.output_ports.len() != node.output_bindings.len()
325 || node.input_contracts.len() != node.input_bindings.len()
326 || node.output_contracts.len() != node.output_bindings.len()
327 || node
328 .input_ports
329 .iter()
330 .zip(&node.input_contracts)
331 .any(|(name, contract)| name != &contract.name || !valid_compiled_port(contract))
332 || node
333 .output_ports
334 .iter()
335 .zip(&node.output_contracts)
336 .any(|(name, contract)| name != &contract.name || !valid_compiled_port(contract))
337 || node.input_ports.iter().any(|port| port.is_empty())
338 || node.output_ports.iter().any(|port| port.is_empty())
339 || node.input_ports.iter().collect::<BTreeSet<_>>().len()
340 != node.input_ports.len()
341 || node.output_ports.iter().collect::<BTreeSet<_>>().len()
342 != node.output_ports.len()
343 || node.input_bindings.iter().any(|binding| {
344 matches!(binding, InputBinding::Buffer(buffer) if buffer.0 as usize >= plan.buffers.len())
345 })
346 || node.input_bindings.iter().any(|binding| {
347 matches!(binding, InputBinding::Invocation(invocation) if *invocation as usize >= plan.invocation_ports.len())
348 })
349 || node.input_bindings.iter().any(|binding| {
350 matches!(binding, InputBinding::Feedback(feedback) if feedback.0 as usize >= plan.feedback.len())
351 })
352 || node
353 .input_bindings
354 .iter()
355 .zip(&node.input_contracts)
356 .any(|(binding, contract)| match binding {
357 InputBinding::Buffer(buffer) => {
358 let buffer = &plan.buffers[buffer.0 as usize];
359 contract.layout != buffer.layout
360 || buffer.capacity_bytes > contract.max_bytes
361 }
362 _ => false,
363 })
364 || node.output_bindings.iter().any(|binding| {
365 matches!(binding, OutputBinding::Buffer(buffer) if buffer.0 as usize >= plan.buffers.len())
366 })
367 || node
368 .output_bindings
369 .iter()
370 .zip(&node.output_contracts)
371 .any(|(binding, contract)| match binding {
372 OutputBinding::Buffer(buffer) => {
373 let buffer = &plan.buffers[buffer.0 as usize];
374 contract.layout != buffer.layout
375 || buffer.capacity_bytes != contract.max_bytes
376 }
377 OutputBinding::Terminal => false,
378 })
379 || node.resources.threads == 0
380 || node.failure.domains.iter().any(|domain| domain.is_empty())
381 || node.failure.domains.windows(2).any(|pair| pair[0] >= pair[1])
382 || (node.partiality == crate::Partiality::ExplicitGaps
383 && node.failure.domains.is_empty())
384 || (node.effect == Effect::AtMostOnce && node.retry_limit > 0)
385 || !crate::compile::valid_state_contract(&node.state)
386 || node.subgraph_path.len() > limits.max_subgraph_depth
387 || node.input_bindings.iter().any(|binding| {
388 matches!(binding, InputBinding::Buffer(buffer) if !plan.buffers[buffer.0 as usize].consumers.contains(&node.id))
389 })
390 || node.output_bindings.iter().any(|binding| {
391 matches!(binding, OutputBinding::Buffer(buffer) if plan.buffers[buffer.0 as usize].producer != node.id)
392 })
393 || (conversion
394 && (node.input_bindings.len() != 1
395 || node.output_bindings.len() != 1
396 || node.input_ports.as_slice() != ["input"]
397 || node.output_ports.as_slice() != ["output"]
398 || node.input_bindings.contains(&InputBinding::Absent)
399 || node.input_bindings.iter().any(|binding| matches!(binding, InputBinding::Feedback(_)))
400 || node.output_bindings.contains(&OutputBinding::Terminal)
401 || node.partiality != crate::Partiality::Atomic
402 || !node.failure.domains.is_empty()
403 || node.effect != Effect::Pure
404 || node.retry_limit != 0
405 || node.state != crate::StateContract::stateless()
406 || !node.subgraph_path.is_empty()))
407 {
408 return Err(PlanDecodeError::InvalidPlan);
409 }
410 if matches!(
411 node.state.scope,
412 crate::StateScope::Session | crate::StateScope::Durable
413 ) {
414 state_bytes = state_bytes
415 .checked_add(node.state.max_bytes)
416 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
417 }
418 if let Some(conversion) = &node.conversion {
419 let (InputBinding::Buffer(input), OutputBinding::Buffer(output)) =
420 (node.input_bindings[0], node.output_bindings[0])
421 else {
422 return Err(PlanDecodeError::InvalidPlan);
423 };
424 let input = &plan.buffers[input.0 as usize];
425 let output = &plan.buffers[output.0 as usize];
426 if input.layout != conversion.from
427 || output.layout != conversion.to
428 || input.capacity_bytes > conversion.max_input_bytes
429 || output.capacity_bytes > conversion.max_output_bytes
430 || node.input_contracts[0].semantic_type != conversion.semantic_type
431 || node.output_contracts[0].semantic_type != conversion.semantic_type
432 || node.input_contracts[0].layout != conversion.from
433 || node.output_contracts[0].layout != conversion.to
434 || node.input_contracts[0].max_bytes != input.capacity_bytes
435 || node.output_contracts[0].max_bytes != output.capacity_bytes
436 {
437 return Err(PlanDecodeError::InvalidPlan);
438 }
439 }
440 let mut workspace = node
441 .resources
442 .peak_bytes
443 .checked_add(node.resources.scratch_bytes)
444 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
445 if node.state.scope == crate::StateScope::Invocation {
446 workspace = workspace
447 .checked_add(node.state.max_bytes)
448 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
449 }
450 workspace_bytes = workspace_bytes.max(workspace);
451 }
452 if failure_entries > limits.max_contract_entries {
453 return Err(PlanDecodeError::LimitExceeded);
454 }
455 if used_invocations.len() != plan.invocation_ports.len()
456 || used_invocations
457 .iter()
458 .copied()
459 .ne(0..plan.invocation_ports.len() as u32)
460 {
461 return Err(PlanDecodeError::InvalidPlan);
462 }
463 if used_feedback.len() != plan.feedback.len()
464 || used_feedback
465 .iter()
466 .copied()
467 .ne((0..plan.feedback.len() as u32).map(crate::FeedbackId))
468 {
469 return Err(PlanDecodeError::InvalidPlan);
470 }
471 for buffer in &plan.buffers {
472 let producer = &plan.nodes[buffer.producer.0 as usize];
473 let producer_contract = producer
474 .output_bindings
475 .iter()
476 .position(|binding| *binding == OutputBinding::Buffer(buffer.id))
477 .and_then(|port| producer.output_contracts.get(port))
478 .ok_or(PlanDecodeError::InvalidPlan)?;
479 for consumer in &buffer.consumers {
480 let consumer = &plan.nodes[consumer.0 as usize];
481 let mut matched = false;
482 for (port, binding) in consumer.input_bindings.iter().enumerate() {
483 if *binding == InputBinding::Buffer(buffer.id) {
484 matched = true;
485 if !crate::compile::compiled_port_contract_satisfies(
486 producer_contract,
487 &consumer.input_contracts[port],
488 ) {
489 return Err(PlanDecodeError::InvalidPlan);
490 }
491 }
492 }
493 if !matched {
494 return Err(PlanDecodeError::InvalidPlan);
495 }
496 }
497 }
498 for (index, feedback) in plan.feedback.iter().enumerate() {
499 let output = plan
500 .nodes
501 .get(feedback.from_step.0 as usize)
502 .and_then(|node| node.output_contracts.get(feedback.from_port as usize));
503 let input = plan
504 .nodes
505 .get(feedback.to_step.0 as usize)
506 .and_then(|node| node.input_contracts.get(feedback.to_port as usize));
507 let expected_state_bytes = output.and_then(|contract| {
508 contract
509 .max_bytes
510 .checked_mul(u64::from(feedback.delay.invocations))
511 });
512 if feedback.id.0 as usize != index
513 || feedback.delay.invocations == 0
514 || feedback.state_bytes == 0
515 || feedback.from_step.0 as usize >= plan.nodes.len()
516 || feedback.to_step.0 as usize >= plan.nodes.len()
517 || output.is_none()
518 || input.is_none()
519 || (matches!(&feedback.delay.initial, crate::DelayInitial::Absent)
520 && input.is_some_and(|contract| !contract.optional))
521 || plan.nodes[feedback.to_step.0 as usize].input_bindings[feedback.to_port as usize]
522 != InputBinding::Feedback(feedback.id)
523 || expected_state_bytes != Some(feedback.state_bytes)
524 || !crate::compile::compiled_port_contract_satisfies(
525 output.expect("checked above"),
526 input.expect("checked above"),
527 )
528 {
529 return Err(PlanDecodeError::InvalidPlan);
530 }
531 state_bytes = state_bytes
532 .checked_add(feedback.state_bytes)
533 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
534 }
535 if state_bytes != plan.persistent_state_bytes
536 || (!plan.feedback.is_empty() && plan.session.is_none())
537 || plan.session.as_ref().is_some_and(|session| {
538 session.namespace.is_empty()
539 || session.max_concurrent_sessions == 0
540 || session.max_idle_millis == 0
541 })
542 || plan.nodes.iter().any(|node| {
543 matches!(
544 node.state.scope,
545 crate::StateScope::Session | crate::StateScope::Durable
546 ) && plan.session.is_none()
547 })
548 {
549 return Err(PlanDecodeError::InvalidPlan);
550 }
551 let mut produced_buffers = alloc::vec![0u8; plan.buffers.len()];
552 for binding in plan
553 .nodes
554 .iter()
555 .flat_map(|node| node.output_bindings.iter())
556 {
557 if let OutputBinding::Buffer(buffer) = binding {
558 produced_buffers[buffer.0 as usize] = produced_buffers[buffer.0 as usize]
559 .checked_add(1)
560 .ok_or(PlanDecodeError::InvalidBuffer)?;
561 }
562 }
563 if produced_buffers.iter().any(|count| *count != 1) {
564 return Err(PlanDecodeError::InvalidBuffer);
565 }
566 let expected_peak = buffer_bytes
567 .checked_add(workspace_bytes)
568 .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
569 if plan.peak_bytes != expected_peak {
570 return Err(PlanDecodeError::ResourceLimitExceeded);
571 }
572 let expected = PlanId(hash_plan(&plan));
573 if plan.plan_id != expected {
574 return Err(PlanDecodeError::IdentityMismatch);
575 }
576 Ok(plan)
577 }
578
579 pub fn from_authorized_aot_bytes(
583 bytes: &[u8],
584 limits: PlanLimits,
585 authorization: PlanAuthorization,
586 ) -> Result<Self, PlanDecodeError> {
587 let plan = Self::from_aot_bytes(bytes, limits)?;
588 if plan.realm != authorization.expected_realm {
589 return Err(PlanDecodeError::RealmMismatch);
590 }
591 if plan.plan_id != authorization.expected_plan_id {
592 return Err(PlanDecodeError::UnauthorizedPlan);
593 }
594 Ok(plan)
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use alloc::vec;
601
602 use super::*;
603 use crate::model::{ExecutionRealm, GraphId};
604
605 fn minimal_plan() -> CompiledPlan {
606 let mut plan = CompiledPlan {
607 schema_version: 3,
608 graph_id: GraphId([1; 32]),
609 plan_id: PlanId([0; 32]),
610 realm: ExecutionRealm::McuAot,
611 order: vec![crate::NodeId(7)],
612 nodes: vec![crate::CompiledNode {
613 id: crate::StepId(0),
614 semantic_nodes: vec![crate::NodeId(7)],
615 semantic_types: vec![crate::NodeTypeRef {
616 type_name: "test".into(),
617 version: 1,
618 }],
619 semantic_configs: vec![alloc::collections::BTreeMap::new()],
620 kernel: crate::KernelId(11),
621 implementation_id: crate::ImplementationId([11; 32]),
622 resources: crate::ResourceEnvelope::bounded(0, 0, 1),
623 determinism: crate::Determinism::BitExact,
624 lowering: "test".into(),
625 conversion: None,
626 input_ports: vec![],
627 output_ports: vec!["out".into()],
628 input_contracts: vec![],
629 output_contracts: vec![crate::CompiledPortContract::opaque(
630 "out",
631 "test",
632 crate::Layout::Canonical,
633 1,
634 )],
635 input_bindings: vec![],
636 output_bindings: vec![crate::OutputBinding::Terminal],
637 partiality: crate::Partiality::Atomic,
638 failure: crate::FailureContract { domains: vec![] },
639 effect: crate::Effect::Pure,
640 retry_limit: 0,
641 state: crate::StateContract::stateless(),
642 subgraph_path: vec![],
643 }],
644 buffers: vec![],
645 feedback: vec![],
646 invocation_ports: vec![],
647 propagated_proofs: vec![],
648 propagated_policy: vec![],
649 resulting_fidelity: u16::MAX,
650 peak_bytes: 0,
651 persistent_state_bytes: 0,
652 session: None,
653 };
654 plan.plan_id = PlanId(hash_plan(&plan));
655 plan
656 }
657
658 #[test]
659 fn bgp2_magic_is_never_reinterpreted_as_bgp3() {
660 let mut bytes = minimal_plan().to_aot_bytes().unwrap();
661 bytes[..4].copy_from_slice(b"BGP2");
662 assert_eq!(
663 CompiledPlan::from_aot_bytes(&bytes, PlanLimits::default()),
664 Err(PlanDecodeError::BadMagic)
665 );
666 }
667
668 #[test]
669 fn self_consistent_port_and_state_forgery_is_structurally_rejected() {
670 let mut port_forgery = minimal_plan();
671 port_forgery.nodes[0].output_contracts[0].name = "different".into();
672 port_forgery.plan_id = PlanId(hash_plan(&port_forgery));
673 assert_eq!(
674 CompiledPlan::from_aot_bytes(
675 &port_forgery.to_aot_bytes().unwrap(),
676 PlanLimits::default(),
677 ),
678 Err(PlanDecodeError::InvalidPlan)
679 );
680
681 let mut state_forgery = minimal_plan();
682 state_forgery.persistent_state_bytes = 1;
683 state_forgery.plan_id = PlanId(hash_plan(&state_forgery));
684 assert_eq!(
685 CompiledPlan::from_aot_bytes(
686 &state_forgery.to_aot_bytes().unwrap(),
687 PlanLimits::default(),
688 ),
689 Err(PlanDecodeError::InvalidPlan)
690 );
691
692 let mut rewire_forgery = minimal_plan();
693 rewire_forgery.order.push(crate::NodeId(8));
694 rewire_forgery.nodes[0].output_bindings[0] =
695 crate::OutputBinding::Buffer(crate::BufferId(0));
696 let mut sink = rewire_forgery.nodes[0].clone();
697 sink.id = crate::StepId(1);
698 sink.semantic_nodes = vec![crate::NodeId(8)];
699 sink.input_ports = vec!["in".into()];
700 sink.output_ports = vec!["out".into()];
701 sink.input_contracts = vec![crate::CompiledPortContract::opaque(
702 "in",
703 "test",
704 crate::Layout::Canonical,
705 1,
706 )];
707 sink.output_contracts = vec![crate::CompiledPortContract::opaque(
708 "out",
709 "test",
710 crate::Layout::Canonical,
711 1,
712 )];
713 sink.input_bindings = vec![InputBinding::Buffer(crate::BufferId(0))];
714 sink.output_bindings = vec![OutputBinding::Terminal];
715 rewire_forgery.nodes.push(sink);
716 rewire_forgery.buffers.push(crate::BufferPlan {
717 id: crate::BufferId(0),
718 layout: crate::Layout::Canonical,
719 capacity_bytes: 1,
720 producer: crate::StepId(0),
721 consumers: vec![crate::StepId(1)],
722 last_consumer: crate::StepId(1),
723 aliases: None,
724 });
725 rewire_forgery.peak_bytes = 1;
726 rewire_forgery.plan_id = PlanId(hash_plan(&rewire_forgery));
727 assert!(
728 CompiledPlan::from_aot_bytes(
729 &rewire_forgery.to_aot_bytes().unwrap(),
730 PlanLimits::default(),
731 )
732 .is_ok()
733 );
734
735 rewire_forgery.nodes[1].input_contracts[0].semantic_type = "different".into();
736 rewire_forgery.plan_id = PlanId(hash_plan(&rewire_forgery));
737 assert_eq!(
738 CompiledPlan::from_aot_bytes(
739 &rewire_forgery.to_aot_bytes().unwrap(),
740 PlanLimits::default(),
741 ),
742 Err(PlanDecodeError::InvalidPlan)
743 );
744 }
745
746 #[test]
747 fn empty_compiled_contract_names_are_rejected() {
748 let mut port_contract = minimal_plan();
749 port_contract.nodes[0].output_contracts[0]
750 .policy
751 .adds
752 .push(String::new());
753 port_contract.plan_id = PlanId(hash_plan(&port_contract));
754 assert_eq!(
755 CompiledPlan::from_aot_bytes(
756 &port_contract.to_aot_bytes().unwrap(),
757 PlanLimits::default(),
758 ),
759 Err(PlanDecodeError::InvalidPlan)
760 );
761
762 let mut propagated_contract = minimal_plan();
763 propagated_contract.propagated_proofs.push(String::new());
764 propagated_contract.plan_id = PlanId(hash_plan(&propagated_contract));
765 assert_eq!(
766 CompiledPlan::from_aot_bytes(
767 &propagated_contract.to_aot_bytes().unwrap(),
768 PlanLimits::default(),
769 ),
770 Err(PlanDecodeError::InvalidPlan)
771 );
772
773 let mut propagated_policy = minimal_plan();
774 propagated_policy.propagated_policy.push(String::new());
775 propagated_policy.plan_id = PlanId(hash_plan(&propagated_policy));
776 assert_eq!(
777 CompiledPlan::from_aot_bytes(
778 &propagated_policy.to_aot_bytes().unwrap(),
779 PlanLimits::default(),
780 ),
781 Err(PlanDecodeError::InvalidPlan)
782 );
783 }
784
785 #[test]
786 fn subgraph_depth_limit_is_enforced_during_structural_decode() {
787 let mut plan = minimal_plan();
788 plan.nodes[0].subgraph_path = vec![crate::SubgraphId([1; 32]), crate::SubgraphId([2; 32])];
789 plan.plan_id = PlanId(hash_plan(&plan));
790 assert_eq!(
791 CompiledPlan::from_authorized_aot_bytes(
792 &plan.to_aot_bytes().unwrap(),
793 PlanLimits {
794 max_subgraph_depth: 1,
795 ..PlanLimits::default()
796 },
797 PlanAuthorization {
798 expected_realm: plan.realm,
799 expected_plan_id: plan.plan_id,
800 },
801 ),
802 Err(PlanDecodeError::InvalidPlan)
803 );
804 }
805
806 #[test]
807 fn plan_round_trip_and_tamper_rejection() {
808 let mut plan = CompiledPlan {
809 schema_version: 3,
810 graph_id: GraphId([1; 32]),
811 plan_id: PlanId([0; 32]),
812 realm: ExecutionRealm::McuAot,
813 order: vec![crate::NodeId(7)],
814 nodes: vec![crate::CompiledNode {
815 id: crate::StepId(0),
816 semantic_nodes: vec![crate::NodeId(7)],
817 semantic_types: vec![crate::NodeTypeRef {
818 type_name: "test".into(),
819 version: 1,
820 }],
821 semantic_configs: vec![alloc::collections::BTreeMap::new()],
822 kernel: crate::KernelId(11),
823 implementation_id: crate::ImplementationId([11; 32]),
824 resources: crate::ResourceEnvelope::bounded(0, 0, 1),
825 determinism: crate::Determinism::BitExact,
826 lowering: "test".into(),
827 conversion: None,
828 input_ports: vec![],
829 output_ports: vec!["out".into()],
830 input_contracts: vec![],
831 output_contracts: vec![crate::CompiledPortContract::opaque(
832 "out",
833 "test",
834 crate::Layout::Canonical,
835 1,
836 )],
837 input_bindings: vec![],
838 output_bindings: vec![crate::OutputBinding::Terminal],
839 partiality: crate::Partiality::Atomic,
840 failure: crate::FailureContract { domains: vec![] },
841 effect: crate::Effect::Pure,
842 retry_limit: 0,
843 state: crate::StateContract::stateless(),
844 subgraph_path: vec![],
845 }],
846 buffers: vec![],
847 feedback: vec![],
848 invocation_ports: vec![],
849 propagated_proofs: vec![],
850 propagated_policy: vec![],
851 resulting_fidelity: u16::MAX,
852 peak_bytes: 0,
853 persistent_state_bytes: 0,
854 session: None,
855 };
856 plan.plan_id = PlanId(hash_plan(&plan));
857 let bytes = plan.to_aot_bytes().unwrap();
858 assert_eq!(
859 CompiledPlan::from_aot_bytes(&bytes, PlanLimits::default()).unwrap(),
860 plan
861 );
862 let mut tampered = bytes;
863 *tampered.last_mut().unwrap() ^= 1;
864 assert!(CompiledPlan::from_aot_bytes(&tampered, PlanLimits::default()).is_err());
865 }
866
867 #[test]
868 fn manifest_authorization_binds_realm_and_plan_identity() {
869 let mut plan = CompiledPlan {
870 schema_version: 3,
871 graph_id: GraphId([1; 32]),
872 plan_id: PlanId([0; 32]),
873 realm: ExecutionRealm::McuAot,
874 order: vec![crate::NodeId(7)],
875 nodes: vec![crate::CompiledNode {
876 id: crate::StepId(0),
877 semantic_nodes: vec![crate::NodeId(7)],
878 semantic_types: vec![crate::NodeTypeRef {
879 type_name: "test".into(),
880 version: 1,
881 }],
882 semantic_configs: vec![alloc::collections::BTreeMap::new()],
883 kernel: crate::KernelId(11),
884 implementation_id: crate::ImplementationId([11; 32]),
885 resources: crate::ResourceEnvelope::bounded(0, 0, 1),
886 determinism: crate::Determinism::BitExact,
887 lowering: "test".into(),
888 conversion: None,
889 input_ports: vec![],
890 output_ports: vec!["out".into()],
891 input_contracts: vec![],
892 output_contracts: vec![crate::CompiledPortContract::opaque(
893 "out",
894 "test",
895 crate::Layout::Canonical,
896 1,
897 )],
898 input_bindings: vec![],
899 output_bindings: vec![crate::OutputBinding::Terminal],
900 partiality: crate::Partiality::Atomic,
901 failure: crate::FailureContract { domains: vec![] },
902 effect: crate::Effect::Pure,
903 retry_limit: 0,
904 state: crate::StateContract::stateless(),
905 subgraph_path: vec![],
906 }],
907 buffers: vec![],
908 feedback: vec![],
909 invocation_ports: vec![],
910 propagated_proofs: vec![],
911 propagated_policy: vec![],
912 resulting_fidelity: u16::MAX,
913 peak_bytes: 0,
914 persistent_state_bytes: 0,
915 session: None,
916 };
917 plan.plan_id = PlanId(hash_plan(&plan));
918 let bytes = plan.to_aot_bytes().unwrap();
919 assert_eq!(
920 CompiledPlan::from_authorized_aot_bytes(
921 &bytes,
922 PlanLimits::default(),
923 PlanAuthorization {
924 expected_realm: ExecutionRealm::HostStream,
925 expected_plan_id: plan.plan_id,
926 },
927 ),
928 Err(PlanDecodeError::RealmMismatch)
929 );
930 assert_eq!(
931 CompiledPlan::from_authorized_aot_bytes(
932 &bytes,
933 PlanLimits::default(),
934 PlanAuthorization {
935 expected_realm: ExecutionRealm::McuAot,
936 expected_plan_id: PlanId([9; 32]),
937 },
938 ),
939 Err(PlanDecodeError::UnauthorizedPlan)
940 );
941 }
942
943 #[test]
944 fn oversized_input_fails_before_decode() {
945 let bytes = vec![0; 9];
946 assert_eq!(
947 CompiledPlan::from_aot_bytes(
948 &bytes,
949 PlanLimits {
950 max_bytes: 8,
951 ..PlanLimits::default()
952 }
953 ),
954 Err(PlanDecodeError::TooLarge)
955 );
956 }
957
958 #[test]
959 fn declared_peak_memory_is_bounded_before_execution() {
960 let mut plan = CompiledPlan {
961 schema_version: 3,
962 graph_id: GraphId([1; 32]),
963 plan_id: PlanId([0; 32]),
964 realm: ExecutionRealm::McuAot,
965 order: vec![crate::NodeId(7)],
966 nodes: vec![crate::CompiledNode {
967 id: crate::StepId(0),
968 semantic_nodes: vec![crate::NodeId(7)],
969 semantic_types: vec![crate::NodeTypeRef {
970 type_name: "test".into(),
971 version: 1,
972 }],
973 semantic_configs: vec![alloc::collections::BTreeMap::new()],
974 kernel: crate::KernelId(11),
975 implementation_id: crate::ImplementationId([11; 32]),
976 resources: crate::ResourceEnvelope::bounded(0, 0, 1),
977 determinism: crate::Determinism::BitExact,
978 lowering: "test".into(),
979 conversion: None,
980 input_ports: vec![],
981 output_ports: vec!["out".into()],
982 input_contracts: vec![],
983 output_contracts: vec![crate::CompiledPortContract::opaque(
984 "out",
985 "test",
986 crate::Layout::Canonical,
987 1,
988 )],
989 input_bindings: vec![],
990 output_bindings: vec![crate::OutputBinding::Terminal],
991 partiality: crate::Partiality::Atomic,
992 failure: crate::FailureContract { domains: vec![] },
993 effect: crate::Effect::Pure,
994 retry_limit: 0,
995 state: crate::StateContract::stateless(),
996 subgraph_path: vec![],
997 }],
998 buffers: vec![],
999 feedback: vec![],
1000 invocation_ports: vec![],
1001 propagated_proofs: vec![],
1002 propagated_policy: vec![],
1003 resulting_fidelity: u16::MAX,
1004 peak_bytes: 4096,
1005 persistent_state_bytes: 0,
1006 session: None,
1007 };
1008 plan.plan_id = PlanId(hash_plan(&plan));
1009 let bytes = plan.to_aot_bytes().unwrap();
1010 assert_eq!(
1011 CompiledPlan::from_aot_bytes(
1012 &bytes,
1013 PlanLimits {
1014 max_peak_bytes: 1024,
1015 ..PlanLimits::default()
1016 }
1017 ),
1018 Err(PlanDecodeError::ResourceLimitExceeded)
1019 );
1020 }
1021}