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 serde::Serialize;
8use std::collections::{HashMap, HashSet};
9use std::fmt::Debug;
10use summary::MemorySummary;
11
12mod name;
13pub use name::ZXName;
14
15pub mod digest;
16pub mod fkernel_serde;
17pub mod fplugin_serde;
18pub mod summary;
19
20#[cfg(target_os = "fuchsia")]
21use {fuchsia_trace::duration, std::ffi::CStr};
22#[cfg(target_os = "fuchsia")]
23const CATEGORY_MEMORY_CAPTURE: &CStr = c"memory:capture";
24
25/// Unique principal identifier across the whole system.
26#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, Serialize)]
27pub struct GlobalPrincipalIdentifier(pub std::num::NonZeroU64);
28
29impl GlobalPrincipalIdentifier {
30    /// Should only be generated by a [GlobalPrincipalIdentifierFactory] except for tests.
31    // #[cfg(test)] cannot be used because it is enabled only for the current crate tests.
32    pub fn new_for_test(value: u64) -> Self {
33        Self(std::num::NonZeroU64::new(value).unwrap())
34    }
35}
36
37impl From<fplugin::PrincipalIdentifier> for GlobalPrincipalIdentifier {
38    fn from(value: fplugin::PrincipalIdentifier) -> Self {
39        Self(std::num::NonZeroU64::new(value.id).unwrap())
40    }
41}
42
43impl Into<fplugin::PrincipalIdentifier> for GlobalPrincipalIdentifier {
44    fn into(self) -> fplugin::PrincipalIdentifier {
45        fplugin::PrincipalIdentifier { id: self.0.get() }
46    }
47}
48
49/// Factory for GlobalPrincipalIdentifier, ensuring their uniqueness.
50#[derive(Debug)]
51pub struct GlobalPrincipalIdentifierFactory {
52    next_id: std::num::NonZeroU64,
53}
54
55impl Default for GlobalPrincipalIdentifierFactory {
56    fn default() -> GlobalPrincipalIdentifierFactory {
57        GlobalPrincipalIdentifierFactory { next_id: std::num::NonZeroU64::new(1).unwrap() }
58    }
59}
60
61impl GlobalPrincipalIdentifierFactory {
62    pub fn next(&mut self) -> GlobalPrincipalIdentifier {
63        let value = GlobalPrincipalIdentifier(self.next_id);
64        // Fail loudly if we are no longer able to generate new [GlobalPrincipalIdentifier]s.
65        self.next_id = self.next_id.checked_add(1).unwrap();
66        return value;
67    }
68}
69
70/// User-understandable description of a Principal
71#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
72pub enum PrincipalDescription {
73    Component(String),
74    Part(String),
75}
76
77impl From<fplugin::Description> for PrincipalDescription {
78    fn from(value: fplugin::Description) -> Self {
79        match value {
80            fplugin::Description::Component(s) => PrincipalDescription::Component(s),
81            fplugin::Description::Part(s) => PrincipalDescription::Part(s),
82            _ => unreachable!(),
83        }
84    }
85}
86
87impl Into<fplugin::Description> for PrincipalDescription {
88    fn into(self) -> fplugin::Description {
89        match self {
90            PrincipalDescription::Component(s) => fplugin::Description::Component(s),
91            PrincipalDescription::Part(s) => fplugin::Description::Part(s),
92        }
93    }
94}
95
96/// Type of a principal.
97#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
98pub enum PrincipalType {
99    Runnable,
100    Part,
101}
102
103impl From<fplugin::PrincipalType> for PrincipalType {
104    fn from(value: fplugin::PrincipalType) -> Self {
105        match value {
106            fplugin::PrincipalType::Runnable => PrincipalType::Runnable,
107            fplugin::PrincipalType::Part => PrincipalType::Part,
108            _ => unreachable!(),
109        }
110    }
111}
112
113impl Into<fplugin::PrincipalType> for PrincipalType {
114    fn into(self) -> fplugin::PrincipalType {
115        match self {
116            PrincipalType::Runnable => fplugin::PrincipalType::Runnable,
117            PrincipalType::Part => fplugin::PrincipalType::Part,
118        }
119    }
120}
121
122#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
123/// A Principal, that can use and claim memory.
124pub struct Principal {
125    // These fields are initialized from [fplugin::Principal].
126    pub identifier: GlobalPrincipalIdentifier,
127    pub description: Option<PrincipalDescription>,
128    pub principal_type: PrincipalType,
129
130    /// Principal that declared this Principal. None if this Principal is at the root of the system
131    /// hierarchy (the root principal is a statically defined Principal encompassing all resources
132    /// on the system). The Principal hierarchy forms a tree (no cycles).
133    pub parent: Option<GlobalPrincipalIdentifier>,
134}
135
136/// Creates a new [Principal] from a [fplugin::Principal] object. The [Principal] object will
137/// contain all the data from [fplugin::Principal] and have its other fields initialized empty.
138impl From<fplugin::Principal> for Principal {
139    fn from(value: fplugin::Principal) -> Self {
140        Principal {
141            identifier: value.identifier.unwrap().try_into().unwrap(),
142            description: value.description.map(Into::into),
143            principal_type: value.principal_type.unwrap().into(),
144            parent: value.parent.map(|id| id.try_into().unwrap()),
145        }
146    }
147}
148
149impl Into<fplugin::Principal> for Principal {
150    fn into(self) -> fplugin::Principal {
151        fplugin::Principal {
152            identifier: Some(self.identifier.into()),
153            description: self.description.map(Into::into),
154            principal_type: Some(self.principal_type.into()),
155            parent: self.parent.map(Into::into),
156            ..Default::default()
157        }
158    }
159}
160
161/// A Principal, with its attribution claims and resources.
162#[derive(Serialize)]
163pub struct InflatedPrincipal {
164    /// The principal definition.
165    principal: Principal,
166
167    // These fields are computed from the rest of the [fplugin::Snapshot] data.
168    /// Map of attribution claims made about this Principal (this Principal is the subject of the
169    /// claim). This map goes from the source Principal to the attribution claim.
170    attribution_claims: HashMap<GlobalPrincipalIdentifier, Attribution>,
171    /// KOIDs of resources attributed to this principal, after resolution of sharing and
172    /// reattributions.
173    resources: HashSet<u64>,
174}
175
176impl InflatedPrincipal {
177    fn new(principal: Principal) -> InflatedPrincipal {
178        InflatedPrincipal {
179            principal,
180            attribution_claims: Default::default(),
181            resources: Default::default(),
182        }
183    }
184}
185
186impl InflatedPrincipal {
187    fn name(&self) -> &str {
188        match &self.principal.description {
189            Some(PrincipalDescription::Component(component_name)) => component_name,
190            Some(PrincipalDescription::Part(part_name)) => part_name,
191            None => "?",
192        }
193    }
194}
195
196/// Type of the claim, that changes depending on how the claim was created.
197#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
198pub enum ClaimType {
199    /// A principal claimed this resource directly.
200    Direct,
201    /// A principal claimed a resource that contains this resource (e.g. a process containing a
202    /// VMO).
203    Indirect,
204    /// A principal claimed a child of this resource (e.g. a copy-on-write VMO child of this VMO).
205    Child,
206}
207
208#[derive(Clone, Copy, PartialEq, Eq, Hash)]
209pub struct Koid(u64);
210
211impl From<u64> for Koid {
212    fn from(value: u64) -> Self {
213        Koid(value)
214    }
215}
216
217/// Attribution claim of a Principal on a Resource.
218///
219/// Note that this object is slightly different from the [fplugin::Attribution] object: it goes from
220/// a Resource to a Principal, and covers also indirect attribution claims of sub- and parent
221/// resources.
222#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
223pub struct Claim {
224    /// Principal to which the resources are attributed.
225    subject: GlobalPrincipalIdentifier,
226    /// Principal making the attribution claim.
227    source: GlobalPrincipalIdentifier,
228    claim_type: ClaimType,
229}
230
231#[derive(Clone, Debug, PartialEq, Serialize)]
232pub struct Resource {
233    pub koid: u64,
234    pub name_index: usize,
235    #[serde(with = "fplugin_serde::ResourceTypeDef")]
236    pub resource_type: fplugin::ResourceType,
237}
238
239impl From<fplugin::Resource> for Resource {
240    fn from(value: fplugin::Resource) -> Self {
241        Resource {
242            koid: value.koid.unwrap(),
243            name_index: value.name_index.unwrap() as usize,
244            resource_type: value.resource_type.unwrap(),
245        }
246    }
247}
248
249impl Into<fplugin::Resource> for Resource {
250    fn into(self) -> fplugin::Resource {
251        fplugin::Resource {
252            koid: Some(self.koid),
253            name_index: Some(self.name_index as u64),
254            resource_type: Some(self.resource_type),
255            ..Default::default()
256        }
257    }
258}
259
260// Claim with a boolean tag, to help find leaves in the claim assignment graph.
261pub struct TaggedClaim(Claim, bool);
262
263#[derive(Clone, Debug, Serialize)]
264pub struct BlobAnnotation {
265    /// Path to the package's manifest.
266    pub manifest: String,
267    /// Path of blob in package.
268    pub path: String,
269}
270
271#[derive(Clone, Debug, Serialize)]
272pub enum ResourceAnnotation {
273    Blob(BlobAnnotation),
274}
275
276/// Resource annotated with additional information.
277#[derive(Debug, Serialize)]
278pub struct InflatedResource {
279    pub resource: Resource,
280    pub claims: HashSet<Claim>,
281    pub annotations: Vec<ResourceAnnotation>,
282}
283
284impl InflatedResource {
285    fn new(resource: Resource) -> InflatedResource {
286        InflatedResource { resource, claims: Default::default(), annotations: Default::default() }
287    }
288
289    fn children(&self) -> Vec<u64> {
290        match &self.resource.resource_type {
291            fplugin::ResourceType::Job(job) => {
292                let mut r: Vec<u64> = job.child_jobs.iter().flatten().map(|k| *k).collect();
293                r.extend(job.processes.iter().flatten().map(|k| *k));
294                r
295            }
296            fplugin::ResourceType::Process(process) => {
297                process.vmos.iter().flatten().map(|k| *k).collect()
298            }
299            fplugin::ResourceType::Vmo(_) => Vec::new(),
300            _ => todo!(),
301        }
302    }
303
304    /// Process the claims made on this resource to disambiguate between reassignment and sharing.
305    ///
306    /// [process_claims] looks at each claim made on this resource, and removes claims that are
307    /// reassigned by another claim. This happens if a principal A gives a resource to principal B,
308    /// and B then gives it to principal C. However, if two independent principals claim this
309    /// resource, then both their claims are kept.
310    /// This is done by:
311    /// (i)  preserving all self claims, and
312    /// (ii) preserving only leaves in the DAG following claim.source to claim.subject edges.
313    fn process_claims(&mut self) {
314        let mut claims_by_source: HashMap<GlobalPrincipalIdentifier, RefCell<Vec<TaggedClaim>>> =
315            Default::default();
316        let mut self_claims = Vec::new();
317
318        for claim in self.claims.iter().cloned() {
319            if claim.source == claim.subject {
320                // Self claims are taken out of the graph because they are never transferred. This
321                // is to implement sharing.
322                self_claims.push(claim);
323            } else {
324                claims_by_source
325                    .entry(claim.source)
326                    .or_default()
327                    .borrow_mut()
328                    .push(TaggedClaim(claim, false));
329            }
330        }
331
332        self.claims = self_claims.into_iter().collect();
333        for (_, claimlist_refcell) in claims_by_source.iter() {
334            let mut claimlist = claimlist_refcell.borrow_mut();
335            for tagged_claim in claimlist.iter_mut() {
336                self.claims.extend(
337                    InflatedResource::process_claims_recursive(tagged_claim, &claims_by_source)
338                        .into_iter(),
339                );
340            }
341        }
342    }
343
344    /// Recursively look at claims to find the ones that are not reassigned.
345    fn process_claims_recursive(
346        tagged_claim: &mut TaggedClaim,
347        claims: &HashMap<GlobalPrincipalIdentifier, RefCell<Vec<TaggedClaim>>>,
348    ) -> Vec<Claim> {
349        let claim = match tagged_claim.1 {
350            true => {
351                // We have visited this claim already, we can skip.
352                return vec![];
353            }
354            false => {
355                // We tag visited claims, so we don't visit them again.
356                tagged_claim.1 = true;
357                tagged_claim.0
358            }
359        };
360        let subject = &claim.subject;
361        // We find if this claim has been reassigned.
362        let mut subject_claims = match claims.get(subject) {
363            Some(value_ref) => {
364                // [subject_claims] mutable borrow is held when recursing below, and
365                // [RefCell::try_borrow_mut] returns an error if called when a mutable borrow is
366                // already held. This ensures an error will be thrown at runtime if there is a
367                // cycle.
368                value_ref.try_borrow_mut().expect("Claims form a cycle, this is not supported")
369            }
370            None => {
371                // The claim is not reassigned, we keep the claim.
372                return vec![claim];
373            }
374        };
375        let mut leaves = vec![];
376        for subject_claim in subject_claims.iter_mut() {
377            leaves.append(&mut InflatedResource::process_claims_recursive(subject_claim, claims));
378        }
379        leaves
380    }
381}
382
383#[derive(Clone, Serialize)]
384/// Holds the list of resources attributed to a Principal (subject) by another Principal (source).
385pub struct Attribution {
386    /// Principal making the attribution claim.
387    pub source: GlobalPrincipalIdentifier,
388    /// Principal to which the resources are attributed.
389    pub subject: GlobalPrincipalIdentifier,
390    /// List of resources attributed to `subject` by `source`.
391    pub resources: Vec<ResourceReference>,
392}
393
394impl From<fplugin::Attribution> for Attribution {
395    fn from(value: fplugin::Attribution) -> Attribution {
396        Attribution {
397            source: value.source.unwrap().into(),
398            subject: value.subject.unwrap().into(),
399            resources: value.resources.unwrap().into_iter().map(|r| r.into()).collect(),
400        }
401    }
402}
403
404impl Into<fplugin::Attribution> for Attribution {
405    fn into(self) -> fplugin::Attribution {
406        fplugin::Attribution {
407            source: Some(self.source.into()),
408            subject: Some(self.subject.into()),
409            resources: Some(self.resources.into_iter().map(|r| r.into()).collect()),
410            ..Default::default()
411        }
412    }
413}
414
415#[derive(Clone, Copy, Serialize)]
416/// References a kernel [`Resource`], or some subset of a [`Resource`] (such as a part of a process
417/// address space).
418pub enum ResourceReference {
419    /// Identifies a kernel object whose memory is being attributed.
420    ///
421    /// Refers to all memory held by VMOs reachable from the object
422    /// (currently a Job, Process or VMO).
423    KernelObject(u64),
424
425    /// Identifies a part of a process address space.
426    ProcessMapped {
427        /// The KOID of the process that this VMAR lives in.
428        process: u64,
429
430        /// Base address of the VMAR.
431        base: u64,
432
433        /// Length of the VMAR.
434        len: u64,
435
436        /// True, when enumerating the handle table on this process is superfluous.
437        /// This is a hint that can be ignored by the client without changing the result.
438        hint_skip_handle_table: bool,
439    },
440}
441
442impl From<fplugin::ResourceReference> for ResourceReference {
443    fn from(value: fplugin::ResourceReference) -> ResourceReference {
444        match value {
445            fplugin::ResourceReference::KernelObject(ko) => ResourceReference::KernelObject(ko),
446            fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
447                process,
448                base,
449                len,
450                hint_skip_handle_table,
451            }) => ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table },
452            _ => unimplemented!(),
453        }
454    }
455}
456
457impl Into<fplugin::ResourceReference> for ResourceReference {
458    fn into(self) -> fplugin::ResourceReference {
459        match self {
460            ResourceReference::KernelObject(ko) => fplugin::ResourceReference::KernelObject(ko),
461            ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table } => {
462                fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
463                    process,
464                    base,
465                    len,
466                    hint_skip_handle_table,
467                })
468            }
469        }
470    }
471}
472
473/// Capture of the current memory usage of a device, as retrieved through the memory attribution
474/// protocol. In this object, memory attribution is not resolved.
475pub struct AttributionData {
476    pub principals_vec: Vec<Principal>,
477    pub resources_vec: Vec<Resource>,
478    pub resource_names: Vec<ZXName>,
479    pub attributions: Vec<Attribution>,
480}
481
482pub trait AttributionDataProvider: Send + Sync {
483    /// Returns all the memory resources and attribution specifications.
484    fn get_attribution_data(&self) -> Result<AttributionData, anyhow::Error>;
485}
486
487/// Processed snapshot of the memory usage of a device, with attribution of memory resources to
488/// Principals resolved.
489pub struct ProcessedAttributionData {
490    pub principals: HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
491    pub resources: HashMap<u64, InflatedResource>,
492    pub resource_names: Vec<ZXName>,
493}
494
495impl ProcessedAttributionData {
496    fn new(
497        principals: HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
498        resources: HashMap<u64, InflatedResource>,
499        resource_names: Vec<ZXName>,
500    ) -> Self {
501        Self { principals, resources, resource_names }
502    }
503
504    /// Create a summary view of the memory attribution_data. See [MemorySummary] for details.
505    pub fn summary(&self) -> MemorySummary {
506        #[cfg(target_os = "fuchsia")]
507        duration!(CATEGORY_MEMORY_CAPTURE, c"ProcessedAttributionData::summary");
508        MemorySummary::build(&self.principals, &self.resources, &self.resource_names)
509    }
510}
511
512/// Process data from a [AttributionData] to resolve attribution claims.
513pub fn attribute_vmos(attribution_data: AttributionData) -> ProcessedAttributionData {
514    #[cfg(target_os = "fuchsia")]
515    duration!(CATEGORY_MEMORY_CAPTURE, c"attribute_vmos");
516
517    // Map from moniker token ID to Principal struct.
518    let principals: HashMap<GlobalPrincipalIdentifier, RefCell<InflatedPrincipal>> =
519        attribution_data
520            .principals_vec
521            .into_iter()
522            .map(|p| (p.identifier.clone(), RefCell::new(InflatedPrincipal::new(p))))
523            .collect();
524
525    // Map from kernel resource koid to Resource struct.
526    let mut resources: HashMap<u64, RefCell<InflatedResource>> = attribution_data
527        .resources_vec
528        .into_iter()
529        .map(|r| (r.koid, RefCell::new(InflatedResource::new(r))))
530        .collect();
531
532    // Add direct claims to resources.
533    for attribution in attribution_data.attributions {
534        principals.get(&attribution.subject.clone().into()).map(|p| {
535            p.borrow_mut().attribution_claims.insert(attribution.source.into(), attribution.clone())
536        });
537        for resource in attribution.resources {
538            match resource {
539                ResourceReference::KernelObject(koid) => {
540                    if !resources.contains_key(&koid) {
541                        continue;
542                    }
543                    resources.get_mut(&koid).unwrap().get_mut().claims.insert(Claim {
544                        source: attribution.source.into(),
545                        subject: attribution.subject.into(),
546                        claim_type: ClaimType::Direct,
547                    });
548                }
549                ResourceReference::ProcessMapped {
550                    process,
551                    base,
552                    len,
553                    hint_skip_handle_table: _,
554                } => {
555                    if !resources.contains_key(&process) {
556                        continue;
557                    }
558                    let mut matched_vmos = Vec::new();
559                    if let fplugin::ResourceType::Process(process_data) =
560                        &resources.get(&process).unwrap().borrow().resource.resource_type
561                    {
562                        for mapping in process_data.mappings.iter().flatten() {
563                            // We consider an entire VMO to be matched if it has a mapping
564                            // within the claimed region.
565                            if mapping.address_base.unwrap() >= base
566                                && mapping.address_base.unwrap() + mapping.size.unwrap()
567                                    <= base + len
568                            {
569                                matched_vmos.push(mapping.vmo.unwrap());
570                            }
571                        }
572                    }
573                    for vmo_koid in matched_vmos {
574                        match resources.get_mut(&vmo_koid) {
575                            Some(resource) => {
576                                resource.get_mut().claims.insert(Claim {
577                                    source: attribution.source.into(),
578                                    subject: attribution.subject.into(),
579                                    claim_type: ClaimType::Direct,
580                                });
581                            }
582                            None => {
583                                // The VMO is unknown. This can happen when a VMO is created between
584                                // the collection of the list of VMOs and the collection of the
585                                // process mappings.
586                            }
587                        }
588                    }
589                }
590            }
591        }
592    }
593
594    // Propagate claims. We propagate direct claims to child resources recursively until we hit a
595    // resource that is directly claimed: this is because we consider that attributors deeper in the
596    // principal hierarchy will not attribute resources higher in the resource hierarchy than the
597    // ones attributed by their ancestors (ie. attribution is always more precise as we go deeper).
598    for (_, resource_refcell) in &resources {
599        let resource = resource_refcell.borrow_mut();
600        // Extract the list of direct claims to propagate.
601        let direct_claims: Vec<&Claim> = resource
602            .claims
603            .iter()
604            .filter(|claim| match claim.claim_type {
605                ClaimType::Direct => true,
606                _ => false,
607            })
608            .collect();
609
610        if direct_claims.is_empty() {
611            // There is no direct claim to propagate, we can skip this resource.
612            continue;
613        }
614
615        let propagated_claims: Vec<Claim> = direct_claims
616            .into_iter()
617            .map(|claim| Claim {
618                source: claim.source,
619                subject: claim.subject,
620                claim_type: ClaimType::Indirect,
621            })
622            .collect();
623        let mut frontier = Vec::new();
624        frontier.extend(resource.children());
625        while !frontier.is_empty() {
626            let child = frontier.pop().unwrap();
627            let mut child_resource = match resources.get(&child) {
628                Some(resource) => resource.borrow_mut(),
629                None => {
630                    // This can happen if a resource is created or disappears while we were
631                    // collecting information about all the resources in the system. This should
632                    // remain a rare event.
633                    continue;
634                }
635            };
636            if child_resource.claims.iter().any(|c| c.claim_type == ClaimType::Direct) {
637                // If there is a direct claim on the resource, don't propagate.
638                continue;
639            }
640            child_resource.claims.extend(propagated_claims.clone().iter());
641            frontier.extend(child_resource.children().iter());
642        }
643    }
644
645    for (_, resource_refcell) in &resources {
646        let mut resource = resource_refcell.borrow_mut();
647        resource.process_claims();
648    }
649
650    // Push claimed resources to principals. We are interested in VMOs as the VMOs are the resources
651    // actually holding memory. We also keep track of the process to display its name in the output.
652    for (resource_id, resource_refcell) in &resources {
653        let resource = resource_refcell.borrow();
654        if let fplugin::ResourceType::Vmo(vmo) = &resource.resource.resource_type {
655            let mut ancestors = vec![*resource_id];
656            // VMOs created by reference don't behave like COW-clones; their byte count is always
657            // zero, and all pages are attributed to their parent. This is not what we want here,
658            // as we prefer to acknowledge that the pages are shared between the parent and its
659            // children. We currently don't have a way to formally distinguish between VMOs created
660            // by reference and the other VMOs, so we use the heuristic of checking that they
661            // report they are always empty.
662            if vmo.total_populated_bytes.unwrap_or_default() == 0 {
663                let mut current_parent = vmo.parent;
664                // Add the parents of a VMO as "Child" claims. This is done so that slices of VMOs,
665                // with possibly no memory of their own, get attributed the resources of their
666                // parent.
667                while let Some(parent_koid) = current_parent {
668                    if parent_koid == 0 {
669                        panic!("Parent is not None but 0.");
670                    }
671                    // Some VMOs' ancestry is self referential: their parent's koid is set to their
672                    // own koid. We want to detect this case and break early to avoid getting
673                    // stuck in an infinite loop.
674                    if parent_koid == resource.resource.koid {
675                        break;
676                    }
677                    ancestors.push(parent_koid);
678                    let mut current_resource = match resources.get(&parent_koid) {
679                        Some(res) => res.borrow_mut(),
680                        None => break,
681                    };
682                    current_resource.claims.extend(resource.claims.iter().map(|c| Claim {
683                        subject: c.subject,
684                        source: c.source,
685                        claim_type: ClaimType::Child,
686                    }));
687                    current_parent = match &current_resource.resource.resource_type {
688                        fplugin::ResourceType::Job(_) => panic!("This should not happen"),
689                        fplugin::ResourceType::Process(_) => panic!("This should not happen"),
690                        fplugin::ResourceType::Vmo(current_vmo) => current_vmo.parent,
691                        _ => unimplemented!(),
692                    };
693                }
694            }
695
696            for claim in &resource.claims {
697                principals
698                    .get(&claim.subject)
699                    .unwrap()
700                    .borrow_mut()
701                    .resources
702                    .extend(ancestors.iter());
703            }
704        } else if let fplugin::ResourceType::Process(_) = &resource.resource.resource_type {
705            for claim in &resource.claims {
706                principals
707                    .get(&claim.subject)
708                    .unwrap()
709                    .borrow_mut()
710                    .resources
711                    .insert(resource.resource.koid);
712            }
713        }
714    }
715
716    ProcessedAttributionData::new(
717        principals.into_iter().map(|(k, v)| (k, v.into_inner())).collect(),
718        resources.into_iter().map(|(k, v)| (k, v.into_inner())).collect(),
719        attribution_data.resource_names,
720    )
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use std::collections::HashMap;
727    use summary::{PrincipalSummary, VmoSummary};
728
729    #[test]
730    fn test_gather_resources() {
731        // Create a fake snapshot with 4 principals:
732        // root (1)
733        //  - runner (2)
734        //    - component 4 (4)
735        //  - component 3 (3)
736        //
737        // and the following job/process/vmo hierarchy:
738        // root_job (1000)
739        //  * root_process (1001)
740        //    . root_vmo (1002)
741        //    . shared_vmo (1003)
742        //  - runner_job (1004)
743        //    * runner_process (1005)
744        //      . runner_vmo (1006)
745        //      . component_vmo (1007)
746        //      . component_vmo2 (1012)
747        //      . component_vmo3 (1013)
748        //  - component_2_job (1008)
749        //    * 2_process (1009)
750        //      . 2_vmo (1010)
751        //      . shared_vmo (1003)
752        // And an additional parent VMO for 2_vmo, 2_vmo_parent (1011).
753
754        let resource_names = vec![
755            ZXName::from_string_lossy("root_job"),
756            ZXName::from_string_lossy("root_process"),
757            ZXName::from_string_lossy("root_vmo"),
758            ZXName::from_string_lossy("shared_vmo"),
759            ZXName::from_string_lossy("runner_job"),
760            ZXName::from_string_lossy("runner_process"),
761            ZXName::from_string_lossy("runner_vmo"),
762            ZXName::from_string_lossy("component_vmo"),
763            ZXName::from_string_lossy("component_2_job"),
764            ZXName::from_string_lossy("2_process"),
765            ZXName::from_string_lossy("2_vmo"),
766            ZXName::from_string_lossy("2_vmo_parent"),
767            ZXName::from_string_lossy("component_vmo_mapped"),
768            ZXName::from_string_lossy("component_vmo_mapped2"),
769        ];
770
771        let attributions = vec![
772            fplugin::Attribution {
773                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
774                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
775                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
776                ..Default::default()
777            },
778            fplugin::Attribution {
779                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
780                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
781                resources: Some(vec![fplugin::ResourceReference::KernelObject(1004)]),
782                ..Default::default()
783            },
784            fplugin::Attribution {
785                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
786                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
787                resources: Some(vec![fplugin::ResourceReference::KernelObject(1008)]),
788                ..Default::default()
789            },
790            fplugin::Attribution {
791                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
792                subject: Some(fplugin::PrincipalIdentifier { id: 4 }),
793                resources: Some(vec![
794                    fplugin::ResourceReference::KernelObject(1007),
795                    fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
796                        process: 1005,
797                        base: 1024,
798                        len: 1024,
799                        hint_skip_handle_table: false,
800                    }),
801                ]),
802                ..Default::default()
803            },
804        ]
805        .into_iter()
806        .map(|a| a.into())
807        .collect();
808
809        let principals = vec![
810            fplugin::Principal {
811                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
812                description: Some(fplugin::Description::Component("component_manager".to_owned())),
813                principal_type: Some(fplugin::PrincipalType::Runnable),
814                parent: None,
815                ..Default::default()
816            },
817            fplugin::Principal {
818                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
819                description: Some(fplugin::Description::Component("runner".to_owned())),
820                principal_type: Some(fplugin::PrincipalType::Runnable),
821                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
822                ..Default::default()
823            },
824            fplugin::Principal {
825                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
826                description: Some(fplugin::Description::Component("component 3".to_owned())),
827                principal_type: Some(fplugin::PrincipalType::Runnable),
828                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
829                ..Default::default()
830            },
831            fplugin::Principal {
832                identifier: Some(fplugin::PrincipalIdentifier { id: 4 }),
833                description: Some(fplugin::Description::Component("component 4".to_owned())),
834                principal_type: Some(fplugin::PrincipalType::Runnable),
835                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
836                ..Default::default()
837            },
838        ]
839        .into_iter()
840        .map(|p| p.into())
841        .collect();
842
843        let resources = vec![
844            fplugin::Resource {
845                koid: Some(1000),
846                name_index: Some(0),
847                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
848                    child_jobs: Some(vec![1004, 1008]),
849                    processes: Some(vec![1001]),
850                    ..Default::default()
851                })),
852                ..Default::default()
853            },
854            fplugin::Resource {
855                koid: Some(1001),
856                name_index: Some(1),
857                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
858                    vmos: Some(vec![1002, 1003]),
859                    mappings: None,
860                    ..Default::default()
861                })),
862                ..Default::default()
863            },
864            fplugin::Resource {
865                koid: Some(1002),
866                name_index: Some(2),
867                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
868                    private_committed_bytes: Some(1024),
869                    private_populated_bytes: Some(2048),
870                    scaled_committed_bytes: Some(1024),
871                    scaled_populated_bytes: Some(2048),
872                    total_committed_bytes: Some(1024),
873                    total_populated_bytes: Some(2048),
874                    ..Default::default()
875                })),
876                ..Default::default()
877            },
878            fplugin::Resource {
879                koid: Some(1003),
880                name_index: Some(3),
881                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
882                    private_committed_bytes: Some(1024),
883                    private_populated_bytes: Some(2048),
884                    scaled_committed_bytes: Some(1024),
885                    scaled_populated_bytes: Some(2048),
886                    total_committed_bytes: Some(1024),
887                    total_populated_bytes: Some(2048),
888                    ..Default::default()
889                })),
890                ..Default::default()
891            },
892            fplugin::Resource {
893                koid: Some(1004),
894                name_index: Some(4),
895                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
896                    child_jobs: Some(vec![]),
897                    processes: Some(vec![1005]),
898                    ..Default::default()
899                })),
900                ..Default::default()
901            },
902            fplugin::Resource {
903                koid: Some(1005),
904                name_index: Some(5),
905                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
906                    vmos: Some(vec![1006, 1007, 1012]),
907                    mappings: Some(vec![
908                        fplugin::Mapping {
909                            vmo: Some(1006),
910                            address_base: Some(0),
911                            size: Some(512),
912                            ..Default::default()
913                        },
914                        fplugin::Mapping {
915                            vmo: Some(1012),
916                            address_base: Some(1024),
917                            size: Some(512),
918                            ..Default::default()
919                        },
920                        fplugin::Mapping {
921                            vmo: Some(1013),
922                            address_base: Some(1536),
923                            size: Some(512),
924                            ..Default::default()
925                        },
926                        fplugin::Mapping {
927                            vmo: Some(1006),
928                            address_base: Some(2048),
929                            size: Some(512),
930                            ..Default::default()
931                        },
932                    ]),
933                    ..Default::default()
934                })),
935                ..Default::default()
936            },
937            fplugin::Resource {
938                koid: Some(1006),
939                name_index: Some(6),
940                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
941                    private_committed_bytes: Some(1024),
942                    private_populated_bytes: Some(2048),
943                    scaled_committed_bytes: Some(1024),
944                    scaled_populated_bytes: Some(2048),
945                    total_committed_bytes: Some(1024),
946                    total_populated_bytes: Some(2048),
947                    ..Default::default()
948                })),
949                ..Default::default()
950            },
951            fplugin::Resource {
952                koid: Some(1007),
953                name_index: Some(7),
954                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
955                    private_committed_bytes: Some(128),
956                    private_populated_bytes: Some(256),
957                    scaled_committed_bytes: Some(128),
958                    scaled_populated_bytes: Some(256),
959                    total_committed_bytes: Some(128),
960                    total_populated_bytes: Some(256),
961                    ..Default::default()
962                })),
963                ..Default::default()
964            },
965            fplugin::Resource {
966                koid: Some(1008),
967                name_index: Some(8),
968                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
969                    child_jobs: Some(vec![]),
970                    processes: Some(vec![1009]),
971                    ..Default::default()
972                })),
973                ..Default::default()
974            },
975            fplugin::Resource {
976                koid: Some(1009),
977                name_index: Some(9),
978                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
979                    vmos: Some(vec![1010, 1003]),
980                    mappings: None,
981                    ..Default::default()
982                })),
983                ..Default::default()
984            },
985            fplugin::Resource {
986                koid: Some(1010),
987                name_index: Some(10),
988                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
989                    parent: Some(1011),
990                    private_committed_bytes: Some(1024),
991                    private_populated_bytes: Some(2048),
992                    scaled_committed_bytes: Some(1024),
993                    scaled_populated_bytes: Some(2048),
994                    total_committed_bytes: Some(1024),
995                    total_populated_bytes: Some(2048),
996                    ..Default::default()
997                })),
998                ..Default::default()
999            },
1000            fplugin::Resource {
1001                koid: Some(1011),
1002                name_index: Some(11),
1003                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1004                    private_committed_bytes: Some(1024),
1005                    private_populated_bytes: Some(2048),
1006                    scaled_committed_bytes: Some(1024),
1007                    scaled_populated_bytes: Some(2048),
1008                    total_committed_bytes: Some(1024),
1009                    total_populated_bytes: Some(2048),
1010                    ..Default::default()
1011                })),
1012                ..Default::default()
1013            },
1014            fplugin::Resource {
1015                koid: Some(1012),
1016                name_index: Some(12),
1017                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1018                    private_committed_bytes: Some(1024),
1019                    private_populated_bytes: Some(2048),
1020                    scaled_committed_bytes: Some(1024),
1021                    scaled_populated_bytes: Some(2048),
1022                    total_committed_bytes: Some(1024),
1023                    total_populated_bytes: Some(2048),
1024                    ..Default::default()
1025                })),
1026                ..Default::default()
1027            },
1028            fplugin::Resource {
1029                koid: Some(1013),
1030                name_index: Some(13),
1031                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1032                    private_committed_bytes: Some(1024),
1033                    private_populated_bytes: Some(2048),
1034                    scaled_committed_bytes: Some(1024),
1035                    scaled_populated_bytes: Some(2048),
1036                    total_committed_bytes: Some(1024),
1037                    total_populated_bytes: Some(2048),
1038                    ..Default::default()
1039                })),
1040                ..Default::default()
1041            },
1042        ]
1043        .into_iter()
1044        .map(|r| r.into())
1045        .collect();
1046
1047        let output = attribute_vmos(AttributionData {
1048            principals_vec: principals,
1049            resources_vec: resources,
1050            resource_names,
1051            attributions,
1052        })
1053        .summary();
1054
1055        assert_eq!(output.unclaimed, 2048);
1056        assert_eq!(output.principals.len(), 4);
1057
1058        let principals: HashMap<u64, PrincipalSummary> =
1059            output.principals.into_iter().map(|p| (p.id, p)).collect();
1060
1061        assert_eq!(
1062            principals.get(&1).unwrap(),
1063            &PrincipalSummary {
1064                id: 1,
1065                name: "component_manager".to_owned(),
1066                principal_type: "R".to_owned(),
1067                committed_private: 1024,
1068                committed_scaled: 1536.0,
1069                committed_total: 2048,
1070                populated_private: 2048,
1071                populated_scaled: 3072.0,
1072                populated_total: 4096,
1073                attributor: None,
1074                processes: vec!["root_process (1001)".to_owned()],
1075                vmos: vec![
1076                    (
1077                        ZXName::from_string_lossy("root_vmo"),
1078                        VmoSummary {
1079                            count: 1,
1080                            committed_private: 1024,
1081                            committed_scaled: 1024.0,
1082                            committed_total: 1024,
1083                            populated_private: 2048,
1084                            populated_scaled: 2048.0,
1085                            populated_total: 2048,
1086                            ..Default::default()
1087                        }
1088                    ),
1089                    (
1090                        ZXName::from_string_lossy("shared_vmo"),
1091                        VmoSummary {
1092                            count: 1,
1093                            committed_private: 0,
1094                            committed_scaled: 512.0,
1095                            committed_total: 1024,
1096                            populated_private: 0,
1097                            populated_scaled: 1024.0,
1098                            populated_total: 2048,
1099                            ..Default::default()
1100                        }
1101                    )
1102                ]
1103                .into_iter()
1104                .collect(),
1105            }
1106        );
1107
1108        assert_eq!(
1109            principals.get(&2).unwrap(),
1110            &PrincipalSummary {
1111                id: 2,
1112                name: "runner".to_owned(),
1113                principal_type: "R".to_owned(),
1114                committed_private: 1024,
1115                committed_scaled: 1024.0,
1116                committed_total: 1024,
1117                populated_private: 2048,
1118                populated_scaled: 2048.0,
1119                populated_total: 2048,
1120                attributor: Some("component_manager".to_owned()),
1121                processes: vec!["runner_process (1005)".to_owned()],
1122                vmos: vec![(
1123                    ZXName::from_string_lossy("runner_vmo"),
1124                    VmoSummary {
1125                        count: 1,
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                        ..Default::default()
1133                    }
1134                )]
1135                .into_iter()
1136                .collect(),
1137            }
1138        );
1139
1140        assert_eq!(
1141            principals.get(&3).unwrap(),
1142            &PrincipalSummary {
1143                id: 3,
1144                name: "component 3".to_owned(),
1145                principal_type: "R".to_owned(),
1146                committed_private: 1024,
1147                committed_scaled: 1536.0,
1148                committed_total: 2048,
1149                populated_private: 2048,
1150                populated_scaled: 3072.0,
1151                populated_total: 4096,
1152                attributor: Some("component_manager".to_owned()),
1153                processes: vec!["2_process (1009)".to_owned()],
1154                vmos: vec![
1155                    (
1156                        ZXName::from_string_lossy("shared_vmo"),
1157                        VmoSummary {
1158                            count: 1,
1159                            committed_private: 0,
1160                            committed_scaled: 512.0,
1161                            committed_total: 1024,
1162                            populated_private: 0,
1163                            populated_scaled: 1024.0,
1164                            populated_total: 2048,
1165                            ..Default::default()
1166                        }
1167                    ),
1168                    (
1169                        ZXName::from_string_lossy("2_vmo"),
1170                        VmoSummary {
1171                            count: 1,
1172                            committed_private: 1024,
1173                            committed_scaled: 1024.0,
1174                            committed_total: 1024,
1175                            populated_private: 2048,
1176                            populated_scaled: 2048.0,
1177                            populated_total: 2048,
1178                            ..Default::default()
1179                        }
1180                    )
1181                ]
1182                .into_iter()
1183                .collect(),
1184            }
1185        );
1186
1187        assert_eq!(
1188            principals.get(&4).unwrap(),
1189            &PrincipalSummary {
1190                id: 4,
1191                name: "component 4".to_owned(),
1192                principal_type: "R".to_owned(),
1193                committed_private: 2176,
1194                committed_scaled: 2176.0,
1195                committed_total: 2176,
1196                populated_private: 4352,
1197                populated_scaled: 4352.0,
1198                populated_total: 4352,
1199                attributor: Some("runner".to_owned()),
1200                processes: vec!["runner_process (1005)".to_owned()],
1201                vmos: vec![
1202                    (
1203                        ZXName::from_string_lossy("component_vmo"),
1204                        VmoSummary {
1205                            count: 1,
1206                            committed_private: 128,
1207                            committed_scaled: 128.0,
1208                            committed_total: 128,
1209                            populated_private: 256,
1210                            populated_scaled: 256.0,
1211                            populated_total: 256,
1212                            ..Default::default()
1213                        }
1214                    ),
1215                    (
1216                        ZXName::from_string_lossy("component_vmo_mapped"),
1217                        VmoSummary {
1218                            count: 1,
1219                            committed_private: 1024,
1220                            committed_scaled: 1024.0,
1221                            committed_total: 1024,
1222                            populated_private: 2048,
1223                            populated_scaled: 2048.0,
1224                            populated_total: 2048,
1225                            ..Default::default()
1226                        }
1227                    ),
1228                    (
1229                        ZXName::from_string_lossy("component_vmo_mapped2"),
1230                        VmoSummary {
1231                            count: 1,
1232                            committed_private: 1024,
1233                            committed_scaled: 1024.0,
1234                            committed_total: 1024,
1235                            populated_private: 2048,
1236                            populated_scaled: 2048.0,
1237                            populated_total: 2048,
1238                            ..Default::default()
1239                        }
1240                    )
1241                ]
1242                .into_iter()
1243                .collect(),
1244            }
1245        );
1246    }
1247
1248    #[test]
1249    fn test_reshare_resources() {
1250        // Create a fake snapshot with 3 principals:
1251        // root (id: 1)
1252        //  - component 2 (id: 1)
1253        //    - component 3 (id: 2)
1254        //
1255        // and the following job/process/vmo hierarchy:
1256        // root_job (1000)
1257        //  - component_job (1001)
1258        //    * component_process (1002)
1259        //      . component_vmo (1003)
1260        //
1261        // In this scenario, component 2 reattributes component_job to component 3 entirely.
1262
1263        let resource_names = vec![
1264            ZXName::from_string_lossy("root_job"),
1265            ZXName::from_string_lossy("component_job"),
1266            ZXName::from_string_lossy("component_process"),
1267            ZXName::from_string_lossy("component_vmo"),
1268        ];
1269        let attributions = vec![
1270            fplugin::Attribution {
1271                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1272                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1273                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1274                ..Default::default()
1275            },
1276            fplugin::Attribution {
1277                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1278                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1279                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1280                ..Default::default()
1281            },
1282            fplugin::Attribution {
1283                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
1284                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
1285                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1286                ..Default::default()
1287            },
1288        ]
1289        .into_iter()
1290        .map(|a| a.into())
1291        .collect();
1292        let principals = vec![
1293            fplugin::Principal {
1294                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1295                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1296                principal_type: Some(fplugin::PrincipalType::Runnable),
1297                parent: None,
1298                ..Default::default()
1299            },
1300            fplugin::Principal {
1301                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1302                description: Some(fplugin::Description::Component("component 2".to_owned())),
1303                principal_type: Some(fplugin::PrincipalType::Runnable),
1304                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1305                ..Default::default()
1306            },
1307            fplugin::Principal {
1308                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
1309                description: Some(fplugin::Description::Component("component 3".to_owned())),
1310                principal_type: Some(fplugin::PrincipalType::Runnable),
1311                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
1312                ..Default::default()
1313            },
1314        ]
1315        .into_iter()
1316        .map(|p| p.into())
1317        .collect();
1318
1319        let resources = vec![
1320            fplugin::Resource {
1321                koid: Some(1000),
1322                name_index: Some(0),
1323                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1324                    child_jobs: Some(vec![1001]),
1325                    processes: Some(vec![]),
1326                    ..Default::default()
1327                })),
1328                ..Default::default()
1329            },
1330            fplugin::Resource {
1331                koid: Some(1001),
1332                name_index: Some(1),
1333                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1334                    child_jobs: Some(vec![]),
1335                    processes: Some(vec![1002]),
1336                    ..Default::default()
1337                })),
1338                ..Default::default()
1339            },
1340            fplugin::Resource {
1341                koid: Some(1002),
1342                name_index: Some(2),
1343                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1344                    vmos: Some(vec![1003]),
1345                    mappings: None,
1346                    ..Default::default()
1347                })),
1348                ..Default::default()
1349            },
1350            fplugin::Resource {
1351                koid: Some(1003),
1352                name_index: Some(3),
1353                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1354                    private_committed_bytes: Some(1024),
1355                    private_populated_bytes: Some(2048),
1356                    scaled_committed_bytes: Some(1024),
1357                    scaled_populated_bytes: Some(2048),
1358                    total_committed_bytes: Some(1024),
1359                    total_populated_bytes: Some(2048),
1360                    ..Default::default()
1361                })),
1362                ..Default::default()
1363            },
1364        ]
1365        .into_iter()
1366        .map(|r| r.into())
1367        .collect();
1368
1369        let output = attribute_vmos(AttributionData {
1370            principals_vec: principals,
1371            resources_vec: resources,
1372            resource_names,
1373            attributions,
1374        })
1375        .summary();
1376
1377        assert_eq!(output.unclaimed, 0);
1378        assert_eq!(output.principals.len(), 3);
1379
1380        let principals: HashMap<u64, PrincipalSummary> =
1381            output.principals.into_iter().map(|p| (p.id, p)).collect();
1382
1383        assert_eq!(
1384            principals.get(&1).unwrap(),
1385            &PrincipalSummary {
1386                id: 1,
1387                name: "component_manager".to_owned(),
1388                principal_type: "R".to_owned(),
1389                committed_private: 0,
1390                committed_scaled: 0.0,
1391                committed_total: 0,
1392                populated_private: 0,
1393                populated_scaled: 0.0,
1394                populated_total: 0,
1395                attributor: None,
1396                processes: vec![],
1397                vmos: vec![].into_iter().collect(),
1398            }
1399        );
1400
1401        assert_eq!(
1402            principals.get(&2).unwrap(),
1403            &PrincipalSummary {
1404                id: 2,
1405                name: "component 2".to_owned(),
1406                principal_type: "R".to_owned(),
1407                committed_private: 0,
1408                committed_scaled: 0.0,
1409                committed_total: 0,
1410                populated_private: 0,
1411                populated_scaled: 0.0,
1412                populated_total: 0,
1413                attributor: Some("component_manager".to_owned()),
1414                processes: vec![],
1415                vmos: vec![].into_iter().collect(),
1416            }
1417        );
1418
1419        assert_eq!(
1420            principals.get(&3).unwrap(),
1421            &PrincipalSummary {
1422                id: 3,
1423                name: "component 3".to_owned(),
1424                principal_type: "R".to_owned(),
1425                committed_private: 1024,
1426                committed_scaled: 1024.0,
1427                committed_total: 1024,
1428                populated_private: 2048,
1429                populated_scaled: 2048.0,
1430                populated_total: 2048,
1431                attributor: Some("component 2".to_owned()),
1432                processes: vec!["component_process (1002)".to_owned()],
1433                vmos: vec![(
1434                    ZXName::from_string_lossy("component_vmo"),
1435                    VmoSummary {
1436                        count: 1,
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                        ..Default::default()
1444                    }
1445                ),]
1446                .into_iter()
1447                .collect(),
1448            }
1449        );
1450    }
1451
1452    #[test]
1453    fn test_conversions() {
1454        let plugin_principal = fplugin::Principal {
1455            identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1456            description: Some(fplugin::Description::Component("component_manager".to_owned())),
1457            principal_type: Some(fplugin::PrincipalType::Runnable),
1458            parent: None,
1459            ..Default::default()
1460        };
1461
1462        let data_principal: Principal = plugin_principal.clone().into();
1463
1464        assert_eq!(plugin_principal, data_principal.into());
1465
1466        let plugin_resources = vec![
1467            fplugin::Resource {
1468                koid: Some(1000),
1469                name_index: Some(0),
1470                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1471                    child_jobs: Some(vec![1004, 1008]),
1472                    processes: Some(vec![1001]),
1473                    ..Default::default()
1474                })),
1475                ..Default::default()
1476            },
1477            fplugin::Resource {
1478                koid: Some(1001),
1479                name_index: Some(1),
1480                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1481                    vmos: Some(vec![1002, 1003]),
1482                    mappings: None,
1483                    ..Default::default()
1484                })),
1485                ..Default::default()
1486            },
1487            fplugin::Resource {
1488                koid: Some(1002),
1489                name_index: Some(2),
1490                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1491                    private_committed_bytes: Some(1024),
1492                    private_populated_bytes: Some(2048),
1493                    scaled_committed_bytes: Some(1024),
1494                    scaled_populated_bytes: Some(2048),
1495                    total_committed_bytes: Some(1024),
1496                    total_populated_bytes: Some(2048),
1497                    ..Default::default()
1498                })),
1499                ..Default::default()
1500            },
1501            fplugin::Resource {
1502                koid: Some(1005),
1503                name_index: Some(5),
1504                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1505                    vmos: Some(vec![1006, 1007, 1012]),
1506                    mappings: Some(vec![
1507                        fplugin::Mapping {
1508                            vmo: Some(1006),
1509                            address_base: Some(0),
1510                            size: Some(512),
1511                            ..Default::default()
1512                        },
1513                        fplugin::Mapping {
1514                            vmo: Some(1012),
1515                            address_base: Some(1024),
1516                            size: Some(512),
1517                            ..Default::default()
1518                        },
1519                    ]),
1520                    ..Default::default()
1521                })),
1522                ..Default::default()
1523            },
1524        ];
1525
1526        let data_resources: Vec<Resource> =
1527            plugin_resources.iter().cloned().map(|r| r.into()).collect();
1528
1529        let actual_resources: Vec<fplugin::Resource> =
1530            data_resources.into_iter().map(|r| r.into()).collect();
1531
1532        assert_eq!(plugin_resources, actual_resources);
1533    }
1534
1535    #[test]
1536    fn test_vmo_reference() {
1537        // Create a fake snapshot with 3 principals:
1538        // root (1)
1539        //  - component 2 (2)
1540        //
1541        // and the following job/process/vmo hierarchy:
1542        // root_job (1000)
1543        //  - component_job (1001)
1544        //    * component_process (1002)
1545        //      . component_vmo (1003)
1546        // component_vmo_parent (1004)
1547        //
1548        // In this scenario, component_vmo is a reference to component_vmo_parent and should get
1549        // shared attribution of its pages.
1550
1551        let resource_names = vec![
1552            name::ZXName::from_string_lossy("root_job"),
1553            name::ZXName::from_string_lossy("component_job"),
1554            name::ZXName::from_string_lossy("component_process"),
1555            name::ZXName::from_string_lossy("component_vmo"),
1556            name::ZXName::from_string_lossy("component_vmo_parent"),
1557        ];
1558        let attributions = vec![
1559            fplugin::Attribution {
1560                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1561                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1562                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1563                ..Default::default()
1564            },
1565            fplugin::Attribution {
1566                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1567                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1568                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1569                ..Default::default()
1570            },
1571        ]
1572        .into_iter()
1573        .map(|a| a.into())
1574        .collect();
1575        let principals = vec![
1576            fplugin::Principal {
1577                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1578                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1579                principal_type: Some(fplugin::PrincipalType::Runnable),
1580                parent: None,
1581                ..Default::default()
1582            },
1583            fplugin::Principal {
1584                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1585                description: Some(fplugin::Description::Component("component 2".to_owned())),
1586                principal_type: Some(fplugin::PrincipalType::Runnable),
1587                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1588                ..Default::default()
1589            },
1590        ]
1591        .into_iter()
1592        .map(|p| p.into())
1593        .collect();
1594
1595        let resources = vec![
1596            fplugin::Resource {
1597                koid: Some(1000),
1598                name_index: Some(0),
1599                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1600                    child_jobs: Some(vec![1001]),
1601                    processes: Some(vec![]),
1602                    ..Default::default()
1603                })),
1604                ..Default::default()
1605            },
1606            fplugin::Resource {
1607                koid: Some(1001),
1608                name_index: Some(1),
1609                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1610                    child_jobs: Some(vec![]),
1611                    processes: Some(vec![1002]),
1612                    ..Default::default()
1613                })),
1614                ..Default::default()
1615            },
1616            fplugin::Resource {
1617                koid: Some(1002),
1618                name_index: Some(2),
1619                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1620                    vmos: Some(vec![1003]),
1621                    mappings: None,
1622                    ..Default::default()
1623                })),
1624                ..Default::default()
1625            },
1626            fplugin::Resource {
1627                koid: Some(1003),
1628                name_index: Some(3),
1629                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1630                    parent: Some(1004),
1631                    private_committed_bytes: Some(0),
1632                    private_populated_bytes: Some(0),
1633                    scaled_committed_bytes: Some(0),
1634                    scaled_populated_bytes: Some(0),
1635                    total_committed_bytes: Some(0),
1636                    total_populated_bytes: Some(0),
1637                    ..Default::default()
1638                })),
1639                ..Default::default()
1640            },
1641            fplugin::Resource {
1642                koid: Some(1004),
1643                name_index: Some(4),
1644                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1645                    private_committed_bytes: Some(1024),
1646                    private_populated_bytes: Some(2048),
1647                    scaled_committed_bytes: Some(1024),
1648                    scaled_populated_bytes: Some(2048),
1649                    total_committed_bytes: Some(1024),
1650                    total_populated_bytes: Some(2048),
1651                    ..Default::default()
1652                })),
1653                ..Default::default()
1654            },
1655        ]
1656        .into_iter()
1657        .map(|r| r.into())
1658        .collect();
1659
1660        let output = attribute_vmos(AttributionData {
1661            principals_vec: principals,
1662            resources_vec: resources,
1663            resource_names,
1664            attributions,
1665        })
1666        .summary();
1667
1668        assert_eq!(output.unclaimed, 0);
1669        assert_eq!(output.principals.len(), 2);
1670
1671        let principals: HashMap<u64, PrincipalSummary> =
1672            output.principals.into_iter().map(|p| (p.id, p)).collect();
1673
1674        assert_eq!(
1675            principals.get(&1).unwrap(),
1676            &PrincipalSummary {
1677                id: 1,
1678                name: "component_manager".to_owned(),
1679                principal_type: "R".to_owned(),
1680                committed_private: 0,
1681                committed_scaled: 0.0,
1682                committed_total: 0,
1683                populated_private: 0,
1684                populated_scaled: 0.0,
1685                populated_total: 0,
1686                attributor: None,
1687                processes: vec![],
1688                vmos: vec![].into_iter().collect(),
1689            }
1690        );
1691
1692        assert_eq!(
1693            principals.get(&2).unwrap(),
1694            &PrincipalSummary {
1695                id: 2,
1696                name: "component 2".to_owned(),
1697                principal_type: "R".to_owned(),
1698                committed_private: 1024,
1699                committed_scaled: 1024.0,
1700                committed_total: 1024,
1701                populated_private: 2048,
1702                populated_scaled: 2048.0,
1703                populated_total: 2048,
1704                attributor: Some("component_manager".to_owned()),
1705                processes: vec!["component_process (1002)".to_owned()],
1706                vmos: vec![
1707                    (
1708                        name::ZXName::from_string_lossy("component_vmo"),
1709                        VmoSummary {
1710                            count: 1,
1711                            committed_private: 0,
1712                            committed_scaled: 0.0,
1713                            committed_total: 0,
1714                            populated_private: 0,
1715                            populated_scaled: 0.0,
1716                            populated_total: 0,
1717                            ..Default::default()
1718                        }
1719                    ),
1720                    (
1721                        name::ZXName::from_string_lossy("component_vmo_parent"),
1722                        VmoSummary {
1723                            count: 1,
1724                            committed_private: 1024,
1725                            committed_scaled: 1024.0,
1726                            committed_total: 1024,
1727                            populated_private: 2048,
1728                            populated_scaled: 2048.0,
1729                            populated_total: 2048,
1730                            ..Default::default()
1731                        }
1732                    ),
1733                ]
1734                .into_iter()
1735                .collect(),
1736            }
1737        );
1738    }
1739}