1use crate::access_policy::{
15 MmioResource, ResourceCeiling, ResourceId, ResourceKind, WritableRegister,
16};
17use crate::digest::Sha256Digest;
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use sha2::{Digest as _, Sha256};
20use std::collections::BTreeMap;
21use std::ops::Range;
22
23pub const TARGET_POLICY_SCHEMA_VERSION: u32 = 1;
25
26pub const MAX_RESOURCES: usize = 64;
28
29pub const MAX_SNAPSHOT_ITEMS: u32 = 64;
31
32pub const MAX_NAME_LENGTH: usize = 64;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
37pub struct RangeDto {
38 pub start: u64,
39 pub end: u64,
40}
41
42impl RangeDto {
43 pub fn new(start: u64, end: u64) -> Result<Self, PolicyValidationError> {
45 if start >= end {
46 return Err(PolicyValidationError::InvalidRange { start, end });
47 }
48 Ok(Self { start, end })
49 }
50
51 pub fn to_range(&self) -> Range<u64> {
53 self.start..self.end
54 }
55
56 pub fn contains_offset(&self, offset: u64) -> bool {
58 self.start <= offset && offset < self.end
59 }
60}
61
62impl Serialize for RangeDto {
63 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64 where
65 S: Serializer,
66 {
67 (self.start, self.end).serialize(serializer)
68 }
69}
70
71#[derive(Deserialize)]
72#[serde(untagged)]
73enum RangeHelper {
74 Seq((u64, u64)),
75 Map { start: u64, end: u64 },
76}
77
78impl<'de> Deserialize<'de> for RangeDto {
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: Deserializer<'de>,
82 {
83 let helper = RangeHelper::deserialize(deserializer)?;
84 let (start, end) = match helper {
85 RangeHelper::Seq((s, e)) => (s, e),
86 RangeHelper::Map { start, end } => (start, end),
87 };
88 RangeDto::new(start, end).map_err(serde::de::Error::custom)
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
96pub struct WritableRegisterDto {
97 pub allow_mask: u32,
99 pub allow_rmw: bool,
101 pub offset: u64,
103 pub precondition_mask: u32,
105 pub readback: bool,
107 pub require_precondition: bool,
109 pub width: u32,
111}
112
113impl WritableRegisterDto {
114 pub fn to_writable_register(&self) -> WritableRegister {
115 WritableRegister {
116 offset: self.offset,
117 width: self.width,
118 allow_mask: self.allow_mask,
119 allow_rmw: self.allow_rmw,
120 require_precondition: self.require_precondition,
121 precondition_mask: self.precondition_mask,
122 readback: self.readback,
123 }
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ResourcePolicyManifest {
132 #[serde(default)]
134 pub allow_interrupt: bool,
135 pub allow_poll: bool,
137 pub allow_unknown_reads: bool,
139 pub hard_denied: Vec<RangeDto>,
141 pub id: ResourceId,
143 pub name: String,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
147 pub writable_registers: Vec<WritableRegisterDto>,
148}
149
150impl ResourcePolicyManifest {
151 pub fn canonicalize(&mut self) -> Result<(), PolicyValidationError> {
153 if self.name.is_empty() {
154 return Err(PolicyValidationError::EmptyResourceName(self.id));
155 }
156 if self.name.len() > MAX_NAME_LENGTH {
157 return Err(PolicyValidationError::ResourceNameTooLong {
158 id: self.id,
159 len: self.name.len(),
160 max: MAX_NAME_LENGTH,
161 });
162 }
163 self.hard_denied = canonicalize_ranges(&self.hard_denied)?;
164 self.writable_registers.sort();
165 let mut seen = std::collections::BTreeSet::new();
166 for reg in &self.writable_registers {
167 if !seen.insert(reg.offset) {
168 return Err(PolicyValidationError::DuplicateWritableRegister {
169 id: self.id,
170 offset: reg.offset,
171 });
172 }
173 if reg.width != 4 {
174 return Err(PolicyValidationError::UnsupportedWidth(reg.width));
175 }
176 if reg.allow_mask == 0 {
177 return Err(PolicyValidationError::InvalidWritableRegisterMask {
178 id: self.id,
179 offset: reg.offset,
180 });
181 }
182 }
183 Ok(())
184 }
185
186 pub fn to_ceiling(&self) -> ResourceCeiling {
188 ResourceCeiling {
189 hard_denied: self.hard_denied.iter().map(|r| r.to_range()).collect(),
190 allow_unknown_reads: self.allow_unknown_reads,
191 allow_poll: self.allow_poll,
192 writable_registers: self
193 .writable_registers
194 .iter()
195 .map(|w| w.to_writable_register())
196 .collect(),
197 protocol: None,
198 allow_interrupt: self.allow_interrupt,
199 }
200 }
201}
202
203#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
207pub struct TargetPolicyManifest {
208 pub allow_mutating_sessions: bool,
210 pub audit_capacity: u64,
212 pub max_snapshot_items: u32,
214 pub resources: Vec<ResourcePolicyManifest>,
216 pub schema_version: u32,
218}
219
220impl TargetPolicyManifest {
221 pub fn engineering_default(resources: &BTreeMap<ResourceId, MmioResource>) -> Self {
223 let resource_manifests = resources
224 .iter()
225 .map(|(id, res)| ResourcePolicyManifest {
226 id: *id,
227 name: res.name.clone(),
228 allow_interrupt: res.kind == ResourceKind::Interrupt,
229 allow_unknown_reads: true,
230 allow_poll: false,
231 hard_denied: vec![],
232 writable_registers: vec![],
233 })
234 .collect();
235
236 Self {
237 schema_version: TARGET_POLICY_SCHEMA_VERSION,
238 allow_mutating_sessions: true,
239 max_snapshot_items: MAX_SNAPSHOT_ITEMS,
240 audit_capacity: 1024,
241 resources: resource_manifests,
242 }
243 }
244
245 pub fn from_bundle_ceiling(
249 resources: &BTreeMap<ResourceId, MmioResource>,
250 ceiling: &BTreeMap<ResourceId, ResourceCeiling>,
251 allow_mutating_sessions: bool,
252 ) -> Self {
253 let resource_manifests = resources
254 .iter()
255 .map(|(id, res)| {
256 if let Some(c) = ceiling.get(id) {
257 ResourcePolicyManifest {
258 id: *id,
259 name: res.name.clone(),
260 allow_interrupt: c.allow_interrupt,
261 allow_unknown_reads: c.allow_unknown_reads,
262 allow_poll: c.allow_poll,
263 hard_denied: c
264 .hard_denied
265 .iter()
266 .filter_map(|r| RangeDto::new(r.start, r.end).ok())
267 .collect(),
268 writable_registers: c
269 .writable_registers
270 .iter()
271 .map(|w| WritableRegisterDto {
272 allow_mask: w.allow_mask,
273 allow_rmw: w.allow_rmw,
274 offset: w.offset,
275 precondition_mask: w.precondition_mask,
276 readback: w.readback,
277 require_precondition: w.require_precondition,
278 width: w.width,
279 })
280 .collect(),
281 }
282 } else {
283 ResourcePolicyManifest {
284 id: *id,
285 name: res.name.clone(),
286 allow_interrupt: res.kind == ResourceKind::Interrupt,
287 allow_unknown_reads: false,
288 allow_poll: false,
289 hard_denied: vec![],
290 writable_registers: vec![],
291 }
292 }
293 })
294 .collect();
295
296 let mut manifest = Self {
297 schema_version: TARGET_POLICY_SCHEMA_VERSION,
298 allow_mutating_sessions,
299 max_snapshot_items: MAX_SNAPSHOT_ITEMS,
300 audit_capacity: 1024,
301 resources: resource_manifests,
302 };
303 let _ = manifest.canonicalize();
304 manifest
305 }
306
307 pub fn canonicalize(&mut self) -> Result<(), PolicyValidationError> {
309 if self.schema_version != TARGET_POLICY_SCHEMA_VERSION {
310 return Err(PolicyValidationError::InvalidSchemaVersion(self.schema_version));
311 }
312 if self.max_snapshot_items == 0 {
313 return Err(PolicyValidationError::ZeroSnapshotLimit);
314 }
315 if self.max_snapshot_items > MAX_SNAPSHOT_ITEMS {
316 return Err(PolicyValidationError::SnapshotLimitExceeded {
317 value: self.max_snapshot_items,
318 max: MAX_SNAPSHOT_ITEMS,
319 });
320 }
321 if self.audit_capacity == 0 {
322 return Err(PolicyValidationError::ZeroAuditCapacity);
323 }
324 if self.resources.len() > MAX_RESOURCES {
325 return Err(PolicyValidationError::TooManyResources {
326 count: self.resources.len(),
327 max: MAX_RESOURCES,
328 });
329 }
330
331 self.resources.sort_by_key(|r| r.id);
332 let mut seen_ids = std::collections::BTreeSet::new();
333 let mut seen_names = std::collections::BTreeSet::new();
334
335 for res in &mut self.resources {
336 if !seen_ids.insert(res.id) {
337 return Err(PolicyValidationError::DuplicateResourceId(res.id));
338 }
339 if !seen_names.insert(res.name.clone()) {
340 return Err(PolicyValidationError::DuplicateResourceName(res.name.clone()));
341 }
342 res.canonicalize()?;
343 }
344
345 Ok(())
346 }
347
348 pub fn to_canonical_json(&self) -> Result<String, serde_json::Error> {
350 serde_json::to_string(self)
351 }
352
353 pub fn policy_digest(&self) -> Sha256Digest {
355 let json_str = self.to_canonical_json().expect("manifest serializes to JSON");
356 let hash = Sha256::digest(json_str.as_bytes());
357 Sha256Digest::from_bytes(hash.into())
358 }
359
360 pub fn to_ceiling_map(&self) -> BTreeMap<ResourceId, ResourceCeiling> {
362 self.resources.iter().map(|r| (r.id, r.to_ceiling())).collect()
363 }
364
365 pub fn narrow_with(&self, runtime: &Self) -> Result<Self, NarrowingError> {
368 if self.schema_version != runtime.schema_version {
369 return Err(NarrowingError::SchemaVersionMismatch);
370 }
371
372 if !self.allow_mutating_sessions && runtime.allow_mutating_sessions {
374 return Err(NarrowingError::WidenedMutatingSessions);
375 }
376
377 if runtime.max_snapshot_items > self.max_snapshot_items {
379 return Err(NarrowingError::WidenedSnapshotLimit {
380 baseline: self.max_snapshot_items,
381 runtime: runtime.max_snapshot_items,
382 });
383 }
384
385 if runtime.audit_capacity > self.audit_capacity {
387 return Err(NarrowingError::WidenedAuditCapacity {
388 baseline: self.audit_capacity,
389 runtime: runtime.audit_capacity,
390 });
391 }
392
393 let base_resources: BTreeMap<ResourceId, &ResourcePolicyManifest> =
394 self.resources.iter().map(|r| (r.id, r)).collect();
395
396 for runtime_res in &runtime.resources {
397 let base_res = base_resources
398 .get(&runtime_res.id)
399 .ok_or(NarrowingError::UnknownResource(runtime_res.id))?;
400
401 if runtime_res.name != base_res.name {
402 return Err(NarrowingError::ResourceNameMismatch {
403 id: runtime_res.id,
404 baseline: base_res.name.clone(),
405 runtime: runtime_res.name.clone(),
406 });
407 }
408
409 if !base_res.allow_unknown_reads && runtime_res.allow_unknown_reads {
411 return Err(NarrowingError::WidenedUnknownReads(runtime_res.id));
412 }
413
414 if !base_res.allow_poll && runtime_res.allow_poll {
416 return Err(NarrowingError::WidenedPoll(runtime_res.id));
417 }
418
419 if !base_res.allow_interrupt && runtime_res.allow_interrupt {
421 return Err(NarrowingError::WidenedInterrupt(runtime_res.id));
422 }
423
424 for base_range in &base_res.hard_denied {
427 let covered = runtime_res.hard_denied.iter().any(|rt_range| {
428 rt_range.start <= base_range.start && base_range.end <= rt_range.end
429 });
430 if !covered {
431 return Err(NarrowingError::ReducedHardDenial {
432 id: runtime_res.id,
433 unconstrained_start: base_range.start,
434 unconstrained_end: base_range.end,
435 });
436 }
437 }
438
439 for rt_reg in &runtime_res.writable_registers {
441 let base_reg =
442 base_res.writable_registers.iter().find(|b| b.offset == rt_reg.offset).ok_or(
443 NarrowingError::WidenedWritableRegister {
444 id: runtime_res.id,
445 offset: rt_reg.offset,
446 },
447 )?;
448 if (rt_reg.allow_mask & !base_reg.allow_mask) != 0 {
449 return Err(NarrowingError::WidenedWritableRegisterMask {
450 id: runtime_res.id,
451 offset: rt_reg.offset,
452 });
453 }
454 if !base_reg.allow_rmw && rt_reg.allow_rmw {
455 return Err(NarrowingError::WidenedWritableRegister {
456 id: runtime_res.id,
457 offset: rt_reg.offset,
458 });
459 }
460 if base_reg.require_precondition && !rt_reg.require_precondition {
461 return Err(NarrowingError::WidenedWritableRegister {
462 id: runtime_res.id,
463 offset: rt_reg.offset,
464 });
465 }
466 if (rt_reg.precondition_mask & !base_reg.precondition_mask) != 0 {
467 return Err(NarrowingError::WidenedWritableRegisterMask {
468 id: runtime_res.id,
469 offset: rt_reg.offset,
470 });
471 }
472 }
473 }
474
475 let mut narrowed = runtime.clone();
476 narrowed.canonicalize().map_err(NarrowingError::Validation)?;
477 Ok(narrowed)
478 }
479}
480
481pub fn canonicalize_ranges(ranges: &[RangeDto]) -> Result<Vec<RangeDto>, PolicyValidationError> {
483 if ranges.is_empty() {
484 return Ok(Vec::new());
485 }
486
487 let mut sorted = ranges.to_vec();
488 for r in &sorted {
489 if r.start >= r.end {
490 return Err(PolicyValidationError::InvalidRange { start: r.start, end: r.end });
491 }
492 }
493 sorted.sort_unstable_by_key(|r| (r.start, r.end));
494
495 let mut merged: Vec<RangeDto> = Vec::with_capacity(sorted.len());
496 for r in sorted {
497 match merged.last_mut() {
498 Some(last) if r.start <= last.end => {
499 last.end = std::cmp::max(last.end, r.end);
501 }
502 _ => merged.push(r),
503 }
504 }
505
506 Ok(merged)
507}
508
509#[derive(Clone, Debug, PartialEq, Eq)]
511pub enum PolicyValidationError {
512 InvalidSchemaVersion(u32),
513 InvalidRange { start: u64, end: u64 },
514 SnapshotLimitExceeded { value: u32, max: u32 },
515 ZeroSnapshotLimit,
516 ZeroAuditCapacity,
517 EmptyResourceName(ResourceId),
518 ResourceNameTooLong { id: ResourceId, len: usize, max: usize },
519 DuplicateResourceId(ResourceId),
520 DuplicateResourceName(String),
521 TooManyResources { count: usize, max: usize },
522 DuplicateWritableRegister { id: ResourceId, offset: u64 },
523 UnsupportedWidth(u32),
524 InvalidWritableRegisterMask { id: ResourceId, offset: u64 },
525}
526
527#[derive(Clone, Debug, PartialEq, Eq)]
529pub enum NarrowingError {
530 SchemaVersionMismatch,
531 WidenedMutatingSessions,
532 WidenedSnapshotLimit { baseline: u32, runtime: u32 },
533 WidenedAuditCapacity { baseline: u64, runtime: u64 },
534 UnknownResource(ResourceId),
535 ResourceNameMismatch { id: ResourceId, baseline: String, runtime: String },
536 WidenedUnknownReads(ResourceId),
537 WidenedPoll(ResourceId),
538 WidenedInterrupt(ResourceId),
539 ReducedHardDenial { id: ResourceId, unconstrained_start: u64, unconstrained_end: u64 },
540 WidenedWritableRegister { id: ResourceId, offset: u64 },
541 WidenedWritableRegisterMask { id: ResourceId, offset: u64 },
542 Validation(PolicyValidationError),
543}
544
545impl std::fmt::Display for PolicyValidationError {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 match self {
548 Self::InvalidSchemaVersion(v) => write!(f, "invalid schema version: {v}"),
549 Self::InvalidRange { start, end } => {
550 write!(f, "invalid range: start {start} >= end {end}")
551 }
552 Self::SnapshotLimitExceeded { value, max } => {
553 write!(f, "snapshot limit {value} exceeds maximum {max}")
554 }
555 Self::ZeroSnapshotLimit => write!(f, "snapshot limit must be greater than zero"),
556 Self::ZeroAuditCapacity => write!(f, "audit capacity must be greater than zero"),
557 Self::EmptyResourceName(id) => write!(f, "empty resource name for resource {id}"),
558 Self::ResourceNameTooLong { id, len, max } => {
559 write!(f, "resource name for {id} length {len} exceeds maximum {max}")
560 }
561 Self::DuplicateResourceId(id) => write!(f, "duplicate resource ID: {id}"),
562 Self::DuplicateResourceName(name) => write!(f, "duplicate resource name: {name}"),
563 Self::TooManyResources { count, max } => {
564 write!(f, "resource count {count} exceeds maximum {max}")
565 }
566 Self::DuplicateWritableRegister { id, offset } => {
567 write!(f, "duplicate writable register for resource {id} at offset {offset}")
568 }
569 Self::UnsupportedWidth(w) => write!(f, "unsupported width: {w}"),
570 Self::InvalidWritableRegisterMask { id, offset } => {
571 write!(f, "invalid allow mask for resource {id} at offset {offset}")
572 }
573 }
574 }
575}
576
577impl std::error::Error for PolicyValidationError {}
578
579impl std::fmt::Display for NarrowingError {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 match self {
582 Self::SchemaVersionMismatch => write!(f, "schema version mismatch"),
583 Self::WidenedMutatingSessions => write!(f, "cannot enable mutating sessions"),
584 Self::WidenedSnapshotLimit { baseline, runtime } => {
585 write!(f, "snapshot limit {runtime} exceeds baseline {baseline}")
586 }
587 Self::WidenedAuditCapacity { baseline, runtime } => {
588 write!(f, "audit capacity {runtime} exceeds baseline {baseline}")
589 }
590 Self::UnknownResource(id) => write!(f, "unknown resource {id} in runtime policy"),
591 Self::ResourceNameMismatch { id, baseline, runtime } => {
592 write!(f, "resource {id} name mismatch: baseline {baseline}, runtime {runtime}")
593 }
594 Self::WidenedUnknownReads(id) => {
595 write!(f, "cannot enable unknown reads on resource {id}")
596 }
597 Self::WidenedPoll(id) => write!(f, "cannot enable polling on resource {id}"),
598 Self::WidenedInterrupt(id) => {
599 write!(f, "cannot enable interrupt observation on resource {id}")
600 }
601 Self::ReducedHardDenial { id, unconstrained_start, unconstrained_end } => {
602 write!(
603 f,
604 "runtime hard denials for resource {id} fail to cover baseline denial [{unconstrained_start}, {unconstrained_end})"
605 )
606 }
607 Self::WidenedWritableRegister { id, offset } => {
608 write!(f, "writable register on resource {id} at offset {offset} widened baseline")
609 }
610 Self::WidenedWritableRegisterMask { id, offset } => {
611 write!(
612 f,
613 "mask for writable register on resource {id} at offset {offset} widened baseline"
614 )
615 }
616 Self::Validation(err) => write!(f, "validation error: {err}"),
617 }
618 }
619}
620
621impl std::error::Error for NarrowingError {}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn range_canonicalization_merges_overlapping_and_adjacent() {
629 let input = vec![
630 RangeDto::new(10, 20).unwrap(),
631 RangeDto::new(15, 25).unwrap(),
632 RangeDto::new(25, 30).unwrap(),
633 RangeDto::new(40, 50).unwrap(),
634 ];
635 let merged = canonicalize_ranges(&input).unwrap();
636 assert_eq!(merged, vec![RangeDto::new(10, 30).unwrap(), RangeDto::new(40, 50).unwrap(),]);
637 }
638
639 #[test]
640 fn range_canonicalization_rejects_inverted_or_empty() {
641 assert_eq!(
642 RangeDto::new(20, 10),
643 Err(PolicyValidationError::InvalidRange { start: 20, end: 10 })
644 );
645 assert_eq!(
646 RangeDto::new(10, 10),
647 Err(PolicyValidationError::InvalidRange { start: 10, end: 10 })
648 );
649 }
650
651 #[test]
652 fn canonical_json_and_digest_determinism() {
653 let mut manifest = TargetPolicyManifest {
654 schema_version: 1,
655 allow_mutating_sessions: false,
656 audit_capacity: 1024,
657 max_snapshot_items: 64,
658 resources: vec![ResourcePolicyManifest {
659 id: 0,
660 name: "mmio0".to_string(),
661 allow_interrupt: false,
662 allow_unknown_reads: true,
663 allow_poll: false,
664 hard_denied: vec![RangeDto::new(64, 68).unwrap()],
665 writable_registers: vec![],
666 }],
667 };
668 manifest.canonicalize().unwrap();
669
670 let json_str = manifest.to_canonical_json().unwrap();
671 assert_eq!(
672 json_str,
673 "{\"allow_mutating_sessions\":false,\"audit_capacity\":1024,\"max_snapshot_items\":64,\"resources\":[{\"allow_interrupt\":false,\"allow_poll\":false,\"allow_unknown_reads\":true,\"hard_denied\":[[64,68]],\"id\":0,\"name\":\"mmio0\"}],\"schema_version\":1}"
674 );
675
676 let digest = manifest.policy_digest();
677 assert_eq!(
678 digest.to_string(),
679 "sha256:5511f2b70b6fcd8ed519f7e57d31e0becb17083e7f0004d48abfd7ab88034b4d"
680 );
681 }
682
683 #[test]
684 fn narrowing_accepts_valid_reductions() {
685 let baseline = TargetPolicyManifest {
686 schema_version: 1,
687 allow_mutating_sessions: true,
688 audit_capacity: 1024,
689 max_snapshot_items: 64,
690 resources: vec![ResourcePolicyManifest {
691 id: 0,
692 name: "mmio0".to_string(),
693 allow_interrupt: true,
694 allow_unknown_reads: true,
695 allow_poll: true,
696 hard_denied: vec![RangeDto::new(64, 68).unwrap()],
697 writable_registers: vec![],
698 }],
699 };
700
701 let runtime = TargetPolicyManifest {
703 schema_version: 1,
704 allow_mutating_sessions: false,
705 audit_capacity: 512,
706 max_snapshot_items: 32,
707 resources: vec![ResourcePolicyManifest {
708 id: 0,
709 name: "mmio0".to_string(),
710 allow_interrupt: false,
711 allow_unknown_reads: false,
712 allow_poll: false,
713 hard_denied: vec![RangeDto::new(64, 68).unwrap(), RangeDto::new(128, 144).unwrap()],
714 writable_registers: vec![],
715 }],
716 };
717
718 let narrowed = baseline.narrow_with(&runtime).unwrap();
719 assert_eq!(narrowed.audit_capacity, 512);
720 assert_eq!(narrowed.max_snapshot_items, 32);
721 assert!(!narrowed.allow_mutating_sessions);
722 assert!(!narrowed.resources[0].allow_unknown_reads);
723 assert!(!narrowed.resources[0].allow_poll);
724 assert!(!narrowed.resources[0].allow_interrupt);
725 assert_eq!(narrowed.resources[0].hard_denied.len(), 2);
726 }
727
728 #[test]
729 fn narrowing_rejects_widenings() {
730 let baseline = TargetPolicyManifest {
731 schema_version: 1,
732 allow_mutating_sessions: false,
733 audit_capacity: 512,
734 max_snapshot_items: 32,
735 resources: vec![ResourcePolicyManifest {
736 id: 0,
737 name: "mmio0".to_string(),
738 allow_interrupt: false,
739 allow_unknown_reads: false,
740 allow_poll: false,
741 hard_denied: vec![RangeDto::new(64, 128).unwrap()],
742 writable_registers: vec![],
743 }],
744 };
745
746 let mut widened = baseline.clone();
748 widened.allow_mutating_sessions = true;
749 assert_eq!(baseline.narrow_with(&widened), Err(NarrowingError::WidenedMutatingSessions));
750
751 let mut widened = baseline.clone();
753 widened.audit_capacity = 1024;
754 assert_eq!(
755 baseline.narrow_with(&widened),
756 Err(NarrowingError::WidenedAuditCapacity { baseline: 512, runtime: 1024 })
757 );
758
759 let mut widened = baseline.clone();
761 widened.max_snapshot_items = 64;
762 assert_eq!(
763 baseline.narrow_with(&widened),
764 Err(NarrowingError::WidenedSnapshotLimit { baseline: 32, runtime: 64 })
765 );
766
767 let mut widened = baseline.clone();
769 widened.resources[0].allow_unknown_reads = true;
770 assert_eq!(baseline.narrow_with(&widened), Err(NarrowingError::WidenedUnknownReads(0)));
771
772 let mut widened = baseline.clone();
774 widened.resources[0].allow_poll = true;
775 assert_eq!(baseline.narrow_with(&widened), Err(NarrowingError::WidenedPoll(0)));
776
777 let mut widened = baseline.clone();
779 widened.resources[0].allow_interrupt = true;
780 assert_eq!(baseline.narrow_with(&widened), Err(NarrowingError::WidenedInterrupt(0)));
781
782 let mut widened = baseline.clone();
784 widened.resources[0].hard_denied = vec![RangeDto::new(64, 100).unwrap()];
785 assert_eq!(
786 baseline.narrow_with(&widened),
787 Err(NarrowingError::ReducedHardDenial {
788 id: 0,
789 unconstrained_start: 64,
790 unconstrained_end: 128
791 })
792 );
793 }
794
795 #[test]
796 fn writable_registers_canonicalization_and_narrowing() {
797 let base_reg = WritableRegisterDto {
798 allow_mask: 0x0000FFFF,
799 allow_rmw: false,
800 offset: 0x10,
801 precondition_mask: 0x00000001,
802 readback: true,
803 require_precondition: true,
804 width: 4,
805 };
806 let mut baseline = TargetPolicyManifest {
807 schema_version: 1,
808 allow_mutating_sessions: true,
809 audit_capacity: 1024,
810 max_snapshot_items: 64,
811 resources: vec![ResourcePolicyManifest {
812 id: 0,
813 name: "mmio0".to_string(),
814 allow_interrupt: false,
815 allow_unknown_reads: true,
816 allow_poll: false,
817 hard_denied: vec![],
818 writable_registers: vec![base_reg.clone()],
819 }],
820 };
821 baseline.canonicalize().unwrap();
822
823 let mut runtime = baseline.clone();
825 runtime.resources[0].writable_registers.push(WritableRegisterDto {
826 allow_mask: 0xFFFFFFFF,
827 allow_rmw: true,
828 offset: 0x20,
829 precondition_mask: 0,
830 readback: false,
831 require_precondition: false,
832 width: 4,
833 });
834 assert_eq!(
835 baseline.narrow_with(&runtime),
836 Err(NarrowingError::WidenedWritableRegister { id: 0, offset: 0x20 })
837 );
838
839 let mut runtime = baseline.clone();
841 runtime.resources[0].writable_registers[0].allow_mask = 0x0001FFFF;
842 assert_eq!(
843 baseline.narrow_with(&runtime),
844 Err(NarrowingError::WidenedWritableRegisterMask { id: 0, offset: 0x10 })
845 );
846
847 let mut runtime = baseline.clone();
849 runtime.resources[0].writable_registers[0].allow_rmw = true;
850 assert_eq!(
851 baseline.narrow_with(&runtime),
852 Err(NarrowingError::WidenedWritableRegister { id: 0, offset: 0x10 })
853 );
854
855 let mut runtime = baseline.clone();
857 runtime.resources[0].writable_registers[0].allow_mask = 0x000000FF;
858 assert!(baseline.narrow_with(&runtime).is_ok());
859
860 let mut runtime = baseline.clone();
862 runtime.resources[0].writable_registers.clear();
863 assert!(baseline.narrow_with(&runtime).is_ok());
864 }
865}