Skip to content

Commit

Permalink
Add test for process symbolization for binary using dedicated mount n…
Browse files Browse the repository at this point in the history
…amespace

This change adds a test that symbolizes an address in a process that
uses a dedicated mount namespace. This is the first test that symbolizes
an address in a "remote" process. As such it may end up being used as
the scaffolding for additional tests that work on processes other than
the test binary itself.

Signed-off-by: Daniel Müller <[email protected]>
  • Loading branch information
d-e-s-o committed Dec 28, 2023
1 parent 5ac8aff commit 8f6d053
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ anyhow = "1.0.71"
blazesym = {path = ".", features = ["generate-unit-test-files", "tracing"]}
criterion = {version = "0.5.1", default-features = false, features = ["rayon", "cargo_bench_support"]}
env_logger = "0.10"
scopeguard = "1.2"
tempfile = "3.4"
test-log = {version = "0.2", default-features = false, features = ["trace"]}
tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]}
Expand Down
3 changes: 3 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ fn prepare_test_files(crate_root: &Path) {
cc(&src, "test-dwarf-v4.bin", &["-gstrict-dwarf", "-gdwarf-4"]);
cc(&src, "test-dwarf-v5.bin", &["-gstrict-dwarf", "-gdwarf-5"]);

let src = crate_root.join("data").join("test-mnt-ns.c");
cc(&src, "test-mnt-ns.bin", &[]);

let src = crate_root.join("data").join("test-stable-addresses.c");
let src_cu2 = crate_root.join("data").join("test-stable-addresses-cu2.c");
let src_cu2 = src_cu2.to_str().unwrap();
Expand Down
147 changes: 147 additions & 0 deletions data/test-mnt-ns.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <unistd.h>

void rm_dir(char **path) {
int rc;
int err;

rc = rmdir(*path);
if (rc != 0) {
err = errno;
fprintf(stderr, "warning: failed to remove directory %s: %s (errno: %d)\n",
*path, strerror(err), err);
}
}

void unmount(char **path) {
int rc;
int err;

rc = umount(*path);
if (rc != 0) {
err = errno;
fprintf(stderr, "warning: failed to unmount %s: %s (errno: %d)\n", *path,
strerror(err), err);
}
}

void close_so(void **handle) {
int rc;
rc = dlclose(*handle);
if (rc != 0) {
fprintf(stderr, "warning: failed to dlclose: %s\n", dlerror());
}
}

void rm_file(const char **path) {
int rc;
int err;

rc = unlink(*path);
if (rc != 0) {
err = errno;
fprintf(stderr, "warning: failed to remove file %s: %s (errno: %d)\n",
*path, strerror(err), err);
}
}

int main(int argc, char **argv) {
int rc;
int err;

if (argc != 2) {
fprintf(stderr, "usage: %s <path-to-libtest.so>\n",
argc > 0 ? argv[0] : "<program>");
return -1;
}

char const *libtest_src = argv[1];
/* Detach ourselves from the default mount namespace, effectively
* creating a new one for this program.
*/
rc = unshare(CLONE_NEWNS);
if (rc != 0) {
err = errno;
fprintf(stderr, "unshare failed: %s (errno: %d)\n", strerror(err), err);
return err;
}

/* Create a temporary directory (now already inside this mount
* namespace) and mount a ramdisk in there.
*/
char tmpl[] = "/tmp/mnt-ns.XXXXXX";
char *dir = mkdtemp(tmpl);

if (dir == NULL) {
err = errno;
fprintf(stderr, "mkdtemp failed: %s (errno: %d)\n", strerror(err), err);
return err;
}
char *_rm_dir __attribute__((cleanup(rm_dir))) = dir;

rc = mount("tmpfs", dir, "tmpfs", 0, "size=16M");
if (rc != 0) {
err = errno;
fprintf(stderr, "mount failed: %s (errno: %d)\n", strerror(err), err);
return err;
}
char *_umount __attribute__((cleanup(unmount))) = dir;

char libtest_buf[256];
rc = snprintf(libtest_buf, sizeof(libtest_buf), "%s/libtest-so.so", dir);
if (rc >= sizeof(libtest_buf)) {
fprintf(
stderr,
"failed to construct destination path: insufficient buffer space\n");
return -1;
}
libtest_buf[rc] = 0;
char const *libtest_dst = libtest_buf;

char cmd_buf[256];
rc = snprintf(cmd_buf, sizeof(cmd_buf), "cp %s %s", libtest_src, libtest_dst);
if (rc >= sizeof(cmd_buf)) {
fprintf(stderr,
"failed to construct cp command: insufficient buffer space\n");
return -1;
}
cmd_buf[rc] = 0;

char const *cp_cmd = cmd_buf;
/* Sorry, not gonna put up with copying a file in C using system
* APIs...
*/
rc = system(cp_cmd);
if (rc != 0) {
err = errno;
fprintf(stderr, "failed to copy %s to %s: %d\n", libtest_src, libtest_dst,
rc);
return err;
}
const char *_rm __attribute__((cleanup(rm_file))) = libtest_dst;

void *handle;
handle = dlopen(libtest_dst, RTLD_NOW);
if (handle == NULL) {
fprintf(stderr, "failed to dlopen %s: %s\n", libtest_dst, dlerror());
return -1;
}
void *_dlclose __attribute__((cleanup(close_so))) = handle;

void *sym;
sym = dlsym(handle, "await_input");
if (sym == NULL) {
fprintf(stderr, "failed to dlsym `await_input` function: %s\n", dlerror());
return -1;
}

int (*await_input)(void) = sym;
return await_input();
}
15 changes: 15 additions & 0 deletions data/test-so.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
#include <errno.h>
#include <stdio.h>
#include <time.h>

int the_answer(void) {
return 42;
}

int await_input(void) {
struct timespec ts = {.tv_sec = 60};

fprintf(stdout, "%p\n", &await_input);
fflush(stdout);

int c;
c = getc(stdin);
return 0;
}
1 change: 1 addition & 0 deletions data/test-so.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
#define _TEST_SO_H

int the_answer(void);
int await_input(void);

#endif
67 changes: 67 additions & 0 deletions tests/blazesym.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
)]

use std::collections::HashSet;
use std::env;
use std::ffi::CString;
use std::ffi::OsStr;
use std::fs::read as read_file;
use std::io::Error;
use std::io::Read as _;
use std::io::Write as _;
use std::os::unix::ffi::OsStringExt as _;
use std::path::Path;
use std::process::Command;
use std::process::Stdio;
use std::str;

use blazesym::helper::read_elf_build_id;
use blazesym::inspect;
Expand All @@ -24,6 +30,11 @@ use blazesym::Addr;
use blazesym::ErrorKind;
use blazesym::Pid;

use libc::kill;
use libc::SIGKILL;

use scopeguard::defer;

use test_log::test;


Expand Down Expand Up @@ -348,6 +359,62 @@ fn symbolize_process() {
);
}

/// Check that we can symbolize an address in a process using a binary
/// located in a local mount namespace accessible binary.
#[test]
fn symbolize_process_in_mount_namespace() {
let test_so = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("libtest-so.so");
let mnt_ns = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("test-mnt-ns.bin");

let mut child = Command::new(mnt_ns)
.arg(test_so)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let pid = child.id();
defer!({
// Best effort only. The child may end up terminating gracefully
// if everything goes as planned.
// TODO: Ideally this kill would be pid FD based to eliminate
// any possibility of killing the wrong entity.
let _rc = unsafe { kill(pid as _, SIGKILL) };
});

let mut buf = [0u8; 64];
let count = child
.stdout
.as_mut()
.unwrap()
.read(&mut buf)
.expect("failed to read child output");
let addr_str = str::from_utf8(&buf[0..count]).unwrap().trim_end();
let addr = Addr::from_str_radix(addr_str.trim_start_matches("0x"), 16).unwrap();

// Make sure to destroy the symbolizer before terminating the child.
// Otherwise something holds on to a reference and cleanup may fail.
// TODO: This needs to be better understood.
{
let src = symbolize::Source::Process(symbolize::Process::new(Pid::from(child.id())));
let symbolizer = Symbolizer::new();
let result = symbolizer
.symbolize_single(&src, symbolize::Input::AbsAddr(addr))
.unwrap()
.into_sym()
.unwrap();
assert_eq!(result.name, "await_input");
}

// "Signal" the child to terminate gracefully.
let () = child.stdin.as_ref().unwrap().write_all(&[0x04]).unwrap();
let _status = child.wait().unwrap();
}

/// Check that we can normalize addresses in an ELF shared object.
#[test]
fn normalize_elf_addr() {
Expand Down

0 comments on commit 8f6d053

Please sign in to comment.