use crate::{
elf::Architecture,
error::{Error, Result},
policy::RuntimeFeature,
source::SymlinkEntry,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
pub type NodeId = u32;
pub const NODES_MAX: usize = 4096;
pub const EDGES_MAX: usize = NODES_MAX * 64;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Digest(pub String);
pub const DIGEST_LEN_HEX: usize = 64;
impl Digest {
pub fn is_well_formed(&self) -> bool {
self.0.len() == DIGEST_LEN_HEX
&& self
.0
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
}
impl std::fmt::Display for Digest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Executable,
Interpreter,
SharedObject,
}
#[derive(Debug, Clone)]
pub struct Node {
pub source: PathBuf,
pub logical: PathBuf,
pub destination: PathBuf,
pub kind: NodeKind,
pub soname: Option<String>,
pub architecture: Architecture,
pub sha256: Digest,
pub size: u64,
pub links: Vec<SymlinkEntry>,
pub dlopen_references: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DependencyReason {
Interpreter,
Needed { soname: String },
RuntimePolicy { feature: RuntimeFeature },
}
#[derive(Debug, Clone)]
pub struct Edge {
pub from: NodeId,
pub to: NodeId,
pub reason: DependencyReason,
}
#[derive(Debug, Clone, Default)]
pub struct DependencyGraph {
pub root: NodeId,
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub declared_interpreter: Option<PathBuf>,
pub executable_search_paths: Vec<String>,
by_logical: HashMap<PathBuf, NodeId>,
outgoing: HashMap<NodeId, Vec<usize>>,
first_incoming: HashMap<NodeId, usize>,
}
impl DependencyGraph {
pub fn new() -> DependencyGraph {
DependencyGraph::default()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn insert(&mut self, node: Node) -> Result<NodeId> {
assert!(node.logical.is_absolute());
assert!(node.destination.is_absolute());
assert!(node.sha256.is_well_formed());
if let Some(&id) = self.by_logical.get(&node.logical) {
let existing = &mut self.nodes[id as usize];
for link in node.links {
if !existing.links.contains(&link) {
existing.links.push(link);
}
}
return Ok(id);
}
if self.nodes.len() >= NODES_MAX {
return Err(Error::LimitExceeded {
resource: "runtime closure",
limit: NODES_MAX,
});
}
let id = NodeId::try_from(self.nodes.len()).expect("node count is bounded by NODES_MAX");
self.by_logical.insert(node.logical.clone(), id);
self.nodes.push(node);
assert_eq!(self.nodes.len(), self.by_logical.len());
Ok(id)
}
pub fn find(&self, logical: &Path) -> Option<NodeId> {
assert!(logical.is_absolute());
self.by_logical.get(logical).copied()
}
pub fn connect(&mut self, from: NodeId, to: NodeId, reason: DependencyReason) -> Result<()> {
assert!(self.contains(from));
assert!(self.contains(to));
if from == to {
return Ok(());
}
let known = self
.edges_from(from)
.any(|e| e.to == to && e.reason == reason);
if known {
return Ok(());
}
if self.edges.len() >= EDGES_MAX {
return Err(Error::LimitExceeded {
resource: "runtime dependency graph",
limit: EDGES_MAX,
});
}
let index = self.edges.len();
self.edges.push(Edge { from, to, reason });
self.outgoing.entry(from).or_default().push(index);
self.first_incoming.entry(to).or_insert(index);
Ok(())
}
pub fn contains(&self, id: NodeId) -> bool {
(id as usize) < self.nodes.len()
}
pub fn node(&self, id: NodeId) -> &Node {
assert!(self.contains(id));
&self.nodes[id as usize]
}
pub fn root_node(&self) -> &Node {
let root = self.node(self.root);
assert_eq!(root.kind, NodeKind::Executable);
root
}
pub fn edges_from(&self, id: NodeId) -> impl Iterator<Item = &Edge> {
self.outgoing
.get(&id)
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
.map(|&index| &self.edges[index])
}
pub fn dependencies(&self, id: NodeId) -> Vec<(&Edge, &Node)> {
assert!(self.contains(id));
self.edges_from(id).map(|e| (e, self.node(e.to))).collect()
}
pub fn first_dependent(&self, id: NodeId) -> Option<(&Edge, &Node)> {
assert!(self.contains(id));
let edge = &self.edges[*self.first_incoming.get(&id)?];
Some((edge, self.node(edge.from)))
}
pub fn iter(&self) -> impl Iterator<Item = (NodeId, &Node)> {
self.nodes.iter().enumerate().map(|(index, node)| {
let id = NodeId::try_from(index).expect("node count is bounded by NODES_MAX");
(id, node)
})
}
pub fn shared_objects(&self) -> impl Iterator<Item = &Node> {
self.nodes
.iter()
.filter(|n| n.kind == NodeKind::SharedObject)
}
pub fn total_size(&self) -> u64 {
self.nodes.iter().map(|n| n.size).sum()
}
pub fn application_closure(&self) -> HashSet<NodeId> {
assert!(self.contains(self.root));
let mut reached = HashSet::from([self.root]);
let mut queue = vec![self.root];
while let Some(id) = queue.pop() {
for edge in self.edges_from(id) {
if matches!(edge.reason, DependencyReason::RuntimePolicy { .. }) {
continue;
}
if reached.insert(edge.to) {
queue.push(edge.to);
}
}
}
reached
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
elf::{ElfClass, Endianness, Machine},
hash::sha256_bytes,
};
fn node(path: PathBuf) -> Node {
Node {
source: path.clone(),
logical: path.clone(),
destination: path,
kind: NodeKind::SharedObject,
soname: None,
architecture: Architecture {
machine: Machine::X86_64,
class: ElfClass::Elf64,
endianness: Endianness::Little,
},
sha256: sha256_bytes(b"test"),
size: 0,
links: Vec::new(),
dlopen_references: Vec::new(),
}
}
#[test]
fn an_oversized_closure_is_an_error() {
let mut graph = DependencyGraph::new();
for index in 0..NODES_MAX {
graph
.insert(node(PathBuf::from(format!("/lib/{index}"))))
.unwrap();
}
let error = graph
.insert(node(PathBuf::from("/lib/overflow")))
.unwrap_err();
assert!(matches!(
&error,
Error::LimitExceeded {
resource: "runtime closure",
limit: NODES_MAX,
}
));
assert_eq!(error.code(), "E1005");
}
#[test]
fn a_digest_is_sixty_four_lowercase_hex_digits() {
assert!(sha256_bytes(b"test").is_well_formed());
assert!(!Digest("z".repeat(DIGEST_LEN_HEX)).is_well_formed());
assert!(!Digest("A".repeat(DIGEST_LEN_HEX)).is_well_formed());
assert!(!Digest("ab".to_string()).is_well_formed());
}
#[test]
fn the_edge_indices_agree_with_the_edge_list() {
let mut graph = DependencyGraph::new();
let ids: Vec<NodeId> = (0..4)
.map(|index| {
graph
.insert(node(PathBuf::from(format!("/lib/lib{index}.so"))))
.unwrap()
})
.collect();
let needed = |name: &str| DependencyReason::Needed {
soname: name.to_string(),
};
graph.connect(ids[0], ids[1], needed("one")).unwrap();
graph.connect(ids[0], ids[2], needed("two")).unwrap();
graph.connect(ids[3], ids[1], needed("one")).unwrap();
graph.connect(ids[0], ids[1], needed("one")).unwrap();
graph
.connect(ids[0], ids[1], DependencyReason::Interpreter)
.unwrap();
assert_eq!(graph.edges.len(), 4, "only the repeat was dropped");
for (id, _) in graph.iter() {
let indexed: Vec<NodeId> = graph.edges_from(id).map(|e| e.to).collect();
let scanned: Vec<NodeId> = graph
.edges
.iter()
.filter(|e| e.from == id)
.map(|e| e.to)
.collect();
assert_eq!(indexed, scanned, "outgoing edges of {id}");
let indexed = graph
.first_dependent(id)
.map(|(_, parent)| parent.logical.clone());
let scanned = graph
.edges
.iter()
.find(|e| e.to == id)
.map(|e| graph.node(e.from).logical.clone());
assert_eq!(indexed, scanned, "first dependent of {id}");
}
}
#[test]
fn a_self_dependency_does_not_add_an_edge() {
let mut graph = DependencyGraph::new();
let id = graph.insert(node(PathBuf::from("/lib/self.so"))).unwrap();
graph
.connect(
id,
id,
DependencyReason::Needed {
soname: "self.so".to_string(),
},
)
.unwrap();
assert!(graph.edges.is_empty());
}
}