From f432f79e06ebd3f8374d5abc85f3abb525151242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:55:41 +0000 Subject: [PATCH 1/3] Initial plan From 12f5bae099df31dab82059e6941dfae1991979cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:15:54 +0000 Subject: [PATCH 2/3] fix: CI test failures and improve ELF export extraction Co-authored-by: unclesp1d3r <251112+unclesp1d3r@users.noreply.github.com> --- .github/workflows/release.yml | 12 +-- Cargo.toml | 1 + dist-workspace.toml | 2 +- src/container/elf.rs | 40 +++++++-- tests/integration_elf.rs | 153 +++------------------------------- 5 files changed, 53 insertions(+), 155 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8021e31..b05b9c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,7 @@ name: Release permissions: - "attestations": "write" "contents": "write" - "id-token": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. @@ -66,7 +64,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: @@ -114,6 +112,10 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + permissions: + "attestations": "write" + "contents": "read" + "id-token": "write" steps: - name: enable windows longpaths run: | @@ -244,8 +246,8 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" diff --git a/Cargo.toml b/Cargo.toml index 6ce1e54..4418ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ thiserror = "2.0.17" [dev-dependencies] criterion = "0.7.0" insta = "1.0" +tempfile = "3.0" # The profile that 'dist' will build with [profile.dist] diff --git a/dist-workspace.toml b/dist-workspace.toml index d8bde69..aafdbfe 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,7 +4,7 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.30.0" +cargo-dist-version = "0.30.2" # CI backends to support ci = "github" # The installers to generate for each app diff --git a/src/container/elf.rs b/src/container/elf.rs index c35dc85..743b3eb 100644 --- a/src/container/elf.rs +++ b/src/container/elf.rs @@ -166,16 +166,44 @@ impl ElfParser { // Extract from dynamic symbol table for sym in &elf.dynsyms { - if sym.st_bind() == goblin::elf::sym::STB_GLOBAL + if (sym.st_bind() == goblin::elf::sym::STB_GLOBAL + || sym.st_bind() == goblin::elf::sym::STB_WEAK) && sym.st_shndx != (goblin::elf::section_header::SHN_UNDEF as usize) && sym.st_value != 0 { if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { - exports.push(ExportInfo { - name: name.to_string(), - address: sym.st_value, - ordinal: None, // ELF doesn't use ordinals - }); + if !name.is_empty() { + exports.push(ExportInfo { + name: name.to_string(), + address: sym.st_value, + ordinal: None, // ELF doesn't use ordinals + }); + } + } + } + } + + // Also check regular symbol table for static exports + for sym in &elf.syms { + if (sym.st_bind() == goblin::elf::sym::STB_GLOBAL + || sym.st_bind() == goblin::elf::sym::STB_WEAK) + && sym.st_shndx != (goblin::elf::section_header::SHN_UNDEF as usize) + && sym.st_value != 0 + && (sym.st_type() == goblin::elf::sym::STT_FUNC + || sym.st_type() == goblin::elf::sym::STT_OBJECT + || sym.st_type() == goblin::elf::sym::STT_NOTYPE) + { + if let Some(name) = elf.strtab.get_at(sym.st_name) { + if !name.is_empty() { + // Avoid duplicates from dynamic symbol table + if !exports.iter().any(|exp| exp.name == name) { + exports.push(ExportInfo { + name: name.to_string(), + address: sym.st_value, + ordinal: None, // ELF doesn't use ordinals + }); + } + } } } } diff --git a/tests/integration_elf.rs b/tests/integration_elf.rs index acbc96d..b4e39b4 100644 --- a/tests/integration_elf.rs +++ b/tests/integration_elf.rs @@ -1,6 +1,8 @@ -use std::fs; +use std::fs::{self, File}; +use std::io::Write; use std::process::Command; use stringy::container::{ContainerParser, ElfParser}; +use tempfile::TempDir; #[test] fn test_elf_import_export_extraction_dynamic() { @@ -31,17 +33,12 @@ int main() { fs::write(&c_file, c_code).expect("Failed to write C file"); // Try to compile it with gcc, attempting to force ELF output - // First try with a cross-compiler for Linux if available + // First try with a cross-compiler for Linux if available (dynamically linked) let mut output = Command::new("x86_64-linux-gnu-gcc") - .args([ - "-static", // Static linking to avoid library dependencies - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) + .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) .output(); - // If cross-compiler not available, try regular gcc + // If cross-compiler not available, try regular gcc (dynamically linked) if output.is_err() { output = Command::new("gcc") .args(["-o", elf_file.to_str().unwrap(), c_file.to_str().unwrap()]) @@ -135,7 +132,6 @@ int main() { } } -#[test] #[test] fn test_elf_import_export_extraction_static() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); @@ -174,12 +170,7 @@ fn test_elf_import_export_extraction_static() { ]) .output(); - if output.is_err() - || !output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { + if output.is_err() || !output.as_ref().map(|o| o.status.success()).unwrap_or(false) { output = Command::new("gcc") .args([ "-static", @@ -221,132 +212,8 @@ fn test_elf_import_export_extraction_static() { .collect(); let has_main = export_names.iter().any(|name| name == "main"); - let has_exported_function = export_names - .iter() - .any(|name| name == "exported_function"); - - assert!( - has_main, - "Static binary should export main function. Found exports: {:?}", - export_names - ); - assert!( - has_exported_function, - "Static binary should export exported_function. Found exports: {:?}", - export_names - ); - } - goblin::Object::Mach(_) => { - println!("Compiled to Mach-O, skipping ELF-specific test"); - } - _ => panic!("Unexpected binary format"), - } - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!( - "Static compilation failed, skipping test. This is expected if static libraries are not available.\nError: {}", - stderr - ); - } - Err(e) => { - println!( - "GCC not available, skipping test. This is expected in some CI environments. Error: {}", - e - ); - } - } -} - -#[test] -fn test_elf_section_classification_integration() { - // Test with the current binary (this test executable) - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let c_file = temp_dir.path().join("test_static.c"); - let elf_file = temp_dir.path().join("test_static"); - - let c_code = r#" - #include - #include - - void exported_function() { - printf("Hello from exported function\n"); - } - - int main() { - void *ptr = malloc(100); - printf("Allocated memory\n"); - free(ptr); - exported_function(); - return 0; - } - "#; - - File::create(&c_file) - .expect("Failed to create C file") - .write_all(c_code.as_bytes()) - .expect("Failed to write C code"); - - // Compile statically-linked binary with -static flag - let mut output = Command::new("x86_64-linux-gnu-gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - - if output.is_err() - || !output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - output = Command::new("gcc") - .args([ - "-static", - "-o", - elf_file.to_str().unwrap(), - c_file.to_str().unwrap(), - ]) - .output(); - } - - match output { - Ok(output) if output.status.success() => { - let elf_data = fs::read(&elf_file).expect("Failed to read ELF file"); - - let format_obj = goblin::Object::parse(&elf_data).expect("Failed to parse with goblin"); - - match format_obj { - goblin::Object::Elf(_elf) => { - let parser = ElfParser::new(); - let container_info = parser.parse(&elf_data).expect("Failed to parse ELF"); - - // Statically-linked binaries typically have no or very few dynamic imports - // since all dependencies are embedded - println!( - "Static binary imports found: {} (expected: 0 or very few)", - container_info.imports.len() - ); - - // Verify exports are still present - assert!( - !container_info.exports.is_empty(), - "Static binary should still have exports like main, exported_function" - ); - - let export_names: Vec = container_info - .exports - .iter() - .map(|e| e.name.clone()) - .collect(); - - let has_main = export_names.iter().any(|name| name == "main"); - let has_exported_function = export_names - .iter() - .any(|name| name == "exported_function"); + let has_exported_function = + export_names.iter().any(|name| name == "exported_function"); assert!( has_main, @@ -456,4 +323,4 @@ fn test_elf_section_classification_integration() { } } } -} \ No newline at end of file +} From 9943114e77c9d3d02100455530a2bbdfc4924207 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:46:00 +0000 Subject: [PATCH 3/3] perf: optimize export deduplication with HashSet (O(1) vs O(n)) Co-authored-by: unclesp1d3r <251112+unclesp1d3r@users.noreply.github.com> --- src/container/elf.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/container/elf.rs b/src/container/elf.rs index 42524fb..6529e41 100644 --- a/src/container/elf.rs +++ b/src/container/elf.rs @@ -5,6 +5,7 @@ use crate::types::{ }; use goblin::Object; use goblin::elf::{Elf, SectionHeader}; +use std::collections::HashSet; /// Parser for ELF (Executable and Linkable Format) binaries pub struct ElfParser; @@ -163,6 +164,7 @@ impl ElfParser { /// Extract basic export information from ELF symbol table fn extract_exports(&self, elf: &Elf) -> Vec { let mut exports = Vec::new(); + let mut seen_names = HashSet::new(); // Extract from dynamic symbol table for sym in &elf.dynsyms { @@ -172,7 +174,7 @@ impl ElfParser { && sym.st_value != 0 { if let Some(name) = elf.dynstrtab.get_at(sym.st_name) { - if !name.is_empty() { + if !name.is_empty() && seen_names.insert(name.to_string()) { exports.push(ExportInfo { name: name.to_string(), address: sym.st_value, @@ -194,15 +196,12 @@ impl ElfParser { || sym.st_type() == goblin::elf::sym::STT_NOTYPE) { if let Some(name) = elf.strtab.get_at(sym.st_name) { - if !name.is_empty() { - // Avoid duplicates from dynamic symbol table - if !exports.iter().any(|exp| exp.name == name) { - exports.push(ExportInfo { - name: name.to_string(), - address: sym.st_value, - ordinal: None, // ELF doesn't use ordinals - }); - } + if !name.is_empty() && seen_names.insert(name.to_string()) { + exports.push(ExportInfo { + name: name.to_string(), + address: sym.st_value, + ordinal: None, // ELF doesn't use ordinals + }); } } }