[memory] Expose memory attribution in Colocated Runner

This change enables the Colocated Runner example to expose memory
attribution information, showing how nested attribution information can be
surfaced.

BUG=307580082

Change-Id: I14514aae22c6a3d890abe1da78827bce614841c8
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/974773
Reviewed-by: Yifei Teng <yifeit@google.com>
Commit-Queue: Étienne J. Membrives <etiennej@google.com>
Reviewed-by: Wez <wez@google.com>
diff --git a/examples/components/runner/colocated/BUILD.gn b/examples/components/runner/colocated/BUILD.gn
index d7e9030..de10635 100644
--- a/examples/components/runner/colocated/BUILD.gn
+++ b/examples/components/runner/colocated/BUILD.gn
@@ -22,21 +22,24 @@
   edition = "2021"
 
   deps = [
+    "//examples/components/runner/colocated/fidl:colocated_rust",
     "//sdk/fidl/fuchsia.component:fuchsia.component_rust",
     "//sdk/fidl/fuchsia.component.runner:fuchsia.component.runner_rust",
+    "//sdk/fidl/fuchsia.memory.attribution:fuchsia.memory.attribution_rust",
     "//sdk/fidl/fuchsia.process:fuchsia.process_rust",
+    "//src/lib/fidl/rust/fidl",
     "//src/lib/fuchsia",
     "//src/lib/fuchsia-async",
     "//src/lib/fuchsia-component",
     "//src/lib/fuchsia-runtime",
-    "//src/lib/mapped-vmo",
+    "//src/lib/fuchsia-sync",
     "//src/lib/zircon/rust:fuchsia-zircon",
+    "//src/performance/memory/attribution",
     "//src/sys/lib/runner",
     "//third_party/rust_crates:anyhow",
     "//third_party/rust_crates:async-lock",
     "//third_party/rust_crates:async-trait",
     "//third_party/rust_crates:futures",
-    "//third_party/rust_crates:rand",
     "//third_party/rust_crates:scopeguard",
     "//third_party/rust_crates:tracing",
   ]
@@ -64,26 +67,14 @@
   manifest = "meta/colocated-runner-example.cml"
 }
 
-fuchsia_component("colocated-component-32mb") {
-  component_name = "colocated-component-32mb"
-  manifest = "meta/colocated-component-32mb.cml"
-}
-
-fuchsia_component("colocated-component-64mb") {
-  component_name = "colocated-component-64mb"
-  manifest = "meta/colocated-component-64mb.cml"
-}
-
-fuchsia_component("colocated-component-128mb") {
-  component_name = "colocated-component-128mb"
-  manifest = "meta/colocated-component-128mb.cml"
+fuchsia_component("colocated-component") {
+  component_name = "colocated-component"
+  manifest = "meta/colocated-component.cml"
 }
 
 fuchsia_package("colocated-runner-example") {
   deps = [
-    ":colocated-component-128mb",
-    ":colocated-component-32mb",
-    ":colocated-component-64mb",
+    ":colocated-component",
     ":colocated-runner",
     ":colocated-runner-example-realm",
   ]
@@ -92,11 +83,14 @@
 fuchsia_unittest_package("colocated-runner-unittests") {
   deps = [
     ":bin_test",
-    ":colocated-component-64mb",
+    ":colocated-component",
   ]
 }
 
 group("hermetic_tests") {
   testonly = true
-  deps = [ ":colocated-runner-unittests" ]
+  deps = [
+    ":colocated-runner-unittests",
+    "integration_tests",
+  ]
 }
diff --git a/examples/components/runner/colocated/README.md b/examples/components/runner/colocated/README.md
index 05977cb..05117e5 100644
--- a/examples/components/runner/colocated/README.md
+++ b/examples/components/runner/colocated/README.md
@@ -7,13 +7,14 @@
 ## What does this runner do
 
 The `colocated` runner demonstrates how to attribute memory to each component
-it runs. A program run by the `colocated` runner will allocate and map a VMO of
-a user-specified size, and then fill it with randomized bytes, to cause the
-pages to be physically allocated.
+it runs. A program run by the `colocated` runner will create and hold a VMO.
+This VMO will be reported by the runner as part of the memory attribution
+protocol.
 
-If the program is started with a `PA_USER0` numbered handle, it will signal the
-`USER_0` signal on the peer handle once it has done filling the VMO, to indicate
-that all the backing pages have been allocated.
+If the program is started with a `PA_USER0` numbered handle, it will serve the
+`fuchsia.examples.colocated.Colocated` protocol over this channel, which can be
+used to retrieve the koid of the VMO created by the colocated component, from
+the component itself.
 
 ## Program schema
 
@@ -50,14 +51,11 @@
 To run a colocated component, provide this URL to `ffx component run`:
 
 ```bash
-$ ffx component run /core/ffx-laboratory:colocated-runner-example/collection:1 'fuchsia-pkg://fuchsia.com/colocated-runner-example#meta/colocated-component-32mb.cm'
+$ ffx component run /core/ffx-laboratory:colocated-runner-example/collection:1 'fuchsia-pkg://fuchsia.com/colocated-runner-example#meta/colocated-component.cm'
 ```
 
-This will start a component that attempts to use 32 MiB of memory, living inside
-the address space of the `colocated` runner.
-
-You may replace `32mb` with `64mb` or `128mb` to test different sizes of memory
-usage.
+This will start a component that lives inside the address space of the
+`colocated` runner.
 
 You may replace `collection:1` with `collection:2` etc. to start multiple
 colocated components in this collection.
diff --git a/examples/components/runner/colocated/fidl/BUILD.gn b/examples/components/runner/colocated/fidl/BUILD.gn
new file mode 100644
index 0000000..0033b96
--- /dev/null
+++ b/examples/components/runner/colocated/fidl/BUILD.gn
@@ -0,0 +1,16 @@
+# Copyright 2024 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+
+fidl("colocated") {
+  # TODO(https://fxbug.dev/42180976) - The structure and location of FIDL libraries along
+  # with their names can be confusing. We should update this once we land on a
+  # decision in the linked bug.
+  name = "fuchsia.examples.colocated"
+
+  sources = [ "colocated.test.fidl" ]
+
+  public_deps = [ "//zircon/vdso/zx" ]
+}
diff --git a/examples/components/runner/colocated/fidl/colocated.test.fidl b/examples/components/runner/colocated/fidl/colocated.test.fidl
new file mode 100644
index 0000000..63a75b2
--- /dev/null
+++ b/examples/components/runner/colocated/fidl/colocated.test.fidl
@@ -0,0 +1,19 @@
+// Copyright 2024 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Library containing a simple calculator protocol.
+library fuchsia.examples.colocated;
+
+using zx;
+
+/// A protocol for reporting one's own VMO usage.
+///
+/// This protocol is used for integration testing.
+@discoverable
+open protocol Colocated {
+    /// Returns a list of VMO Koids used by the component,
+    GetVmos() -> (struct {
+        vmos vector<zx.Koid>:MAX;
+    });
+};
diff --git a/examples/components/runner/colocated/integration_tests/BUILD.gn b/examples/components/runner/colocated/integration_tests/BUILD.gn
new file mode 100644
index 0000000..a5dde7a
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/BUILD.gn
@@ -0,0 +1,58 @@
+# Copyright 2023 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+assert(is_fuchsia, "These targets are only compiled in the fuchsia toolchain.")
+
+import("//build/components.gni")
+import("//build/rust/rustc_test.gni")
+
+rustc_test("bin") {
+  name = "colocated_runner_integration_test_bin"
+  edition = "2021"
+
+  deps = [
+    "//examples/components/runner/colocated/fidl:colocated_rust",
+    "//sdk/fidl/fuchsia.component:fuchsia.component_rust",
+    "//sdk/fidl/fuchsia.component.decl:fuchsia.component.decl_rust",
+    "//sdk/fidl/fuchsia.memory.attribution:fuchsia.memory.attribution_rust",
+    "//sdk/fidl/fuchsia.process:fuchsia.process_rust",
+    "//src/lib/fuchsia",
+    "//src/lib/fuchsia-async",
+    "//src/lib/fuchsia-component-test",
+    "//src/lib/fuchsia-runtime",
+    "//src/lib/zircon/rust:fuchsia-zircon",
+    "//third_party/rust_crates:async-channel",
+    "//third_party/rust_crates:futures-util",
+  ]
+
+  sources = [ "src/lib.rs" ]
+}
+
+fuchsia_test_component("colocated_runner_integration_test") {
+  component_name = "colocated_runner_integration_test"
+  manifest = "meta/colocated_runner_integration_test.cml"
+  deps = [ ":bin" ]
+}
+
+fuchsia_component("test_realm") {
+  component_name = "test_realm"
+  manifest = "meta/test_realm.cml"
+  testonly = true
+}
+
+fuchsia_test_package("colocated-runner-integration-test") {
+  test_components = [ ":colocated_runner_integration_test" ]
+  deps = [
+    ":test_realm",
+    "//examples/components/runner/colocated:colocated-component",
+    "//examples/components/runner/colocated:colocated-runner",
+    "//src/sys/component_manager:component-manager-realm-builder-cmp",
+    "//src/sys/component_manager:elf_runner",
+  ]
+}
+
+group("integration_tests") {
+  testonly = true
+  deps = [ ":colocated-runner-integration-test" ]
+}
diff --git a/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml b/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml
new file mode 100644
index 0000000..903f09f
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml
@@ -0,0 +1,14 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+    include: [
+        "//src/lib/fuchsia-component-test/meta/nested_component_manager.shard.cml",
+        "//src/sys/test_runners/rust/default.shard.cml",
+        "sys/component/realm_builder.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/colocated_runner_integration_test_bin",
+    },
+}
diff --git a/examples/components/runner/colocated/integration_tests/meta/test_realm.cml b/examples/components/runner/colocated/integration_tests/meta/test_realm.cml
new file mode 100644
index 0000000..514dcf9
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/meta/test_realm.cml
@@ -0,0 +1,53 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+    include: [ "syslog/offer.shard.cml" ],
+    children: [
+        {
+            name: "elf_runner",
+            url: "#meta/elf_runner.cm",
+        },
+        {
+            name: "colocated-runner",
+            url: "#meta/colocated-runner.cm",
+            environment: "#colocated-runner-env",
+        },
+    ],
+    collections: [
+        {
+            name: "collection",
+            environment: "#colocated-env",
+            durability: "single_run",
+        },
+    ],
+    offer: [
+        {
+            protocol: "fuchsia.process.Launcher",
+            from: "parent",
+            to: "#elf_runner",
+        },
+    ],
+    environments: [
+        {
+            name: "colocated-runner-env",
+            extends: "realm",
+            runners: [
+                {
+                    runner: "elf",
+                    from: "#elf_runner",
+                },
+            ],
+        },
+        {
+            name: "colocated-env",
+            extends: "realm",
+            runners: [
+                {
+                    runner: "colocated",
+                    from: "#colocated-runner",
+                },
+            ],
+        },
+    ],
+}
diff --git a/examples/components/runner/colocated/integration_tests/src/lib.rs b/examples/components/runner/colocated/integration_tests/src/lib.rs
new file mode 100644
index 0000000..5e2bba2
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/src/lib.rs
@@ -0,0 +1,263 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![cfg(test)]
+
+use fidl_fuchsia_component as fcomponent;
+use fidl_fuchsia_component_decl as fdecl;
+use fidl_fuchsia_examples_colocated as fcolocated;
+use fidl_fuchsia_memory_attribution as fattribution;
+use fidl_fuchsia_process::HandleInfo;
+use fuchsia_async as fasync;
+use fuchsia_component_test::{
+    Capability, ChildOptions, RealmBuilder, RealmBuilderParams, Ref, Route,
+};
+use fuchsia_runtime::HandleType;
+use fuchsia_zircon as zx;
+use futures_util::{future::BoxFuture, FutureExt};
+use std::{collections::HashMap, sync::Arc};
+
+/// Attribute resource given the set of resources at the root of the system,
+/// and a protocol to attribute resources under different principals.
+fn attribute_memory<'a>(
+    name: String,
+    attribution_provider: fattribution::ProviderProxy,
+    introspector: &'a fcomponent::IntrospectorProxy,
+) -> BoxFuture<'a, Node> {
+    async move {
+        // Otherwise, check if there are attribution information.
+        let attributions = attribution_provider
+            .get()
+            .await
+            .unwrap_or_else(|e| panic!("Failed to get AttributionResponse for {name}: {e}"))
+            .unwrap_or_else(|e| panic!("Failed call to AttributionResponse for {name}: {e:?}"))
+            .attributions
+            .unwrap_or_else(|| panic!("Failed memory attribution for {name}"));
+
+        let mut node = Node::new(name);
+
+        // If there are children, resources assigned to this node by its parent
+        // will be re-assigned to children if applicable.
+        let mut children = HashMap::<String, Node>::new();
+        for attribution in attributions {
+            // Recursively attribute memory in this child principal.
+            match attribution {
+                fattribution::AttributionUpdate::Add(new_principal) => {
+                    let identifier =
+                        get_identifier_string(new_principal.identifier, &node.name, introspector)
+                            .await;
+                    let child = if let Some(client) = new_principal.detailed_attribution {
+                        attribute_memory(identifier, client.into_proxy().unwrap(), introspector)
+                            .await
+                    } else {
+                        Node::new(identifier)
+                    };
+                    children.insert(child.name.clone(), child);
+                }
+                fattribution::AttributionUpdate::Update(updated_principal) => {
+                    let identifier = get_identifier_string(
+                        updated_principal.identifier,
+                        &node.name,
+                        introspector,
+                    )
+                    .await;
+
+                    let child = children.get_mut(&identifier).unwrap();
+                    match updated_principal.resources.unwrap() {
+                        fattribution::Resources::Data(d) => {
+                            child.resources = d
+                                .into_iter()
+                                .filter_map(|r| {
+                                    if let fattribution::Resource::KernelObject(koid) = r {
+                                        Some(koid)
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect();
+                        }
+                        _ => todo!("unimplemented"),
+                    };
+                }
+                fattribution::AttributionUpdate::Remove(_) => todo!(),
+                _ => panic!("Unimplemented"),
+            };
+        }
+        node.children.extend(children.into_values());
+        node
+    }
+    .boxed()
+}
+
+async fn get_identifier_string(
+    identifier: Option<fattribution::Identifier>,
+    name: &String,
+    introspector: &fcomponent::IntrospectorProxy,
+) -> String {
+    match identifier.unwrap() {
+        fattribution::Identifier::Self_(_) => todo!("self attribution not supported"),
+        fattribution::Identifier::Component(c) => introspector
+            .get_moniker(c)
+            .await
+            .expect("Inspector call failed")
+            .expect("Inspector::GetMoniker call failed"),
+        fattribution::Identifier::Part(sc) => format!("{}/{}", name, sc).to_owned(),
+        _ => todo!(),
+    }
+}
+
+#[derive(Debug)]
+struct Node {
+    name: String,
+    resources: Vec<u64>,
+    children: Vec<Node>,
+}
+
+impl Node {
+    pub fn new(identifier: String) -> Node {
+        Node { name: identifier, resources: vec![], children: vec![] }
+    }
+}
+
+#[fuchsia::test]
+async fn test_attribute_memory() {
+    // Starts a component manager and obtain its root job, so that we can simulate
+    // traversing the root job of the system, the kind done in `memory_monitor`.
+    let builder = RealmBuilder::with_params(
+        RealmBuilderParams::new().from_relative_url("#meta/test_realm.cm"),
+    )
+    .await
+    .expect("Failed to create test realm builder");
+
+    // Add a child to receive these capabilities so that we can use them in this test.
+    // - fuchsia.memory.attribution.Provider
+    // - fuchsia.kernel.RootJobForInspect
+    // - fuchsia.component.Realm
+    struct Capabilities {
+        attribution_provider: fattribution::ProviderProxy,
+        introspector: fcomponent::IntrospectorProxy,
+        realm: fcomponent::RealmProxy,
+    }
+    let (capabilities_sender, capabilities_receiver) = async_channel::unbounded::<Capabilities>();
+    let capabilities_sender = Arc::new(capabilities_sender);
+    let receiver = builder
+        .add_local_child(
+            "receiver",
+            move |handles| {
+                let capabilities_sender = capabilities_sender.clone();
+                async move {
+                    capabilities_sender
+                        .send(Capabilities {
+                            attribution_provider: handles
+                                .connect_to_protocol::<fattribution::ProviderMarker>()
+                                .unwrap(),
+                            introspector: handles
+                                .connect_to_protocol::<fcomponent::IntrospectorMarker>()
+                                .unwrap(),
+                            realm: handles
+                                .connect_to_protocol::<fcomponent::RealmMarker>()
+                                .unwrap(),
+                        })
+                        .await
+                        .unwrap();
+                    // TODO(https://fxbug.dev/303919602): Until the component framework reliably
+                    // drains capability requests when a component is stopped, we need to
+                    // keep running the component.
+                    std::future::pending().await
+                }
+                .boxed()
+            },
+            ChildOptions::new().eager(),
+        )
+        .await
+        .expect("Failed to add child");
+
+    builder
+        .add_route(
+            Route::new()
+                .capability(Capability::protocol_by_name("fuchsia.memory.attribution.Provider"))
+                .from(Ref::child("elf_runner"))
+                .to(&receiver),
+        )
+        .await
+        .unwrap();
+
+    builder
+        .add_route(
+            Route::new()
+                .capability(Capability::protocol_by_name("fuchsia.component.Realm"))
+                .from(Ref::framework())
+                .to(&receiver),
+        )
+        .await
+        .unwrap();
+
+    builder
+        .add_route(
+            Route::new()
+                .capability(Capability::protocol_by_name("fuchsia.component.Introspector"))
+                .from(Ref::framework())
+                .to(&receiver),
+        )
+        .await
+        .unwrap();
+
+    let _realm =
+        builder.build_in_nested_component_manager("#meta/component_manager.cm").await.unwrap();
+
+    let capabilities = capabilities_receiver.recv().await.unwrap();
+
+    // Start a colocated component.
+    let collection = fdecl::CollectionRef { name: "collection".to_string() };
+    let decl = fdecl::Child {
+        name: Some("colocated-component".to_string()),
+        url: Some("#meta/colocated-component.cm".to_string()),
+        startup: Some(fdecl::StartupMode::Lazy),
+        ..Default::default()
+    };
+    let (user0, user0_peer) = zx::Channel::create();
+    let args = fcomponent::CreateChildArgs {
+        numbered_handles: Some(vec![HandleInfo {
+            handle: user0_peer.into(),
+            id: fuchsia_runtime::HandleInfo::new(HandleType::User0, 0).as_raw(),
+        }]),
+        ..Default::default()
+    };
+    capabilities.realm.create_child(&collection, &decl, args).await.unwrap().unwrap();
+
+    let colocated_component_vmos =
+        fcolocated::ColocatedProxy::new(fasync::Channel::from_channel(user0))
+            .get_vmos()
+            .await
+            .unwrap();
+
+    assert!(!colocated_component_vmos.is_empty());
+
+    // Starting from the ELF runner, ask about the resource usage.
+    let elf_runner = attribute_memory(
+        "elf_runner.cm".to_owned(),
+        capabilities.attribution_provider,
+        &capabilities.introspector,
+    )
+    .await;
+
+    // We should get the following tree:
+    //
+    // - elf_runner.cm
+    //     - colocated_runner.cm
+    //         - colocated_component-64mb.cm
+    //             - Some VMO
+    //         - overhead
+    //     - overhead
+    eprintln!("{:?}", elf_runner);
+    assert_eq!(elf_runner.children.len(), 1);
+    assert!(elf_runner.children[0].name.contains("colocated-runner"));
+    assert_eq!(elf_runner.children[0].children.len(), 1usize);
+    // Name of the colocated component
+    assert_eq!(&elf_runner.children[0].children[0].name, "collection:colocated-component");
+    let resource = &elf_runner.children[0].children[0].resources;
+    for vmo_koid in colocated_component_vmos {
+        assert!(resource.contains(&vmo_koid));
+    }
+}
diff --git a/examples/components/runner/colocated/meta/colocated-component-32mb.cml b/examples/components/runner/colocated/meta/colocated-component-32mb.cml
deleted file mode 100644
index e39fc7e..0000000
--- a/examples/components/runner/colocated/meta/colocated-component-32mb.cml
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2023 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
-    program: {
-        runner: "colocated",
-        vmo_size: "33554432",
-    },
-}
diff --git a/examples/components/runner/colocated/meta/colocated-component-64mb.cml b/examples/components/runner/colocated/meta/colocated-component-64mb.cml
deleted file mode 100644
index 7efaa9b..0000000
--- a/examples/components/runner/colocated/meta/colocated-component-64mb.cml
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2023 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
-    program: {
-        runner: "colocated",
-        vmo_size: "67108864",
-    },
-}
diff --git a/examples/components/runner/colocated/meta/colocated-component-128mb.cml b/examples/components/runner/colocated/meta/colocated-component.cml
similarity index 87%
rename from examples/components/runner/colocated/meta/colocated-component-128mb.cml
rename to examples/components/runner/colocated/meta/colocated-component.cml
index cde1166..ce40757 100644
--- a/examples/components/runner/colocated/meta/colocated-component-128mb.cml
+++ b/examples/components/runner/colocated/meta/colocated-component.cml
@@ -4,6 +4,5 @@
 {
     program: {
         runner: "colocated",
-        vmo_size: "134217728",
     },
 }
diff --git a/examples/components/runner/colocated/meta/colocated-runner.cml b/examples/components/runner/colocated/meta/colocated-runner.cml
index 5bc7277..d0a2759 100644
--- a/examples/components/runner/colocated/meta/colocated-runner.cml
+++ b/examples/components/runner/colocated/meta/colocated-runner.cml
@@ -6,6 +6,7 @@
     program: {
         runner: "elf",
         binary: "bin/colocated_runner",
+        memory_attribution: "true",
     },
     capabilities: [
         { protocol: "fuchsia.component.runner.ComponentRunner" },
@@ -13,6 +14,7 @@
             runner: "colocated",
             path: "/svc/fuchsia.component.runner.ComponentRunner",
         },
+        { protocol: "fuchsia.memory.attribution.Provider" },
     ],
     expose: [
         {
@@ -23,5 +25,9 @@
             runner: "colocated",
             from: "self",
         },
+        {
+            protocol: "fuchsia.memory.attribution.Provider",
+            from: "self",
+        },
     ],
 }
diff --git a/examples/components/runner/colocated/src/main.rs b/examples/components/runner/colocated/src/main.rs
index 3feb0c1..bd5e4b3 100644
--- a/examples/components/runner/colocated/src/main.rs
+++ b/examples/components/runner/colocated/src/main.rs
@@ -2,41 +2,71 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
+use attribution::{AttributionServer, AttributionServerHandle, Observer, Publisher};
+use fidl::endpoints::ControlHandle;
+use fidl::endpoints::RequestStream;
 use fidl_fuchsia_component as fcomponent;
 use fidl_fuchsia_component_runner as fcrunner;
+use fidl_fuchsia_memory_attribution as fattribution;
 use fuchsia_async as fasync;
 use fuchsia_component::server::ServiceFs;
+use fuchsia_sync::Mutex;
 use fuchsia_zircon as zx;
 use futures::{StreamExt, TryStreamExt};
 use runner::component::{ChannelEpitaph, Controllable, Controller};
-use std::future::Future;
+use std::{
+    collections::HashMap,
+    future::Future,
+    sync::{
+        atomic::{AtomicU64, Ordering},
+        Arc,
+    },
+};
+
 use tracing::{info, warn};
+use zx::{HandleBased, Koid};
 
 mod program;
 
 use crate::program::ColocatedProgram;
 
-const VMO_SIZE: &str = "vmo_size";
-
 enum IncomingRequest {
     Runner(fcrunner::ComponentRunnerRequestStream),
+    Memory(fattribution::ProviderRequestStream),
 }
 
 #[fuchsia::main]
 async fn main() -> Result<()> {
+    let resource_tracker = Arc::new(ResourceTracker { resources: Mutex::new(Default::default()) });
+
     let mut service_fs = ServiceFs::new_local();
     service_fs.dir("svc").add_fidl_service(IncomingRequest::Runner);
+    service_fs.dir("svc").add_fidl_service(IncomingRequest::Memory);
     service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;
 
+    let memory_server_handle = get_memory_server(resource_tracker.clone());
+
     service_fs
-        .for_each_concurrent(None, |request: IncomingRequest| async move {
+        .for_each_concurrent(None, |request: IncomingRequest| async {
             match request {
                 IncomingRequest::Runner(stream) => {
-                    if let Err(err) = handle_runner_request(stream).await {
+                    if let Err(err) = handle_runner_request(
+                        stream,
+                        resource_tracker.clone(),
+                        memory_server_handle.clone(),
+                    )
+                    .await
+                    {
                         warn!("Error while serving ComponentRunner: {err}");
                     }
                 }
+                IncomingRequest::Memory(stream) => {
+                    let observer = memory_server_handle.new_observer(stream.control_handle());
+                    if let Err(err) = handle_memory_request(stream, observer).await {
+                        warn!("Error while serving AttributionProvider: {err}");
+                    }
+                }
             }
         })
         .await;
@@ -44,15 +74,24 @@
     Ok(())
 }
 
+fn get_memory_server(resource_tracker: Arc<ResourceTracker>) -> AttributionServerHandle {
+    let state_fn = Box::new(move || get_attribution(resource_tracker.clone()));
+    AttributionServer::new(state_fn)
+}
+
 /// Handles `fuchsia.component.runner/ComponentRunner` requests over a FIDL connection.
-async fn handle_runner_request(mut stream: fcrunner::ComponentRunnerRequestStream) -> Result<()> {
+async fn handle_runner_request(
+    mut stream: fcrunner::ComponentRunnerRequestStream,
+    resource_tracker: Arc<ResourceTracker>,
+    memory_server_handle: AttributionServerHandle,
+) -> Result<()> {
     while let Some(request) =
         stream.try_next().await.context("failed to serve ComponentRunner protocol")?
     {
         let fcrunner::ComponentRunnerRequest::Start { start_info, controller, .. } = request;
         let url = start_info.resolved_url.clone().unwrap_or_else(|| "unknown url".to_string());
         info!("Colocated runner is going to start component {url}");
-        match start(start_info) {
+        match start(start_info, resource_tracker.clone(), memory_server_handle.new_publisher()) {
             Ok((program, on_exit)) => {
                 let controller = Controller::new(program, controller.into_stream().unwrap());
                 fasync::Task::spawn(controller.serve(on_exit)).detach();
@@ -69,16 +108,115 @@
     Ok(())
 }
 
+async fn handle_memory_request(
+    mut stream: fattribution::ProviderRequestStream,
+    subscriber: Observer,
+) -> Result<()> {
+    while let Some(request) =
+        stream.try_next().await.context("failed to serve AttributionProvider protocol")?
+    {
+        match request {
+            fattribution::ProviderRequest::Get { responder } => {
+                subscriber.next(responder);
+            }
+            fattribution::ProviderRequest::_UnknownMethod { ordinal, control_handle, .. } => {
+                warn!("Invalid request to AttributionProvider: {ordinal}");
+                control_handle.shutdown_with_epitaph(zx::Status::INVALID_ARGS);
+            }
+        }
+    }
+
+    Ok(())
+}
+
+fn get_attribution(resource_tracker: Arc<ResourceTracker>) -> Vec<fattribution::AttributionUpdate> {
+    let mut children = vec![];
+    for (_, (token, koid)) in resource_tracker.resources.lock().iter() {
+        children.push(fattribution::AttributionUpdate::Add(fattribution::NewPrincipal {
+            identifier: Some(fattribution::Identifier::Component(
+                token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+            )),
+            detailed_attribution: None,
+            ..Default::default()
+        }));
+        children.push(fattribution::AttributionUpdate::Update(fattribution::UpdatedPrincipal {
+            identifier: Some(fattribution::Identifier::Component(
+                token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+            )),
+            resources: Some(fattribution::Resources::Data(vec![
+                fattribution::Resource::KernelObject(koid.raw_koid()),
+            ])),
+            ..Default::default()
+        }));
+    }
+    children
+}
+
+/// Tracks resources used by each [`ColocatedProgram`]. Since each program just allocates
+/// one VMO, we only need to track one KOID here.
+struct ResourceTracker {
+    resources: Mutex<HashMap<ProgramId, (zx::Event, Koid)>>,
+}
+
+type ProgramId = u64;
+
+static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
 /// Starts a colocated component.
 fn start(
     start_info: fcrunner::ComponentStartInfo,
+    resource_tracker: Arc<ResourceTracker>,
+    publisher: Publisher,
 ) -> Result<(impl Controllable, impl Future<Output = ChannelEpitaph> + Unpin)> {
-    let vmo_size = runner::get_program_string(&start_info, VMO_SIZE)
-        .ok_or(anyhow!("Missing vmo_size argument in program block"))?;
-    let vmo_size: u64 = vmo_size.parse().context("vmo_size is not a valid number")?;
     let numbered_handles = start_info.numbered_handles.unwrap_or(vec![]);
-    let program = ColocatedProgram::new(vmo_size, numbered_handles)?;
+    let program = ColocatedProgram::new(numbered_handles)?;
+    let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+    let vmo_koid = program.get_vmo_koid().raw_koid();
+    // Register this VMO.
+    let instance_token = start_info.component_instance.as_ref().unwrap();
+
+    let mut resources = resource_tracker.resources.lock();
+    resources.insert(
+        id,
+        (
+            instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+            program.get_vmo_koid(),
+        ),
+    );
+
+    let mut updates = vec![];
+    updates.push(fattribution::AttributionUpdate::Add(fattribution::NewPrincipal {
+        identifier: Some(fattribution::Identifier::Component(
+            instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+        )),
+        detailed_attribution: None,
+        ..Default::default()
+    }));
+    updates.push(fattribution::AttributionUpdate::Update(fattribution::UpdatedPrincipal {
+        identifier: Some(fattribution::Identifier::Component(
+            instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+        )),
+        resources: Some(fattribution::Resources::Data(vec![fattribution::Resource::KernelObject(
+            vmo_koid,
+        )])),
+        ..Default::default()
+    }));
+    publisher.on_update(updates).unwrap();
     let termination = program.wait_for_termination();
+    let termination_clone = program.wait_for_termination();
+    // Remove this VMO when the program has terminated.
+    let tracker = resource_tracker.clone();
+    fasync::Task::spawn(async move {
+        termination_clone.await;
+        if let Some((token, _)) = tracker.resources.lock().remove(&id) {
+            publisher
+                .on_update(vec![fattribution::AttributionUpdate::Remove(
+                    fattribution::Identifier::Component(token),
+                )])
+                .unwrap();
+        }
+    })
+    .detach();
     Ok((program, termination))
 }
 
@@ -88,74 +226,59 @@
     use super::*;
     use fidl::endpoints::Proxy;
     use fidl_fuchsia_component_decl as fdecl;
+    use fidl_fuchsia_examples_colocated as fcolocated;
     use fidl_fuchsia_process::HandleInfo;
     use fuchsia_runtime::HandleType;
 
     #[fuchsia::test]
     async fn test_start_stop_component() {
+        let resource_tracker =
+            Arc::new(ResourceTracker { resources: Mutex::new(Default::default()) });
         let (runner, runner_stream) =
             fidl::endpoints::create_proxy_and_stream::<fcrunner::ComponentRunnerMarker>().unwrap();
-        let server = fasync::Task::spawn(handle_runner_request(runner_stream));
+        let memory_server = get_memory_server(resource_tracker.clone());
+        let server = fasync::Task::spawn(handle_runner_request(
+            runner_stream,
+            resource_tracker,
+            memory_server.clone(),
+        ));
 
-        // Measure how much private RAM our own process is using.
-        let usage_initial = private_ram();
-
-        // Start a component using 64 MiB of RAM.
+        // Start a colocated component.
         let decl = fuchsia_fs::file::read_in_namespace_to_fidl::<fdecl::Component>(
-            "/pkg/meta/colocated-component-64mb.cm",
+            "/pkg/meta/colocated-component.cm",
         )
         .await
         .unwrap();
         let (controller, controller_server_end) = fidl::endpoints::create_endpoints();
-        let (user0, user0_peer) = zx::EventPair::create();
+        let (user0, user0_peer) = zx::Channel::create();
         let start_info = fcrunner::ComponentStartInfo {
             program: decl.program.unwrap().info,
             numbered_handles: Some(vec![HandleInfo {
                 handle: user0_peer.into(),
                 id: fuchsia_runtime::HandleInfo::new(HandleType::User0, 0).as_raw(),
             }]),
+            component_instance: Some(zx::Event::create()),
             ..Default::default()
         };
         runner.start(start_info, controller_server_end).unwrap();
 
         // Wait until the program has allocated 64 MiB worth of pages.
-        _ = fasync::OnSignals::new(&user0, zx::Signals::USER_0).await.unwrap();
+        let colocated_component_vmos =
+            fcolocated::ColocatedProxy::new(fasync::Channel::from_channel(user0))
+                .get_vmos()
+                .await
+                .unwrap();
 
         // Measure our private memory usage again. It should increase by roughly that much more.
-        let usage_started = private_ram();
-        assert!(
-            usage_started > usage_initial,
-            "initial: {usage_initial}, started: {usage_started}"
-        );
-        assert!(
-            usage_started - usage_initial > 60 * 1024 * 1024,
-            "initial: {usage_initial}, started: {usage_started}"
-        );
+        assert!(!colocated_component_vmos.is_empty());
 
         // Stop the component.
         let controller = controller.into_proxy().unwrap();
         controller.stop().unwrap();
         controller.on_closed().await.unwrap();
 
-        // Measure our private memory usage again. It should roughly go back to before.
-        let usage_stopped = private_ram();
-        assert!(
-            usage_stopped < usage_started,
-            "started: {usage_started}, stopped: {usage_stopped}"
-        );
-        assert!(
-            usage_started - usage_stopped > 60 * 1024 * 1024,
-            "started: {usage_started}, stopped: {usage_stopped}"
-        );
-
         // Close the connection and verify the server task ends successfully.
         drop(runner);
         server.await.unwrap();
     }
-
-    #[track_caller]
-    fn private_ram() -> usize {
-        let process = fuchsia_runtime::process_self();
-        process.task_stats().unwrap().mem_private_bytes
-    }
 }
diff --git a/examples/components/runner/colocated/src/program.rs b/examples/components/runner/colocated/src/program.rs
index 72f8ee5..e3fe566 100644
--- a/examples/components/runner/colocated/src/program.rs
+++ b/examples/components/runner/colocated/src/program.rs
@@ -4,37 +4,34 @@
 
 use async_lock::OnceCell;
 use async_trait::async_trait;
+use fidl::endpoints::RequestStream;
+use fidl_fuchsia_examples_colocated as fcolocated;
 use fidl_fuchsia_process::HandleInfo;
 use fuchsia_async as fasync;
 use fuchsia_runtime::HandleType;
 use fuchsia_zircon as zx;
-use futures::{
-    channel::oneshot,
-    future::{BoxFuture, FutureExt},
-};
-use mapped_vmo::Mapping;
+use futures::future::{BoxFuture, FutureExt};
+use futures::TryStreamExt;
 use runner::component::{ChannelEpitaph, Controllable};
-use std::{ops::DerefMut, sync::Arc};
+use std::sync::Arc;
 use tracing::warn;
-use zx::Peered;
+use zx::{AsHandleRef, Koid};
 
 /// [`ColocatedProgram `] represents an instance of a program run by the
 /// colocated runner. Its state is held in this struct and its behavior
 /// is run in the `task`.
 pub struct ColocatedProgram {
     task: Option<fasync::Task<()>>,
-    filled: Option<oneshot::Receiver<Mapping>>,
     terminated: Arc<OnceCell<()>>,
+    vmo_koid: Koid,
 }
 
 impl ColocatedProgram {
-    pub fn new(vmo_size: u64, numbered_handles: Vec<HandleInfo>) -> Result<Self, anyhow::Error> {
-        let vmo = zx::Vmo::create(vmo_size)?;
-        let vmo_size = vmo.get_size()?;
-        let (filled_sender, filled) = oneshot::channel();
+    pub fn new(numbered_handles: Vec<HandleInfo>) -> Result<Self, anyhow::Error> {
+        let vmo = zx::Vmo::create(1024)?;
+        let vmo_koid = vmo.get_koid()?;
+
         let terminated = Arc::new(OnceCell::new());
-        let fill_vmo_task =
-            fasync::unblock(move || ColocatedProgram::fill_vmo(vmo, vmo_size, filled_sender));
         let terminated_clone = terminated.clone();
         let guard = scopeguard::guard((), move |()| {
             _ = terminated_clone.set_blocking(());
@@ -44,22 +41,29 @@
             // which happens when this task is dropped.
             let _guard = guard;
 
-            fill_vmo_task.await;
-
             // Signal to the outside world that the pages have been allocated.
-            for info in numbered_handles.into_iter().filter(|info| {
-                match fuchsia_runtime::HandleInfo::try_from(info.id) {
+            let handle_info = numbered_handles
+                .into_iter()
+                .filter(|info| match fuchsia_runtime::HandleInfo::try_from(info.id) {
                     Ok(handle_info) => {
                         handle_info == fuchsia_runtime::HandleInfo::new(HandleType::User0, 0)
                     }
                     Err(_) => false,
-                }
-            }) {
-                let handle = zx::EventPair::from(info.handle);
-                match handle.signal_peer(zx::Signals::empty(), zx::Signals::USER_0) {
-                    Ok(()) => {}
-                    Err(status) => {
-                        warn!("Failed to signal USER0 handle: {status}");
+                })
+                .next()
+                .unwrap();
+
+            let channel = zx::Channel::from(handle_info.handle);
+            let mut request_stream = fcolocated::ColocatedRequestStream::from_channel(
+                fasync::Channel::from_channel(channel),
+            );
+            while let Some(request) = request_stream.try_next().await.unwrap() {
+                match request {
+                    fcolocated::ColocatedRequest::GetVmos { responder } => {
+                        responder.send(&[vmo_koid.raw_koid()]).unwrap();
+                    }
+                    fcolocated::ColocatedRequest::_UnknownMethod { .. } => {
+                        panic!("Unknown method");
                     }
                 }
             }
@@ -68,41 +72,7 @@
             std::future::pending().await
         };
         let task = fasync::Task::spawn(task);
-        Ok(Self { task: Some(task), filled: Some(filled), terminated })
-    }
-
-    fn fill_vmo(vmo: zx::Vmo, vmo_size: u64, filled: oneshot::Sender<Mapping>) {
-        // Map the VMO into the address space.
-        let vmo_size = vmo_size as usize;
-        let mut mapping = Mapping::create_from_vmo(
-            &vmo,
-            vmo_size,
-            zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE,
-        )
-        .unwrap();
-        let buffer = mapping.deref_mut();
-
-        // Fill the VMO with randomized bytes, to cause pages to be physically allocated.
-        // This approach will defeat page deduplication and page compression, for ease of
-        // memory usage analysis. This program should more or less use `vmo_size` bytes.
-        use rand::RngCore;
-        let mut rng = rand::thread_rng();
-        let mut offset: usize = 0;
-        const BLOCK_SIZE: usize = 512;
-        let mut bytes = vec![0u8; BLOCK_SIZE];
-        loop {
-            rng.fill_bytes(&mut bytes);
-            buffer.write_at(offset, &bytes);
-            offset += BLOCK_SIZE;
-            if offset > vmo_size {
-                break;
-            }
-        }
-        buffer.release_writes();
-
-        // Send the mapping to the program to be kept alive. This will keep those pages
-        // committed.
-        _ = filled.send(mapping);
+        Ok(Self { task: Some(task), terminated, vmo_koid })
     }
 
     /// Returns a future that will resolve when the program is terminated.
@@ -114,6 +84,12 @@
         }
         .boxed()
     }
+
+    /// Returns the koid of the program's VMO, so the runner can report its memory as attributed to
+    /// this component.
+    pub fn get_vmo_koid(&self) -> Koid {
+        self.vmo_koid
+    }
 }
 
 #[async_trait]
@@ -125,11 +101,7 @@
 
     fn stop<'a>(&mut self) -> BoxFuture<'a, ()> {
         let task = self.task.take();
-        let filled = self.filled.take();
         async {
-            if let Some(filled) = filled {
-                _ = filled.await;
-            }
             if let Some(task) = task {
                 _ = task.cancel();
             }
diff --git a/src/performance/memory/attribution/src/attribution_server.rs b/src/performance/memory/attribution/src/attribution_server.rs
index 6a282a1..45a3001b 100644
--- a/src/performance/memory/attribution/src/attribution_server.rs
+++ b/src/performance/memory/attribution/src/attribution_server.rs
@@ -39,23 +39,17 @@
 /// [Observer] object using [AttributionServer::new_observer].
 /// [Publisher]s, created using [AttributionServer::new_publisher], should be
 /// used to push attribution changes.
-pub struct AttributionServer {
-    inner: Arc<Mutex<AttributionServerInner>>,
+#[derive(Clone)]
+pub struct AttributionServerHandle {
+    inner: Arc<Mutex<AttributionServer>>,
 }
 
-impl AttributionServer {
-    /// Create a new memory attribution server.
-    ///
-    /// `state` is a function returning the complete attribution state (not partial updates).
-    pub fn new(state: Box<GetAttributionFn>) -> Self {
-        Self { inner: Arc::new(Mutex::new(AttributionServerInner::new(state))) }
-    }
-
+impl AttributionServerHandle {
     /// Create a new [Observer] that represents a single client.
     ///
     /// Each FIDL client connection should get its own [Observer] object.
     pub fn new_observer(&self, control_handle: fattribution::ProviderControlHandle) -> Observer {
-        AttributionServerInner::register(&self.inner, control_handle)
+        AttributionServer::register(&self.inner, control_handle)
     }
 
     /// Create a new [Publisher] that can push updates to observers.
@@ -68,7 +62,7 @@
 /// get calls. These will be notified when the state changes or immediately the first time
 /// an `Observer` registers an observation.
 pub struct Observer {
-    inner: Arc<Mutex<AttributionServerInner>>,
+    inner: Arc<Mutex<AttributionServer>>,
 }
 
 impl Observer {
@@ -92,7 +86,7 @@
 
 /// A [Publisher] should be used to send updates to [Observer]s.
 pub struct Publisher {
-    inner: Arc<Mutex<AttributionServerInner>>,
+    inner: Arc<Mutex<AttributionServer>>,
 }
 
 impl Publisher {
@@ -109,14 +103,19 @@
     }
 }
 
-struct AttributionServerInner {
+pub struct AttributionServer {
     state: Box<GetAttributionFn>,
     consumer: Option<AttributionConsumer>,
 }
 
-impl AttributionServerInner {
-    pub fn new(state: Box<GetAttributionFn>) -> Self {
-        Self { state, consumer: None }
+impl AttributionServer {
+    /// Create a new memory attribution server.
+    ///
+    /// `state` is a function returning the complete attribution state (not partial updates).
+    pub fn new(state: Box<GetAttributionFn>) -> AttributionServerHandle {
+        AttributionServerHandle {
+            inner: Arc::new(Mutex::new(AttributionServer { state, consumer: None })),
+        }
     }
 
     pub fn on_update(
@@ -266,7 +265,12 @@
     /// Create a new [AttributionConsumer] without an `observer` and an initial `dirty`
     /// value of `true`.
     pub fn new(observer_control_handle: fattribution::ProviderControlHandle) -> Self {
-        AttributionConsumer { first: true, pending: HashMap::new(), observer_control_handle: observer_control_handle, responder: None }
+        AttributionConsumer {
+            first: true,
+            pending: HashMap::new(),
+            observer_control_handle: observer_control_handle,
+            responder: None,
+        }
     }
 
     /// Register a new observation request. The observer will be notified immediately if
@@ -488,7 +492,6 @@
 
         assert_matches!(result2, Err(ClientChannelClosed { status: zx::Status::BAD_STATE, .. }));
         assert_matches!(result, Err(ClientChannelClosed { status: zx::Status::BAD_STATE, .. }));
-
     }
 
     /// Tests that the first get call returns the full state, not updates.
@@ -513,14 +516,14 @@
         .detach();
 
         server
-        .new_publisher()
-        .on_update(vec![fattribution::AttributionUpdate::Update(
-            fattribution::UpdatedPrincipal {
-                identifier: Some(fattribution::Identifier::Self_(fattribution::Self_)),
-                ..Default::default()
-            },
-        )])
-        .expect("Error sending the update");
+            .new_publisher()
+            .on_update(vec![fattribution::AttributionUpdate::Update(
+                fattribution::UpdatedPrincipal {
+                    identifier: Some(fattribution::Identifier::Self_(fattribution::Self_)),
+                    ..Default::default()
+                },
+            )])
+            .expect("Error sending the update");
 
         // As this is the first call, we should get the full state, not the update.
         let attributions =
diff --git a/src/performance/memory/attribution/src/lib.rs b/src/performance/memory/attribution/src/lib.rs
index b032c2b..d3dcc98 100644
--- a/src/performance/memory/attribution/src/lib.rs
+++ b/src/performance/memory/attribution/src/lib.rs
@@ -4,4 +4,7 @@
 
 mod attribution_server;
 
-pub use attribution_server::{AttributionServer, AttributionServerObservationError};
+pub use attribution_server::{
+    AttributionServer, AttributionServerHandle, AttributionServerObservationError, Observer,
+    Publisher,
+};
diff --git a/src/sys/component_manager/src/framework/introspector.rs b/src/sys/component_manager/src/framework/introspector.rs
index 8fd9ddd..74e2be2 100644
--- a/src/sys/component_manager/src/framework/introspector.rs
+++ b/src/sys/component_manager/src/framework/introspector.rs
@@ -121,6 +121,9 @@
         lazy_static! {
             static ref MEMORY_MONITOR: Moniker =
                 Moniker::parse_str("/core/memory_monitor").unwrap();
+            /// Moniker for integration tests.
+            static ref RECEIVER: Moniker =
+                Moniker::parse_str("receiver").unwrap();
         };
         // TODO(https://fxbug.dev/318904493): Temporary workaround to prevent other components from
         // using `Introspector` while improvements to framework capability allowlists are under way.
@@ -131,7 +134,10 @@
         // realm, then exposed from `/`.
         //
         // All other cases are disallowed.
-        if target.moniker != *MEMORY_MONITOR && !target.moniker.is_root() {
+        if target.moniker != *MEMORY_MONITOR
+            && target.moniker != *RECEIVER
+            && !target.moniker.is_root()
+        {
             return Box::new(AccessDeniedCapabilityProvider {
                 target,
                 source_moniker: scope.moniker,
diff --git a/src/sys/lib/elf_runner/src/memory/reporter.rs b/src/sys/lib/elf_runner/src/memory/reporter.rs
index dfcaba8..4c5ea85 100644
--- a/src/sys/lib/elf_runner/src/memory/reporter.rs
+++ b/src/sys/lib/elf_runner/src/memory/reporter.rs
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use attribution::AttributionServer;
+use attribution::{AttributionServer, AttributionServerHandle};
 use fidl::endpoints::RequestStream;
 use fidl::endpoints::{ControlHandle, DiscoverableProtocolMarker};
 use fidl_fuchsia_io as fio;
@@ -16,7 +16,7 @@
 use crate::{component::ElfComponentInfo, ComponentSet};
 
 pub struct MemoryReporter {
-    server: AttributionServer,
+    server: AttributionServerHandle,
     components: Arc<ComponentSet>,
 }