Summary
The Preferred Executable Format (PEF) loader enters an infinite loop when importing a crafted PEF file whose packed-data section ends while a packed-data length value is still being read. SectionHeader.unpackNextValue() reads a base-128 continuation value but treats end-of-stream (InputStream.read() returning -1) as a continuation byte, so the loop never terminates. Importing such a file pins Ghidra at 100% CPU indefinitely; the user must kill the process, losing any unsaved work. No user privileges are required beyond opening/importing the file, which is the routine action an analyst performs on untrusted binaries.
Details
In Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pef/SectionHeader.java, unpackNextValue() (lines 221-232):
private int unpackNextValue(InputStream input) throws IOException {
int unpacked = 0;
while (true) {
unpacked <<= 7;
int value = input.read(); // returns -1 at end of stream
unpacked += (value & 0x7f); // (-1 & 0x7f) == 0x7f
if ((value & 0x80) == 0x00) { // (-1 & 0x80) == 0x80 -> never true
break;
}
}
return unpacked;
}
At end of stream input.read() returns -1 on every call; (-1 & 0x80) is 0x80, so the break is never taken and the loop spins forever.
Reachability (normal import path, no analysis required):
PefLoader.load() -> processSections() (PefLoader.java:429) iterates the sections and, for every section whose kind is PackedData, calls section.getUnpackedData(monitor).
SectionHeader.getUnpackedData() (line 117+) reads the first opcode byte value; when value & 0x1f == 0 it calls unpackNextValue(input) to read the count (line 134).
- If the packed stream is exhausted at that point,
unpackNextValue loops forever.
A single packed byte 0x00 (an opcode with a zero low-5-bit count) followed by end-of-stream is sufficient: the main loop reads the 0x00, sees count 0, calls unpackNextValue, and hangs.
Steps to Reproduce
Build a 153-byte PEF (Joy!/peff/pwpc container, one empty Loader section so loader initialization succeeds, one PackedData section whose data is a single 0x00 byte at end-of-stream):
import struct
def sechdr(kind, tot, unp, clen, coff):
return (struct.pack(">i",-1)+struct.pack(">i",0)+struct.pack(">i",tot)
+struct.pack(">i",unp)+struct.pack(">i",clen)
+struct.pack(">i",coff)+bytes([kind,0,0,0]))
ch = b"Joy!"+b"peff"+b"pwpc"+struct.pack(">iiiii",0,0,0,0,0)+struct.pack(">hh",2,2)+struct.pack(">i",0)
sh_loader = sechdr(4, 56, 56, 56, 96) # Loader section -> 56-byte zero block @96
sh_packed = sechdr(2, 16, 16, 1, 152) # PackedData section -> byte @152
open("evilpef.bin","wb").write(ch+sh_loader+sh_packed+b"\x00"*56+bytes([0x00]))
Import it (the hang reproduces on GUI import as well):
./support/analyzeHeadless /tmp/proj p1 -import evilpef.bin -noanalysis
Observed on Ghidra 12.1.2 PUBLIC: the JVM sits at ~130% CPU and the import never returns (a headless run had to be killed by a 25-second timeout). The log shows Using Loader: Preferred Executable Format (PEF) immediately before the hang.
Impact
Denial of service (CWE-835 / CWE-400). An analyst who imports or opens a crafted PEF file — a routine action on untrusted samples — causes Ghidra to hang at 100% CPU with no progress and no way to cancel cleanly, requiring the process to be killed and losing unsaved analysis. This is the same class as previously fixed Ghidra loader DoS issues (ELF GNU hash table infinite loop, Mach-O export trie circular reference).
Suggested fix
In unpackNextValue, treat read() == -1 as end-of-stream: break out (or throw the same IllegalStateException the caller already throws for truncated input) instead of folding -1 into the accumulated value. For example, bail when value < 0.
Note on tooling
Claude Opus (Anthropic's coding assistant) was used to help audit the source, craft the
proof-of-concept files, and run the tests. All findings were verified at runtime against the
Ghidra 12.1.2 PUBLIC release before reporting.
Summary
The Preferred Executable Format (PEF) loader enters an infinite loop when importing a crafted PEF file whose packed-data section ends while a packed-data length value is still being read.
SectionHeader.unpackNextValue()reads a base-128 continuation value but treats end-of-stream (InputStream.read()returning-1) as a continuation byte, so the loop never terminates. Importing such a file pins Ghidra at 100% CPU indefinitely; the user must kill the process, losing any unsaved work. No user privileges are required beyond opening/importing the file, which is the routine action an analyst performs on untrusted binaries.Details
In
Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pef/SectionHeader.java,unpackNextValue()(lines 221-232):At end of stream
input.read()returns-1on every call;(-1 & 0x80)is0x80, so thebreakis never taken and the loop spins forever.Reachability (normal import path, no analysis required):
PefLoader.load()->processSections()(PefLoader.java:429) iterates the sections and, for every section whose kind isPackedData, callssection.getUnpackedData(monitor).SectionHeader.getUnpackedData()(line 117+) reads the first opcode bytevalue; whenvalue & 0x1f == 0it callsunpackNextValue(input)to read the count (line 134).unpackNextValueloops forever.A single packed byte
0x00(an opcode with a zero low-5-bit count) followed by end-of-stream is sufficient: the main loop reads the0x00, sees count 0, callsunpackNextValue, and hangs.Steps to Reproduce
Build a 153-byte PEF (
Joy!/peff/pwpccontainer, one emptyLoadersection so loader initialization succeeds, onePackedDatasection whose data is a single0x00byte at end-of-stream):Import it (the hang reproduces on GUI import as well):
Observed on Ghidra 12.1.2 PUBLIC: the JVM sits at ~130% CPU and the import never returns (a headless run had to be killed by a 25-second timeout). The log shows
Using Loader: Preferred Executable Format (PEF)immediately before the hang.Impact
Denial of service (CWE-835 / CWE-400). An analyst who imports or opens a crafted PEF file — a routine action on untrusted samples — causes Ghidra to hang at 100% CPU with no progress and no way to cancel cleanly, requiring the process to be killed and losing unsaved analysis. This is the same class as previously fixed Ghidra loader DoS issues (ELF GNU hash table infinite loop, Mach-O export trie circular reference).
Suggested fix
In
unpackNextValue, treatread() == -1as end-of-stream: break out (or throw the sameIllegalStateExceptionthe caller already throws for truncated input) instead of folding-1into the accumulated value. For example, bail whenvalue < 0.Note on tooling
Claude Opus (Anthropic's coding assistant) was used to help audit the source, craft the
proof-of-concept files, and run the tests. All findings were verified at runtime against the
Ghidra 12.1.2 PUBLIC release before reporting.