Skip to content

Conversation

@kkent030315
Copy link
Contributor

@kkent030315 kkent030315 commented Jun 6, 2025

Added delay-load import directory parser for PE32/64.

Using Vec as well as existing normal import parser. The design decision behind that Vec is that each import entry requires RVA resolution. I'd say it is not a good design, as both normal and delay import parser runs fully upon goblin::pe::PE::parse that can raise malformed errors -- even if the consumer doesn't need that import info.

I guess almost entire parsing logic can be moved to TryFromCtx if we could build some specific scroll ctx with a hell of lifetimes, so that utils::find_offset became callable inside the TryFromCtx. That's too much hustle but it should theoretically be possible.

/// A binary parsing context for PE parser, including the container size, underlying byte endianness and params for resolving RVAs
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
  pub container: Container,
  pub le: scroll::Endian,

  pub sections: &[section_table::SectionTable],
  pub file_alignment: u32,
  pub opts: &options::ParseOptions,
  // full binary view that arbitrary RVA could point to
  pub bytes: &'a [u8],
}
impl<'a> Iterator for DelayImportEntryIterator<'a>
impl<'a> Iterator for DelayImportDescriptorIterator<'a>

Ref:

@kkent030315
Copy link
Contributor Author

kkent030315 commented Jun 9, 2025

I made a brief example implementation of my idea to have PeCtx in 2fd0a44. Consider the code an experimental code that aren't ready to ship.

I've added custom Ctx:

/// A binary parsing context for PE parser
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
    pub container: Container,
    pub le: scroll::Endian,
    pub sections: &'a [section_table::SectionTable],
    pub file_alignment: u32,
    pub opts: options::ParseOptions,
    pub bytes: &'a [u8], // full binary view
}

The issue is that we cannot acrually borrow the &'a sections in DelayImportData<'a>. We cannot make sections: Vec<section_table::SectionTable> because Ctx must implement Copy that Vec doesn't.

The solution to that problem is to require consumers provide sections when they needed to get delay import info.

impl<'a> DelayImportDll<'a> {
    pub fn functions(
        &self,
        sections: &'a [section_table::SectionTable],
    ) -> DelayImportFunctionIterator<'a> {
        // Replace sections with an actual sections ref
        let pectx = PeCtx::new(
            self.ctx.container,
            self.ctx.le,
            &sections,
            self.ctx.file_alignment,
            self.ctx.opts,
            self.ctx.bytes,
        );
        DelayImportFunctionIterator {
            ctx: pectx,
            bytes: self.bytes,
            offset: 0,
            descriptor: self.descriptor,
        }
    }
}

impl<'a> DelayImportData<'a> {
    pub fn dlls(&self, sections: &'a [section_table::SectionTable]) -> DelayImportDllIterator<'a> {
        // Replace sections with an actual sections ref
        let pectx = PeCtx::new(
            self.ctx.container,
            self.ctx.le,
            &sections,
            self.ctx.file_alignment,
            self.ctx.opts,
            self.ctx.bytes,
        );
        DelayImportDllIterator {
            ctx: pectx,
            bytes: self.bytes,
            offset: 0,
        }
    }
}

I think, the best is to have an offset of section table and parsing it when needed. So that we eliminate the odd sections parameter in public APIs.

/// A binary parsing context for PE parser
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
    pub container: Container,
    pub le: scroll::Endian,
-   pub sections: &'a [section_table::SectionTable],
+   // let sections = ctx.bytes.pread_with(ctx.sections_offset, scroll::LE)?;
+   pub sections_offset: usize, 
    pub file_alignment: u32,
    pub opts: options::ParseOptions,
    pub bytes: &'a [u8], // full binary view
}

Otherwise it looks very clean design for me. It makes parsing of delay imports deferred to when it's needed.

let res = goblin::pe::PE::parse(&buffer)?;
let di = res.delay_import_data.unwrap();
let sections = &res.sections;
let delayimports = di
  .dlls(&sections)
  .filter_map(Result::ok)
  .flat_map(|x| x.functions(&sections))
  .filter_map(Result::ok)
  .collect::<Vec<_>>();

@m4b
Copy link
Owner

m4b commented Jun 16, 2025

can you rebase, CI had an issue on nightly

@m4b
Copy link
Owner

m4b commented Jun 16, 2025

Have not fully read over the PeCtx idea, looks interesting though; few initial notes:

  1. why is

The issue is that we cannot acrually borrow the &'a sections in DelayImportData<'a>

the case? is it some kind of cycle issue or?
2. for

        let pectx = PeCtx::new(
            self.ctx.container,
            self.ctx.le,
            &sections,
            self.ctx.file_alignment,
            self.ctx.opts,
            self.ctx.bytes,
        );

you can rewrite this as:

let pectx = PeCtx { sections: &sections, ..self.ctx) }

as long as all fields of PeCtx are accessible in the use context (e.g., public, or within the file if private, module if pub(crate)). In other languages this is called a "functional record update" or something to that effect, ocaml has a similar syntactic mechanism, for example.

@kkent030315
Copy link
Contributor Author

the case? is it some kind of cycle issue or?

Nah it's the issue where sections is Vec in PE::parse that cannot outlives. &'a bytes has longer lifetime because it is being passed from consumer.

goblin/src/pe/mod.rs

Lines 51 to 52 in f1b6cae

/// A list of the sections in this PE binary
pub sections: Vec<section_table::SectionTable>,

let sections = header.coff_header.sections(bytes, offset)?;

@m4b
Copy link
Owner

m4b commented Aug 15, 2025

sorry this didn't make it in for 0.10.1, but it does need a rebase; i did have some concerns about this, as it looked a little complicated, but i'll just have to re-review again when it's ready. Also I can't remember if it had breaking changes, so if it did it wouldn't have made it into 0.10.1; with this, I think i've caught up to your absolutely massive amount of PRs :D

@kkent030315
Copy link
Contributor Author

kkent030315 commented Aug 15, 2025

@m4b This PR needs a direction on which way this PR should go with: A) traditional Vec or B) new PeCtx<'a> and "deferred" iterators. I personally prefer challenging on PeCtx<'a> that might also be applied to import/export parser rework in near future. WDYT?

edit: current patch includes both case.

}

#[derive(Clone, Copy, Debug)]
pub struct DelayImportFunctionIterator<'a> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to DelayImportEntryIterator since data symbols can also be delay-imported and might cause a confusion.

@kkent030315
Copy link
Contributor Author

kkent030315 commented Aug 15, 2025

Once #239 gets merged we could add section_table: &'a [u8] in PeCtx that will get parsed inside FromCtx and such.

/// A binary parsing context for PE parser, including the container size, underlying byte endianness and params for resolving RVAs
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
  pub container: Container,
  pub le: scroll::Endian,
  pub section_table: &'a [u8],
  pub file_alignment: u32,
  pub opts: &options::ParseOptions,
  pub bytes: &'a [u8], // full binary view
}
impl<'a> ctx::TryFromCtx<'a, (DelayImportDescriptor, PeCtx<'a>)> for DelayImportFunction<'a> {
    type Error = crate::error::Error;
    fn try_from_ctx(
        bytes: &'a [u8],
        ctx: (DelayImportDescriptor, PeCtx<'a>),
    ) -> error::Result<(Self, usize)> {
        let sections_it = SectionTableIterator { // Fused, ExactSize
            data: ctx.section_table, // &'a [u8]
        };
        // Actually doesnt need to alloc here, passing iterator to utils::find_offset_ex directly or smth
        let sections: Vec<SectionHeader> = sections_it.collect<Result<Vec<_>>>()?;
        // Or even `ctx.section_table.pread::<&[SectionHeader]>(0)` maybe?
        let name = if is_ordinal {
            None
        } else {
            let dll_name_rva = descriptor.name_rva;
            let dll_name_offset = utils::find_offset(
                dll_name_rva as usize,
                sections,
                ctx.file_alignment,
                &ctx.opts,
            )
            .ok_or_else(|| {
                error::Error::Malformed(format!(
                    "cannot map delay import dll name rva {dll_name_rva:#x}"
                ))
            })?;
            let dll_name = ctx.bytes.pread::<&'a str>(dll_name_offset)?;
            Some(dll_name)
        };
		// ...
    }
}

Copy link
Owner

@m4b m4b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this LGTM, i'd like to see:

PeCtx made pub(crate); as I note, we can iterate more on the design when it's pub(crate) as we have no public api constraints/backwards compat to worry about.

other than that the other minor things, like pread::<&[u8]> (this was written before that was a common feedback I gave iirc), and some other things I note. Otherwise this should be about ready to go, thanks for your patience!

}

/// Return a dubious pointer/address byte size for the container
pub fn size(self) -> usize {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and all above should be pub(crate) for now

) -> error::Result<(Self, usize)> {
let mut offset = 0;

let (descriptor, ctx) = ctx;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can actually do this in the function args, e.g.:

    fn try_from_ctx(
        bytes: &'a [u8],
        (descriptor, ctx): (DelayImportDescriptor, PeCtx<'a>),

Comment on lines +89 to +107
let ordinal = if is_ordinal { thunk.ordinal() } else { 0 };
let name = if is_ordinal {
None
} else {
let dll_name_rva = descriptor.name_rva;
let dll_name_offset = utils::find_offset(
dll_name_rva as usize,
ctx.sections,
ctx.file_alignment,
&ctx.opts,
)
.ok_or_else(|| {
error::Error::Malformed(format!(
"cannot map delay import dll name rva {dll_name_rva:#x}"
))
})?;
let dll_name = ctx.bytes.pread::<&'a str>(dll_name_offset)?;
Some(dll_name)
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to combine these two checks and return a tuple, eg.:

let (name, ordinal) = if is_ordinal { (None, thunk.ordinal()) } else {
  // etc.
} 

Comment on lines +100 to +104
.ok_or_else(|| {
error::Error::Malformed(format!(
"cannot map delay import dll name rva {dll_name_rva:#x}"
))
})?;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this use the new permissive api?

Comment on lines +14 to +22
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
pub container: Container,
pub le: scroll::Endian,
pub sections: &'a [section_table::SectionTable],
pub file_alignment: u32,
pub opts: options::ParseOptions,
pub bytes: &'a [u8], // full binary view
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is meant to be a generic Ctx for parsing PE binaries, then it shouldn't be in this module, for example. a better place is mod or even better a ctx.rs file in the src/pe directory.

))
})?;
let hint = bytes.pread_with::<u16>(func_name_offset, scroll::LE)?;
let name = bytes.pread::<&'a str>(func_name_offset + 2)?; // + 2 = sizeof(hint)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gread probably preferred here.

Comment on lines +466 to +474
imports.push(DelayImportEntry {
descriptor,
offset: current_name_offset as u32,
rva,
hint: 0,
dll: dll_name,
name: None,
ordinal,
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i prefer the fields to be constructed/returned from ordinal, which is just:

ordinal, name, and hint

you can then have a single push, e.g.:

let (ordinal, name, hint) = if is_ordiinal {
  (thunk.ordinal(), None, 0)
} else {
 (0, // logic in other branch)
};
imports.push(DelayImportEntry {
     descriptor,
     offset: current_name_offset as u32,
     rva,
     hint,
     dll: dll_name,
     name,
     ordinal,,
 });


/// Internal helper struct to simplify bitflag operations.
#[derive(Debug, SizeWith)]
struct DelayImportThunk(pub u64);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this should have been pub struct ?

#[derive(Debug)]
pub struct DelayImportDll<'a> {
pub descriptor: DelayImportDescriptor,
ctx: PeCtx<'a>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I suspect we can just put sections as a field in here and so don't have to constantly reconstruct PeCtx


/// A binary parsing context for PE parser
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct PeCtx<'a> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now let's make this pub(crate) as nothing public appears to need/use it, this way we can iterate on the design however we like, and it can unblock getting this nice PR in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants