From ade2b289d20a902736f6272adda7e1d01d30ba77 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 04:54:11 +0000 Subject: [PATCH 1/2] Fix false-positive undefined symbols across global scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit elf-verify checked each bundled library's undefined symbols only against that library's own DT_NEEDED chain. At runtime the dynamic loader instead resolves every loaded object against a single global scope made up of the executable and its whole dependency closure, so a library's imports can be satisfied by a sibling library the executable also loads without any direct DT_NEEDED link between them. This produced false "undefined symbol" reports for libraries that import from a sibling — e.g. a libEGL.so.1 shim whose gl* imports are provided by the sibling libGLESv2.so.2 (apps-repo PR #190 flagged ~275 gl* symbols as undefined on every firmware). After resolving each bundled library against its own DT_NEEDED chain, also resolve any remaining undefined symbols against the executable's global scope. Genuinely missing libraries and symbols are still reported. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011jY7WzoWeU9TWRBhWJ2Wbq --- common/verify/src/ipk/component.rs | 84 ++++++++++++++++---- packages/ipk-verify/tests/global_scope.rs | 95 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 packages/ipk-verify/tests/global_scope.rs diff --git a/common/verify/src/ipk/component.rs b/common/verify/src/ipk/component.rs index 94fc4269..d98ec84f 100644 --- a/common/verify/src/ipk/component.rs +++ b/common/verify/src/ipk/component.rs @@ -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(&self, name: &str, find_library: &F) -> Option + where + F: Fn(&str) -> Option; + fn verify_bin(&self, bin: &BinaryInfo, find_library: &F) -> BinVerifyResult where F: Fn(&str) -> Option; + + fn resolve_in_global_scope(&self, undefined: &mut Vec, find_library: &F) + where + F: Fn(&str) -> Option; } impl VerifyResult for ComponentVerifyResult { @@ -28,25 +39,66 @@ impl VerifyResult for ComponentVerifyResult { } impl ComponentImpl for Component { + /// 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(&self, name: &str, find_library: &F) -> Option + where + F: Fn(&str) -> Option, + { + 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(&self, bin: &BinaryInfo, find_library: &F) -> BinVerifyResult where F: Fn(&str) -> Option, { - 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(&self, undefined: &mut Vec, find_library: &F) + where + F: Fn(&str) -> Option, + { + let Some(exe) = &self.exe else { + return; + }; + let resolver = |name: &str| self.resolve_lib(name, find_library); + let mut visited: HashSet = 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); + } } } @@ -81,7 +133,7 @@ impl Verify for Component { ); } } - let verify_result = self.verify_bin( + let mut verify_result = self.verify_bin( &BinaryInfo { name: lib.name.clone(), rpath: Default::default(), @@ -90,6 +142,10 @@ impl Verify for Component { }, 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() { diff --git a/packages/ipk-verify/tests/global_scope.rs b/packages/ipk-verify/tests/global_scope.rs new file mode 100644 index 00000000..b837add1 --- /dev/null +++ b/packages/ipk-verify/tests/global_scope.rs @@ -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 = 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) -> 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:?}"), + } +} From a31de38c7692a65d578fbe4de23878ca5ed5f567 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 04:58:21 +0000 Subject: [PATCH 2/2] Bump verify-lib and ipk-verify for global-scope symbol fix The global-scope symbol resolution fix changes ipk-verify's output (libraries whose imports are satisfied by a sibling no longer report false undefined-symbol errors). Bump the affected crates so the new behaviour ships as a distinct release. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011jY7WzoWeU9TWRBhWJ2Wbq --- Cargo.lock | 6 +++--- common/verify/Cargo.toml | 2 +- packages/ipk-verify/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 419b4140..bd20e269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "ipk-verify" -version = "0.1.4" +version = "0.1.5" dependencies = [ "bin-lib", "clap", @@ -1337,7 +1337,7 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" [[package]] name = "verify-lib" -version = "0.1.2" +version = "0.1.3" dependencies = [ "bin-lib", "ipk-lib", diff --git a/common/verify/Cargo.toml b/common/verify/Cargo.toml index 6cb824d8..ae398928 100644 --- a/common/verify/Cargo.toml +++ b/common/verify/Cargo.toml @@ -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 diff --git a/packages/ipk-verify/Cargo.toml b/packages/ipk-verify/Cargo.toml index 31ed9015..0b1bd6ae 100644 --- a/packages/ipk-verify/Cargo.toml +++ b/packages/ipk-verify/Cargo.toml @@ -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 "]