-
Notifications
You must be signed in to change notification settings - Fork 775
Refactor PEFile and PEHeader to use ReadOnlySpan exclusively with zero-copy buffer sharing #2317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
78c2cb5
bd267a9
675ff78
6026d57
2f6476d
fd344b1
fe420c0
ff06902
e41e449
de5fab3
b00ebf4
1031ef0
e0b7361
d46532d
fb99913
c99640b
d11501f
e68bfc1
a04994b
4f05a4f
9d4a29d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace PEFile | ||
|
|
@@ -25,13 +26,13 @@ public PEFile(string filePath) | |
| m_stream = File.OpenRead(filePath); | ||
| m_headerBuff = new PEBuffer(m_stream); | ||
|
|
||
| byte* ptr = m_headerBuff.Fetch(0, 1024); | ||
| ReadOnlySpan<byte> span = m_headerBuff.FetchSpan(0, 1024); | ||
| if (m_headerBuff.Length < 512) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| Header = new PEHeader(ptr); | ||
| Header = new PEHeader(span); | ||
|
|
||
| if (Header.PEHeaderSize > 1024 * 64) // prevent insane numbers; | ||
| { | ||
|
|
@@ -41,13 +42,13 @@ public PEFile(string filePath) | |
| // We did not read in the complete header, Try again using the right sized buffer. | ||
| if (Header.PEHeaderSize > m_headerBuff.Length) | ||
| { | ||
| ptr = m_headerBuff.Fetch(0, Header.PEHeaderSize); | ||
| span = m_headerBuff.FetchSpan(0, Header.PEHeaderSize); | ||
| if (m_headerBuff.Length < Header.PEHeaderSize) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| Header = new PEHeader(ptr); | ||
| Header = new PEHeader(span); | ||
| } | ||
| return; | ||
| ThrowBadHeader: | ||
|
|
@@ -386,6 +387,70 @@ public PEHeader(void* startOfPEFile) | |
| throw new InvalidOperationException("Bad PE Header."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a PEHeader for ReadOnlySpan of bytes in memory. Validates buffer bounds. | ||
| /// </summary> | ||
| public PEHeader(ReadOnlySpan<byte> peFileData) | ||
| { | ||
| // We need to copy the span data to a byte array since we can't store spans as fields | ||
| m_buffer = peFileData.ToArray(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to avoid copying the buffer here. Would it make more sense to fold
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed by eliminating the buffer copy. Added an internal |
||
|
|
||
| if (m_buffer.Length < sizeof(IMAGE_DOS_HEADER)) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| IMAGE_DOS_HEADER dosHdr; | ||
| fixed (byte* bufferPtr = m_buffer) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should be able to call AsSpan() here and then operate with the ReadOnlySpan instead of using unsafe here. That will ensure that we don't read off the end of the buffer.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to use AsSpan() and MemoryMarshal.Read instead of unsafe pointer access in commit ec49a3a. |
||
| { | ||
| dosHdr = *(IMAGE_DOS_HEADER*)bufferPtr; | ||
| } | ||
|
|
||
| if (dosHdr.e_magic != IMAGE_DOS_HEADER.IMAGE_DOS_SIGNATURE) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| var imageHeaderOffset = dosHdr.e_lfanew; | ||
| if (!(sizeof(IMAGE_DOS_HEADER) <= imageHeaderOffset && imageHeaderOffset <= 512)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the imageHeaderOffset <= 512 requirement. Specifically, we do want to make sure that the offset is at least sizeof(IMAGE_DOS_HEADER), but otherwise, it can be larger than 512.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the |
||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| if (m_buffer.Length < imageHeaderOffset + sizeof(IMAGE_NT_HEADERS)) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| m_ntHeaderOffset = imageHeaderOffset; | ||
| IMAGE_NT_HEADERS ntHdr; | ||
| fixed (byte* bufferPtr = m_buffer) | ||
| { | ||
| ntHdr = *(IMAGE_NT_HEADERS*)(bufferPtr + m_ntHeaderOffset); | ||
| } | ||
|
|
||
| var optionalHeaderSize = ntHdr.FileHeader.SizeOfOptionalHeader; | ||
| if (!(sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_OPTIONAL_HEADER32) <= optionalHeaderSize)) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| m_sectionsOffset = m_ntHeaderOffset + sizeof(IMAGE_NT_HEADERS) + ntHdr.FileHeader.SizeOfOptionalHeader; | ||
| if (m_sectionsOffset >= 1024) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this check
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the |
||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| if (m_buffer.Length < m_sectionsOffset + sizeof(IMAGE_SECTION_HEADER) * ntHdr.FileHeader.NumberOfSections) | ||
| { | ||
| goto ThrowBadHeader; | ||
| } | ||
|
|
||
| return; | ||
| ThrowBadHeader: | ||
| throw new InvalidOperationException("Bad PE Header."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The total s,ize of the header, including section array of the the PE header. | ||
| /// </summary> | ||
|
|
@@ -975,12 +1040,118 @@ internal int FileOffsetOfResources | |
| } | ||
| } | ||
|
|
||
| private IMAGE_OPTIONAL_HEADER32* OptionalHeader32 { get { return (IMAGE_OPTIONAL_HEADER32*)(((byte*)ntHeader) + sizeof(IMAGE_NT_HEADERS)); } } | ||
| private IMAGE_OPTIONAL_HEADER64* OptionalHeader64 { get { return (IMAGE_OPTIONAL_HEADER64*)(((byte*)ntHeader) + sizeof(IMAGE_NT_HEADERS)); } } | ||
| // Helper method to get a span from the buffer with bounds checking | ||
| private ReadOnlySpan<byte> GetBufferSpan(int offset, int length) | ||
| { | ||
| if (m_buffer == null) | ||
| { | ||
| throw new InvalidOperationException("Buffer not available in pointer-based PEHeader."); | ||
| } | ||
| if (offset < 0 || offset + length > m_buffer.Length) | ||
| { | ||
| throw new ArgumentOutOfRangeException($"Attempted to read {length} bytes at offset {offset}, but buffer is only {m_buffer.Length} bytes."); | ||
| } | ||
| return new ReadOnlySpan<byte>(m_buffer, offset, length); | ||
| } | ||
|
|
||
| // Helper properties to access structures from span with bounds checking | ||
| private ref readonly IMAGE_DOS_HEADER DosHeader | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| var span = GetBufferSpan(0, sizeof(IMAGE_DOS_HEADER)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_DOS_HEADER>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("DosHeader property only available with span-based PEHeader."); | ||
| } | ||
| } | ||
|
|
||
| private ref readonly IMAGE_NT_HEADERS NtHeader | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| var span = GetBufferSpan(m_ntHeaderOffset, sizeof(IMAGE_NT_HEADERS)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_NT_HEADERS>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("NtHeader property only available with span-based PEHeader."); | ||
| } | ||
| } | ||
|
|
||
| private ref readonly IMAGE_SECTION_HEADER GetSectionHeader(int index) | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| int offset = m_sectionsOffset + index * sizeof(IMAGE_SECTION_HEADER); | ||
| var span = GetBufferSpan(offset, sizeof(IMAGE_SECTION_HEADER)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_SECTION_HEADER>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("GetSectionHeader only available with span-based PEHeader."); | ||
| } | ||
|
|
||
| private IMAGE_OPTIONAL_HEADER32* OptionalHeader32 | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| throw new InvalidOperationException("Use OptionalHeader32Span with span-based PEHeader."); | ||
| } | ||
| return (IMAGE_OPTIONAL_HEADER32*)(((byte*)ntHeader) + sizeof(IMAGE_NT_HEADERS)); | ||
| } | ||
| } | ||
|
|
||
| private ref readonly IMAGE_OPTIONAL_HEADER32 OptionalHeader32Span | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this have a Span suffix?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an artifact of an earlier iteration of the change when copilot was trying to build this class to have two constructors - one that took a pointer and one that took a span. I've removed the suffix. |
||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| int offset = m_ntHeaderOffset + sizeof(IMAGE_NT_HEADERS); | ||
| var span = GetBufferSpan(offset, sizeof(IMAGE_OPTIONAL_HEADER32)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_OPTIONAL_HEADER32>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("OptionalHeader32Span only available with span-based PEHeader."); | ||
| } | ||
| } | ||
|
|
||
| private IMAGE_OPTIONAL_HEADER64* OptionalHeader64 | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| throw new InvalidOperationException("Use OptionalHeader64Span with span-based PEHeader."); | ||
| } | ||
| return (IMAGE_OPTIONAL_HEADER64*)(((byte*)ntHeader) + sizeof(IMAGE_NT_HEADERS)); | ||
| } | ||
| } | ||
|
|
||
| private ref readonly IMAGE_OPTIONAL_HEADER64 OptionalHeader64Span | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| int offset = m_ntHeaderOffset + sizeof(IMAGE_NT_HEADERS); | ||
| var span = GetBufferSpan(offset, sizeof(IMAGE_OPTIONAL_HEADER64)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_OPTIONAL_HEADER64>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("OptionalHeader64Span only available with span-based PEHeader."); | ||
| } | ||
| } | ||
|
|
||
| private IMAGE_DATA_DIRECTORY* ntDirectories | ||
| { | ||
| get | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| throw new InvalidOperationException("Use GetDirectory with span-based PEHeader."); | ||
| } | ||
| if (IsPE64) | ||
| { | ||
| return (IMAGE_DATA_DIRECTORY*)(((byte*)ntHeader) + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_OPTIONAL_HEADER64)); | ||
|
|
@@ -992,9 +1163,34 @@ private IMAGE_DATA_DIRECTORY* ntDirectories | |
| } | ||
| } | ||
|
|
||
| private ref readonly IMAGE_DATA_DIRECTORY GetDirectory(int index) | ||
| { | ||
| if (m_buffer != null) | ||
| { | ||
| int dirOffset; | ||
| if (IsPE64) | ||
| { | ||
| dirOffset = m_ntHeaderOffset + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_OPTIONAL_HEADER64); | ||
| } | ||
| else | ||
| { | ||
| dirOffset = m_ntHeaderOffset + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_OPTIONAL_HEADER32); | ||
| } | ||
| dirOffset += index * sizeof(IMAGE_DATA_DIRECTORY); | ||
| var span = GetBufferSpan(dirOffset, sizeof(IMAGE_DATA_DIRECTORY)); | ||
| return ref MemoryMarshal.Cast<byte, IMAGE_DATA_DIRECTORY>(span)[0]; | ||
| } | ||
| throw new InvalidOperationException("GetDirectory only available with span-based PEHeader."); | ||
| } | ||
|
|
||
| private IMAGE_DOS_HEADER* dosHeader; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot, we shouldn't have two different paths with different fields that only work based on the constructor that was used. Instead, please move everything to be span based and remove the constructor that takes a raw pointer.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed all dual-path logic in commit 7f1edfe. Eliminated the pointer-based constructor and all pointer fields (dosHeader, ntHeader, sections). Everything now uses the span-based approach exclusively with PEBufferedSlice and MemoryMarshal for safe, bounds-checked access. |
||
| private IMAGE_NT_HEADERS* ntHeader; | ||
| private IMAGE_SECTION_HEADER* sections; | ||
|
|
||
| // Span-based fields (used when constructed with ReadOnlySpan<byte>) | ||
| private byte[] m_buffer; | ||
| private int m_ntHeaderOffset; | ||
| private int m_sectionsOffset; | ||
| #endregion | ||
| } | ||
|
|
||
|
|
@@ -1142,6 +1338,33 @@ public PEBuffer(Stream stream, int buffSize = 512) | |
| } | ||
| return &m_buffPtr[filePos - m_buffPos]; | ||
| } | ||
| public ReadOnlySpan<byte> FetchSpan(int filePos, int size) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed to |
||
| { | ||
| if (size > m_buff.Length) | ||
| { | ||
| GetBuffer(size); | ||
| } | ||
| if (!(m_buffPos <= filePos && filePos + size <= m_buffPos + m_buffLen)) | ||
| { | ||
| // Read in the block of 'size' bytes at filePos | ||
| m_buffPos = filePos; | ||
| m_stream.Seek(m_buffPos, SeekOrigin.Begin); | ||
| m_buffLen = 0; | ||
| while (m_buffLen < m_buff.Length) | ||
| { | ||
| var count = m_stream.Read(m_buff, m_buffLen, size - m_buffLen); | ||
| if (count == 0) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| m_buffLen += count; | ||
| } | ||
| } | ||
| int offset = filePos - m_buffPos; | ||
| int actualSize = Math.Min(size, m_buffLen - offset); | ||
| return new ReadOnlySpan<byte>(m_buff, offset, actualSize); | ||
| } | ||
| public int Length { get { return m_buffLen; } } | ||
| public void Dispose() | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constructor is unused. Please remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the unused constructor in commit ec49a3a.