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 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            fidl_fuchsia_memory_attribution_plugin::ResourceReference::KernelObject(ko) => {
446                ResourceReference::KernelObject(ko)
447            }
448            fidl_fuchsia_memory_attribution_plugin::ResourceReference::ProcessMapped(
449                fplugin::ProcessMapped { process, base, len, hint_skip_handle_table },
450            ) => ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table },
451            _ => unimplemented!(),
452        }
453    }
454}
455
456impl Into<fplugin::ResourceReference> for ResourceReference {
457    fn into(self) -> fplugin::ResourceReference {
458        match self {
459            ResourceReference::KernelObject(ko) => {
460                fidl_fuchsia_memory_attribution_plugin::ResourceReference::KernelObject(ko)
461            }
462            ResourceReference::ProcessMapped { process, base, len, hint_skip_handle_table } => {
463                fidl_fuchsia_memory_attribution_plugin::ResourceReference::ProcessMapped(
464                    fplugin::ProcessMapped { process, base, len, hint_skip_handle_table },
465                )
466            }
467        }
468    }
469}
470
471/// Capture of the current memory usage of a device, as retrieved through the memory attribution
472/// protocol. In this object, memory attribution is not resolved.
473pub struct AttributionData {
474    pub principals_vec: Vec<Principal>,
475    pub resources_vec: Vec<Resource>,
476    pub resource_names: Vec<ZXName>,
477    pub attributions: Vec<Attribution>,
478}
479
480pub trait AttributionDataProvider: Send + Sync {
481    /// Returns all the memory resources and attribution specifications.
482    fn get_attribution_data(&self) -> Result<AttributionData, anyhow::Error>;
483}
484
485/// Processed snapshot of the memory usage of a device, with attribution of memory resources to
486/// Principals resolved.
487pub struct ProcessedAttributionData {
488    pub principals: HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
489    pub resources: HashMap<u64, InflatedResource>,
490    pub resource_names: Vec<ZXName>,
491}
492
493impl ProcessedAttributionData {
494    fn new(
495        principals: HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
496        resources: HashMap<u64, InflatedResource>,
497        resource_names: Vec<ZXName>,
498    ) -> Self {
499        Self { principals, resources, resource_names }
500    }
501
502    /// Create a summary view of the memory attribution_data. See [MemorySummary] for details.
503    pub fn summary(&self) -> MemorySummary {
504        #[cfg(target_os = "fuchsia")]
505        duration!(CATEGORY_MEMORY_CAPTURE, c"ProcessedAttributionData::summary");
506        MemorySummary::build(&self.principals, &self.resources, &self.resource_names)
507    }
508}
509
510/// Process data from a [AttributionData] to resolve attribution claims.
511pub fn attribute_vmos(attribution_data: AttributionData) -> ProcessedAttributionData {
512    #[cfg(target_os = "fuchsia")]
513    duration!(CATEGORY_MEMORY_CAPTURE, c"attribute_vmos");
514
515    // Map from moniker token ID to Principal struct.
516    let principals: HashMap<GlobalPrincipalIdentifier, RefCell<InflatedPrincipal>> =
517        attribution_data
518            .principals_vec
519            .into_iter()
520            .map(|p| (p.identifier.clone(), RefCell::new(InflatedPrincipal::new(p))))
521            .collect();
522
523    // Map from kernel resource koid to Resource struct.
524    let mut resources: HashMap<u64, RefCell<InflatedResource>> = attribution_data
525        .resources_vec
526        .into_iter()
527        .map(|r| (r.koid, RefCell::new(InflatedResource::new(r))))
528        .collect();
529
530    // Add direct claims to resources.
531    for attribution in attribution_data.attributions {
532        principals.get(&attribution.subject.clone().into()).map(|p| {
533            p.borrow_mut().attribution_claims.insert(attribution.source.into(), attribution.clone())
534        });
535        for resource in attribution.resources {
536            match resource {
537                ResourceReference::KernelObject(koid) => {
538                    if !resources.contains_key(&koid) {
539                        continue;
540                    }
541                    resources.get_mut(&koid).unwrap().get_mut().claims.insert(Claim {
542                        source: attribution.source.into(),
543                        subject: attribution.subject.into(),
544                        claim_type: ClaimType::Direct,
545                    });
546                }
547                ResourceReference::ProcessMapped {
548                    process,
549                    base,
550                    len,
551                    hint_skip_handle_table: _,
552                } => {
553                    if !resources.contains_key(&process) {
554                        continue;
555                    }
556                    let mut matched_vmos = Vec::new();
557                    if let fplugin::ResourceType::Process(process_data) =
558                        &resources.get(&process).unwrap().borrow().resource.resource_type
559                    {
560                        for mapping in process_data.mappings.iter().flatten() {
561                            // We consider an entire VMO to be matched if it has a mapping
562                            // within the claimed region.
563                            if mapping.address_base.unwrap() >= base
564                                && mapping.address_base.unwrap() + mapping.size.unwrap()
565                                    <= base + len
566                            {
567                                matched_vmos.push(mapping.vmo.unwrap());
568                            }
569                        }
570                    }
571                    for vmo_koid in matched_vmos {
572                        match resources.get_mut(&vmo_koid) {
573                            Some(resource) => {
574                                resource.get_mut().claims.insert(Claim {
575                                    source: attribution.source.into(),
576                                    subject: attribution.subject.into(),
577                                    claim_type: ClaimType::Direct,
578                                });
579                            }
580                            None => {
581                                // The VMO is unknown. This can happen when a VMO is created between
582                                // the collection of the list of VMOs and the collection of the
583                                // process mappings.
584                            }
585                        }
586                    }
587                }
588            }
589        }
590    }
591
592    // Propagate claims. We propagate direct claims to child resources recursively until we hit a
593    // resource that is directly claimed: this is because we consider that attributors deeper in the
594    // principal hierarchy will not attribute resources higher in the resource hierarchy than the
595    // ones attributed by their ancestors (ie. attribution is always more precise as we go deeper).
596    for (_, resource_refcell) in &resources {
597        let resource = resource_refcell.borrow_mut();
598        // Extract the list of direct claims to propagate.
599        let direct_claims: Vec<&Claim> = resource
600            .claims
601            .iter()
602            .filter(|claim| match claim.claim_type {
603                ClaimType::Direct => true,
604                _ => false,
605            })
606            .collect();
607
608        if direct_claims.is_empty() {
609            // There is no direct claim to propagate, we can skip this resource.
610            continue;
611        }
612
613        let propagated_claims: Vec<Claim> = direct_claims
614            .into_iter()
615            .map(|claim| Claim {
616                source: claim.source,
617                subject: claim.subject,
618                claim_type: ClaimType::Indirect,
619            })
620            .collect();
621        let mut frontier = Vec::new();
622        frontier.extend(resource.children());
623        while !frontier.is_empty() {
624            let child = frontier.pop().unwrap();
625            let mut child_resource = match resources.get(&child) {
626                Some(resource) => resource.borrow_mut(),
627                None => {
628                    // This can happen if a resource is created or disappears while we were
629                    // collecting information about all the resources in the system. This should
630                    // remain a rare event.
631                    continue;
632                }
633            };
634            if child_resource.claims.iter().any(|c| c.claim_type == ClaimType::Direct) {
635                // If there is a direct claim on the resource, don't propagate.
636                continue;
637            }
638            child_resource.claims.extend(propagated_claims.clone().iter());
639            frontier.extend(child_resource.children().iter());
640        }
641    }
642
643    for (_, resource_refcell) in &resources {
644        let mut resource = resource_refcell.borrow_mut();
645        resource.process_claims();
646    }
647
648    // Push claimed resources to principals. We are interested in VMOs as the VMOs are the resources
649    // actually holding memory. We also keep track of the process to display its name in the output.
650    for (resource_id, resource_refcell) in &resources {
651        let resource = resource_refcell.borrow();
652        if let fplugin::ResourceType::Vmo(vmo) = &resource.resource.resource_type {
653            let mut ancestors = vec![*resource_id];
654            // VMOs created by reference don't behave like COW-clones; their byte count is always
655            // zero, and all pages are attributed to their parent. This is not what we want here,
656            // as we prefer to acknowledge that the pages are shared between the parent and its
657            // children. We currently don't have a way to formally distinguish between VMOs created
658            // by reference and the other VMOs, so we use the heuristic of checking that they
659            // report they are always empty.
660            if vmo.total_populated_bytes.unwrap_or_default() == 0 {
661                let mut current_parent = vmo.parent;
662                // Add the parents of a VMO as "Child" claims. This is done so that slices of VMOs,
663                // with possibly no memory of their own, get attributed the resources of their
664                // parent.
665                while let Some(parent_koid) = current_parent {
666                    if parent_koid == 0 {
667                        panic!("Parent is not None but 0.");
668                    }
669                    // Some VMOs' ancestry is self referential: their parent's koid is set to their
670                    // own koid. We want to detect this case and break early to avoid getting
671                    // stuck in an infinite loop.
672                    if parent_koid == resource.resource.koid {
673                        break;
674                    }
675                    ancestors.push(parent_koid);
676                    let mut current_resource = match resources.get(&parent_koid) {
677                        Some(res) => res.borrow_mut(),
678                        None => break,
679                    };
680                    current_resource.claims.extend(resource.claims.iter().map(|c| Claim {
681                        subject: c.subject,
682                        source: c.source,
683                        claim_type: ClaimType::Child,
684                    }));
685                    current_parent = match &current_resource.resource.resource_type {
686                        fplugin::ResourceType::Job(_) => panic!("This should not happen"),
687                        fplugin::ResourceType::Process(_) => panic!("This should not happen"),
688                        fplugin::ResourceType::Vmo(current_vmo) => current_vmo.parent,
689                        _ => unimplemented!(),
690                    };
691                }
692            }
693
694            for claim in &resource.claims {
695                principals
696                    .get(&claim.subject)
697                    .unwrap()
698                    .borrow_mut()
699                    .resources
700                    .extend(ancestors.iter());
701            }
702        } else if let fplugin::ResourceType::Process(_) = &resource.resource.resource_type {
703            for claim in &resource.claims {
704                principals
705                    .get(&claim.subject)
706                    .unwrap()
707                    .borrow_mut()
708                    .resources
709                    .insert(resource.resource.koid);
710            }
711        }
712    }
713
714    ProcessedAttributionData::new(
715        principals.into_iter().map(|(k, v)| (k, v.into_inner())).collect(),
716        resources.into_iter().map(|(k, v)| (k, v.into_inner())).collect(),
717        attribution_data.resource_names,
718    )
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use std::collections::HashMap;
725    use summary::{PrincipalSummary, VmoSummary};
726
727    #[test]
728    fn test_gather_resources() {
729        // Create a fake snapshot with 4 principals:
730        // root (1)
731        //  - runner (2)
732        //    - component 4 (4)
733        //  - component 3 (3)
734        //
735        // and the following job/process/vmo hierarchy:
736        // root_job (1000)
737        //  * root_process (1001)
738        //    . root_vmo (1002)
739        //    . shared_vmo (1003)
740        //  - runner_job (1004)
741        //    * runner_process (1005)
742        //      . runner_vmo (1006)
743        //      . component_vmo (1007)
744        //      . component_vmo2 (1012)
745        //      . component_vmo3 (1013)
746        //  - component_2_job (1008)
747        //    * 2_process (1009)
748        //      . 2_vmo (1010)
749        //      . shared_vmo (1003)
750        // And an additional parent VMO for 2_vmo, 2_vmo_parent (1011).
751
752        let resource_names = vec![
753            ZXName::from_string_lossy("root_job"),
754            ZXName::from_string_lossy("root_process"),
755            ZXName::from_string_lossy("root_vmo"),
756            ZXName::from_string_lossy("shared_vmo"),
757            ZXName::from_string_lossy("runner_job"),
758            ZXName::from_string_lossy("runner_process"),
759            ZXName::from_string_lossy("runner_vmo"),
760            ZXName::from_string_lossy("component_vmo"),
761            ZXName::from_string_lossy("component_2_job"),
762            ZXName::from_string_lossy("2_process"),
763            ZXName::from_string_lossy("2_vmo"),
764            ZXName::from_string_lossy("2_vmo_parent"),
765            ZXName::from_string_lossy("component_vmo_mapped"),
766            ZXName::from_string_lossy("component_vmo_mapped2"),
767        ];
768
769        let attributions = vec![
770            fplugin::Attribution {
771                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
772                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
773                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
774                ..Default::default()
775            },
776            fplugin::Attribution {
777                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
778                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
779                resources: Some(vec![fplugin::ResourceReference::KernelObject(1004)]),
780                ..Default::default()
781            },
782            fplugin::Attribution {
783                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
784                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
785                resources: Some(vec![fplugin::ResourceReference::KernelObject(1008)]),
786                ..Default::default()
787            },
788            fplugin::Attribution {
789                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
790                subject: Some(fplugin::PrincipalIdentifier { id: 4 }),
791                resources: Some(vec![
792                    fplugin::ResourceReference::KernelObject(1007),
793                    fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
794                        process: 1005,
795                        base: 1024,
796                        len: 1024,
797                        hint_skip_handle_table: false,
798                    }),
799                ]),
800                ..Default::default()
801            },
802        ]
803        .into_iter()
804        .map(|a| a.into())
805        .collect();
806
807        let principals = vec![
808            fplugin::Principal {
809                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
810                description: Some(fplugin::Description::Component("component_manager".to_owned())),
811                principal_type: Some(fplugin::PrincipalType::Runnable),
812                parent: None,
813                ..Default::default()
814            },
815            fplugin::Principal {
816                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
817                description: Some(fplugin::Description::Component("runner".to_owned())),
818                principal_type: Some(fplugin::PrincipalType::Runnable),
819                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
820                ..Default::default()
821            },
822            fplugin::Principal {
823                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
824                description: Some(fplugin::Description::Component("component 3".to_owned())),
825                principal_type: Some(fplugin::PrincipalType::Runnable),
826                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
827                ..Default::default()
828            },
829            fplugin::Principal {
830                identifier: Some(fplugin::PrincipalIdentifier { id: 4 }),
831                description: Some(fplugin::Description::Component("component 4".to_owned())),
832                principal_type: Some(fplugin::PrincipalType::Runnable),
833                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
834                ..Default::default()
835            },
836        ]
837        .into_iter()
838        .map(|p| p.into())
839        .collect();
840
841        let resources = vec![
842            fplugin::Resource {
843                koid: Some(1000),
844                name_index: Some(0),
845                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
846                    child_jobs: Some(vec![1004, 1008]),
847                    processes: Some(vec![1001]),
848                    ..Default::default()
849                })),
850                ..Default::default()
851            },
852            fplugin::Resource {
853                koid: Some(1001),
854                name_index: Some(1),
855                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
856                    vmos: Some(vec![1002, 1003]),
857                    mappings: None,
858                    ..Default::default()
859                })),
860                ..Default::default()
861            },
862            fplugin::Resource {
863                koid: Some(1002),
864                name_index: Some(2),
865                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
866                    private_committed_bytes: Some(1024),
867                    private_populated_bytes: Some(2048),
868                    scaled_committed_bytes: Some(1024),
869                    scaled_populated_bytes: Some(2048),
870                    total_committed_bytes: Some(1024),
871                    total_populated_bytes: Some(2048),
872                    ..Default::default()
873                })),
874                ..Default::default()
875            },
876            fplugin::Resource {
877                koid: Some(1003),
878                name_index: Some(3),
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(1004),
892                name_index: Some(4),
893                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
894                    child_jobs: Some(vec![]),
895                    processes: Some(vec![1005]),
896                    ..Default::default()
897                })),
898                ..Default::default()
899            },
900            fplugin::Resource {
901                koid: Some(1005),
902                name_index: Some(5),
903                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
904                    vmos: Some(vec![1006, 1007, 1012]),
905                    mappings: Some(vec![
906                        fplugin::Mapping {
907                            vmo: Some(1006),
908                            address_base: Some(0),
909                            size: Some(512),
910                            ..Default::default()
911                        },
912                        fplugin::Mapping {
913                            vmo: Some(1012),
914                            address_base: Some(1024),
915                            size: Some(512),
916                            ..Default::default()
917                        },
918                        fplugin::Mapping {
919                            vmo: Some(1013),
920                            address_base: Some(1536),
921                            size: Some(512),
922                            ..Default::default()
923                        },
924                        fplugin::Mapping {
925                            vmo: Some(1006),
926                            address_base: Some(2048),
927                            size: Some(512),
928                            ..Default::default()
929                        },
930                    ]),
931                    ..Default::default()
932                })),
933                ..Default::default()
934            },
935            fplugin::Resource {
936                koid: Some(1006),
937                name_index: Some(6),
938                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
939                    private_committed_bytes: Some(1024),
940                    private_populated_bytes: Some(2048),
941                    scaled_committed_bytes: Some(1024),
942                    scaled_populated_bytes: Some(2048),
943                    total_committed_bytes: Some(1024),
944                    total_populated_bytes: Some(2048),
945                    ..Default::default()
946                })),
947                ..Default::default()
948            },
949            fplugin::Resource {
950                koid: Some(1007),
951                name_index: Some(7),
952                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
953                    private_committed_bytes: Some(128),
954                    private_populated_bytes: Some(256),
955                    scaled_committed_bytes: Some(128),
956                    scaled_populated_bytes: Some(256),
957                    total_committed_bytes: Some(128),
958                    total_populated_bytes: Some(256),
959                    ..Default::default()
960                })),
961                ..Default::default()
962            },
963            fplugin::Resource {
964                koid: Some(1008),
965                name_index: Some(8),
966                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
967                    child_jobs: Some(vec![]),
968                    processes: Some(vec![1009]),
969                    ..Default::default()
970                })),
971                ..Default::default()
972            },
973            fplugin::Resource {
974                koid: Some(1009),
975                name_index: Some(9),
976                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
977                    vmos: Some(vec![1010, 1003]),
978                    mappings: None,
979                    ..Default::default()
980                })),
981                ..Default::default()
982            },
983            fplugin::Resource {
984                koid: Some(1010),
985                name_index: Some(10),
986                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
987                    parent: Some(1011),
988                    private_committed_bytes: Some(1024),
989                    private_populated_bytes: Some(2048),
990                    scaled_committed_bytes: Some(1024),
991                    scaled_populated_bytes: Some(2048),
992                    total_committed_bytes: Some(1024),
993                    total_populated_bytes: Some(2048),
994                    ..Default::default()
995                })),
996                ..Default::default()
997            },
998            fplugin::Resource {
999                koid: Some(1011),
1000                name_index: Some(11),
1001                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
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(1012),
1014                name_index: Some(12),
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(1013),
1028                name_index: Some(13),
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        ]
1041        .into_iter()
1042        .map(|r| r.into())
1043        .collect();
1044
1045        let output = attribute_vmos(AttributionData {
1046            principals_vec: principals,
1047            resources_vec: resources,
1048            resource_names,
1049            attributions,
1050        })
1051        .summary();
1052
1053        assert_eq!(output.unclaimed, 2048);
1054        assert_eq!(output.principals.len(), 4);
1055
1056        let principals: HashMap<u64, PrincipalSummary> =
1057            output.principals.into_iter().map(|p| (p.id, p)).collect();
1058
1059        assert_eq!(
1060            principals.get(&1).unwrap(),
1061            &PrincipalSummary {
1062                id: 1,
1063                name: "component_manager".to_owned(),
1064                principal_type: "R".to_owned(),
1065                committed_private: 1024,
1066                committed_scaled: 1536.0,
1067                committed_total: 2048,
1068                populated_private: 2048,
1069                populated_scaled: 3072.0,
1070                populated_total: 4096,
1071                attributor: None,
1072                processes: vec!["root_process (1001)".to_owned()],
1073                vmos: vec![
1074                    (
1075                        ZXName::from_string_lossy("root_vmo"),
1076                        VmoSummary {
1077                            count: 1,
1078                            committed_private: 1024,
1079                            committed_scaled: 1024.0,
1080                            committed_total: 1024,
1081                            populated_private: 2048,
1082                            populated_scaled: 2048.0,
1083                            populated_total: 2048,
1084                            ..Default::default()
1085                        }
1086                    ),
1087                    (
1088                        ZXName::from_string_lossy("shared_vmo"),
1089                        VmoSummary {
1090                            count: 1,
1091                            committed_private: 0,
1092                            committed_scaled: 512.0,
1093                            committed_total: 1024,
1094                            populated_private: 0,
1095                            populated_scaled: 1024.0,
1096                            populated_total: 2048,
1097                            ..Default::default()
1098                        }
1099                    )
1100                ]
1101                .into_iter()
1102                .collect(),
1103            }
1104        );
1105
1106        assert_eq!(
1107            principals.get(&2).unwrap(),
1108            &PrincipalSummary {
1109                id: 2,
1110                name: "runner".to_owned(),
1111                principal_type: "R".to_owned(),
1112                committed_private: 1024,
1113                committed_scaled: 1024.0,
1114                committed_total: 1024,
1115                populated_private: 2048,
1116                populated_scaled: 2048.0,
1117                populated_total: 2048,
1118                attributor: Some("component_manager".to_owned()),
1119                processes: vec!["runner_process (1005)".to_owned()],
1120                vmos: vec![(
1121                    ZXName::from_string_lossy("runner_vmo"),
1122                    VmoSummary {
1123                        count: 1,
1124                        committed_private: 1024,
1125                        committed_scaled: 1024.0,
1126                        committed_total: 1024,
1127                        populated_private: 2048,
1128                        populated_scaled: 2048.0,
1129                        populated_total: 2048,
1130                        ..Default::default()
1131                    }
1132                )]
1133                .into_iter()
1134                .collect(),
1135            }
1136        );
1137
1138        assert_eq!(
1139            principals.get(&3).unwrap(),
1140            &PrincipalSummary {
1141                id: 3,
1142                name: "component 3".to_owned(),
1143                principal_type: "R".to_owned(),
1144                committed_private: 1024,
1145                committed_scaled: 1536.0,
1146                committed_total: 2048,
1147                populated_private: 2048,
1148                populated_scaled: 3072.0,
1149                populated_total: 4096,
1150                attributor: Some("component_manager".to_owned()),
1151                processes: vec!["2_process (1009)".to_owned()],
1152                vmos: vec![
1153                    (
1154                        ZXName::from_string_lossy("shared_vmo"),
1155                        VmoSummary {
1156                            count: 1,
1157                            committed_private: 0,
1158                            committed_scaled: 512.0,
1159                            committed_total: 1024,
1160                            populated_private: 0,
1161                            populated_scaled: 1024.0,
1162                            populated_total: 2048,
1163                            ..Default::default()
1164                        }
1165                    ),
1166                    (
1167                        ZXName::from_string_lossy("2_vmo"),
1168                        VmoSummary {
1169                            count: 1,
1170                            committed_private: 1024,
1171                            committed_scaled: 1024.0,
1172                            committed_total: 1024,
1173                            populated_private: 2048,
1174                            populated_scaled: 2048.0,
1175                            populated_total: 2048,
1176                            ..Default::default()
1177                        }
1178                    )
1179                ]
1180                .into_iter()
1181                .collect(),
1182            }
1183        );
1184
1185        assert_eq!(
1186            principals.get(&4).unwrap(),
1187            &PrincipalSummary {
1188                id: 4,
1189                name: "component 4".to_owned(),
1190                principal_type: "R".to_owned(),
1191                committed_private: 2176,
1192                committed_scaled: 2176.0,
1193                committed_total: 2176,
1194                populated_private: 4352,
1195                populated_scaled: 4352.0,
1196                populated_total: 4352,
1197                attributor: Some("runner".to_owned()),
1198                processes: vec!["runner_process (1005)".to_owned()],
1199                vmos: vec![
1200                    (
1201                        ZXName::from_string_lossy("component_vmo"),
1202                        VmoSummary {
1203                            count: 1,
1204                            committed_private: 128,
1205                            committed_scaled: 128.0,
1206                            committed_total: 128,
1207                            populated_private: 256,
1208                            populated_scaled: 256.0,
1209                            populated_total: 256,
1210                            ..Default::default()
1211                        }
1212                    ),
1213                    (
1214                        ZXName::from_string_lossy("component_vmo_mapped"),
1215                        VmoSummary {
1216                            count: 1,
1217                            committed_private: 1024,
1218                            committed_scaled: 1024.0,
1219                            committed_total: 1024,
1220                            populated_private: 2048,
1221                            populated_scaled: 2048.0,
1222                            populated_total: 2048,
1223                            ..Default::default()
1224                        }
1225                    ),
1226                    (
1227                        ZXName::from_string_lossy("component_vmo_mapped2"),
1228                        VmoSummary {
1229                            count: 1,
1230                            committed_private: 1024,
1231                            committed_scaled: 1024.0,
1232                            committed_total: 1024,
1233                            populated_private: 2048,
1234                            populated_scaled: 2048.0,
1235                            populated_total: 2048,
1236                            ..Default::default()
1237                        }
1238                    )
1239                ]
1240                .into_iter()
1241                .collect(),
1242            }
1243        );
1244    }
1245
1246    #[test]
1247    fn test_reshare_resources() {
1248        // Create a fake snapshot with 3 principals:
1249        // root (id: 1)
1250        //  - component 2 (id: 1)
1251        //    - component 3 (id: 2)
1252        //
1253        // and the following job/process/vmo hierarchy:
1254        // root_job (1000)
1255        //  - component_job (1001)
1256        //    * component_process (1002)
1257        //      . component_vmo (1003)
1258        //
1259        // In this scenario, component 2 reattributes component_job to component 3 entirely.
1260
1261        let resource_names = vec![
1262            ZXName::from_string_lossy("root_job"),
1263            ZXName::from_string_lossy("component_job"),
1264            ZXName::from_string_lossy("component_process"),
1265            ZXName::from_string_lossy("component_vmo"),
1266        ];
1267        let attributions = vec![
1268            fplugin::Attribution {
1269                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1270                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1271                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1272                ..Default::default()
1273            },
1274            fplugin::Attribution {
1275                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1276                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1277                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1278                ..Default::default()
1279            },
1280            fplugin::Attribution {
1281                source: Some(fplugin::PrincipalIdentifier { id: 2 }),
1282                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
1283                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1284                ..Default::default()
1285            },
1286        ]
1287        .into_iter()
1288        .map(|a| a.into())
1289        .collect();
1290        let principals = vec![
1291            fplugin::Principal {
1292                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1293                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1294                principal_type: Some(fplugin::PrincipalType::Runnable),
1295                parent: None,
1296                ..Default::default()
1297            },
1298            fplugin::Principal {
1299                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1300                description: Some(fplugin::Description::Component("component 2".to_owned())),
1301                principal_type: Some(fplugin::PrincipalType::Runnable),
1302                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1303                ..Default::default()
1304            },
1305            fplugin::Principal {
1306                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
1307                description: Some(fplugin::Description::Component("component 3".to_owned())),
1308                principal_type: Some(fplugin::PrincipalType::Runnable),
1309                parent: Some(fplugin::PrincipalIdentifier { id: 2 }),
1310                ..Default::default()
1311            },
1312        ]
1313        .into_iter()
1314        .map(|p| p.into())
1315        .collect();
1316
1317        let resources = vec![
1318            fplugin::Resource {
1319                koid: Some(1000),
1320                name_index: Some(0),
1321                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1322                    child_jobs: Some(vec![1001]),
1323                    processes: Some(vec![]),
1324                    ..Default::default()
1325                })),
1326                ..Default::default()
1327            },
1328            fplugin::Resource {
1329                koid: Some(1001),
1330                name_index: Some(1),
1331                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1332                    child_jobs: Some(vec![]),
1333                    processes: Some(vec![1002]),
1334                    ..Default::default()
1335                })),
1336                ..Default::default()
1337            },
1338            fplugin::Resource {
1339                koid: Some(1002),
1340                name_index: Some(2),
1341                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1342                    vmos: Some(vec![1003]),
1343                    mappings: None,
1344                    ..Default::default()
1345                })),
1346                ..Default::default()
1347            },
1348            fplugin::Resource {
1349                koid: Some(1003),
1350                name_index: Some(3),
1351                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1352                    private_committed_bytes: Some(1024),
1353                    private_populated_bytes: Some(2048),
1354                    scaled_committed_bytes: Some(1024),
1355                    scaled_populated_bytes: Some(2048),
1356                    total_committed_bytes: Some(1024),
1357                    total_populated_bytes: Some(2048),
1358                    ..Default::default()
1359                })),
1360                ..Default::default()
1361            },
1362        ]
1363        .into_iter()
1364        .map(|r| r.into())
1365        .collect();
1366
1367        let output = attribute_vmos(AttributionData {
1368            principals_vec: principals,
1369            resources_vec: resources,
1370            resource_names,
1371            attributions,
1372        })
1373        .summary();
1374
1375        assert_eq!(output.unclaimed, 0);
1376        assert_eq!(output.principals.len(), 3);
1377
1378        let principals: HashMap<u64, PrincipalSummary> =
1379            output.principals.into_iter().map(|p| (p.id, p)).collect();
1380
1381        assert_eq!(
1382            principals.get(&1).unwrap(),
1383            &PrincipalSummary {
1384                id: 1,
1385                name: "component_manager".to_owned(),
1386                principal_type: "R".to_owned(),
1387                committed_private: 0,
1388                committed_scaled: 0.0,
1389                committed_total: 0,
1390                populated_private: 0,
1391                populated_scaled: 0.0,
1392                populated_total: 0,
1393                attributor: None,
1394                processes: vec![],
1395                vmos: vec![].into_iter().collect(),
1396            }
1397        );
1398
1399        assert_eq!(
1400            principals.get(&2).unwrap(),
1401            &PrincipalSummary {
1402                id: 2,
1403                name: "component 2".to_owned(),
1404                principal_type: "R".to_owned(),
1405                committed_private: 0,
1406                committed_scaled: 0.0,
1407                committed_total: 0,
1408                populated_private: 0,
1409                populated_scaled: 0.0,
1410                populated_total: 0,
1411                attributor: Some("component_manager".to_owned()),
1412                processes: vec![],
1413                vmos: vec![].into_iter().collect(),
1414            }
1415        );
1416
1417        assert_eq!(
1418            principals.get(&3).unwrap(),
1419            &PrincipalSummary {
1420                id: 3,
1421                name: "component 3".to_owned(),
1422                principal_type: "R".to_owned(),
1423                committed_private: 1024,
1424                committed_scaled: 1024.0,
1425                committed_total: 1024,
1426                populated_private: 2048,
1427                populated_scaled: 2048.0,
1428                populated_total: 2048,
1429                attributor: Some("component 2".to_owned()),
1430                processes: vec!["component_process (1002)".to_owned()],
1431                vmos: vec![(
1432                    ZXName::from_string_lossy("component_vmo"),
1433                    VmoSummary {
1434                        count: 1,
1435                        committed_private: 1024,
1436                        committed_scaled: 1024.0,
1437                        committed_total: 1024,
1438                        populated_private: 2048,
1439                        populated_scaled: 2048.0,
1440                        populated_total: 2048,
1441                        ..Default::default()
1442                    }
1443                ),]
1444                .into_iter()
1445                .collect(),
1446            }
1447        );
1448    }
1449
1450    #[test]
1451    fn test_conversions() {
1452        let plugin_principal = fplugin::Principal {
1453            identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1454            description: Some(fplugin::Description::Component("component_manager".to_owned())),
1455            principal_type: Some(fplugin::PrincipalType::Runnable),
1456            parent: None,
1457            ..Default::default()
1458        };
1459
1460        let data_principal: Principal = plugin_principal.clone().into();
1461
1462        assert_eq!(plugin_principal, data_principal.into());
1463
1464        let plugin_resources = vec![
1465            fplugin::Resource {
1466                koid: Some(1000),
1467                name_index: Some(0),
1468                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1469                    child_jobs: Some(vec![1004, 1008]),
1470                    processes: Some(vec![1001]),
1471                    ..Default::default()
1472                })),
1473                ..Default::default()
1474            },
1475            fplugin::Resource {
1476                koid: Some(1001),
1477                name_index: Some(1),
1478                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1479                    vmos: Some(vec![1002, 1003]),
1480                    mappings: None,
1481                    ..Default::default()
1482                })),
1483                ..Default::default()
1484            },
1485            fplugin::Resource {
1486                koid: Some(1002),
1487                name_index: Some(2),
1488                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1489                    private_committed_bytes: Some(1024),
1490                    private_populated_bytes: Some(2048),
1491                    scaled_committed_bytes: Some(1024),
1492                    scaled_populated_bytes: Some(2048),
1493                    total_committed_bytes: Some(1024),
1494                    total_populated_bytes: Some(2048),
1495                    ..Default::default()
1496                })),
1497                ..Default::default()
1498            },
1499            fplugin::Resource {
1500                koid: Some(1005),
1501                name_index: Some(5),
1502                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1503                    vmos: Some(vec![1006, 1007, 1012]),
1504                    mappings: Some(vec![
1505                        fplugin::Mapping {
1506                            vmo: Some(1006),
1507                            address_base: Some(0),
1508                            size: Some(512),
1509                            ..Default::default()
1510                        },
1511                        fplugin::Mapping {
1512                            vmo: Some(1012),
1513                            address_base: Some(1024),
1514                            size: Some(512),
1515                            ..Default::default()
1516                        },
1517                    ]),
1518                    ..Default::default()
1519                })),
1520                ..Default::default()
1521            },
1522        ];
1523
1524        let data_resources: Vec<Resource> =
1525            plugin_resources.iter().cloned().map(|r| r.into()).collect();
1526
1527        let actual_resources: Vec<fplugin::Resource> =
1528            data_resources.into_iter().map(|r| r.into()).collect();
1529
1530        assert_eq!(plugin_resources, actual_resources);
1531    }
1532
1533    #[test]
1534    fn test_vmo_reference() {
1535        // Create a fake snapshot with 3 principals:
1536        // root (1)
1537        //  - component 2 (2)
1538        //
1539        // and the following job/process/vmo hierarchy:
1540        // root_job (1000)
1541        //  - component_job (1001)
1542        //    * component_process (1002)
1543        //      . component_vmo (1003)
1544        // component_vmo_parent (1004)
1545        //
1546        // In this scenario, component_vmo is a reference to component_vmo_parent and should get
1547        // shared attribution of its pages.
1548
1549        let resource_names = vec![
1550            name::ZXName::from_string_lossy("root_job"),
1551            name::ZXName::from_string_lossy("component_job"),
1552            name::ZXName::from_string_lossy("component_process"),
1553            name::ZXName::from_string_lossy("component_vmo"),
1554            name::ZXName::from_string_lossy("component_vmo_parent"),
1555        ];
1556        let attributions = vec![
1557            fplugin::Attribution {
1558                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1559                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1560                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1561                ..Default::default()
1562            },
1563            fplugin::Attribution {
1564                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1565                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1566                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1567                ..Default::default()
1568            },
1569        ]
1570        .into_iter()
1571        .map(|a| a.into())
1572        .collect();
1573        let principals = vec![
1574            fplugin::Principal {
1575                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1576                description: Some(fplugin::Description::Component("component_manager".to_owned())),
1577                principal_type: Some(fplugin::PrincipalType::Runnable),
1578                parent: None,
1579                ..Default::default()
1580            },
1581            fplugin::Principal {
1582                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1583                description: Some(fplugin::Description::Component("component 2".to_owned())),
1584                principal_type: Some(fplugin::PrincipalType::Runnable),
1585                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1586                ..Default::default()
1587            },
1588        ]
1589        .into_iter()
1590        .map(|p| p.into())
1591        .collect();
1592
1593        let resources = vec![
1594            fplugin::Resource {
1595                koid: Some(1000),
1596                name_index: Some(0),
1597                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1598                    child_jobs: Some(vec![1001]),
1599                    processes: Some(vec![]),
1600                    ..Default::default()
1601                })),
1602                ..Default::default()
1603            },
1604            fplugin::Resource {
1605                koid: Some(1001),
1606                name_index: Some(1),
1607                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1608                    child_jobs: Some(vec![]),
1609                    processes: Some(vec![1002]),
1610                    ..Default::default()
1611                })),
1612                ..Default::default()
1613            },
1614            fplugin::Resource {
1615                koid: Some(1002),
1616                name_index: Some(2),
1617                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1618                    vmos: Some(vec![1003]),
1619                    mappings: None,
1620                    ..Default::default()
1621                })),
1622                ..Default::default()
1623            },
1624            fplugin::Resource {
1625                koid: Some(1003),
1626                name_index: Some(3),
1627                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1628                    parent: Some(1004),
1629                    private_committed_bytes: Some(0),
1630                    private_populated_bytes: Some(0),
1631                    scaled_committed_bytes: Some(0),
1632                    scaled_populated_bytes: Some(0),
1633                    total_committed_bytes: Some(0),
1634                    total_populated_bytes: Some(0),
1635                    ..Default::default()
1636                })),
1637                ..Default::default()
1638            },
1639            fplugin::Resource {
1640                koid: Some(1004),
1641                name_index: Some(4),
1642                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1643                    private_committed_bytes: Some(1024),
1644                    private_populated_bytes: Some(2048),
1645                    scaled_committed_bytes: Some(1024),
1646                    scaled_populated_bytes: Some(2048),
1647                    total_committed_bytes: Some(1024),
1648                    total_populated_bytes: Some(2048),
1649                    ..Default::default()
1650                })),
1651                ..Default::default()
1652            },
1653        ]
1654        .into_iter()
1655        .map(|r| r.into())
1656        .collect();
1657
1658        let output = attribute_vmos(AttributionData {
1659            principals_vec: principals,
1660            resources_vec: resources,
1661            resource_names,
1662            attributions,
1663        })
1664        .summary();
1665
1666        assert_eq!(output.unclaimed, 0);
1667        assert_eq!(output.principals.len(), 2);
1668
1669        let principals: HashMap<u64, PrincipalSummary> =
1670            output.principals.into_iter().map(|p| (p.id, p)).collect();
1671
1672        assert_eq!(
1673            principals.get(&1).unwrap(),
1674            &PrincipalSummary {
1675                id: 1,
1676                name: "component_manager".to_owned(),
1677                principal_type: "R".to_owned(),
1678                committed_private: 0,
1679                committed_scaled: 0.0,
1680                committed_total: 0,
1681                populated_private: 0,
1682                populated_scaled: 0.0,
1683                populated_total: 0,
1684                attributor: None,
1685                processes: vec![],
1686                vmos: vec![].into_iter().collect(),
1687            }
1688        );
1689
1690        assert_eq!(
1691            principals.get(&2).unwrap(),
1692            &PrincipalSummary {
1693                id: 2,
1694                name: "component 2".to_owned(),
1695                principal_type: "R".to_owned(),
1696                committed_private: 1024,
1697                committed_scaled: 1024.0,
1698                committed_total: 1024,
1699                populated_private: 2048,
1700                populated_scaled: 2048.0,
1701                populated_total: 2048,
1702                attributor: Some("component_manager".to_owned()),
1703                processes: vec!["component_process (1002)".to_owned()],
1704                vmos: vec![
1705                    (
1706                        name::ZXName::from_string_lossy("component_vmo"),
1707                        VmoSummary {
1708                            count: 1,
1709                            committed_private: 0,
1710                            committed_scaled: 0.0,
1711                            committed_total: 0,
1712                            populated_private: 0,
1713                            populated_scaled: 0.0,
1714                            populated_total: 0,
1715                            ..Default::default()
1716                        }
1717                    ),
1718                    (
1719                        name::ZXName::from_string_lossy("component_vmo_parent"),
1720                        VmoSummary {
1721                            count: 1,
1722                            committed_private: 1024,
1723                            committed_scaled: 1024.0,
1724                            committed_total: 1024,
1725                            populated_private: 2048,
1726                            populated_scaled: 2048.0,
1727                            populated_total: 2048,
1728                            ..Default::default()
1729                        }
1730                    ),
1731                ]
1732                .into_iter()
1733                .collect(),
1734            }
1735        );
1736    }
1737}