underhill_core/inspect_internal.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Generally we hold the position that inspect paths are completely unstable,
5//! may change at any time, and can not be depended on by anything. However, we
6//! at Microsoft have implemented stable, versioned, supported, internal
7//! diagnostic tools that depend on inspect paths under the hood. This would be
8//! fine so long as we are guaranteed that our diagnostic tooling and OpenHCL
9//! builds are always in sync, and indeed they are most of the time. However
10//! there are cases where they may diverge temporarily, such as during servicing.
11//! We still want to be able to provide as much information as possible during
12//! these time periods, but in order for that to be possible we need some kind
13//! of stable-ish interface to talk to OpenHCL with.
14//!
15//! This module provides that interface by creating known, controlled inspect
16//! paths that are unlikely to change accidentally. The implementation details
17//! of these paths are free to change in order to preserve their interface, and
18//! all diagnostics commands should have extensive tests to ensure compatibility
19//! with a matching build of OpenHCL.
20//!
21//! This approach is not particularly scalable, especially if other parties want
22//! to add their own stabilized diagnostics and versioning schemes. If such a
23//! need arises we should consider a more general and extensible solution.
24//!
25//! At the time of writing, our support policy is that these interfaces will be
26//! preserved for at least 2 internal releases. This means that diagnostic
27//! commands may fail in any way they see fit if the version of OpenHCL in the
28//! VM is more than 2 releases old.
29//!
30//! In the future we may choose a different interface for these commands,
31//! and this code may be deleted, however such a change will still follow the
32//! above support policy.
33
34use inspect::Deferred;
35use inspect::InspectionBuilder;
36use inspect::Node;
37use inspect::Request;
38use inspect::Response;
39use inspect::SensitivityLevel;
40use mesh::Sender;
41use pal_async::DefaultDriver;
42use pal_async::task::Spawn;
43
44pub(crate) fn inspect_internal_diagnostics(
45 req: Request<'_>,
46 reinspect: Sender<Deferred>,
47 driver: DefaultDriver,
48) {
49 req.respond()
50 .sensitivity_field("build_info", SensitivityLevel::Safe, build_info::get())
51 .sensitivity_child("net", SensitivityLevel::Safe, |req| {
52 net(req, reinspect, driver)
53 });
54}
55
56fn net(req: Request<'_>, reinspect: Sender<Deferred>, driver: DefaultDriver) {
57 let defer = req.defer();
58 let driver2 = driver.clone();
59 driver
60 .spawn("inspect-diagnostics-net", async move {
61 // Note the use of Sensitive here so we can inspect under the VM node,
62 // which isn't Safe. The data we produce will still use the underlying
63 // sensitivity of the data nodes, so nothing will be improperly exposed.
64 let mut vm_inspection = InspectionBuilder::new("vm")
65 .depth(Some(0))
66 .sensitivity(Some(SensitivityLevel::Sensitive))
67 .inspect(inspect::adhoc(|req| reinspect.send(req.defer())));
68 vm_inspection.resolve().await;
69
70 let Node::Dir(nodes) = vm_inspection.results() else {
71 return defer.value("Error: No VM node.");
72 };
73
74 defer.respond(|resp| {
75 for nic_entry in nodes
76 .into_iter()
77 .filter(|entry| entry.name.starts_with("net:f8615163-"))
78 {
79 // Inspect node names for MANA nics are in the format:
80 // net:f8615163-0000-1000-2000-<mac address>
81 // So the mac address string starts at index 28
82 let mac_name = nic_entry.name[28..].to_owned();
83
84 // The existence of a mac address is always known to the host, so this can always be Safe.
85 resp.sensitivity_child(&mac_name, SensitivityLevel::Safe, |req| {
86 net_nic(req, nic_entry.name, reinspect.clone(), driver2.clone());
87 });
88 }
89 })
90 })
91 .detach();
92}
93
94// net/mac_address
95// Format for mac address is no separators, lowercase letters, e.g. 00155d121212.
96fn net_nic(req: Request<'_>, name: String, reinspect: Sender<Deferred>, driver: DefaultDriver) {
97 let defer = req.defer();
98 driver
99 .spawn("inspect-diagnostics-net-nic", async move {
100 // Note the use of Sensitive here so we can inspect under the VM node,
101 // which isn't Safe. The data we produce will still use the underlying
102 // sensitivity of the data nodes, so nothing will be improperly exposed.
103 let mut vm_inspection = InspectionBuilder::new(&format!("vm/{name}"))
104 .depth(Some(5))
105 .sensitivity(Some(SensitivityLevel::Sensitive))
106 .inspect(inspect::adhoc(|req| reinspect.send(req.defer())));
107 vm_inspection.resolve().await;
108
109 if let Node::Dir(nodes) = vm_inspection.results() {
110 defer.respond(|resp| {
111 for entry in nodes {
112 let sensitivity = entry.sensitivity;
113 if [
114 "endpoint",
115 "ndis_config",
116 "offload_support",
117 "primary_channel_state",
118 ]
119 .contains(&&*entry.name)
120 {
121 flatten_with_prefix(resp, &entry.name, entry.node, sensitivity, &[]);
122 } else if entry.name == "queues" {
123 let Node::Dir(queues) = entry.node else {
124 continue;
125 };
126 resp.sensitivity_child("queues", sensitivity, |req| {
127 let mut resp = req.respond();
128 for queue_entry in queues {
129 let queue_sensitivity = queue_entry.sensitivity;
130 resp.sensitivity_child(
131 &queue_entry.name,
132 queue_sensitivity,
133 |req| {
134 flatten_with_prefix(
135 &mut req.respond(),
136 "",
137 queue_entry.node,
138 queue_sensitivity,
139 &["ring"],
140 );
141 },
142 );
143 }
144 });
145 }
146 }
147 })
148 } else {
149 defer.value(format!("Unexpected node when looking for NIC {name}."));
150 }
151 })
152 .detach();
153}
154
155fn flatten_with_prefix(
156 resp: &mut Response<'_>,
157 prefix: &str,
158 node: Node,
159 sensitivity: SensitivityLevel,
160 ignore_list: &[&str],
161) {
162 match node {
163 Node::Dir(d) => {
164 for entry in d {
165 if ignore_list.contains(&&*entry.name) {
166 continue;
167 }
168 let next_prefix = if !prefix.is_empty() {
169 format!("{}_{}", prefix, entry.name)
170 } else {
171 entry.name
172 };
173 // Since we're traversing multiple nodes and emitting only one,
174 // emit the final node as the highest sensitivity of all the
175 // nodes we traversed, to be safe.
176 flatten_with_prefix(
177 resp,
178 &next_prefix,
179 entry.node,
180 sensitivity.max(entry.sensitivity),
181 ignore_list,
182 );
183 }
184 }
185 Node::Value(v) => {
186 resp.sensitivity_field(prefix, sensitivity, v);
187 }
188 Node::Failed(_) | Node::Unevaluated => {}
189 }
190}