1use fuchsia_trace::{ArgValue, Scope, TraceCategoryContext, trace_site_t};
36use lab_proxy_core::access_policy::{Denial, MmioResource, ResourceId};
37use lab_proxy_core::audit_ring::{AuditObserver, AuditRecord, Decision, OpStatus};
38use std::collections::BTreeMap;
39use std::ffi::CStr;
40use std::sync::Arc;
41
42pub const CATEGORY: &CStr = c"driver_lab";
44
45pub const IRQ_CATEGORY: &CStr = c"driver_lab:irq";
47
48const INTERRUPT_TAP: &str = "interrupt_tap";
50
51const WRITE32: &str = "write32";
53
54static CATEGORY_SITE: trace_site_t = trace_site_t::new(0);
55static IRQ_CATEGORY_SITE: trace_site_t = trace_site_t::new(0);
56
57#[derive(Debug)]
59pub struct AuditTracer {
60 node: String,
61 resource_names: BTreeMap<ResourceId, String>,
62 counter_names: BTreeMap<ResourceId, String>,
63}
64
65impl AuditTracer {
66 pub fn new(node: &str, resources: &BTreeMap<ResourceId, MmioResource>) -> Self {
69 let resource_names: BTreeMap<ResourceId, String> =
70 resources.iter().map(|(id, resource)| (*id, resource.name.clone())).collect();
71 let counter_names =
72 resource_names.iter().map(|(id, name)| (*id, counter_name(node, name))).collect();
73 Self { node: node.to_string(), resource_names, counter_names }
74 }
75
76 pub fn observer(self: &Arc<Self>) -> AuditObserver {
80 let tracer = Arc::clone(self);
81 Box::new(move |seq, record| tracer.record(Some(seq), record))
82 }
83
84 pub fn record(&self, seq: Option<u64>, record: &AuditRecord) {
88 let context = if record.operation == INTERRUPT_TAP {
89 TraceCategoryContext::acquire_cached(IRQ_CATEGORY, &IRQ_CATEGORY_SITE)
90 } else {
91 TraceCategoryContext::acquire_cached(CATEGORY, &CATEGORY_SITE)
92 };
93 let Some(context) = context else {
94 return;
95 };
96
97 let resource_name = record.resource.and_then(|id| self.resource_names.get(&id));
98 let denial = match record.decision {
99 Decision::Allowed => None,
100 Decision::Denied(denial) => Some(denial_label(denial)),
101 };
102
103 let mut args = Vec::with_capacity(13);
104 args.push(ArgValue::of("node", self.node.as_str()));
105 if let Some(seq) = seq {
106 args.push(ArgValue::of("seq", seq));
107 }
108 if let Some(session) = record.session {
109 args.push(ArgValue::of("session", session));
110 }
111 if let Some(resource) = record.resource {
112 args.push(ArgValue::of("resource", resource));
113 }
114 if let Some(name) = resource_name {
115 args.push(ArgValue::of("resource_name", name.as_str()));
116 }
117 if let Some(offset) = record.offset {
118 args.push(ArgValue::of("offset", offset));
119 }
120 args.push(ArgValue::of("decision", if denial.is_some() { "denied" } else { "allowed" }));
121 if let Some(denial) = denial.as_deref() {
122 args.push(ArgValue::of("denial", denial));
123 }
124 args.push(ArgValue::of("status", status_label(record.status)));
125 if let Some(value) = record.value {
126 args.push(ArgValue::of("value", value));
127 }
128 args.push(ArgValue::of("timestamp_ns", record.timestamp_ns));
129 if let Some(run_id) = record.run_id.as_deref() {
130 args.push(ArgValue::of("run_id", run_id));
131 }
132 if let Some(item_index) = record.item_index {
133 args.push(ArgValue::of("item_index", item_index));
134 }
135 fuchsia_trace::instant(&context, record.operation, Scope::Thread, &args);
136
137 if record.operation == WRITE32
138 && record.decision == Decision::Allowed
139 && record.status == OpStatus::Ok
140 {
141 if let (Some(resource), Some(offset), Some(value)) =
142 (record.resource, record.offset, record.value)
143 {
144 self.write_counter(&context, resource, offset, value);
145 }
146 }
147 }
148
149 pub fn interrupt(&self, resource: ResourceId, sequence: u64, timestamp_ns: i64) {
153 self.record(
154 None,
155 &AuditRecord {
156 session: None,
157 resource: Some(resource),
158 operation: INTERRUPT_TAP,
159 offset: None,
160 decision: Decision::Allowed,
161 status: OpStatus::Ok,
162 value: Some(sequence as u32),
165 timestamp_ns,
166 run_id: None,
167 item_index: None,
168 },
169 );
170 }
171
172 pub fn counter(&self, resource: ResourceId, offset: u64, value: u32) {
178 if let Some(context) = TraceCategoryContext::acquire_cached(CATEGORY, &CATEGORY_SITE) {
179 self.write_counter(&context, resource, offset, value);
180 }
181 }
182
183 fn write_counter(
184 &self,
185 context: &TraceCategoryContext,
186 resource: ResourceId,
187 offset: u64,
188 value: u32,
189 ) {
190 let unnamed;
191 let name = match self.counter_names.get(&resource) {
192 Some(name) => name,
193 None => {
194 unnamed = counter_name(&self.node, &resource.to_string());
195 &unnamed
196 }
197 };
198 let series = offset_series(offset);
199 fuchsia_trace::counter(context, name, 0, &[ArgValue::of(series.as_str(), value)]);
200 }
201}
202
203fn counter_name(node: &str, resource_name: &str) -> String {
204 format!("{node}/{resource_name}")
205}
206
207fn offset_series(offset: u64) -> String {
208 format!("{offset:#x}")
209}
210
211fn denial_label(denial: Denial) -> String {
214 let name = format!("{denial:?}");
215 let mut label = String::with_capacity(name.len() + 4);
216 for (index, ch) in name.chars().enumerate() {
217 if ch.is_ascii_uppercase() {
218 if index > 0 {
219 label.push('_');
220 }
221 label.push(ch.to_ascii_lowercase());
222 } else {
223 label.push(ch);
224 }
225 }
226 label
227}
228
229fn status_label(status: OpStatus) -> &'static str {
231 match status {
232 OpStatus::Ok => "ok",
233 OpStatus::Rejected => "rejected",
234 OpStatus::BackendFault => "backend_fault",
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn denial_labels_match_evidence_format() {
244 assert_eq!(denial_label(Denial::NotInAllowlist), "not_in_allowlist");
245 assert_eq!(denial_label(Denial::HardDenied), "hard_denied");
246 assert_eq!(denial_label(Denial::UnknownReadsNotPermitted), "unknown_reads_not_permitted");
247 assert_eq!(denial_label(Denial::Timeout), "timeout");
248 }
249
250 #[test]
251 fn status_labels_match_evidence_format() {
252 assert_eq!(status_label(OpStatus::Ok), "ok");
253 assert_eq!(status_label(OpStatus::Rejected), "rejected");
254 assert_eq!(status_label(OpStatus::BackendFault), "backend_fault");
255 }
256
257 #[test]
258 fn counter_tracks_are_named_by_node_resource_and_offset() {
259 let mut resources = BTreeMap::new();
260 resources.insert(0, MmioResource::mmio("regs", 0x100, 0x1000));
261 resources.insert(3, MmioResource::interrupt("irq0"));
262 let tracer = AuditTracer::new("sample-driver", &resources);
263 assert_eq!(tracer.counter_names.get(&0).map(String::as_str), Some("sample-driver/regs"));
264 assert_eq!(tracer.counter_names.get(&3).map(String::as_str), Some("sample-driver/irq0"));
265 assert_eq!(offset_series(0x1c), "0x1c");
266 assert_eq!(offset_series(0), "0x0");
267 }
268
269 #[test]
270 fn emitting_without_an_active_trace_session_is_a_no_op() {
271 let mut resources = BTreeMap::new();
272 resources.insert(0, MmioResource::mmio("regs", 0x100, 0x1000));
273 let tracer = Arc::new(AuditTracer::new("sample-driver", &resources));
274 let observer = tracer.observer();
275 observer(
276 7,
277 &AuditRecord {
278 session: Some(1),
279 resource: Some(0),
280 operation: WRITE32,
281 offset: Some(0x1c),
282 decision: Decision::Allowed,
283 status: OpStatus::Ok,
284 value: Some(1),
285 timestamp_ns: 1,
286 run_id: Some("run".to_string()),
287 item_index: Some(2),
288 },
289 );
290 observer(
291 8,
292 &AuditRecord {
293 decision: Decision::Denied(Denial::HardDenied),
294 status: OpStatus::Rejected,
295 ..AuditRecord::lifecycle("read32", 2)
296 },
297 );
298 tracer.interrupt(0, 1, 3);
299 tracer.counter(0, 0x1c, 0);
300 tracer.counter(99, 0x1c, 0);
301 }
302}