From dcc6b84a8803cfee37ab4e138c89616f1fc1b002 Mon Sep 17 00:00:00 2001 From: Andrew Werner Date: Tue, 1 Aug 2023 20:50:23 -0400 Subject: [PATCH 1/2] programs/uprobe: extract library path resolving The function is extracted so that a test could be written. This test is valid on linux-gnu targets, and it doesn't need any special privileges. This is in anticipation of removing the code that uses this functionality (seemingly incidentally) from integration tests. --- aya/src/programs/uprobe.rs | 89 +++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/aya/src/programs/uprobe.rs b/aya/src/programs/uprobe.rs index 8c42ced0..9c018233 100644 --- a/aya/src/programs/uprobe.rs +++ b/aya/src/programs/uprobe.rs @@ -82,36 +82,7 @@ impl UProbe { target: T, pid: Option, ) -> Result { - let target = target.as_ref(); - let target_str = &*target.as_os_str().to_string_lossy(); - - let mut path = if let Some(pid) = pid { - find_lib_in_proc_maps(pid, target_str).map_err(|io_error| UProbeError::FileError { - filename: format!("/proc/{pid}/maps"), - io_error, - })? - } else { - None - }; - - if path.is_none() { - path = if target.is_absolute() { - Some(target_str) - } else { - let cache = - LD_SO_CACHE - .as_ref() - .map_err(|error| UProbeError::InvalidLdSoCache { - io_error: error.clone(), - })?; - cache.resolve(target_str) - } - .map(String::from) - }; - - let path = path.ok_or(UProbeError::InvalidTarget { - path: target.to_owned(), - })?; + let path = resolve_attach_path(target, pid)?; let sym_offset = if let Some(fn_name) = fn_name { resolve_symbol(&path, fn_name).map_err(|error| UProbeError::SymbolError { @@ -152,6 +123,64 @@ impl UProbe { } } +fn resolve_attach_path>( + target: T, + pid: Option, +) -> Result { + // Look up the path for the target. If it there is a pid, and the target is a library name that + // is in the process's memory map, use the path of that library. Otherwise, use the target + // as-is. + let target = target.as_ref(); + let target_str = &*target.as_os_str().to_string_lossy(); + + let mut path = if let Some(pid) = pid { + find_lib_in_proc_maps(pid, target_str).map_err(|io_error| UProbeError::FileError { + filename: format!("/proc/{pid}/maps"), + io_error, + })? + } else { + None + }; + + if path.is_none() { + path = if target.is_absolute() { + Some(target_str) + } else { + let cache = LD_SO_CACHE + .as_ref() + .map_err(|error| UProbeError::InvalidLdSoCache { + io_error: error.clone(), + })?; + cache.resolve(target_str) + } + .map(String::from) + }; + + path.ok_or(UProbeError::InvalidTarget { + path: target.to_owned(), + }) +} + +// Only run this test on linux with glibc because only in that configuration do we know that we'll +// be dynamically linked to libc and can exercise resolving the path to libc via the current +// process's memory map. +#[test] +#[cfg_attr( + any(miri, not(all(target_os = "linux", target_env = "gnu"))), + ignore = "requires glibc, doesn't work in miri" +)] +fn test_resolve_attach_path() { + // Look up the current process's pid. + let pid = std::process::id().try_into().unwrap(); + + // Now let's resolve the path to libc. It should exist in the current process's memory map and + // then in the ld.so.cache. + let libc_path = resolve_attach_path("libc", Some(pid)).unwrap(); + + // Make sure we got a path that contains libc. + assert!(libc_path.contains("libc"), "libc_path: {}", libc_path); +} + define_link_wrapper!( /// The link used by [UProbe] programs. UProbeLink, From 79c9489a4f6d0c35ce1f317b7461fd3187986edd Mon Sep 17 00:00:00 2001 From: Andrew Werner Date: Tue, 1 Aug 2023 20:52:33 -0400 Subject: [PATCH 2/2] integration-test: attach uprobes to self For unclear reasons, two of the integration tests related to uprobes were resolving a symbol in libc. The integration-test binary can be built statically, in which case it would not load or reference libc. Statically linking the integration tests and running them in a VM without a userland is a convenient mechanism to exercise the tests against different kernel versions. The fact that the statically linked integration-test binary does not load libc is not the only reason these tests failed in such an environment. In fact, the logic to look in the process's memory maps was not running (because no pid was being passed). Separate logic to determine which object file to use when attempting to resolve a symbol for attaching a uprobe changes its behavior based on whether that target is an absolute path. If the target is not an absolute path, the code searches through the LdSoCache. This cache does not always exist in linux systems; when an attach call is made with a relative path target and there is no /etc/ld.so.cache file, the attach call will fail. This commit does not change that behavior, it merely sidesteps it. --- test/integration-test/src/tests/load.rs | 43 ++++++++++++++----------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs index ece72daf..0b0d1532 100644 --- a/test/integration-test/src/tests/load.rs +++ b/test/integration-test/src/tests/load.rs @@ -201,7 +201,9 @@ fn basic_uprobe() { prog.load().unwrap(); assert_loaded("test_uprobe"); - let link = prog.attach(Some("sleep"), 0, "libc", None).unwrap(); + let link = prog + .attach(Some("uprobe_function"), 0, "/proc/self/exe", None) + .unwrap(); { let _link_owned = prog.take_link(link).unwrap(); @@ -213,7 +215,8 @@ fn basic_uprobe() { prog.load().unwrap(); assert_loaded("test_uprobe"); - prog.attach(Some("sleep"), 0, "libc", None).unwrap(); + prog.attach(Some("uprobe_function"), 0, "/proc/self/exe", None) + .unwrap(); assert_loaded("test_uprobe"); prog.unload().unwrap(); @@ -424,14 +427,23 @@ fn pin_lifecycle_kprobe() { assert_unloaded("test_kprobe"); } +#[no_mangle] +#[inline(never)] +extern "C" fn uprobe_function() { + core::hint::black_box(uprobe_function); +} + #[test] fn pin_lifecycle_uprobe() { + const FIRST_PIN_PATH: &str = "/sys/fs/bpf/aya-uprobe-test-prog-1"; + const SECOND_PIN_PATH: &str = "/sys/fs/bpf/aya-uprobe-test-prog-2"; + // 1. Load Program and Pin { let mut bpf = Bpf::load(crate::TEST).unwrap(); let prog: &mut UProbe = bpf.program_mut("test_uprobe").unwrap().try_into().unwrap(); prog.load().unwrap(); - prog.pin("/sys/fs/bpf/aya-uprobe-test-prog").unwrap(); + prog.pin(FIRST_PIN_PATH).unwrap(); } // should still be loaded since prog was pinned @@ -439,11 +451,7 @@ fn pin_lifecycle_uprobe() { // 2. Load program from bpffs but don't attach it { - let _ = UProbe::from_pin( - "/sys/fs/bpf/aya-uprobe-test-prog", - aya::programs::ProbeKind::UProbe, - ) - .unwrap(); + let _ = UProbe::from_pin(FIRST_PIN_PATH, aya::programs::ProbeKind::UProbe).unwrap(); } // should still be loaded since prog was pinned @@ -451,17 +459,13 @@ fn pin_lifecycle_uprobe() { // 3. Load program from bpffs and attach { - let mut prog = UProbe::from_pin( - "/sys/fs/bpf/aya-uprobe-test-prog", - aya::programs::ProbeKind::UProbe, - ) - .unwrap(); - let link_id = prog.attach(Some("sleep"), 0, "libc", None).unwrap(); + let mut prog = UProbe::from_pin(FIRST_PIN_PATH, aya::programs::ProbeKind::UProbe).unwrap(); + let link_id = prog + .attach(Some("uprobe_function"), 0, "/proc/self/exe", None) + .unwrap(); let link = prog.take_link(link_id).unwrap(); let fd_link: FdLink = link.try_into().unwrap(); - fd_link - .pin("/sys/fs/bpf/aya-uprobe-test-bash-sleep") - .unwrap(); + fd_link.pin(SECOND_PIN_PATH).unwrap(); // Unpin the program. It will stay attached since its links were pinned. prog.unpin().unwrap(); @@ -472,7 +476,7 @@ fn pin_lifecycle_uprobe() { // 4. unpin link, and make sure everything is unloaded { - PinnedLink::from_pin("/sys/fs/bpf/aya-uprobe-test-bash-sleep") + PinnedLink::from_pin(SECOND_PIN_PATH) .unwrap() .unpin() .unwrap(); @@ -480,4 +484,7 @@ fn pin_lifecycle_uprobe() { // program should be unloaded assert_unloaded("test_uprobe"); + + // Make sure the function isn't optimized out. + uprobe_function(); }