A PDF is not a document you open. It is a small program you run. Every embedded font is a stack-based interpreter waiting for charstrings, every image is a decoder fed width, height, and bit-depth fields the file chose, and every stream arrives wrapped in filters whose parameters the file set. None of those numbers are yours. They came from whoever produced the file, which on a real workload is a customer's invoice or an attachment from an unknown sender. The decoders that turn those bytes into pixels and glyphs are the attack surface, and a parser that trusts its input there is one malformed file away from a crash or worse
PDF Library for Delphi treats the whole decode path as hostile, across the font programs (TrueType, Type1, CFF, and the CMap tables), the image decoders (PNG, GIF, TIFF, JBIG2, and CCITT Group 3 and Group 4), and the stream filters (LZW, ASCII85, and the Flate predictors). What follows are five memory-safety traps that path has to close, each grounded in the specific Delphi behavior that makes it possible, and the same shapes recur in any Pascal code that parses untrusted input
An integer overflow that hands you an undersized buffer
The classic memory-safety bug in an image decoder is a dimension product that wraps. A decoder reads width, height, component count, and bit depth, multiplies them to size its output, allocates that many bytes, then writes the image at its true dimensions. If the multiplication is done in 32-bit arithmetic, the product can wrap to a small value even when every individual factor is within a sane range, so the allocation succeeds, comes out far too small, and the decode walks off the end of it. This is CWE-190, integer overflow, leading to a heap out-of-bounds write (CWE-787) one step later
Clamping each dimension to 65535 on a shared image path is not enough when a standalone decoder computes its own sizes. A row-bytes-times-height expression such as ByteCount * FHeight, or a per-pixel expression such as FWidth * Components * BitDepth, is a 32-bit product in Delphi when both operands are 32-bit integers, regardless of how wide the variable you assign the result to is. A width and a height of 60000 are each plausible for a large scan, but their product in bytes overruns a signed 32-bit range and the length comes out small. The same trap sits in a predictor stride such as BitsPerComponent * Colors * Columns
The defense is to make at least one operand Int64 so the whole expression is evaluated in 64-bit, then compare against MaxInt and refuse the file before narrowing back down to call SetLength
// Reject before allocating, not after writing.
// Evaluate the product in Int64 so it cannot wrap at 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // hostile or unsupportable dimensions; refuse the image
SetLength(Buffer, RowBytes * FHeight);
What makes this a Delphi problem rather than a generic one is the silent narrowing. Assigning a too-wide expression into a 32-bit destination is a legal conversion the compiler will not warn about by default, and range checking does not catch a wrap that happens before the value is ever used as an index. Leave the product at 32 bits and the language quietly gives you a length that lies about how much memory the decode is about to touch
A field type that makes a guard impossible to fire
A TIFF file is a chain of image file directories, each carrying the byte offset of the next. A malicious file can point that chain back at itself, and a reader that walks it without a stop condition runs forever. That is CWE-835, an infinite loop driven by attacker-controlled input, and the defense is a counter that stops once it passes a limit no legitimate file would reach
Now declare the page counter as Word, which in Delphi holds 0 to 65535, and give the loop a termination guard of the form "stop when the page count exceeds 65535". It reads as correct until you notice the operand and the threshold share an upper bound. A Word can never be greater than 65535, so the comparison is structurally always false: when the counter reaches 65535 the next increment wraps it back to 0, the guard never sees a value above the ceiling, and a looping IFD chain keeps the reader spinning
The cure is to widen the field so the guard can express a value the counter can actually hold. TPDFTIFF.FPageCount is declared as Integer, so the FPageCount > 65535 comparison is reachable and the loop terminates. Whenever a bound check has the shape Value > MaxValueOfType(Value) and the operand is already typed at exactly that maximum, the condition is a constant false: widen the type, or test equality against the maximum so it can trigger
Range checking turned off on a hot path
With range checking on, Delphi inserts a bounds check on every array and string index, which is the difference between an out-of-range index raising a catchable ERangeError and that same index reading or writing memory that does not belong to the structure. Hot paths sometimes disable it with a local {$R-} directive, which is defensible right up until the indices stop being trustworthy
The list accessor the font interpreters lean on, TPDFlibStringList.Get, is exactly such a path. On Windows it is compiled with range checking off and indexes its backing store directly, so an out-of-range index would be a raw memory access rather than an error. That is fine when the index is always valid, and it demands care inside a CFF or Type2 charstring interpreter, where the index can come from the file. A charstring that pops an operand off an empty stack produces an index of negative one; a glyph identifier off by one against the glyph count indexes one slot past the end. Left unchecked, both would be a genuine out-of-bounds access instead of a catchable exception, and because the slots hold reference-counted AnsiString values, a stray read can also corrupt a string's reference count
PDF Library for Delphi keeps range checking off on that hot path and makes the indices provably valid instead: before taking the top of the operand stack the interpreter checks the stack is non-empty, and every index guard is a strict less-than against the count rather than a less-than-or-equal that admits the off-by-one. The directive moves responsibility for bounds from the compiler to you, and the validation it removed has to be put back by hand at every entry point
Unbounded recursion in a charstring interpreter
A Type2 charstring can call a subroutine, and a subroutine is itself a charstring that can call another, so the local and global subroutine call operators let the file decide how deep it goes. A subroutine that calls itself, directly or through a cycle, recurses without end until the native stack is exhausted and the process dies. That is CWE-674, uncontrolled recursion
The Type1 interpreter guards against this with a call-depth counter and a ceiling, PLType1MaxCallDepth, and refuses to descend past it, which reflects the depth limit the Type1 specification itself names. The trap is that a structurally similar interpreter written later, such as the Type2 one, does not inherit that guard on its own, and a hand-built font with a subroutine that calls its own number walks straight through a missing check into a stack overflow
// The shape of the depth guard both charstring interpreters need.
// Track depth across nested calls and refuse to recurse past it.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // hostile self-referential subroutine; stop descending
// ... interpret the subroutine, then Dec(CallDepth) on the way out
PDF Library for Delphi gives the Type2 path the same bounded depth as its Type1 sibling. Any recursive descent over attacker-controlled structure, whether font subroutines, a nested array, or a cross-reference chain, needs a depth ceiling the input cannot lift
Uninitialized memory that leaks into the output
The most subtle trap leaks heap contents into decrypted output, and the cause is a property of SetLength that is easy to forget. When you grow an AnsiString with SetLength, Delphi allocates the bytes but does not zero them, so the new region holds whatever was previously in that heap memory. If every byte is subsequently written, this never matters; if a path leaves part of the buffer unwritten and then returns it as data, those stale bytes ride out with the result. That is CWE-457, use of uninitialized memory, and when the result crosses a trust boundary it becomes an information leak
An AES-CBC decryption path is exactly where this bites. Size the output buffer with SetLength, process the ciphertext one 16-byte block at a time, and when the ciphertext length is not a multiple of 16, a length an attacker can choose, the trailing partial block is never written, so those final bytes keep the heap contents SetLength left behind and the buffer goes back as the decrypted plaintext of a document object. The remedy is two guards, and neither alone is enough: the PDF Library for Delphi decryption entry point rejects any ciphertext whose length is not a multiple of the block size, and as a backstop the output is cleared with FillChar before use so any path that fails to write a region returns zeros rather than heap residue
What the five traps have in common
The five traps are different bugs, but they rhyme. An integer width that wraps a product, a field type that pins a guard to a constant false, a range check disabled where the indices stopped being safe, a recursion with no floor, and a buffer the language declined to zero. In each one Delphi does exactly what it defines, because the language gives you arithmetic that wraps, narrowing that is silent, range checks you can switch off, recursion with no built-in limit, and allocation that does not initialize. That is the contract, and a Pascal parser meets it by owning four things by hand at every boundary the file controls: integer width, range checking, recursion depth, and buffer initialization
PDF Library for Delphi, the engine for Delphi and C++Builder, guards its decode path against all five. If your work also reaches into how a file claims to be protected, the companion notes on auditing encryption and permissions and on PDF/A and PDF/UA preflight cover the analysis side of the same parser, and all of it ships inside the PDF Library for Delphi Delphi PDF Library alongside the loading, rendering, and signing APIs covered elsewhere on this blog