1use crate::{
4 elf::Architecture,
5 error::{Error, Result},
6 policy::RuntimeFeature,
7 source::SymlinkEntry,
8};
9use serde::{Deserialize, Serialize};
10use std::{
11 collections::{HashMap, HashSet},
12 path::{Path, PathBuf},
13};
14
15pub type NodeId = u32;
17
18pub const NODES_MAX: usize = 4096;
23
24pub const EDGES_MAX: usize = NODES_MAX * 64;
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct Digest(pub String);
31
32pub const DIGEST_LEN_HEX: usize = 64;
34
35impl Digest {
36 pub fn is_well_formed(&self) -> bool {
39 self.0.len() == DIGEST_LEN_HEX
40 && self
41 .0
42 .bytes()
43 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
44 }
45}
46
47impl std::fmt::Display for Digest {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.write_str(&self.0)
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum NodeKind {
56 Executable,
57 Interpreter,
58 SharedObject,
59}
60
61#[derive(Debug, Clone)]
62pub struct Node {
63 pub source: PathBuf,
65 pub logical: PathBuf,
67 pub destination: PathBuf,
69 pub kind: NodeKind,
70 pub soname: Option<String>,
71 pub architecture: Architecture,
72 pub sha256: Digest,
73 pub size: u64,
74 pub links: Vec<SymlinkEntry>,
76 pub dlopen_references: Vec<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum DependencyReason {
82 Interpreter,
83 Needed { soname: String },
84 RuntimePolicy { feature: RuntimeFeature },
85}
86
87#[derive(Debug, Clone)]
88pub struct Edge {
89 pub from: NodeId,
90 pub to: NodeId,
91 pub reason: DependencyReason,
92}
93
94#[derive(Debug, Clone, Default)]
95pub struct DependencyGraph {
96 pub root: NodeId,
97 pub nodes: Vec<Node>,
98 pub edges: Vec<Edge>,
100 pub declared_interpreter: Option<PathBuf>,
103 pub executable_search_paths: Vec<String>,
107 by_logical: HashMap<PathBuf, NodeId>,
108 outgoing: HashMap<NodeId, Vec<usize>>,
112 first_incoming: HashMap<NodeId, usize>,
114}
115
116impl DependencyGraph {
117 pub fn new() -> DependencyGraph {
118 DependencyGraph::default()
119 }
120
121 pub fn node_count(&self) -> usize {
122 self.nodes.len()
123 }
124
125 pub fn insert(&mut self, node: Node) -> Result<NodeId> {
131 assert!(node.logical.is_absolute());
132 assert!(node.destination.is_absolute());
133 assert!(node.sha256.is_well_formed());
134
135 if let Some(&id) = self.by_logical.get(&node.logical) {
136 let existing = &mut self.nodes[id as usize];
137 for link in node.links {
138 if !existing.links.contains(&link) {
139 existing.links.push(link);
140 }
141 }
142 return Ok(id);
143 }
144
145 if self.nodes.len() >= NODES_MAX {
146 return Err(Error::LimitExceeded {
147 resource: "runtime closure",
148 limit: NODES_MAX,
149 });
150 }
151 let id = NodeId::try_from(self.nodes.len()).expect("node count is bounded by NODES_MAX");
152 self.by_logical.insert(node.logical.clone(), id);
153 self.nodes.push(node);
154 assert_eq!(self.nodes.len(), self.by_logical.len());
155 Ok(id)
156 }
157
158 pub fn find(&self, logical: &Path) -> Option<NodeId> {
159 assert!(logical.is_absolute());
160 self.by_logical.get(logical).copied()
161 }
162
163 pub fn connect(&mut self, from: NodeId, to: NodeId, reason: DependencyReason) -> Result<()> {
164 assert!(self.contains(from));
165 assert!(self.contains(to));
166 if from == to {
167 return Ok(());
170 }
171
172 let known = self
173 .edges_from(from)
174 .any(|e| e.to == to && e.reason == reason);
175 if known {
176 return Ok(());
177 }
178 if self.edges.len() >= EDGES_MAX {
179 return Err(Error::LimitExceeded {
180 resource: "runtime dependency graph",
181 limit: EDGES_MAX,
182 });
183 }
184 let index = self.edges.len();
185 self.edges.push(Edge { from, to, reason });
186 self.outgoing.entry(from).or_default().push(index);
187 self.first_incoming.entry(to).or_insert(index);
189 Ok(())
190 }
191
192 pub fn contains(&self, id: NodeId) -> bool {
193 (id as usize) < self.nodes.len()
194 }
195
196 pub fn node(&self, id: NodeId) -> &Node {
197 assert!(self.contains(id));
198 &self.nodes[id as usize]
199 }
200
201 pub fn root_node(&self) -> &Node {
202 let root = self.node(self.root);
203 assert_eq!(root.kind, NodeKind::Executable);
204 root
205 }
206
207 pub fn edges_from(&self, id: NodeId) -> impl Iterator<Item = &Edge> {
209 self.outgoing
210 .get(&id)
211 .map(Vec::as_slice)
212 .unwrap_or_default()
213 .iter()
214 .map(|&index| &self.edges[index])
215 }
216
217 pub fn dependencies(&self, id: NodeId) -> Vec<(&Edge, &Node)> {
219 assert!(self.contains(id));
220 self.edges_from(id).map(|e| (e, self.node(e.to))).collect()
221 }
222
223 pub fn first_dependent(&self, id: NodeId) -> Option<(&Edge, &Node)> {
225 assert!(self.contains(id));
226 let edge = &self.edges[*self.first_incoming.get(&id)?];
227 Some((edge, self.node(edge.from)))
228 }
229
230 pub fn iter(&self) -> impl Iterator<Item = (NodeId, &Node)> {
233 self.nodes.iter().enumerate().map(|(index, node)| {
234 let id = NodeId::try_from(index).expect("node count is bounded by NODES_MAX");
235 (id, node)
236 })
237 }
238
239 pub fn shared_objects(&self) -> impl Iterator<Item = &Node> {
240 self.nodes
241 .iter()
242 .filter(|n| n.kind == NodeKind::SharedObject)
243 }
244
245 pub fn total_size(&self) -> u64 {
246 self.nodes.iter().map(|n| n.size).sum()
247 }
248
249 pub fn application_closure(&self) -> HashSet<NodeId> {
255 assert!(self.contains(self.root));
256
257 let mut reached = HashSet::from([self.root]);
258 let mut queue = vec![self.root];
259 while let Some(id) = queue.pop() {
262 for edge in self.edges_from(id) {
263 if matches!(edge.reason, DependencyReason::RuntimePolicy { .. }) {
264 continue;
265 }
266 if reached.insert(edge.to) {
267 queue.push(edge.to);
268 }
269 }
270 }
271 reached
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::{
279 elf::{ElfClass, Endianness, Machine},
280 hash::sha256_bytes,
281 };
282
283 fn node(path: PathBuf) -> Node {
284 Node {
285 source: path.clone(),
286 logical: path.clone(),
287 destination: path,
288 kind: NodeKind::SharedObject,
289 soname: None,
290 architecture: Architecture {
291 machine: Machine::X86_64,
292 class: ElfClass::Elf64,
293 endianness: Endianness::Little,
294 },
295 sha256: sha256_bytes(b"test"),
296 size: 0,
297 links: Vec::new(),
298 dlopen_references: Vec::new(),
299 }
300 }
301
302 #[test]
303 fn an_oversized_closure_is_an_error() {
304 let mut graph = DependencyGraph::new();
305 for index in 0..NODES_MAX {
306 graph
307 .insert(node(PathBuf::from(format!("/lib/{index}"))))
308 .unwrap();
309 }
310
311 let error = graph
312 .insert(node(PathBuf::from("/lib/overflow")))
313 .unwrap_err();
314 assert!(matches!(
315 &error,
316 Error::LimitExceeded {
317 resource: "runtime closure",
318 limit: NODES_MAX,
319 }
320 ));
321 assert_eq!(error.code(), "E1005");
322 }
323
324 #[test]
325 fn a_digest_is_sixty_four_lowercase_hex_digits() {
326 assert!(sha256_bytes(b"test").is_well_formed());
327 assert!(!Digest("z".repeat(DIGEST_LEN_HEX)).is_well_formed());
329 assert!(!Digest("A".repeat(DIGEST_LEN_HEX)).is_well_formed());
330 assert!(!Digest("ab".to_string()).is_well_formed());
331 }
332
333 #[test]
336 fn the_edge_indices_agree_with_the_edge_list() {
337 let mut graph = DependencyGraph::new();
338 let ids: Vec<NodeId> = (0..4)
339 .map(|index| {
340 graph
341 .insert(node(PathBuf::from(format!("/lib/lib{index}.so"))))
342 .unwrap()
343 })
344 .collect();
345
346 let needed = |name: &str| DependencyReason::Needed {
347 soname: name.to_string(),
348 };
349 graph.connect(ids[0], ids[1], needed("one")).unwrap();
350 graph.connect(ids[0], ids[2], needed("two")).unwrap();
351 graph.connect(ids[3], ids[1], needed("one")).unwrap();
352 graph.connect(ids[0], ids[1], needed("one")).unwrap();
354 graph
355 .connect(ids[0], ids[1], DependencyReason::Interpreter)
356 .unwrap();
357 assert_eq!(graph.edges.len(), 4, "only the repeat was dropped");
358
359 for (id, _) in graph.iter() {
360 let indexed: Vec<NodeId> = graph.edges_from(id).map(|e| e.to).collect();
361 let scanned: Vec<NodeId> = graph
362 .edges
363 .iter()
364 .filter(|e| e.from == id)
365 .map(|e| e.to)
366 .collect();
367 assert_eq!(indexed, scanned, "outgoing edges of {id}");
368
369 let indexed = graph
370 .first_dependent(id)
371 .map(|(_, parent)| parent.logical.clone());
372 let scanned = graph
373 .edges
374 .iter()
375 .find(|e| e.to == id)
376 .map(|e| graph.node(e.from).logical.clone());
377 assert_eq!(indexed, scanned, "first dependent of {id}");
378 }
379 }
380
381 #[test]
382 fn a_self_dependency_does_not_add_an_edge() {
383 let mut graph = DependencyGraph::new();
384 let id = graph.insert(node(PathBuf::from("/lib/self.so"))).unwrap();
385 graph
386 .connect(
387 id,
388 id,
389 DependencyReason::Needed {
390 soname: "self.so".to_string(),
391 },
392 )
393 .unwrap();
394 assert!(graph.edges.is_empty());
395 }
396}