Skip to content

pe: skip raw writes for virtual-only sections - #541

Open
mq1n wants to merge 1 commit into
m4b:masterfrom
mq1n:codex/pe-virtual-only-sections
Open

pe: skip raw writes for virtual-only sections#541
mq1n wants to merge 1 commit into
m4b:masterfrom
mq1n:codex/pe-virtual-only-sections

Conversation

@mq1n

@mq1n mq1n commented Jun 17, 2026

Copy link
Copy Markdown

Summary

  • treat PE sections with SizeOfRawData == 0 as virtual-only on disk
  • preserve virtual-only section table entries while skipping nonexistent raw bytes during PE rewrite
  • add synthetic regression coverage for the writer path without requiring an executable fixture

Root cause

Some PE files, including the local Free Pascal sample used to reproduce this, contain a .bss section with a nonzero virtual size but no raw on-disk data. The writer previously expanded that virtual-only section into zero bytes and wrote them at PointerToRawData == 0, which could corrupt the DOS/PE headers.

Validation

  • cargo fmt --check
  • cargo test
  • focused virtual-only section regression tests
  • touched-file strict Clippy filter
  • local freepascal_cli.exe rewrite remained byte-identical by SHA256

Notes

This is a narrow PE writer fix and does not special-case Free Pascal binaries. The regression test uses synthetic section data instead of adding a binary executable fixture.

PE sections such as .bss can reserve virtual memory without carrying raw on-disk data. The writer previously expanded those virtual bytes and could write zeroes at file offset 0, corrupting the DOS/PE headers during rewrite.

Treat zero-raw-size sections as virtual-only, preserve their section table entries, and skip writing nonexistent section bytes. The regression coverage uses synthetic section data so the test does not require a bundled executable fixture.

Constraint: Virtual-only PE sections may legally have SizeOfRawData == 0 and PointerToRawData == 0
@mq1n
mq1n marked this pull request as ready for review June 17, 2026 19:43
@m4b

m4b commented Aug 30, 2026

Copy link
Copy Markdown
Owner

@kkent030315 if you could review would appreciate :)

@m4b m4b left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

ah ok this is for the writer

Comment thread src/pe/mod.rs
let mut output = vec![HEADER_SENTINEL; SECTION_TABLE_OFFSET + 0x80];
let mut section_table_offset = SECTION_TABLE_OFFSET;

pe.write_sections(&mut output, &mut section_table_offset, None, scroll::LE)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

it's also generally a good idea for a test to "round trip", e.g, you write a PE file, and then we can parse it back again

Comment thread src/pe/section_table.rs

let section_start: usize = self.pointer_to_raw_data.try_into().map_err(|_| {
Error::Malformed(format!("Virtual address cannot fit in platform `usize`"))
Error::Malformed("Virtual address cannot fit in platform `usize`".to_string())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

generally this is nice but it is technically diff noise (and not related to your PR)

Comment thread src/pe/section_table.rs
Comment on lines +355 to +360
let section = SectionTable {
pointer_to_raw_data: 4,
size_of_raw_data: 4,
virtual_size: 4,
..SectionTable::default()
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

also nice, but diff noise

Comment thread src/pe/section_table.rs
let section_end: usize = section_start
+ usize::try_from(self.size_of_raw_data).map_err(|_| {
Error::Malformed(format!("Virtual size cannot fit in platform `usize`"))
Error::Malformed("Virtual size cannot fit in platform `usize`".to_string())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

ditto

Comment thread src/pe/section_table.rs
let original_bytes = pe_bytes.get(section_start..section_end).map(Cow::Borrowed);
let original_bytes = match pe_bytes.get(section_start..section_end) {
Some(bytes) => bytes,
None => return Ok(None),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think this changes the semantics; you now return none if we can't get the original bytes, but before it would return Ok(original_bytes) if the is_some() check did not pass. Is this intentional or refactor artifact? Could you give some motivation about why this change was made?

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