Skip to content

Commit 5d97551

Browse files
authored
Provide a way of detecting system library leakage (#5259)
After #5158 was integrated @Rain noticed that attempting to run a build of `omdb` in the switch zone suddenly stopped working and filed oxidecomputer/helios-omicron-brand#15. @jgallagher ended up fixing this by splitting out the sled-hardware types into their own crate in #5245. We decided it would be good if we added some sort of CI check to omicron to catch these library leakages earlier. This PR introduces that check and adds it to the helios build and test buildomat job. I have also added some notes to the readme for others that may end up adding a new library dependency. Locally I modified the allow list so that it would produce errors, those errors end up looking like: ``` $ cargo xtask verify-libraries Finished dev [unoptimized + debuginfo] target(s) in 0.42s Running `target/debug/xtask verify-libraries` Finished dev [unoptimized + debuginfo] target(s) in 4.11s Error: Found library issues with the following: installinator UNEXPECTED dependency on libipcc.so.1 omicron-dev UNEXPECTED dependency on libipcc.so.1 UNEXPECTED dependency on libresolv.so.2 sp-sim UNEXPECTED dependency on libipcc.so.1 UNEXPECTED dependency on libresolv.so.2 sled-agent NEEDS libnvme.so.1 but is not allowed mgs UNEXPECTED dependency on libipcc.so.1 UNEXPECTED dependency on libresolv.so.2 If depending on a new library was intended please add it to xtask.toml ```
1 parent 946e4db commit 5d97551

File tree

7 files changed

+224
-2
lines changed

7 files changed

+224
-2
lines changed

.cargo/xtask.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# This config file is used by `cargo xtask verify-libraries`
2+
3+
4+
# These are libraries that we expect to show up in any executable produced
5+
# by the omicron repo.
6+
[libraries."libc.so.1"]
7+
[libraries."libcontract.so.1"]
8+
[libraries."libcrypto.so.3"]
9+
[libraries."libdevinfo.so.1"]
10+
[libraries."libdlpi.so.1"]
11+
[libraries."libdoor.so.1"]
12+
[libraries."libefi.so.1"]
13+
[libraries."libgcc_s.so.1"]
14+
[libraries."libipcc.so.1"]
15+
[libraries."libkstat.so.1"]
16+
[libraries."libm.so.2"]
17+
[libraries."libnsl.so.1"]
18+
[libraries."libnvpair.so.1"]
19+
[libraries."libpq.so.5"]
20+
[libraries."libpthread.so.1"]
21+
[libraries."libresolv.so.2"]
22+
[libraries."librt.so.1"]
23+
[libraries."libscf.so.1"]
24+
[libraries."libsocket.so.1"]
25+
[libraries."libssl.so.3"]
26+
[libraries."libumem.so.1"]
27+
[libraries."libxml2.so.2"]
28+
[libraries."libxmlsec1.so.1"]
29+
30+
# libnvme is a global zone only library and therefore we must be sure that only
31+
# programs running in the gz require it. Additionally only sled-agent should be
32+
# managing a sled's hardware.
33+
[libraries."libnvme.so.1"]
34+
binary_allow_list = [
35+
"installinator",
36+
"omicron-dev",
37+
"omicron-package",
38+
"services-ledger-check-migrate",
39+
"sled-agent",
40+
"sled-agent-sim",
41+
]

.github/buildomat/build-and-test.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ export RUSTC_BOOTSTRAP=1
7676
# We report build progress to stderr, and the "--timings=json" output goes to stdout.
7777
ptime -m cargo build -Z unstable-options --timings=json --workspace --tests --locked --verbose 1> "$OUTPUT_DIR/crate-build-timings.json"
7878

79+
# If we are running on illumos we want to verify that we are not requiring
80+
# system libraries outside of specific binaries. If we encounter this situation
81+
# we bail.
82+
# NB: `cargo xtask verify-libraries` runs `cargo build --bins` to ensure it can
83+
# check the final executables.
84+
if [[ $target_os == "illumos" ]]; then
85+
banner verify-libraries
86+
# This has a separate timeout from `cargo nextest` since `timeout` expects
87+
# to run an external command and therefore we cannot run bash functions or
88+
# subshells.
89+
ptime -m timeout 10m cargo xtask verify-libraries
90+
fi
91+
7992
#
8093
# We apply our own timeout to ensure that we get a normal failure on timeout
8194
# rather than a buildomat timeout. See oxidecomputer/buildomat#8.

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ When you're happy with things and want to make sure you haven't missed something
106106
cargo nextest run
107107
```
108108

109+
=== Adding a new system library dependency
110+
111+
We check that certain system library dependencies are not leaked outside of their intended binaries via `cargo xtask verify-libraries` in CI. If you are adding a new dependency on a illumos/helios library it is recommended that you update xref:.cargo/xtask.toml[] with an allow list of where you expect the dependency to show up. For example some libraries such as `libnvme.so.1` are only available in the global zone and therefore will not be present in any other zone. This check is here to help us catch any leakage before we go to deploy on a rack. You can inspect a compiled binary in the target directory for what it requires by using `elfedit` - for example `elfedit -r -e 'dyn:tag NEEDED' /path/to/omicron/target/debug/sled-agent`.
112+
109113
=== Rust packages in Omicron
110114

111115
NOTE: The term "package" is overloaded: most programming languages and operating systems have their own definitions of a package. On top of that, Omicron bundles up components into our own kind of "package" that gets delivered via the install and update systems. These are described in the `package-manifest.toml` file in the root of the repo. In this section, we're just concerned with Rust packages.

dev-tools/xtask/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ camino.workspace = true
1010
cargo_toml = "0.19"
1111
cargo_metadata = "0.18"
1212
clap.workspace = true
13+
serde.workspace = true
14+
toml.workspace = true
15+
fs-err.workspace = true
16+
swrite.workspace = true

dev-tools/xtask/src/illumos.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use anyhow::{bail, Context, Result};
6+
use camino::Utf8Path;
7+
use cargo_metadata::Message;
8+
use fs_err as fs;
9+
use serde::Deserialize;
10+
use std::{
11+
collections::{BTreeMap, BTreeSet},
12+
io::BufReader,
13+
process::{Command, Stdio},
14+
};
15+
use swrite::{swriteln, SWrite};
16+
17+
use crate::load_workspace;
18+
19+
#[derive(Deserialize, Debug)]
20+
struct LibraryConfig {
21+
binary_allow_list: Option<BTreeSet<String>>,
22+
}
23+
24+
#[derive(Deserialize, Debug)]
25+
struct XtaskConfig {
26+
libraries: BTreeMap<String, LibraryConfig>,
27+
}
28+
29+
#[derive(Debug)]
30+
enum LibraryError {
31+
Unexpected(String),
32+
NotAllowed(String),
33+
}
34+
35+
/// Verify that the binary at the provided path complies with the rules laid out
36+
/// in the xtask.toml config file. Errors are pushed to a hashmap so that we can
37+
/// display to a user the entire list of issues in one go.
38+
fn verify_executable(
39+
config: &XtaskConfig,
40+
path: &Utf8Path,
41+
errors: &mut BTreeMap<String, Vec<LibraryError>>,
42+
) -> Result<()> {
43+
let binary = path.file_name().context("basename of executable")?;
44+
45+
let command = Command::new("elfedit")
46+
.args(["-o", "simple", "-r", "-e", "dyn:tag NEEDED"])
47+
.arg(path)
48+
.output()
49+
.context("exec elfedit")?;
50+
51+
if !command.status.success() {
52+
bail!("Failed to execute elfedit successfully {}", command.status);
53+
}
54+
55+
let stdout = String::from_utf8(command.stdout)?;
56+
// `elfedit -o simple -r -e "dyn:tag NEEDED" /file/path` will return
57+
// a new line seperated list of required libraries so we walk over
58+
// them looking for a match in our configuration file. If we find
59+
// the library we make sure the binary is allowed to pull it in via
60+
// the whitelist.
61+
for library in stdout.lines() {
62+
let library_config = match config.libraries.get(library.trim()) {
63+
Some(config) => config,
64+
None => {
65+
errors
66+
.entry(binary.to_string())
67+
.or_default()
68+
.push(LibraryError::Unexpected(library.to_string()));
69+
70+
continue;
71+
}
72+
};
73+
74+
if let Some(allowed) = &library_config.binary_allow_list {
75+
if !allowed.contains(binary) {
76+
errors
77+
.entry(binary.to_string())
78+
.or_default()
79+
.push(LibraryError::NotAllowed(library.to_string()));
80+
}
81+
}
82+
}
83+
84+
Ok(())
85+
}
86+
pub fn cmd_verify_libraries() -> Result<()> {
87+
let metadata = load_workspace()?;
88+
let mut config_path = metadata.workspace_root;
89+
config_path.push(".cargo/xtask.toml");
90+
let config = read_xtask_toml(&config_path)?;
91+
92+
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
93+
let mut command = Command::new(cargo)
94+
.args(["build", "--bins", "--message-format=json-render-diagnostics"])
95+
.stdout(Stdio::piped())
96+
.spawn()
97+
.context("failed to spawn cargo build")?;
98+
99+
let reader = BufReader::new(command.stdout.take().context("take stdout")?);
100+
101+
let mut errors = Default::default();
102+
for message in cargo_metadata::Message::parse_stream(reader) {
103+
if let Message::CompilerArtifact(artifact) = message? {
104+
// We are only interested in artifacts that are binaries
105+
if let Some(executable) = artifact.executable {
106+
verify_executable(&config, &executable, &mut errors)?;
107+
}
108+
}
109+
}
110+
111+
let status = command.wait()?;
112+
if !status.success() {
113+
bail!("Failed to execute cargo build successfully {}", status);
114+
}
115+
116+
if !errors.is_empty() {
117+
let mut msg = String::new();
118+
errors.iter().for_each(|(binary, errors)| {
119+
swriteln!(msg, "{binary}");
120+
errors.iter().for_each(|error| match error {
121+
LibraryError::Unexpected(lib) => {
122+
swriteln!(msg, "\tUNEXPECTED dependency on {lib}");
123+
}
124+
LibraryError::NotAllowed(lib) => {
125+
swriteln!(msg, "\tNEEDS {lib} but is not allowed");
126+
}
127+
});
128+
});
129+
130+
bail!(
131+
"Found library issues with the following:\n{msg}\n\n\
132+
If depending on a new library was intended please add it to xtask.toml"
133+
);
134+
}
135+
136+
Ok(())
137+
}
138+
139+
fn read_xtask_toml(path: &Utf8Path) -> Result<XtaskConfig> {
140+
let config_str = fs::read_to_string(path)?;
141+
toml::from_str(&config_str).with_context(|| format!("parse {:?}", path))
142+
}

dev-tools/xtask/src/main.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ use camino::Utf8Path;
1111
use cargo_metadata::Metadata;
1212
use cargo_toml::{Dependency, Manifest};
1313
use clap::{Parser, Subcommand};
14+
use fs_err as fs;
1415
use std::{collections::BTreeMap, process::Command};
1516

17+
#[cfg(target_os = "illumos")]
18+
mod illumos;
19+
#[cfg(target_os = "illumos")]
20+
use illumos::cmd_verify_libraries;
21+
1622
#[derive(Parser)]
1723
#[command(name = "cargo xtask", about = "Workspace-related developer tools")]
1824
struct Args {
@@ -27,6 +33,9 @@ enum Cmds {
2733
CheckWorkspaceDeps,
2834
/// Run configured clippy checks
2935
Clippy(ClippyArgs),
36+
/// Verify we are not leaking library bindings outside of intended
37+
/// crates
38+
VerifyLibraries,
3039
}
3140

3241
#[derive(Parser)]
@@ -41,6 +50,7 @@ fn main() -> Result<()> {
4150
match args.cmd {
4251
Cmds::Clippy(args) => cmd_clippy(args),
4352
Cmds::CheckWorkspaceDeps => cmd_check_workspace_deps(),
53+
Cmds::VerifyLibraries => cmd_verify_libraries(),
4454
}
4555
}
4656

@@ -213,9 +223,13 @@ fn cmd_check_workspace_deps() -> Result<()> {
213223
Ok(())
214224
}
215225

226+
#[cfg(not(target_os = "illumos"))]
227+
fn cmd_verify_libraries() -> Result<()> {
228+
unimplemented!("Library verification is only available on illumos!")
229+
}
230+
216231
fn read_cargo_toml(path: &Utf8Path) -> Result<Manifest> {
217-
let bytes =
218-
std::fs::read(path).with_context(|| format!("read {:?}", path))?;
232+
let bytes = fs::read(path)?;
219233
Manifest::from_slice(&bytes).with_context(|| format!("parse {:?}", path))
220234
}
221235

0 commit comments

Comments
 (0)