Skip to main content

attribution_processing/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use core::cell::RefCell;
5use core::convert::Into;
6use fidl_fuchsia_memory_attribution_plugin_common as fplugin;
7use rustc_hash::FxHashMap;
8use serde::Serialize;
9use std::collections::HashSet;
10use std::fmt::Debug;
11use summary::MemorySummary;
12
13mod name;
14pub use name::ZXName;
15pub mod digest;
16pub mod fkernel_serde;
17pub mod fplugin_serde;
18mod macros;
19pub mod summary;
20
21#[cfg(target_os = "fuchsia")]
22use {fuchsia_trace::duration, std::ffi::CStr};
23#[cfg(target_os = "fuchsia")]
24const CATEGORY_MEMORY_CAPTURE: &CStr = c"memory:capture";
25
26/// Unique principal identifier across the whole system.
27#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, Serialize)]
28pub struct GlobalPrincipalIdentifier(pub std::num::NonZeroU64);
29
30impl GlobalPrincipalIdentifier {
31    /// Should only be generated by a [GlobalPrincipalIdentifierFactory] except for tests.
32    // #[cfg(test)] cannot be used because it is enabled only for the current crate tests.
33    pub fn new_for_test(value: u64) -> Self {
34        Self(std::num::NonZeroU64::new(value).unwrap())
35    }
36}
37
38impl From<fplugin::PrincipalIdentifier> for GlobalPrincipalIdentifier {
39    fn from(value: fplugin::PrincipalIdentifier) -> Self {
40        Self(std::num::NonZeroU64::new(value.id).unwrap())
41    }
42}
43
44impl From<GlobalPrincipalIdentifier> for fplugin::PrincipalIdentifier {
45    fn from(value: GlobalPrincipalIdentifier) -> fplugin::PrincipalIdentifier {
46        fplugin::PrincipalIdentifier { id: value.0.get() }
47    }
48}
49
50/// Factory for GlobalPrincipalIdentifier, ensuring their uniqueness.
51#[derive(Debug)]
52pub struct GlobalPrincipalIdentifierFactory {
53    next_id: std::num::NonZeroU64,
54}
55
56impl Default for GlobalPrincipalIdentifierFactory {
57    fn default() -> GlobalPrincipalIdentifierFactory {
58        GlobalPrincipalIdentifierFactory { next_id: std::num::NonZeroU64::new(1).unwrap() }
59    }
60}
61
62impl GlobalPrincipalIdentifierFactory {
63    pub fn next(&mut self) -> GlobalPrincipalIdentifier {
64        let value = GlobalPrincipalIdentifier(self.next_id);
65        // Fail loudly if we are no longer able to generate new [GlobalPrincipalIdentifier]s.
66        self.next_id = self.next_id.checked_add(1).unwrap();
67        return value;
68    }
69}
70
71/// User-understandable description of a Principal
72#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
73pub enum PrincipalDescription {
74    Component(String),
75    Part(String),
76}
77
78impl From<fplugin::Description> for PrincipalDescription {
79    fn from(value: fplugin::Description) -> Self {
80        match value {
81            fplugin::Description::Component(s) => PrincipalDescription::Component(s),
82            fplugin::Description::Part(s) => PrincipalDescription::Part(s),
83            _ => unreachable!(),
84        }
85    }
86}
87
88impl From<PrincipalDescription> for fplugin::Description {
89    fn from(value: PrincipalDescription) -> fplugin::Description {
90        match value {
91            PrincipalDescription::Component(s) => fplugin::Description::Component(s),
92            PrincipalDescription::Part(s) => fplugin::Description::Part(s),
93        }
94    }
95}
96
97/// Type of a principal.
98#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
99pub enum PrincipalType {
100    Runnable,
101    Part,
102}
103
104impl From<fplugin::PrincipalType> for PrincipalType {
105    fn from(value: fplugin::PrincipalType) -> Self {
106        match value {
107            fplugin::PrincipalType::Runnable => PrincipalType::Runnable,
108            fplugin::PrincipalType::Part => PrincipalType::Part,
109            _ => unreachable!(),
110        }
111    }
112}
113
114impl From<PrincipalType> for fplugin::PrincipalType {
115    fn from(value: PrincipalType) -> fplugin::PrincipalType {
116        match value {
117            PrincipalType::Runnable => fplugin::PrincipalType::Runnable,
118            PrincipalType::Part => fplugin::PrincipalType::Part,
119        }
120    }
121}
122
123#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
124/// A Principal, that can use and claim memory.
125pub struct Principal {
126    // These fields are initialized from [fplugin::Principal].
127    pub identifier: GlobalPrincipalIdentifier,
128    pub description: Option<PrincipalDescription>,
129    pub principal_type: PrincipalType,
130
131    /// Principal that declared this Principal. None if this Principal is at the root of the system
132    /// hierarchy (the root principal is a statically defined Principal encompassing all resources
133    /// on the system). The Principal hierarchy forms a tree (no cycles).
134    pub parent: Option<GlobalPrincipalIdentifier>,
135}
136
137/// Creates a new [Principal] from a [fplugin::Principal] object. The [Principal] object will
138/// contain all the data from [fplugin::Principal] and have its other fields initialized empty.
139impl From<fplugin::Principal> for Principal {
140    fn from(value: fplugin::Principal) -> Self {
141        Principal {
142            identifier: value.identifier.unwrap().try_into().unwrap(),
143            description: value.description.map(Into::into),
144            principal_type: value.principal_type.unwrap().into(),
145            parent: value.parent.map(|id| id.try_into().unwrap()),
146        }
147    }
148}
149
150impl From<Principal> for fplugin::Principal {
151    fn from(value: Principal) -> fplugin::Principal {
152        fplugin::Principal {
153            identifier: Some(value.identifier.into()),
154            description: value.description.map(Into::into),
155            principal_type: Some(value.principal_type.into()),
156            parent: value.parent.map(Into::into),
157            ..Default::default()
158        }
159    }
160}
161
162/// A Principal, with its attribution claims and resources.
163#[derive(Serialize)]
164pub struct InflatedPrincipal {
165    /// The principal definition.
166    principal: Principal,
167
168    // These fields are computed from the rest of the [fplugin::Snapshot] data.
169    /// KOIDs of processes that have mapped regions attributed to this principal.
170    mapped_processes: Vec<u64>,
171
172    /// KOIDs of resources attributed to this principal, after resolution of sharing and
173    /// reattributions.
174    resources: Vec<u64>,
175}
176
177impl InflatedPrincipal {
178    fn new(principal: Principal) -> InflatedPrincipal {
179        InflatedPrincipal {
180            principal,
181            mapped_processes: Default::default(),
182            resources: Default::default(),
183        }
184    }
185}
186
187impl InflatedPrincipal {
188    fn name(&self) -> &str {
189        match &self.principal.description {
190            Some(PrincipalDescription::Component(component_name)) => component_name,
191            Some(PrincipalDescription::Part(part_name)) => part_name,
192            None => "?",
193        }
194    }
195}
196
197/// Type of the claim, that changes depending on how the claim was created.
198#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
199pub enum ClaimType {
200    /// A principal claimed this resource directly.
201    Direct,
202    /// A principal claimed a resource that contains this resource (e.g. a process containing a
203    /// VMO).
204    Indirect,
205    /// A principal claimed a child of this resource (e.g. a copy-on-write VMO child of this VMO).
206    Child,
207}
208
209#[derive(Clone, Copy, PartialEq, Eq, Hash)]
210pub struct Koid(u64);
211
212impl From<u64> for Koid {
213    fn from(value: u64) -> Self {
214        Koid(value)
215    }
216}
217
218/// Attribution claim of a Principal on a Resource.
219///
220/// Note that this object is slightly different from the [fplugin::Attribution] object: it goes from
221/// a Resource to a Principal, and covers also indirect attribution claims of sub- and parent
222/// resources.
223#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
224pub struct Claim {
225    /// Principal to which the resources are attributed.
226    pub(crate) subject: GlobalPrincipalIdentifier,
227    /// Principal making the attribution claim.
228    pub(crate) source: GlobalPrincipalIdentifier,
229    pub(crate) claim_type: ClaimType,
230}
231
232#[derive(Clone, Debug, PartialEq, Serialize)]
233pub struct Resource {
234    pub koid: u64,
235    pub name_index: usize,
236    #[serde(with = "fplugin_serde::ResourceTypeDef")]
237    pub resource_type: fplugin::ResourceType,
238}
239
240impl From<fplugin::Resource> for Resource {
241    fn from(value: fplugin::Resource) -> Self {
242        Resource {
243            koid: value.koid.unwrap(),
244            name_index: value.name_index.unwrap() as usize,
245            resource_type: value.resource_type.unwrap(),
246        }
247    }
248}
249
250impl From<Resource> for fplugin::Resource {
251    fn from(value: Resource) -> fplugin::Resource {
252        fplugin::Resource {
253            koid: Some(value.koid),
254            name_index: Some(value.name_index as u64),
255            resource_type: Some(value.resource_type),
256            ..Default::default()
257        }
258    }
259}
260
261// Claim with a boolean tag, to help find leaves in the claim assignment graph.
262pub struct TaggedClaim(Claim, bool);
263
264#[derive(Clone, Debug, Serialize)]
265pub struct BlobAnnotation {
266    /// Path to the package's manifest.
267    pub manifest: String,
268    /// Path of blob in package.
269    pub path: String,
270}
271
272#[derive(Clone, Debug, Serialize)]
273pub enum ResourceAnnotation {
274    Blob(BlobAnnotation),
275}
276
277/// Resource annotated with additional information.
278#[derive(Debug, Serialize)]
279pub struct InflatedResource {
280    pub resource: Resource,
281    pub claims: HashSet<Claim>,
282    pub annotations: Vec<ResourceAnnotation>,
283}
284
285impl InflatedResource {
286    fn new(resource: Resource) -> InflatedResource {
287        InflatedResource { resource, claims: Default::default(), annotations: Default::default() }
288    }
289
290    fn children(&self) -> Vec<u64> {
291        match &self.resource.resource_type {
292            fplugin::ResourceType::Job(job) => {
293                let mut r: Vec<u64> = job.child_jobs.iter().flatten().map(|k| *k).collect();
294                r.extend(job.processes.iter().flatten().map(|k| *k));
295                r
296            }
297            fplugin::ResourceType::Process(process) => {
298                process.vmos.iter().flatten().map(|k| *k).collect()
299            }
300            fplugin::ResourceType::Vmo(_) => Vec::new(),
301            _ => todo!(),
302        }
303    }
304
305    /// Process the claims made on this resource to disambiguate between reassignment and sharing.
306    ///
307    /// [process_claims] looks at each claim made on this resource, and removes claims that are
308    /// reassigned by another claim. This happens if a principal A gives a resource to principal B,
309    /// and B then gives it to principal C. However, if two independent principals claim this
310    /// resource, then both their claims are kept.
311    /// This is done by:
312    /// (i)  preserving all self claims, and
313    /// (ii) preserving only leaves in the DAG following claim.source to claim.subject edges.
314    fn process_claims(&mut self) {
315        let mut claims_by_source: FxHashMap<GlobalPrincipalIdentifier, RefCell<Vec<TaggedClaim>>> =
316            Default::default();
317        let mut self_claims = Vec::new();
318
319        for claim in self.claims.iter().cloned() {
320            if claim.source == claim.subject {
321                // Self claims are taken out of the graph because they are never transferred. This
322                // is to implement sharing.
323                self_claims.push(claim);
324            } else {
325                claims_by_source
326                    .entry(claim.source)
327                    .or_default()
328                    .borrow_mut()
329                    .push(TaggedClaim(claim, false));
330            }
331        }
332
333        self.claims = self_claims.into_iter().collect();
334        for (_, claimlist_refcell) in claims_by_source.iter() {
335            let mut claimlist = claimlist_refcell.borrow_mut();
336            for tagged_claim in claimlist.iter_mut() {
337                self.claims.extend(
338                    InflatedResource::process_claims_recursive(tagged_claim, &claims_by_source)
339                        .into_iter(),
340                );
341            }
342        }
343    }
344
345    /// Recursively look at claims to find the ones that are not reassigned.
346    fn process_claims_recursive(
347        tagged_claim: &mut TaggedClaim,
348        claims: &FxHashMap<GlobalPrincipalIdentifier, RefCell<Vec<TaggedClaim>>>,
349    ) -> Vec<Claim> {
350        let claim = match tagged_claim.1 {
351            true => {
352                // We have visited this claim already, we can skip.
353                return vec![];
354            }
355            false => {
356                // We tag visited claims, so we don't visit them again.
357                tagged_claim.1 = true;
358                tagged_claim.0
359            }
360        };
361        let subject = &claim.subject;
362        // We find if this claim has been reassigned.
363        let mut subject_claims = match claims.get(subject) {
364            Some(value_ref) => {
365                // [subject_claims] mutable borrow is held when recursing below, and
366                // [RefCell::try_borrow_mut] returns an error if called when a mutable borrow is
367                // already held. This ensures an error will be thrown at runtime if there is a
368                // cycle.
369                value_ref.try_borrow_mut().expect("Claims form a cycle, this is not supported")
370            }
371            None => {
372                // The claim is not reassigned, we keep the claim.
373                return vec![claim];
374            }
375        };
376        let mut leaves = vec![];
377        for subject_claim in subject_claims.iter_mut() {
378            leaves.append(&mut InflatedResource::process_claims_recursive(subject_claim, claims));
379        }
380        leaves
381    }
382}
383
384#[derive(Clone, Serialize)]
385/// Holds the list of resources attributed to a Principal (subject) by another Principal (source).
386pub struct Attribution {
387    /// Principal making the attribution claim.
388    pub source: GlobalPrincipalIdentifier,
389    /// Principal to which the resources are attributed.
390    pub subject: GlobalPrincipalIdentifier,
391    /// List of resources attributed to `subject` by `source`.
392    pub resources: Vec<ResourceReference>,
393}
394
395impl From<fplugin::Attribution> for Attribution {
396    fn from(value: fplugin::Attribution) -> Attribution {
397        Attribution {
398            source: value.source.unwrap().into(),
399            subject: value.subject.unwrap().into(),
400            resources: value.resources.unwrap().into_iter().map(|r| r.into()).collect(),
401        }
402    }
403}
404
405impl From<Attribution> for fplugin::Attribution {
406    fn from(value: Attribution) -> fplugin::Attribution {
407        fplugin::Attribution {
408            source: Some(value.source.into()),
409            subject: Some(value.subject.into()),
410            resources: Some(value.resources.into_iter().map(|r| r.into()).collect()),
411            ..Default::default()
412        }
413    }
414}
415
416#[derive(Clone, Copy, Serialize)]
417/// References a kernel [`Resource`], or some subset of a [`Resource`] (such as a part of a process
418/// address space).
419pub enum ResourceReference {
420    /// Identifies a kernel object whose memory is being attributed.
421    ///
422    /// Refers to all memory held by VMOs reachable from the object
423    /// (currently a Job, Process or VMO).
424    KernelObject(u64),
425
426    /// Identifies a part of a process address space.
427    ProcessMapped {
428        /// The KOID of the process that this VMAR lives in.
429        process: u64,
430
431        /// Base address of the VMAR.
432        base: u64,
433
434        /// Length of the VMAR.
435        len: u64,
436
437        /// True, when enumerating the handle table on this process is superfluous.
438        /// This is a hint that can be ignored by the client without changing the result.
439        hint_skip_handle_table: bool,
440    },
441}
442
443impl From<fplugin::ResourceReference> for ResourceReference {
444    fn from(value: fplugin::ResourceReference) -> ResourceReference {
445        match value {
446            fplugin::ResourceReference::KernelObject(ko) => ResourceReference::KernelObject(ko),
447            fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
448                process,
449                base,
450                len,
451                hint_skip_handle_table,
452            }) => ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table },
453            _ => unimplemented!(),
454        }
455    }
456}
457
458impl From<ResourceReference> for fplugin::ResourceReference {
459    fn from(value: ResourceReference) -> fplugin::ResourceReference {
460        match value {
461            ResourceReference::KernelObject(ko) => fplugin::ResourceReference::KernelObject(ko),
462            ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table } => {
463                fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
464                    process,
465                    base,
466                    len,
467                    hint_skip_handle_table,
468                })
469            }
470        }
471    }
472}
473
474/// Capture of the current memory usage of a device, as retrieved through the memory attribution
475/// protocol. In this object, memory attribution is not resolved.
476pub struct AttributionData {
477    pub principals_vec: Vec<Principal>,
478    pub resources_vec: Vec<Resource>,
479    pub resource_names: Vec<ZXName>,
480    pub attributions: Vec<Attribution>,
481}
482
483pub trait AttributionDataProvider: Send + Sync {
484    /// Returns all the memory resources and attribution specifications.
485    fn get_attribution_data(&self) -> Result<AttributionData, anyhow::Error>;
486}
487
488/// Processed snapshot of the memory usage of a device, with attribution of memory resources to
489/// Principals resolved.
490pub struct ProcessedAttributionData {
491    pub principals: FxHashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
492    pub resources: FxHashMap<u64, InflatedResource>,
493    pub resource_names: Vec<ZXName>,
494}
495
496impl ProcessedAttributionData {
497    fn new(
498        principals: FxHashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
499        resources: FxHashMap<u64, InflatedResource>,
500        resource_names: Vec<ZXName>,
501    ) -> Self {
502        Self { principals, resources, resource_names }
503    }
504
505    /// Create a summary view of the memory attribution_data. See [MemorySummary] for details.
506    pub fn summary(&self) -> MemorySummary {
507        #[cfg(target_os = "fuchsia")]
508        duration!(CATEGORY_MEMORY_CAPTURE, c"ProcessedAttributionData::summary");
509        MemorySummary::build(&self.principals, &self.resources, &self.resource_names)
510    }
511}
512
513/// Process data from a [AttributionData] to resolve attribution claims.
514pub fn attribute_vmos(attribution_data: AttributionData) -> ProcessedAttributionData {
515    #[cfg(target_os = "fuchsia")]
516    duration!(CATEGORY_MEMORY_CAPTURE, c"attribute_vmos");
517
518    // We use a vector + map of indexes instead of a single map to avoid using RefCells to allow
519    // modification while iterating. This improves performance.
520
521    // Vector of principals with dense integer indexing.
522    let mut principals: Vec<InflatedPrincipal> =
523        attribution_data.principals_vec.into_iter().map(InflatedPrincipal::new).collect();
524
525    let principal_by_id: FxHashMap<GlobalPrincipalIdentifier, usize> =
526        principals.iter().enumerate().map(|(idx, p)| (p.principal.identifier, idx)).collect();
527
528    // Vector of resources with dense integer indexing.
529    let mut resources: Vec<InflatedResource> =
530        attribution_data.resources_vec.into_iter().map(InflatedResource::new).collect();
531
532    let resource_by_koid: FxHashMap<u64, usize> =
533        resources.iter().enumerate().map(|(idx, r)| (r.resource.koid, idx)).collect();
534
535    // Cache used to avoid repeated heap allocations.
536    let mut matched_vmo_indices: Vec<usize> = Vec::new();
537
538    // Add direct claims to resources.
539    for attribution in attribution_data.attributions {
540        for resource in attribution.resources {
541            match resource {
542                ResourceReference::KernelObject(koid) => {
543                    if let Some(&r_idx) = resource_by_koid.get(&koid) {
544                        resources[r_idx].claims.insert(Claim {
545                            source: attribution.source,
546                            subject: attribution.subject,
547                            claim_type: ClaimType::Direct,
548                        });
549                    }
550                }
551                ResourceReference::ProcessMapped {
552                    process,
553                    base,
554                    len,
555                    hint_skip_handle_table: _,
556                } => {
557                    if let Some(&principal_idx) = principal_by_id.get(&attribution.subject) {
558                        principals[principal_idx].mapped_processes.push(process);
559                    }
560                    let Some(&process_idx) = resource_by_koid.get(&process) else {
561                        continue;
562                    };
563                    matched_vmo_indices.clear();
564                    if let fplugin::ResourceType::Process(process_data) =
565                        &resources[process_idx].resource.resource_type
566                    {
567                        if let Some(mappings) = &process_data.mappings {
568                            // Process mappings in Zircon are sorted in ascending order by
569                            // address_base. We use partition_point to binary search for the first
570                            // mapping in O(log M).
571                            let start_idx =
572                                mappings.partition_point(|m| m.address_base.unwrap_or(0) < base);
573                            let end_bound = base.saturating_add(len);
574                            for mapping in &mappings[start_idx..] {
575                                let Some(mapping_base) = mapping.address_base else {
576                                    continue;
577                                };
578                                // A mapping has a non-zero size, so we know it is out of bounds.
579                                if mapping_base >= end_bound {
580                                    break;
581                                }
582                                let Some(mapping_size) = mapping.size else {
583                                    continue;
584                                };
585                                // We consider an entire VMO to be matched if it has a mapping
586                                // within the claimed region.
587                                if mapping_base.saturating_add(mapping_size) <= end_bound {
588                                    if let Some(vmo_koid) = mapping.vmo {
589                                        if let Some(&vmo_idx) = resource_by_koid.get(&vmo_koid) {
590                                            matched_vmo_indices.push(vmo_idx);
591                                        }
592                                    }
593                                }
594                            }
595                        }
596                    }
597                    for &vmo_idx in &matched_vmo_indices {
598                        resources[vmo_idx].claims.insert(Claim {
599                            source: attribution.source,
600                            subject: attribution.subject,
601                            claim_type: ClaimType::Direct,
602                        });
603                    }
604                }
605            }
606        }
607    }
608
609    // Propagate claims. We propagate direct claims to child resources recursively until we hit a
610    // resource that is directly claimed: this is because we consider that attributors deeper in the
611    // principal hierarchy will not attribute resources higher in the resource hierarchy than the
612    // ones attributed by their ancestors (ie. attribution is always more precise as we go deeper).
613    for res_idx in 0..resources.len() {
614        if !resources[res_idx].claims.iter().any(|c| c.claim_type == ClaimType::Direct) {
615            // There is no direct claim to propagate, we can skip this resource.
616            continue;
617        }
618
619        let propagated_claims: Vec<Claim> = resources[res_idx]
620            .claims
621            .iter()
622            .filter(|c| c.claim_type == ClaimType::Direct)
623            .map(|claim| Claim {
624                source: claim.source,
625                subject: claim.subject,
626                claim_type: ClaimType::Indirect,
627            })
628            .collect();
629        let mut frontier = Vec::new();
630        frontier.extend(resources[res_idx].children());
631        while let Some(child) = frontier.pop() {
632            let Some(&child_idx) = resource_by_koid.get(&child) else {
633                // This can happen if a resource is created or disappears while we were
634                // collecting information about all the resources in the system. This should
635                // remain a rare event.
636                continue;
637            };
638            if resources[child_idx].claims.iter().any(|c| c.claim_type == ClaimType::Direct) {
639                // If there is a direct claim on the resource, don't propagate.
640                continue;
641            }
642            resources[child_idx].claims.extend(propagated_claims.iter().cloned());
643            frontier.extend(resources[child_idx].children());
644        }
645    }
646
647    for resource in &mut resources {
648        resource.process_claims();
649    }
650
651    // Reusable buffers to avoid heap allocations per VMO.
652    let mut ancestors_buf: Vec<u64> = Vec::with_capacity(8);
653    let mut child_claims_buf: Vec<Claim> = Vec::new();
654
655    // Push claimed resources to principals. We are interested in VMOs as the VMOs are the resources
656    // actually holding memory. We also keep track of the process to display its name in the output.
657    for res_idx in 0..resources.len() {
658        let resource_koid = resources[res_idx].resource.koid;
659        if let fplugin::ResourceType::Vmo(vmo) = &resources[res_idx].resource.resource_type {
660            ancestors_buf.clear();
661            ancestors_buf.push(resource_koid);
662
663            // VMOs created by reference don't behave like COW-clones; their byte count is always
664            // zero, and all pages are attributed to their parent. This is not what we want here,
665            // as we prefer to acknowledge that the pages are shared between the parent and its
666            // children. We currently don't have a way to formally distinguish between VMOs created
667            // by reference and the other VMOs, so we use the heuristic of checking that they
668            // report they are always empty.
669            if vmo.total_populated_bytes.unwrap_or_default() == 0 {
670                let mut current_parent = vmo.parent;
671                child_claims_buf.clear();
672                for c in &resources[res_idx].claims {
673                    child_claims_buf.push(Claim {
674                        subject: c.subject,
675                        source: c.source,
676                        claim_type: ClaimType::Child,
677                    });
678                }
679                // Add the parents of a VMO as "Child" claims. This is done so that slices of VMOs,
680                // with possibly no memory of their own, get attributed the resources of their
681                // parent.
682                while let Some(parent_koid) = current_parent {
683                    if parent_koid == 0 {
684                        panic!("Parent is not None but 0.");
685                    }
686                    // Some VMOs' ancestry is self referential: their parent's koid is set to their
687                    // own koid. We want to detect this case and break early to avoid getting
688                    // stuck in an infinite loop.
689                    if parent_koid == resource_koid {
690                        break;
691                    }
692                    ancestors_buf.push(parent_koid);
693                    let Some(&parent_idx) = resource_by_koid.get(&parent_koid) else {
694                        break;
695                    };
696                    resources[parent_idx].claims.extend(child_claims_buf.iter().cloned());
697                    current_parent = match &resources[parent_idx].resource.resource_type {
698                        fplugin::ResourceType::Job(_) => panic!("This should not happen"),
699                        fplugin::ResourceType::Process(_) => panic!("This should not happen"),
700                        fplugin::ResourceType::Vmo(current_vmo) => current_vmo.parent,
701                        _ => unimplemented!(),
702                    };
703                }
704            }
705
706            for claim in &resources[res_idx].claims {
707                if let Some(&p_idx) = principal_by_id.get(&claim.subject) {
708                    principals[p_idx].resources.extend_from_slice(&ancestors_buf);
709                }
710            }
711        } else if let fplugin::ResourceType::Process(_) = &resources[res_idx].resource.resource_type
712        {
713            for claim in &resources[res_idx].claims {
714                if let Some(&p_idx) = principal_by_id.get(&claim.subject) {
715                    principals[p_idx].resources.push(resource_koid);
716                }
717            }
718        }
719    }
720
721    // Sort, deduplicate, and populate principal resource sets once per principal.
722    for p in &mut principals {
723        if !p.resources.is_empty() {
724            p.resources.sort_unstable();
725            p.resources.dedup();
726        }
727    }
728
729    let principals_map = principals.into_iter().map(|p| (p.principal.identifier, p)).collect();
730    let resources_map = resources.into_iter().map(|r| (r.resource.koid, r)).collect();
731
732    ProcessedAttributionData::new(principals_map, resources_map, attribution_data.resource_names)
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use std::collections::HashMap;
739    use summary::{PrincipalSummary, VmoSummary};
740
741    #[test]
742    fn test_gather_resources() {
743        // Create a fake snapshot with 4 principals:
744        // root (1)
745        //  - runner (2)
746        //    - component 4 (4)
747        //  - component 3 (3)
748        //
749        // and the following job/process/vmo hierarchy:
750        // root_job (1000)
751        //  * root_process (1001)
752        //    . root_vmo (1002)
753        //    . shared_vmo (1003)
754        //  - runner_job (1004)
755        //    * runner_process (1005)
756        //      . runner_vmo (1006)
757        //      . component_vmo (1007)
758        //      . component_vmo2 (1012)
759        //      . component_vmo3 (1013)
760        //  - component_2_job (1008)
761        //    * 2_process (1009)
762        //      . 2_vmo (1010)
763        //      . shared_vmo (1003)
764        // And an additional parent VMO for 2_vmo, 2_vmo_parent (1011).
765
766        let resource_names = vec![
767            ZXName::from_string_lossy("root_job"),
768            ZXName::from_string_lossy("root_process"),
769            ZXName::from_string_lossy("root_vmo"),
770            ZXName::from_string_lossy("shared_vmo"),
771            ZXName::from_string_lossy("runner_job"),
772            ZXName::from_string_lossy("runner_process"),
773            ZXName::from_string_lossy("runner_vmo"),
774            ZXName::from_string_lossy("component_vmo"),
775            ZXName::from_string_lossy("component_2_job"),
776            ZXName::from_string_lossy("2_process"),
777            ZXName::from_string_lossy("2_vmo"),
778            ZXName::from_string_lossy("2_vmo_parent"),
779            ZXName::from_string_lossy("component_vmo_mapped"),
780            ZXName::from_string_lossy("component_vmo_mapped2"),
781        ];
782
783        let attributions = vec![
784            fplugin::Attribution {
785                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
786                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
787                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
788                ..Default::default()
789            },
790            fplugin::Attribution {
791                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
792                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
793                resources: Some(vec![fplugin::ResourceReference::KernelObject(1004)]),
794                ..Default::default()
795            },
796            fplugin::Attribution {
797                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
798                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
799                resources: Some(vec![fplugin::ResourceReference::KernelObject(1008)]),
800                ..Default::default()
801            },
802            fplugin::Attribution {
803                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
804                subject: Some(fplugin::PrincipalIdentifier { id: 4 }),
805                resources: Some(vec![
806                    fplugin::ResourceReference::KernelObject(1007),
807                    fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
808                        process: 1005,
809                        base: 1024,
810                        len: 1024,
811                        hint_skip_handle_table: false,
812                    }),
813                ]),
814                ..Default::default()
815            },
816        ]
817        .into_iter()
818        .map(|a| a.into())
819        .collect();
820
821        let principals = vec![
822            fplugin::Principal {
823                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
824                description: Some(fplugin::Description::Component("component_manager".to_owned())),
825                principal_type: Some(fplugin::PrincipalType::Runnable),
826                parent: None,
827                ..Default::default()
828            },
829            fplugin::Principal {
830                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
831                description: Some(fplugin::Description::Component("runner".to_owned())),
832                principal_type: Some(fplugin::PrincipalType::Runnable),
833                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
834                ..Default::default()
835            },
836            fplugin::Principal {
837                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
838                description: Some(fplugin::Description::Component("component 3".to_owned())),
839                principal_type: Some(fplugin::PrincipalType::Runnable),
840                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
841                ..Default::default()
842            },
843            fplugin::Principal {
844                identifier: Some(fplugin::PrincipalIdentifier { id: 4 }),
845                description: Some(fplugin::Description::Component("component 4".to_owned())),
846                principal_type: Some(fplugin::PrincipalType::Runnable),
847                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
848                ..Default::default()
849            },
850        ]
851        .into_iter()
852        .map(|p| p.into())
853        .collect();
854
855        let resources = vec![
856            fplugin::Resource {
857                koid: Some(1000),
858                name_index: Some(0),
859                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
860                    child_jobs: Some(vec![1004, 1008]),
861                    processes: Some(vec![1001]),
862                    ..Default::default()
863                })),
864                ..Default::default()
865            },
866            fplugin::Resource {
867                koid: Some(1001),
868                name_index: Some(1),
869                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
870                    vmos: Some(vec![1002, 1003]),
871                    mappings: None,
872                    ..Default::default()
873                })),
874                ..Default::default()
875            },
876            fplugin::Resource {
877                koid: Some(1002),
878                name_index: Some(2),
879                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
880                    private_committed_bytes: Some(1024),
881                    private_populated_bytes: Some(2048),
882                    scaled_committed_bytes: Some(1024),
883                    scaled_populated_bytes: Some(2048),
884                    total_committed_bytes: Some(1024),
885                    total_populated_bytes: Some(2048),
886                    ..Default::default()
887                })),
888                ..Default::default()
889            },
890            fplugin::Resource {
891                koid: Some(1003),
892                name_index: Some(3),
893                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
894                    private_committed_bytes: Some(1024),
895                    private_populated_bytes: Some(2048),
896                    scaled_committed_bytes: Some(1024),
897                    scaled_populated_bytes: Some(2048),
898                    total_committed_bytes: Some(1024),
899                    total_populated_bytes: Some(2048),
900                    ..Default::default()
901                })),
902                ..Default::default()
903            },
904            fplugin::Resource {
905                koid: Some(1004),
906                name_index: Some(4),
907                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
908                    child_jobs: Some(vec![]),
909                    processes: Some(vec![1005]),
910                    ..Default::default()
911                })),
912                ..Default::default()
913            },
914            fplugin::Resource {
915                koid: Some(1005),
916                name_index: Some(5),
917                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
918                    vmos: Some(vec![1006, 1007, 1012]),
919                    mappings: Some(vec![
920                        fplugin::Mapping {
921                            vmo: Some(1006),
922                            address_base: Some(0),
923                            size: Some(512),
924                            ..Default::default()
925                        },
926                        fplugin::Mapping {
927                            vmo: Some(1012),
928                            address_base: Some(1024),
929                            size: Some(512),
930                            ..Default::default()
931                        },
932                        fplugin::Mapping {
933                            vmo: Some(1013),
934                            address_base: Some(1536),
935                            size: Some(512),
936                            ..Default::default()
937                        },
938                        fplugin::Mapping {
939                            vmo: Some(1006),
940                            address_base: Some(2048),
941                            size: Some(512),
942                            ..Default::default()
943                        },
944                    ]),
945                    ..Default::default()
946                })),
947                ..Default::default()
948            },
949            fplugin::Resource {
950                koid: Some(1006),
951                name_index: Some(6),
952                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
953                    private_committed_bytes: Some(1024),
954                    private_populated_bytes: Some(2048),
955                    scaled_committed_bytes: Some(1024),
956                    scaled_populated_bytes: Some(2048),
957                    total_committed_bytes: Some(1024),
958                    total_populated_bytes: Some(2048),
959                    ..Default::default()
960                })),
961                ..Default::default()
962            },
963            fplugin::Resource {
964                koid: Some(1007),
965                name_index: Some(7),
966                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
967                    private_committed_bytes: Some(128),
968                    private_populated_bytes: Some(256),
969                    scaled_committed_bytes: Some(128),
970                    scaled_populated_bytes: Some(256),
971                    total_committed_bytes: Some(128),
972                    total_populated_bytes: Some(256),
973                    ..Default::default()
974                })),
975                ..Default::default()
976            },
977            fplugin::Resource {
978                koid: Some(1008),
979                name_index: Some(8),
980                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
981                    child_jobs: Some(vec![]),
982                    processes: Some(vec![1009]),
983                    ..Default::default()
984                })),
985                ..Default::default()
986            },
987            fplugin::Resource {
988                koid: Some(1009),
989                name_index: Some(9),
990                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
991                    vmos: Some(vec![1010, 1003]),
992                    mappings: None,
993                    ..Default::default()
994                })),
995                ..Default::default()
996            },
997            fplugin::Resource {
998                koid: Some(1010),
999                name_index: Some(10),
1000                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1001                    parent: Some(1011),
1002                    private_committed_bytes: Some(1024),
1003                    private_populated_bytes: Some(2048),
1004                    scaled_committed_bytes: Some(1024),
1005                    scaled_populated_bytes: Some(2048),
1006                    total_committed_bytes: Some(1024),
1007                    total_populated_bytes: Some(2048),
1008                    ..Default::default()
1009                })),
1010                ..Default::default()
1011            },
1012            fplugin::Resource {
1013                koid: Some(1011),
1014                name_index: Some(11),
1015                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1016                    private_committed_bytes: Some(1024),
1017                    private_populated_bytes: Some(2048),
1018                    scaled_committed_bytes: Some(1024),
1019                    scaled_populated_bytes: Some(2048),
1020                    total_committed_bytes: Some(1024),
1021                    total_populated_bytes: Some(2048),
1022                    ..Default::default()
1023                })),
1024                ..Default::default()
1025            },
1026            fplugin::Resource {
1027                koid: Some(1012),
1028                name_index: Some(12),
1029                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1030                    private_committed_bytes: Some(1024),
1031                    private_populated_bytes: Some(2048),
1032                    scaled_committed_bytes: Some(1024),
1033                    scaled_populated_bytes: Some(2048),
1034                    total_committed_bytes: Some(1024),
1035                    total_populated_bytes: Some(2048),
1036                    ..Default::default()
1037                })),
1038                ..Default::default()
1039            },
1040            fplugin::Resource {
1041                koid: Some(1013),
1042                name_index: Some(13),
1043                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1044                    private_committed_bytes: Some(1024),
1045                    private_populated_bytes: Some(2048),
1046                    scaled_committed_bytes: Some(1024),
1047                    scaled_populated_bytes: Some(2048),
1048                    total_committed_bytes: Some(1024),
1049                    total_populated_bytes: Some(2048),
1050                    ..Default::default()
1051                })),
1052                ..Default::default()
1053            },
1054        ]
1055        .into_iter()
1056        .map(|r| r.into())
1057        .collect();
1058
1059        let output = attribute_vmos(AttributionData {
1060            principals_vec: principals,
1061            resources_vec: resources,
1062            resource_names,
1063            attributions,
1064        })
1065        .summary();
1066
1067        assert_eq!(output.unclaimed, 2048);
1068        assert_eq!(output.principals.len(), 4);
1069
1070        let principals: HashMap<u64, PrincipalSummary> =
1071            output.principals.into_iter().map(|p| (p.id, p)).collect();
1072
1073        assert_eq!(
1074            principals.get(&1).unwrap(),
1075            &PrincipalSummary {
1076                id: 1,
1077                name: "component_manager".to_owned(),
1078                principal_type: "R".to_owned(),
1079                committed_private: 1024,
1080                committed_scaled: 1536.0,
1081                committed_total: 2048,
1082                populated_private: 2048,
1083                populated_scaled: 3072.0,
1084                populated_total: 4096,
1085                attributor: None,
1086                processes: vec!["root_process (1001)".to_owned()],
1087                vmos: vec![
1088                    (
1089                        ZXName::from_string_lossy("root_vmo"),
1090                        VmoSummary {
1091                            count: 1,
1092                            committed_private: 1024,
1093                            committed_scaled: 1024.0,
1094                            committed_total: 1024,
1095                            populated_private: 2048,
1096                            populated_scaled: 2048.0,
1097                            populated_total: 2048,
1098                            ..Default::default()
1099                        }
1100                    ),
1101                    (
1102                        ZXName::from_string_lossy("shared_vmo"),
1103                        VmoSummary {
1104                            count: 1,
1105                            committed_private: 0,
1106                            committed_scaled: 512.0,
1107                            committed_total: 1024,
1108                            populated_private: 0,
1109                            populated_scaled: 1024.0,
1110                            populated_total: 2048,
1111                            ..Default::default()
1112                        }
1113                    )
1114                ]
1115                .into_iter()
1116                .collect(),
1117            }
1118        );
1119
1120        assert_eq!(
1121            principals.get(&2).unwrap(),
1122            &PrincipalSummary {
1123                id: 2,
1124                name: "runner".to_owned(),
1125                principal_type: "R".to_owned(),
1126                committed_private: 1024,
1127                committed_scaled: 1024.0,
1128                committed_total: 1024,
1129                populated_private: 2048,
1130                populated_scaled: 2048.0,
1131                populated_total: 2048,
1132                attributor: Some("component_manager".to_owned()),
1133                processes: vec!["runner_process (1005)".to_owned()],
1134                vmos: vec![(
1135                    ZXName::from_string_lossy("runner_vmo"),
1136                    VmoSummary {
1137                        count: 1,
1138                        committed_private: 1024,
1139                        committed_scaled: 1024.0,
1140                        committed_total: 1024,
1141                        populated_private: 2048,
1142                        populated_scaled: 2048.0,
1143                        populated_total: 2048,
1144                        ..Default::default()
1145                    }
1146                )]
1147                .into_iter()
1148                .collect(),
1149            }
1150        );
1151
1152        assert_eq!(
1153            principals.get(&3).unwrap(),
1154            &PrincipalSummary {
1155                id: 3,
1156                name: "component 3".to_owned(),
1157                principal_type: "R".to_owned(),
1158                committed_private: 1024,
1159                committed_scaled: 1536.0,
1160                committed_total: 2048,
1161                populated_private: 2048,
1162                populated_scaled: 3072.0,
1163                populated_total: 4096,
1164                attributor: Some("component_manager".to_owned()),
1165                processes: vec!["2_process (1009)".to_owned()],
1166                vmos: vec![
1167                    (
1168                        ZXName::from_string_lossy("shared_vmo"),
1169                        VmoSummary {
1170                            count: 1,
1171                            committed_private: 0,
1172                            committed_scaled: 512.0,
1173                            committed_total: 1024,
1174                            populated_private: 0,
1175                            populated_scaled: 1024.0,
1176                            populated_total: 2048,
1177                            ..Default::default()
1178                        }
1179                    ),
1180                    (
1181                        ZXName::from_string_lossy("2_vmo"),
1182                        VmoSummary {
1183                            count: 1,
1184                            committed_private: 1024,
1185                            committed_scaled: 1024.0,
1186                            committed_total: 1024,
1187                            populated_private: 2048,
1188                            populated_scaled: 2048.0,
1189                            populated_total: 2048,
1190                            ..Default::default()
1191                        }
1192                    )
1193                ]
1194                .into_iter()
1195                .collect(),
1196            }
1197        );
1198
1199        assert_eq!(
1200            principals.get(&4).unwrap(),
1201            &PrincipalSummary {
1202                id: 4,
1203                name: "component 4".to_owned(),
1204                principal_type: "R".to_owned(),
1205                committed_private: 2176,
1206                committed_scaled: 2176.0,
1207                committed_total: 2176,
1208                populated_private: 4352,
1209                populated_scaled: 4352.0,
1210                populated_total: 4352,
1211                attributor: Some("runner".to_owned()),
1212                processes: vec!["runner_process (1005)".to_owned()],
1213                vmos: vec![
1214                    (
1215                        ZXName::from_string_lossy("component_vmo"),
1216                        VmoSummary {
1217                            count: 1,
1218                            committed_private: 128,
1219                            committed_scaled: 128.0,
1220                            committed_total: 128,
1221                            populated_private: 256,
1222                            populated_scaled: 256.0,
1223                            populated_total: 256,
1224                            ..Default::default()
1225                        }
1226                    ),
1227                    (
1228                        ZXName::from_string_lossy("component_vmo_mapped"),
1229                        VmoSummary {
1230                            count: 1,
1231                            committed_private: 1024,
1232                            committed_scaled: 1024.0,
1233                            committed_total: 1024,
1234                            populated_private: 2048,
1235                            populated_scaled: 2048.0,
1236                            populated_total: 2048,
1237                            ..Default::default()
1238                        }
1239                    ),
1240                    (
1241                        ZXName::from_string_lossy("component_vmo_mapped2"),
1242                        VmoSummary {
1243                            count: 1,
1244                            committed_private: 1024,
1245                            committed_scaled: 1024.0,
1246                            committed_total: 1024,
1247                            populated_private: 2048,
1248                            populated_scaled: 2048.0,
1249                            populated_total: 2048,
1250                            ..Default::default()
1251                        }
1252                    )
1253                ]
1254                .into_iter()
1255                .collect(),
1256            }
1257        );
1258    }
1259
1260    #[test]
1261    fn test_reshare_resources() {
1262        // Create a fake snapshot with 3 principals:
1263        // root (id: 1)
1264        //  - component 2 (id: 1)
1265        //    - component 3 (id: 2)
1266        //
1267        // and the following job/process/vmo hierarchy:
1268        // root_job (1000)
1269        //  - component_job (1001)
1270        //    * component_process (1002)
1271        //      . component_vmo (1003)
1272        //
1273        // In this scenario, component 2 reattributes component_job to component 3 entirely.
1274
1275        let resource_names = vec![
1276            ZXName::from_string_lossy("root_job"),
1277            ZXName::from_string_lossy("component_job"),
1278            ZXName::from_string_lossy("component_process"),
1279            ZXName::from_string_lossy("component_vmo"),
1280        ];
1281        let attributions = vec![
1282            fplugin::Attribution {
1283                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1284                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1285                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1286                ..Default::default()
1287            },
1288            fplugin::Attribution {
1289                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1290                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1291                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1292                ..Default::default()
1293            },
1294            fplugin::Attribution {
1295                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
1296                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
1297                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1298                ..Default::default()
1299            },
1300        ]
1301        .into_iter()
1302        .map(|a| a.into())
1303        .collect();
1304        let principals = vec![
1305            fplugin::Principal {
1306                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1307                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1308                principal_type: Some(fplugin::PrincipalType::Runnable),
1309                parent: None,
1310                ..Default::default()
1311            },
1312            fplugin::Principal {
1313                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1314                description: Some(fplugin::Description::Component("component 2".to_owned())),
1315                principal_type: Some(fplugin::PrincipalType::Runnable),
1316                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1317                ..Default::default()
1318            },
1319            fplugin::Principal {
1320                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
1321                description: Some(fplugin::Description::Component("component 3".to_owned())),
1322                principal_type: Some(fplugin::PrincipalType::Runnable),
1323                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
1324                ..Default::default()
1325            },
1326        ]
1327        .into_iter()
1328        .map(|p| p.into())
1329        .collect();
1330
1331        let resources = vec![
1332            fplugin::Resource {
1333                koid: Some(1000),
1334                name_index: Some(0),
1335                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1336                    child_jobs: Some(vec![1001]),
1337                    processes: Some(vec![]),
1338                    ..Default::default()
1339                })),
1340                ..Default::default()
1341            },
1342            fplugin::Resource {
1343                koid: Some(1001),
1344                name_index: Some(1),
1345                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1346                    child_jobs: Some(vec![]),
1347                    processes: Some(vec![1002]),
1348                    ..Default::default()
1349                })),
1350                ..Default::default()
1351            },
1352            fplugin::Resource {
1353                koid: Some(1002),
1354                name_index: Some(2),
1355                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1356                    vmos: Some(vec![1003]),
1357                    mappings: None,
1358                    ..Default::default()
1359                })),
1360                ..Default::default()
1361            },
1362            fplugin::Resource {
1363                koid: Some(1003),
1364                name_index: Some(3),
1365                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1366                    private_committed_bytes: Some(1024),
1367                    private_populated_bytes: Some(2048),
1368                    scaled_committed_bytes: Some(1024),
1369                    scaled_populated_bytes: Some(2048),
1370                    total_committed_bytes: Some(1024),
1371                    total_populated_bytes: Some(2048),
1372                    ..Default::default()
1373                })),
1374                ..Default::default()
1375            },
1376        ]
1377        .into_iter()
1378        .map(|r| r.into())
1379        .collect();
1380
1381        let output = attribute_vmos(AttributionData {
1382            principals_vec: principals,
1383            resources_vec: resources,
1384            resource_names,
1385            attributions,
1386        })
1387        .summary();
1388
1389        assert_eq!(output.unclaimed, 0);
1390        assert_eq!(output.principals.len(), 3);
1391
1392        let principals: HashMap<u64, PrincipalSummary> =
1393            output.principals.into_iter().map(|p| (p.id, p)).collect();
1394
1395        assert_eq!(
1396            principals.get(&1).unwrap(),
1397            &PrincipalSummary {
1398                id: 1,
1399                name: "component_manager".to_owned(),
1400                principal_type: "R".to_owned(),
1401                committed_private: 0,
1402                committed_scaled: 0.0,
1403                committed_total: 0,
1404                populated_private: 0,
1405                populated_scaled: 0.0,
1406                populated_total: 0,
1407                attributor: None,
1408                processes: vec![],
1409                vmos: vec![].into_iter().collect(),
1410            }
1411        );
1412
1413        assert_eq!(
1414            principals.get(&2).unwrap(),
1415            &PrincipalSummary {
1416                id: 2,
1417                name: "component 2".to_owned(),
1418                principal_type: "R".to_owned(),
1419                committed_private: 0,
1420                committed_scaled: 0.0,
1421                committed_total: 0,
1422                populated_private: 0,
1423                populated_scaled: 0.0,
1424                populated_total: 0,
1425                attributor: Some("component_manager".to_owned()),
1426                processes: vec![],
1427                vmos: vec![].into_iter().collect(),
1428            }
1429        );
1430
1431        assert_eq!(
1432            principals.get(&3).unwrap(),
1433            &PrincipalSummary {
1434                id: 3,
1435                name: "component 3".to_owned(),
1436                principal_type: "R".to_owned(),
1437                committed_private: 1024,
1438                committed_scaled: 1024.0,
1439                committed_total: 1024,
1440                populated_private: 2048,
1441                populated_scaled: 2048.0,
1442                populated_total: 2048,
1443                attributor: Some("component 2".to_owned()),
1444                processes: vec!["component_process (1002)".to_owned()],
1445                vmos: vec![(
1446                    ZXName::from_string_lossy("component_vmo"),
1447                    VmoSummary {
1448                        count: 1,
1449                        committed_private: 1024,
1450                        committed_scaled: 1024.0,
1451                        committed_total: 1024,
1452                        populated_private: 2048,
1453                        populated_scaled: 2048.0,
1454                        populated_total: 2048,
1455                        ..Default::default()
1456                    }
1457                ),]
1458                .into_iter()
1459                .collect(),
1460            }
1461        );
1462    }
1463
1464    #[test]
1465    fn test_conversions() {
1466        let plugin_principal = fplugin::Principal {
1467            identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1468            description: Some(fplugin::Description::Component("component_manager".to_owned())),
1469            principal_type: Some(fplugin::PrincipalType::Runnable),
1470            parent: None,
1471            ..Default::default()
1472        };
1473
1474        let data_principal: Principal = plugin_principal.clone().into();
1475
1476        assert_eq!(plugin_principal, data_principal.into());
1477
1478        let plugin_resources = vec![
1479            fplugin::Resource {
1480                koid: Some(1000),
1481                name_index: Some(0),
1482                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1483                    child_jobs: Some(vec![1004, 1008]),
1484                    processes: Some(vec![1001]),
1485                    ..Default::default()
1486                })),
1487                ..Default::default()
1488            },
1489            fplugin::Resource {
1490                koid: Some(1001),
1491                name_index: Some(1),
1492                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1493                    vmos: Some(vec![1002, 1003]),
1494                    mappings: None,
1495                    ..Default::default()
1496                })),
1497                ..Default::default()
1498            },
1499            fplugin::Resource {
1500                koid: Some(1002),
1501                name_index: Some(2),
1502                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1503                    private_committed_bytes: Some(1024),
1504                    private_populated_bytes: Some(2048),
1505                    scaled_committed_bytes: Some(1024),
1506                    scaled_populated_bytes: Some(2048),
1507                    total_committed_bytes: Some(1024),
1508                    total_populated_bytes: Some(2048),
1509                    ..Default::default()
1510                })),
1511                ..Default::default()
1512            },
1513            fplugin::Resource {
1514                koid: Some(1005),
1515                name_index: Some(5),
1516                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1517                    vmos: Some(vec![1006, 1007, 1012]),
1518                    mappings: Some(vec![
1519                        fplugin::Mapping {
1520                            vmo: Some(1006),
1521                            address_base: Some(0),
1522                            size: Some(512),
1523                            ..Default::default()
1524                        },
1525                        fplugin::Mapping {
1526                            vmo: Some(1012),
1527                            address_base: Some(1024),
1528                            size: Some(512),
1529                            ..Default::default()
1530                        },
1531                    ]),
1532                    ..Default::default()
1533                })),
1534                ..Default::default()
1535            },
1536        ];
1537
1538        let data_resources: Vec<Resource> =
1539            plugin_resources.iter().cloned().map(|r| r.into()).collect();
1540
1541        let actual_resources: Vec<fplugin::Resource> =
1542            data_resources.into_iter().map(|r| r.into()).collect();
1543
1544        assert_eq!(plugin_resources, actual_resources);
1545    }
1546
1547    #[test]
1548    fn test_vmo_reference() {
1549        // Create a fake snapshot with 3 principals:
1550        // root (1)
1551        //  - component 2 (2)
1552        //
1553        // and the following job/process/vmo hierarchy:
1554        // root_job (1000)
1555        //  - component_job (1001)
1556        //    * component_process (1002)
1557        //      . component_vmo (1003)
1558        // component_vmo_parent (1004)
1559        //
1560        // In this scenario, component_vmo is a reference to component_vmo_parent and should get
1561        // shared attribution of its pages.
1562
1563        let resource_names = vec![
1564            name::ZXName::from_string_lossy("root_job"),
1565            name::ZXName::from_string_lossy("component_job"),
1566            name::ZXName::from_string_lossy("component_process"),
1567            name::ZXName::from_string_lossy("component_vmo"),
1568            name::ZXName::from_string_lossy("component_vmo_parent"),
1569        ];
1570        let attributions = vec![
1571            fplugin::Attribution {
1572                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1573                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1574                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1575                ..Default::default()
1576            },
1577            fplugin::Attribution {
1578                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1579                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1580                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1581                ..Default::default()
1582            },
1583        ]
1584        .into_iter()
1585        .map(|a| a.into())
1586        .collect();
1587        let principals = vec![
1588            fplugin::Principal {
1589                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1590                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1591                principal_type: Some(fplugin::PrincipalType::Runnable),
1592                parent: None,
1593                ..Default::default()
1594            },
1595            fplugin::Principal {
1596                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1597                description: Some(fplugin::Description::Component("component 2".to_owned())),
1598                principal_type: Some(fplugin::PrincipalType::Runnable),
1599                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1600                ..Default::default()
1601            },
1602        ]
1603        .into_iter()
1604        .map(|p| p.into())
1605        .collect();
1606
1607        let resources = vec![
1608            fplugin::Resource {
1609                koid: Some(1000),
1610                name_index: Some(0),
1611                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1612                    child_jobs: Some(vec![1001]),
1613                    processes: Some(vec![]),
1614                    ..Default::default()
1615                })),
1616                ..Default::default()
1617            },
1618            fplugin::Resource {
1619                koid: Some(1001),
1620                name_index: Some(1),
1621                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1622                    child_jobs: Some(vec![]),
1623                    processes: Some(vec![1002]),
1624                    ..Default::default()
1625                })),
1626                ..Default::default()
1627            },
1628            fplugin::Resource {
1629                koid: Some(1002),
1630                name_index: Some(2),
1631                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1632                    vmos: Some(vec![1003]),
1633                    mappings: None,
1634                    ..Default::default()
1635                })),
1636                ..Default::default()
1637            },
1638            fplugin::Resource {
1639                koid: Some(1003),
1640                name_index: Some(3),
1641                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1642                    parent: Some(1004),
1643                    private_committed_bytes: Some(0),
1644                    private_populated_bytes: Some(0),
1645                    scaled_committed_bytes: Some(0),
1646                    scaled_populated_bytes: Some(0),
1647                    total_committed_bytes: Some(0),
1648                    total_populated_bytes: Some(0),
1649                    ..Default::default()
1650                })),
1651                ..Default::default()
1652            },
1653            fplugin::Resource {
1654                koid: Some(1004),
1655                name_index: Some(4),
1656                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1657                    private_committed_bytes: Some(1024),
1658                    private_populated_bytes: Some(2048),
1659                    scaled_committed_bytes: Some(1024),
1660                    scaled_populated_bytes: Some(2048),
1661                    total_committed_bytes: Some(1024),
1662                    total_populated_bytes: Some(2048),
1663                    ..Default::default()
1664                })),
1665                ..Default::default()
1666            },
1667        ]
1668        .into_iter()
1669        .map(|r| r.into())
1670        .collect();
1671
1672        let output = attribute_vmos(AttributionData {
1673            principals_vec: principals,
1674            resources_vec: resources,
1675            resource_names,
1676            attributions,
1677        })
1678        .summary();
1679
1680        assert_eq!(output.unclaimed, 0);
1681        assert_eq!(output.principals.len(), 2);
1682
1683        let principals: HashMap<u64, PrincipalSummary> =
1684            output.principals.into_iter().map(|p| (p.id, p)).collect();
1685
1686        assert_eq!(
1687            principals.get(&1).unwrap(),
1688            &PrincipalSummary {
1689                id: 1,
1690                name: "component_manager".to_owned(),
1691                principal_type: "R".to_owned(),
1692                committed_private: 0,
1693                committed_scaled: 0.0,
1694                committed_total: 0,
1695                populated_private: 0,
1696                populated_scaled: 0.0,
1697                populated_total: 0,
1698                attributor: None,
1699                processes: vec![],
1700                vmos: vec![].into_iter().collect(),
1701            }
1702        );
1703
1704        assert_eq!(
1705            principals.get(&2).unwrap(),
1706            &PrincipalSummary {
1707                id: 2,
1708                name: "component 2".to_owned(),
1709                principal_type: "R".to_owned(),
1710                committed_private: 1024,
1711                committed_scaled: 1024.0,
1712                committed_total: 1024,
1713                populated_private: 2048,
1714                populated_scaled: 2048.0,
1715                populated_total: 2048,
1716                attributor: Some("component_manager".to_owned()),
1717                processes: vec!["component_process (1002)".to_owned()],
1718                vmos: vec![
1719                    (
1720                        name::ZXName::from_string_lossy("component_vmo"),
1721                        VmoSummary {
1722                            count: 1,
1723                            committed_private: 0,
1724                            committed_scaled: 0.0,
1725                            committed_total: 0,
1726                            populated_private: 0,
1727                            populated_scaled: 0.0,
1728                            populated_total: 0,
1729                            ..Default::default()
1730                        }
1731                    ),
1732                    (
1733                        name::ZXName::from_string_lossy("component_vmo_parent"),
1734                        VmoSummary {
1735                            count: 1,
1736                            committed_private: 1024,
1737                            committed_scaled: 1024.0,
1738                            committed_total: 1024,
1739                            populated_private: 2048,
1740                            populated_scaled: 2048.0,
1741                            populated_total: 2048,
1742                            ..Default::default()
1743                        }
1744                    ),
1745                ]
1746                .into_iter()
1747                .collect(),
1748            }
1749        );
1750    }
1751
1752    fn make_principal_def(id: u64, name: &str) -> fplugin::Principal {
1753        fplugin::Principal {
1754            identifier: Some(fplugin::PrincipalIdentifier { id }),
1755            description: Some(fplugin::Description::Component(name.to_owned())),
1756            principal_type: Some(fplugin::PrincipalType::Runnable),
1757            parent: None,
1758            ..Default::default()
1759        }
1760    }
1761
1762    fn make_attribution_def(
1763        source: u64,
1764        subject: u64,
1765        resources: Vec<fplugin::ResourceReference>,
1766    ) -> fplugin::Attribution {
1767        fplugin::Attribution {
1768            source: Some(fplugin::PrincipalIdentifier { id: source }),
1769            subject: Some(fplugin::PrincipalIdentifier { id: subject }),
1770            resources: Some(resources),
1771            ..Default::default()
1772        }
1773    }
1774
1775    fn make_test_attribution_data(
1776        principals: Vec<fplugin::Principal>,
1777        resources: Vec<fplugin::Resource>,
1778        resource_names: Vec<name::ZXName>,
1779        attributions: Vec<fplugin::Attribution>,
1780    ) -> AttributionData {
1781        AttributionData {
1782            principals_vec: principals.into_iter().map(|p| p.into()).collect(),
1783            resources_vec: resources.into_iter().map(|r| r.into()).collect(),
1784            resource_names,
1785            attributions: attributions.into_iter().map(|a| a.into()).collect(),
1786        }
1787    }
1788
1789    /// What is tested: Reassigned claim DAG resolution (`InflatedResource::process_claims`) across
1790    /// diamond / branching claim topologies.
1791    ///
1792    /// Expectations verified:
1793    /// - Given a resource with claims `1 -> 2`, `2 -> 3`, and `2 -> 4`, `process_claims()` resolves
1794    ///   the reassignment chain so that Principal 1's claim is reattributed to both leaf subjects
1795    ///   (`3` and `4`).
1796    /// - Verifies that the final set of claim subjects on the resource is exactly `{3, 4}`.
1797    #[test]
1798    fn test_process_claims_fast_path_and_dag() {
1799        let mut res = InflatedResource::new(
1800            fplugin::Resource {
1801                koid: Some(100),
1802                name_index: Some(0),
1803                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo::default())),
1804                ..Default::default()
1805            }
1806            .into(),
1807        );
1808        res.claims.insert(Claim {
1809            source: GlobalPrincipalIdentifier::new_for_test(1),
1810            subject: GlobalPrincipalIdentifier::new_for_test(2),
1811            claim_type: ClaimType::Direct,
1812        });
1813        res.claims.insert(Claim {
1814            source: GlobalPrincipalIdentifier::new_for_test(2),
1815            subject: GlobalPrincipalIdentifier::new_for_test(3),
1816            claim_type: ClaimType::Direct,
1817        });
1818        res.claims.insert(Claim {
1819            source: GlobalPrincipalIdentifier::new_for_test(2),
1820            subject: GlobalPrincipalIdentifier::new_for_test(4),
1821            claim_type: ClaimType::Direct,
1822        });
1823
1824        res.process_claims();
1825
1826        let mut final_subjects: Vec<u64> = res.claims.iter().map(|c| c.subject.0.get()).collect();
1827        final_subjects.sort_unstable();
1828        assert_eq!(final_subjects, vec![3, 4]);
1829    }
1830
1831    /// What is tested: Top-down indirect claim propagation across multi-level resource hierarchies
1832    /// (`Job -> Child Job -> Process -> VMO`) and direct claim overriding.
1833    ///
1834    /// Expectations verified:
1835    /// - When a parent Job (1000) is claimed by Principal 1 and a child Job (1001) is claimed by
1836    ///   Principal 2, the child Job's direct claim overrides propagation from the parent Job.
1837    /// - Verifies that the descendant VMO (1003) receives ONLY Principal 2's claim (`subject == 2`)
1838    ///   and not Principal 1's claim.
1839    #[test]
1840    fn test_claim_propagation_hierarchy_and_overrides() {
1841        let resource_names = vec![
1842            name::ZXName::from_string_lossy("root_job"),
1843            name::ZXName::from_string_lossy("child_job"),
1844            name::ZXName::from_string_lossy("child_proc"),
1845            name::ZXName::from_string_lossy("child_vmo"),
1846        ];
1847        let principals = vec![
1848            make_principal_def(1, "parent_principal"),
1849            make_principal_def(2, "child_principal"),
1850        ];
1851        let resources = vec![
1852            fplugin::Resource {
1853                koid: Some(1000),
1854                name_index: Some(0),
1855                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1856                    child_jobs: Some(vec![1001]),
1857                    processes: Some(vec![]),
1858                    ..Default::default()
1859                })),
1860                ..Default::default()
1861            },
1862            fplugin::Resource {
1863                koid: Some(1001),
1864                name_index: Some(1),
1865                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1866                    child_jobs: Some(vec![]),
1867                    processes: Some(vec![1002]),
1868                    ..Default::default()
1869                })),
1870                ..Default::default()
1871            },
1872            fplugin::Resource {
1873                koid: Some(1002),
1874                name_index: Some(2),
1875                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1876                    vmos: Some(vec![1003]),
1877                    mappings: None,
1878                    ..Default::default()
1879                })),
1880                ..Default::default()
1881            },
1882            fplugin::Resource {
1883                koid: Some(1003),
1884                name_index: Some(3),
1885                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1886                    private_populated_bytes: Some(1000),
1887                    scaled_populated_bytes: Some(1000),
1888                    total_populated_bytes: Some(1000),
1889                    ..Default::default()
1890                })),
1891                ..Default::default()
1892            },
1893        ];
1894        let attributions = vec![
1895            make_attribution_def(1, 1, vec![fplugin::ResourceReference::KernelObject(1000)]),
1896            make_attribution_def(2, 2, vec![fplugin::ResourceReference::KernelObject(1001)]),
1897        ];
1898
1899        let processed = attribute_vmos(make_test_attribution_data(
1900            principals,
1901            resources,
1902            resource_names,
1903            attributions,
1904        ));
1905
1906        let vmo_res = processed.resources.get(&1003).expect("VMO 1003 should exist");
1907        let vmo_subjects: HashSet<u64> = vmo_res.claims.iter().map(|c| c.subject.0.get()).collect();
1908        assert_eq!(vmo_subjects, HashSet::from([2]));
1909    }
1910
1911    /// What is tested: VMO ancestry traversal in `attribute_vmos` when a child VMO has 0 populated
1912    /// bytes (`total_populated_bytes == 0`).
1913    ///
1914    /// Expectations verified:
1915    /// - Multi-generation ancestry (`Child VMO 2003 -> Parent VMO 2002 -> Grandparent VMO 2001`):
1916    ///   verifies that `ClaimType::Child` claims are added to both parent (2002) and grandparent
1917    ///   (2001) when the child has 0 populated bytes.
1918    /// - Self-referential VMO parent (`koid 2004` where `parent = 2004`): verifies that the
1919    ///   ancestry loop breaks early without infinite looping or hanging.
1920    /// - Verifies that the principal's `resources` set includes all traversed ancestor KOIDs
1921    ///   (`2001, 2002, 2003, 2004`).
1922    #[test]
1923    fn test_vmo_ancestry_edge_cases() {
1924        let resource_names = vec![
1925            name::ZXName::from_string_lossy("grandparent"),
1926            name::ZXName::from_string_lossy("parent"),
1927            name::ZXName::from_string_lossy("child"),
1928            name::ZXName::from_string_lossy("self_ref"),
1929        ];
1930        let principals = vec![make_principal_def(1, "vmo_owner")];
1931        let resources = vec![
1932            fplugin::Resource {
1933                koid: Some(2001),
1934                name_index: Some(0),
1935                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1936                    parent: None,
1937                    total_populated_bytes: Some(4096),
1938                    ..Default::default()
1939                })),
1940                ..Default::default()
1941            },
1942            fplugin::Resource {
1943                koid: Some(2002),
1944                name_index: Some(1),
1945                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1946                    parent: Some(2001),
1947                    total_populated_bytes: Some(4096),
1948                    ..Default::default()
1949                })),
1950                ..Default::default()
1951            },
1952            fplugin::Resource {
1953                koid: Some(2003),
1954                name_index: Some(2),
1955                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1956                    parent: Some(2002),
1957                    total_populated_bytes: Some(0),
1958                    ..Default::default()
1959                })),
1960                ..Default::default()
1961            },
1962            fplugin::Resource {
1963                koid: Some(2004),
1964                name_index: Some(3),
1965                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1966                    parent: Some(2004),
1967                    total_populated_bytes: Some(0),
1968                    ..Default::default()
1969                })),
1970                ..Default::default()
1971            },
1972        ];
1973        let attributions = vec![
1974            make_attribution_def(1, 1, vec![fplugin::ResourceReference::KernelObject(2003)]),
1975            make_attribution_def(1, 1, vec![fplugin::ResourceReference::KernelObject(2004)]),
1976        ];
1977
1978        let processed = attribute_vmos(make_test_attribution_data(
1979            principals,
1980            resources,
1981            resource_names,
1982            attributions,
1983        ));
1984
1985        let parent_res = processed.resources.get(&2002).unwrap();
1986        assert!(parent_res.claims.iter().any(|c| c.claim_type == ClaimType::Child));
1987        let grandparent_res = processed.resources.get(&2001).unwrap();
1988        assert!(grandparent_res.claims.iter().any(|c| c.claim_type == ClaimType::Child));
1989
1990        let p1 = processed.principals.get(&GlobalPrincipalIdentifier::new_for_test(1)).unwrap();
1991        assert!(p1.resources.contains(&2001));
1992        assert!(p1.resources.contains(&2002));
1993        assert!(p1.resources.contains(&2003));
1994        assert!(p1.resources.contains(&2004));
1995    }
1996
1997    /// What is tested: `ResourceReference::ProcessMapped` attribution mapping in `attribute_vmos`
1998    /// and resilience against missing process KOIDs.
1999    ///
2000    /// Expectations verified:
2001    /// - A valid `ProcessMapped` reference pointing to Process 3001 correctly attributes its
2002    ///   mapped VMO (3002) to Principal 1.
2003    /// - A `ProcessMapped` reference pointing to a non-existent Process KOID (`9999`) is silently
2004    ///   ignored without panicking or aborting attribution processing.
2005    #[test]
2006    fn test_process_mapped_attributions() {
2007        let resource_names =
2008            vec![name::ZXName::from_string_lossy("proc1"), name::ZXName::from_string_lossy("vmo1")];
2009        let principals = vec![make_principal_def(1, "mapper_principal")];
2010        let resources = vec![
2011            fplugin::Resource {
2012                koid: Some(3001),
2013                name_index: Some(0),
2014                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
2015                    vmos: Some(vec![3002]),
2016                    mappings: Some(vec![fplugin::Mapping {
2017                        vmo: Some(3002),
2018                        address_base: Some(0x1000),
2019                        size: Some(0x1000),
2020                        ..Default::default()
2021                    }]),
2022                    ..Default::default()
2023                })),
2024                ..Default::default()
2025            },
2026            fplugin::Resource {
2027                koid: Some(3002),
2028                name_index: Some(1),
2029                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2030                    total_populated_bytes: Some(4096),
2031                    ..Default::default()
2032                })),
2033                ..Default::default()
2034            },
2035        ];
2036        let attributions = vec![make_attribution_def(
2037            1,
2038            1,
2039            vec![
2040                fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
2041                    process: 3001,
2042                    base: 0x1000,
2043                    len: 0x1000,
2044                    hint_skip_handle_table: false,
2045                }),
2046                fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
2047                    process: 9999,
2048                    base: 0x2000,
2049                    len: 0x1000,
2050                    hint_skip_handle_table: false,
2051                }),
2052            ],
2053        )];
2054
2055        let processed = attribute_vmos(make_test_attribution_data(
2056            principals,
2057            resources,
2058            resource_names,
2059            attributions,
2060        ));
2061
2062        let vmo_res = processed.resources.get(&3002).unwrap();
2063        let vmo_subjects: HashSet<u64> = vmo_res.claims.iter().map(|c| c.subject.0.get()).collect();
2064        assert_eq!(vmo_subjects, HashSet::from([1]));
2065    }
2066
2067    /// What is tested: Deduplication of KOIDs in `principal.resources` when multiple claims
2068    /// (Direct, Indirect, Child) apply to the same hierarchy.
2069    ///
2070    /// Expectations verified:
2071    /// - When a principal has both a direct claim on Job 5001 and a direct claim on VMO 5003,
2072    ///   `principal.resources` contains exactly 2 KOIDs (Process 5002 and VMO 5003, since Jobs are
2073    ///   not stored in `principal.resources`) without duplicates.
2074    #[test]
2075    fn test_attribute_vmos_principal_resources_dedup() {
2076        let resource_names = vec![
2077            name::ZXName::from_string_lossy("job"),
2078            name::ZXName::from_string_lossy("proc"),
2079            name::ZXName::from_string_lossy("vmo"),
2080        ];
2081        let principals = vec![make_principal_def(1, "p1")];
2082        let resources = vec![
2083            fplugin::Resource {
2084                koid: Some(5001),
2085                name_index: Some(0),
2086                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
2087                    child_jobs: Some(vec![]),
2088                    processes: Some(vec![5002]),
2089                    ..Default::default()
2090                })),
2091                ..Default::default()
2092            },
2093            fplugin::Resource {
2094                koid: Some(5002),
2095                name_index: Some(1),
2096                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
2097                    vmos: Some(vec![5003]),
2098                    mappings: None,
2099                    ..Default::default()
2100                })),
2101                ..Default::default()
2102            },
2103            fplugin::Resource {
2104                koid: Some(5003),
2105                name_index: Some(2),
2106                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2107                    total_populated_bytes: Some(4096),
2108                    ..Default::default()
2109                })),
2110                ..Default::default()
2111            },
2112        ];
2113        let attributions = vec![make_attribution_def(
2114            1,
2115            1,
2116            vec![
2117                fplugin::ResourceReference::KernelObject(5001),
2118                fplugin::ResourceReference::KernelObject(5003),
2119            ],
2120        )];
2121
2122        let processed = attribute_vmos(make_test_attribution_data(
2123            principals,
2124            resources,
2125            resource_names,
2126            attributions,
2127        ));
2128
2129        let p1 = processed.principals.get(&GlobalPrincipalIdentifier::new_for_test(1)).unwrap();
2130        assert_eq!(p1.resources.len(), 2);
2131        assert!(p1.resources.contains(&5002));
2132        assert!(p1.resources.contains(&5003));
2133    }
2134
2135    /// What is tested: Multi-level VMO ancestry chain resolution and principal resource set
2136    /// accumulation with multiple principals and overlapping claims.
2137    ///
2138    /// Expectations verified:
2139    /// - Deep ancestry chain: Root VMO 8001 -> Child VMO 8002 (0 bytes) -> Leaf VMO 8003 (0 bytes).
2140    /// - Leaf VMO 8004 (0 bytes) also shares parent 8002.
2141    /// - P1 claims Leaf VMO 8003 and Process 8000; P2 claims Leaf VMO 8004.
2142    /// - Verifies P1 gets {8000, 8001, 8002, 8003} and P2 gets {8001, 8002, 8004}.
2143    /// - Verifies Child claims are correctly propagated to ancestors 8001 and 8002.
2144    #[test]
2145    fn test_vmo_ancestry_and_principal_resource_accumulation() {
2146        let resource_names = vec![
2147            name::ZXName::from_string_lossy("proc"),
2148            name::ZXName::from_string_lossy("root_vmo"),
2149            name::ZXName::from_string_lossy("mid_vmo"),
2150            name::ZXName::from_string_lossy("leaf_vmo1"),
2151            name::ZXName::from_string_lossy("leaf_vmo2"),
2152        ];
2153        let principals = vec![make_principal_def(1, "p1"), make_principal_def(2, "p2")];
2154        let resources = vec![
2155            fplugin::Resource {
2156                koid: Some(8000),
2157                name_index: Some(0),
2158                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
2159                    vmos: Some(vec![8001, 8002, 8003, 8004]),
2160                    mappings: None,
2161                    ..Default::default()
2162                })),
2163                ..Default::default()
2164            },
2165            fplugin::Resource {
2166                koid: Some(8001),
2167                name_index: Some(1),
2168                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2169                    parent: None,
2170                    total_populated_bytes: Some(8192),
2171                    ..Default::default()
2172                })),
2173                ..Default::default()
2174            },
2175            fplugin::Resource {
2176                koid: Some(8002),
2177                name_index: Some(2),
2178                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2179                    parent: Some(8001),
2180                    total_populated_bytes: Some(0),
2181                    ..Default::default()
2182                })),
2183                ..Default::default()
2184            },
2185            fplugin::Resource {
2186                koid: Some(8003),
2187                name_index: Some(3),
2188                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2189                    parent: Some(8002),
2190                    total_populated_bytes: Some(0),
2191                    ..Default::default()
2192                })),
2193                ..Default::default()
2194            },
2195            fplugin::Resource {
2196                koid: Some(8004),
2197                name_index: Some(4),
2198                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2199                    parent: Some(8002),
2200                    total_populated_bytes: Some(0),
2201                    ..Default::default()
2202                })),
2203                ..Default::default()
2204            },
2205        ];
2206        let attributions = vec![
2207            make_attribution_def(
2208                1,
2209                1,
2210                vec![
2211                    fplugin::ResourceReference::KernelObject(8000),
2212                    fplugin::ResourceReference::KernelObject(8003),
2213                ],
2214            ),
2215            make_attribution_def(1, 2, vec![fplugin::ResourceReference::KernelObject(8004)]),
2216        ];
2217
2218        let processed = attribute_vmos(make_test_attribution_data(
2219            principals,
2220            resources,
2221            resource_names,
2222            attributions,
2223        ));
2224
2225        let p1 = processed.principals.get(&GlobalPrincipalIdentifier::new_for_test(1)).unwrap();
2226        assert_eq!(p1.resources.len(), 4);
2227        assert!(p1.resources.contains(&8000));
2228        assert!(p1.resources.contains(&8001));
2229        assert!(p1.resources.contains(&8002));
2230        assert!(p1.resources.contains(&8003));
2231
2232        let p2 = processed.principals.get(&GlobalPrincipalIdentifier::new_for_test(2)).unwrap();
2233        assert_eq!(p2.resources.len(), 3);
2234        assert!(p2.resources.contains(&8001));
2235        assert!(p2.resources.contains(&8002));
2236        assert!(p2.resources.contains(&8004));
2237
2238        let mid_vmo = processed.resources.get(&8002).unwrap();
2239        assert!(
2240            mid_vmo
2241                .claims
2242                .iter()
2243                .any(|c| c.claim_type == ClaimType::Child && c.subject.0.get() == 1)
2244        );
2245        assert!(
2246            mid_vmo
2247                .claims
2248                .iter()
2249                .any(|c| c.claim_type == ClaimType::Child && c.subject.0.get() == 2)
2250        );
2251
2252        let root_vmo = processed.resources.get(&8001).unwrap();
2253        assert!(
2254            root_vmo
2255                .claims
2256                .iter()
2257                .any(|c| c.claim_type == ClaimType::Child && c.subject.0.get() == 1)
2258        );
2259        assert!(
2260            root_vmo
2261                .claims
2262                .iter()
2263                .any(|c| c.claim_type == ClaimType::Child && c.subject.0.get() == 2)
2264        );
2265    }
2266
2267    /// What is tested: `ResourceReference::ProcessMapped` binary search (`partition_point`) over
2268    /// sorted process address mappings and boundary conditions.
2269    ///
2270    /// Expectations verified:
2271    /// - Given 6 mappings sorted by address base across a process address space:
2272    ///   - mapping 0: `[0x1000, 0x2000]` (VMO 6001) - before range `[0x3000, 0x7000]` -> skipped
2273    ///   - mapping 1: `[0x3000, 0x4000]` (VMO 6002) - fully inside range -> matched
2274    ///   - mapping 2: `[0x4000, 0x6000]` (VMO 6003) - fully inside range -> matched
2275    ///   - mapping 3: `[0x6000, 0x8000]` (VMO 6004) - exceeds upper bound (0x8000 > 0x7000)
2276    ///                 -> not matched
2277    ///   - mapping 4: `[0x7000, 0x8000]` (VMO 6005) - base == 0x7000 == end_bound
2278    ///                 -> triggers early loop break (mapping_base >= end_bound)
2279    ///   - mapping 5: `[0xa000, 0xb000]` (VMO 6006) - after break -> never visited
2280    /// - Verifies that only VMO 6002 and VMO 6003 are attributed, and `PrincipalSummary.processes`
2281    ///   correctly contains the mapped process.
2282    #[test]
2283    fn test_process_mapped_binary_search_and_boundaries() {
2284        let resource_names = vec![
2285            name::ZXName::from_string_lossy("test_proc"),
2286            name::ZXName::from_string_lossy("vmo1"),
2287            name::ZXName::from_string_lossy("vmo2"),
2288            name::ZXName::from_string_lossy("vmo3"),
2289            name::ZXName::from_string_lossy("vmo4"),
2290            name::ZXName::from_string_lossy("vmo5"),
2291            name::ZXName::from_string_lossy("vmo6"),
2292        ];
2293        let principals = vec![make_principal_def(1, "vmar_principal")];
2294        let resources = vec![
2295            fplugin::Resource {
2296                koid: Some(6000),
2297                name_index: Some(0),
2298                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
2299                    vmos: Some(vec![6001, 6002, 6003, 6004, 6005, 6006]),
2300                    mappings: Some(vec![
2301                        fplugin::Mapping {
2302                            vmo: Some(6001),
2303                            address_base: Some(0x1000),
2304                            size: Some(0x1000),
2305                            ..Default::default()
2306                        },
2307                        fplugin::Mapping {
2308                            vmo: Some(6002),
2309                            address_base: Some(0x3000),
2310                            size: Some(0x1000),
2311                            ..Default::default()
2312                        },
2313                        fplugin::Mapping {
2314                            vmo: Some(6003),
2315                            address_base: Some(0x4000),
2316                            size: Some(0x2000),
2317                            ..Default::default()
2318                        },
2319                        fplugin::Mapping {
2320                            vmo: Some(6004),
2321                            address_base: Some(0x6000),
2322                            size: Some(0x2000),
2323                            ..Default::default()
2324                        },
2325                        fplugin::Mapping {
2326                            vmo: Some(6005),
2327                            address_base: Some(0x7000),
2328                            size: Some(0x1000),
2329                            ..Default::default()
2330                        },
2331                        fplugin::Mapping {
2332                            vmo: Some(6006),
2333                            address_base: Some(0xa000),
2334                            size: Some(0x1000),
2335                            ..Default::default()
2336                        },
2337                    ]),
2338                    ..Default::default()
2339                })),
2340                ..Default::default()
2341            },
2342            fplugin::Resource {
2343                koid: Some(6001),
2344                name_index: Some(1),
2345                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2346                    private_committed_bytes: Some(4096),
2347                    private_populated_bytes: Some(4096),
2348                    scaled_committed_bytes: Some(4096),
2349                    scaled_populated_bytes: Some(4096),
2350                    total_committed_bytes: Some(4096),
2351                    total_populated_bytes: Some(4096),
2352                    ..Default::default()
2353                })),
2354                ..Default::default()
2355            },
2356            fplugin::Resource {
2357                koid: Some(6002),
2358                name_index: Some(2),
2359                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2360                    private_committed_bytes: Some(4096),
2361                    private_populated_bytes: Some(4096),
2362                    scaled_committed_bytes: Some(4096),
2363                    scaled_populated_bytes: Some(4096),
2364                    total_committed_bytes: Some(4096),
2365                    total_populated_bytes: Some(4096),
2366                    ..Default::default()
2367                })),
2368                ..Default::default()
2369            },
2370            fplugin::Resource {
2371                koid: Some(6003),
2372                name_index: Some(3),
2373                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2374                    private_committed_bytes: Some(8192),
2375                    private_populated_bytes: Some(8192),
2376                    scaled_committed_bytes: Some(8192),
2377                    scaled_populated_bytes: Some(8192),
2378                    total_committed_bytes: Some(8192),
2379                    total_populated_bytes: Some(8192),
2380                    ..Default::default()
2381                })),
2382                ..Default::default()
2383            },
2384            fplugin::Resource {
2385                koid: Some(6004),
2386                name_index: Some(4),
2387                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2388                    private_committed_bytes: Some(8192),
2389                    private_populated_bytes: Some(8192),
2390                    scaled_committed_bytes: Some(8192),
2391                    scaled_populated_bytes: Some(8192),
2392                    total_committed_bytes: Some(8192),
2393                    total_populated_bytes: Some(8192),
2394                    ..Default::default()
2395                })),
2396                ..Default::default()
2397            },
2398            fplugin::Resource {
2399                koid: Some(6005),
2400                name_index: Some(5),
2401                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2402                    private_committed_bytes: Some(4096),
2403                    private_populated_bytes: Some(4096),
2404                    scaled_committed_bytes: Some(4096),
2405                    scaled_populated_bytes: Some(4096),
2406                    total_committed_bytes: Some(4096),
2407                    total_populated_bytes: Some(4096),
2408                    ..Default::default()
2409                })),
2410                ..Default::default()
2411            },
2412            fplugin::Resource {
2413                koid: Some(6006),
2414                name_index: Some(6),
2415                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
2416                    private_committed_bytes: Some(4096),
2417                    private_populated_bytes: Some(4096),
2418                    scaled_committed_bytes: Some(4096),
2419                    scaled_populated_bytes: Some(4096),
2420                    total_committed_bytes: Some(4096),
2421                    total_populated_bytes: Some(4096),
2422                    ..Default::default()
2423                })),
2424                ..Default::default()
2425            },
2426        ];
2427        let attributions = vec![make_attribution_def(
2428            1,
2429            1,
2430            vec![fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
2431                process: 6000,
2432                base: 0x3000,
2433                len: 0x4000,
2434                hint_skip_handle_table: false,
2435            })],
2436        )];
2437
2438        let processed = attribute_vmos(make_test_attribution_data(
2439            principals,
2440            resources,
2441            resource_names,
2442            attributions,
2443        ));
2444
2445        // VMO 6002 and 6003 should have claims from Principal 1
2446        assert_eq!(
2447            processed
2448                .resources
2449                .get(&6002)
2450                .unwrap()
2451                .claims
2452                .iter()
2453                .map(|c| c.subject.0.get())
2454                .collect::<Vec<_>>(),
2455            vec![1]
2456        );
2457        assert_eq!(
2458            processed
2459                .resources
2460                .get(&6003)
2461                .unwrap()
2462                .claims
2463                .iter()
2464                .map(|c| c.subject.0.get())
2465                .collect::<Vec<_>>(),
2466            vec![1]
2467        );
2468        // VMO 6001, 6004, 6005, 6006 should have no claims
2469        assert!(processed.resources.get(&6001).unwrap().claims.is_empty());
2470        assert!(processed.resources.get(&6004).unwrap().claims.is_empty());
2471        assert!(processed.resources.get(&6005).unwrap().claims.is_empty());
2472        assert!(processed.resources.get(&6006).unwrap().claims.is_empty());
2473
2474        let summary = processed.summary();
2475        assert_eq!(summary.principals.len(), 1);
2476        assert_eq!(summary.principals[0].processes, vec!["test_proc (6000)".to_owned()]);
2477    }
2478}