Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion common/verify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "verify-lib"
version = "0.1.2"
version = "0.1.3"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
84 changes: 70 additions & 14 deletions common/verify/src/ipk/component.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
use std::collections::HashSet;

use bin_lib::{BinaryInfo, LibraryInfo, LibraryPriority};
use ipk_lib::Component;

use crate::bin::binary::recursive_resolve_symbols;
use crate::ipk::{ComponentBinVerifyResult, ComponentVerifyResult};
use crate::{bin::BinVerifyResult, Verify, VerifyResult};

trait ComponentImpl {
fn resolve_lib<F>(&self, name: &str, find_library: &F) -> Option<LibraryInfo>
where
F: Fn(&str) -> Option<LibraryInfo>;

fn verify_bin<F>(&self, bin: &BinaryInfo, find_library: &F) -> BinVerifyResult
where
F: Fn(&str) -> Option<LibraryInfo>;

fn resolve_in_global_scope<F>(&self, undefined: &mut Vec<String>, find_library: &F)
where
F: Fn(&str) -> Option<LibraryInfo>;
}

impl VerifyResult for ComponentVerifyResult {
Expand All @@ -28,25 +39,66 @@ impl VerifyResult for ComponentVerifyResult {
}

impl<T> ComponentImpl for Component<T> {
/// Resolve a needed library by name the same way the dynamic loader would
/// for this component: a library bundled on the rpath takes precedence,
/// otherwise the firmware (system) copy is preferred over a non-rpath
/// bundled copy.
fn resolve_lib<F>(&self, name: &str, find_library: &F) -> Option<LibraryInfo>
where
F: Fn(&str) -> Option<LibraryInfo>,
{
if let Some(lib) = self.find_lib(name) {
if lib.priority == LibraryPriority::Rpath {
return Some(lib.clone());
}
if let Some(sys) = find_library(name) {
return Some(sys);
}
return Some(lib.clone());
}
return find_library(name);
}

fn verify_bin<F>(&self, bin: &BinaryInfo, find_library: &F) -> BinVerifyResult
where
F: Fn(&str) -> Option<LibraryInfo>,
{
return bin.verify(&|name| {
let lib = self.find_lib(name);
return if let Some(lib) = lib {
if lib.priority == LibraryPriority::Rpath {
return Some(lib.clone());
}
return bin.verify(&|name| self.resolve_lib(name, find_library));
}

if let Some(sys) = find_library(name) {
return Some(sys.clone());
}
Some(lib.clone())
} else {
find_library(name)
/// Strike off undefined symbols that are satisfied by the executable's
/// global symbol scope.
///
/// The dynamic loader places the executable and every library in its
/// dependency closure into a single global scope, and resolves each loaded
/// object's undefined symbols against that whole scope. A library's imports
/// can therefore be satisfied by a *sibling* library the executable also
/// loads, even without a direct `DT_NEEDED` link between the two — for
/// example a `libEGL.so.1` shim whose `gl*` imports are provided by the
/// sibling `libGLESv2.so.2`. Verifying each library only against its own
/// `DT_NEEDED` chain misses this and produces false "undefined symbol"
/// reports, so resolve whatever is left against the global scope.
fn resolve_in_global_scope<F>(&self, undefined: &mut Vec<String>, find_library: &F)
where
F: Fn(&str) -> Option<LibraryInfo>,
{
let Some(exe) = &self.exe else {
return;
};
let resolver = |name: &str| self.resolve_lib(name, find_library);
let mut visited: HashSet<String> = Default::default();
for needed in &exe.needed {
if undefined.is_empty() {
break;
}
if !visited.insert(needed.clone()) {
continue;
}
let Some(lib) = resolver(needed) else {
continue;
};
});
recursive_resolve_symbols(&lib, undefined, &mut visited, &resolver);
}
}
}

Expand Down Expand Up @@ -81,7 +133,7 @@ impl<T> Verify<ComponentVerifyResult> for Component<T> {
);
}
}
let verify_result = self.verify_bin(
let mut verify_result = self.verify_bin(
&BinaryInfo {
name: lib.name.clone(),
rpath: Default::default(),
Expand All @@ -90,6 +142,10 @@ impl<T> Verify<ComponentVerifyResult> for Component<T> {
},
find_library,
);
// A bundled library's imports may be provided by a sibling
// library co-loaded by the executable, not just by its own
// DT_NEEDED chain. Resolve the leftovers against that scope.
self.resolve_in_global_scope(&mut verify_result.undefined_sym, find_library);
(
required,
if verify_result.is_good() {
Expand Down
2 changes: 1 addition & 1 deletion packages/ipk-verify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ipk-verify"
version = "0.1.4"
version = "0.1.5"
edition = "2021"
description = "Command line tool for checking symbols in an exectuable and libraries in an IPK file"
authors = ["Mariotaku Lee <mariotaku.lee@gmail.com>"]
Expand Down
95 changes: 95 additions & 0 deletions packages/ipk-verify/tests/global_scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Regression tests for symbol resolution across the executable's global scope.
//!
//! The dynamic loader resolves every loaded object's undefined symbols against
//! a single global scope made up of the executable and its whole dependency
//! closure. A bundled library's imports can therefore be satisfied by a sibling
//! library the executable also loads, even with no direct `DT_NEEDED` link
//! between them (e.g. a `libEGL.so.1` shim whose `gl*` imports live in the
//! sibling `libGLESv2.so.2`). These tests pin that behaviour.

use bin_lib::{BinaryInfo, LibraryInfo, LibraryPriority};
use ipk_lib::Component;
use verify_lib::ipk::ComponentBinVerifyResult;
use verify_lib::Verify;

fn bundled_lib(name: &str, needed: &[&str], symbols: &[&str], undefined: &[&str]) -> LibraryInfo {
let mut symbols: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
symbols.sort_unstable();
LibraryInfo {
name: name.to_string(),
package: None,
needed: needed.iter().map(|s| s.to_string()).collect(),
symbols,
names: vec![name.to_string()],
undefined: undefined.iter().map(|s| s.to_string()).collect(),
priority: LibraryPriority::Rpath,
}
}

fn component(exe_needed: &[&str], libs: Vec<LibraryInfo>) -> Component<()> {
Component {
id: "test".to_string(),
info: (),
exe: Some(BinaryInfo {
name: "app".to_string(),
rpath: vec![],
needed: exe_needed.iter().map(|s| s.to_string()).collect(),
undefined: vec![],
}),
libs,
}
}

fn lib_result<'a>(
result: &'a verify_lib::ipk::ComponentVerifyResult,
name: &str,
) -> &'a ComponentBinVerifyResult {
&result
.libs
.iter()
.find(|(_, lib)| lib.name() == name)
.unwrap_or_else(|| panic!("no result for {name}"))
.1
}

/// A `gl*` symbol imported by `libEGL.so.1` but only defined by the sibling
/// `libGLESv2.so.2` (not in libEGL's `DT_NEEDED`) must not be reported as
/// undefined. This is the exact false positive from apps-repo PR #190.
#[test]
fn sibling_library_satisfies_undefined_symbol() {
// libGLESv2 exports the versioned symbol; libEGL imports it unversioned.
let libegl = bundled_lib("libEGL.so.1", &[], &["eglGetDisplay"], &["glActiveTexture"]);
let libgles = bundled_lib("libGLESv2.so.2", &[], &["glActiveTexture@GLES_3_2"], &[]);
let component = component(&["libEGL.so.1", "libGLESv2.so.2"], vec![libegl, libgles]);

// No firmware libraries available.
let result = component.verify(&|_name| None);

assert!(
matches!(lib_result(&result, "libEGL.so.1"), ComponentBinVerifyResult::Ok { .. }),
"libEGL.so.1 should pass: gl* provided by sibling libGLESv2.so.2; got {:?}",
lib_result(&result, "libEGL.so.1")
);
assert!(matches!(
lib_result(&result, "libGLESv2.so.2"),
ComponentBinVerifyResult::Ok { .. }
));
}

/// A symbol that no library in the global scope provides must still be reported
/// as undefined — the global-scope resolution must not mask genuine misses.
#[test]
fn truly_missing_symbol_still_fails() {
let libegl = bundled_lib("libEGL.so.1", &[], &["eglGetDisplay"], &["someMissingSymbol"]);
let libgles = bundled_lib("libGLESv2.so.2", &[], &["glActiveTexture@GLES_3_2"], &[]);
let component = component(&["libEGL.so.1", "libGLESv2.so.2"], vec![libegl, libgles]);

let result = component.verify(&|_name| None);

match lib_result(&result, "libEGL.so.1") {
ComponentBinVerifyResult::Failed(r) => {
assert!(r.undefined_sym.iter().any(|s| s == "someMissingSymbol"));
}
other => panic!("libEGL.so.1 should fail on the missing symbol, got {other:?}"),
}
}
Loading