With overflow checking off, 2000000000 + 2000000000 evaluates to -294,967,296 when both operands are Delphi Integer values. That number stops being trivia the moment a container parser reads an offset and a length out of a record header and adds them to see whether an embedded PDF fits inside the buffer. The sum wraps negative, the comparison against Length(Data) passes, and the next line takes the address of a byte roughly two gigabytes past the end of the array. Loading a PDF from the middle of a larger buffer needs a range check that never adds, and a clear answer to who owns the bytes once the document is open
This article covers documents that are already in RAM, sitting at a known offset inside something else. Documents too large to hold in memory are a different problem, handled by pulling blocks from disk on demand as described in streaming large PDFs on demand with PDFium VCL. The last section draws the line
An embedded PDF at a known offset
A mail store keeps message bodies and attachments in one record. An archive container concatenates a manifest, a few images and a PDF. A custom wire protocol frames a document behind a length-prefixed header. Suppose the record is 40 MB, the PDF starts at byte 1,182,336 and runs for 312 KB. The habitual code calls Copy(Data, Index, Count) to cut a slice, then hands that slice to a loader that copies it again into its own buffer. Counting the container, the same 312 KB now exists three times, and a mailbox scan repeats that for every message
PDFium Component addresses the window directly. TPdf declares LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True), where Index is a zero-based byte position and Count is a byte length. The method validates both numbers, computes a single pointer, and forwards to the pointer overload LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean), which opens the document through FPDF_LoadMemDocument64. The whole-array form LoadDocument(const Data: TBytes; Buffered: Boolean) is a one-line call to the byte-range form with 0 and Length(Data), so every TBytes load passes through the same validation
In code, the slice simply disappears. LocateEmbeddedPdf stands for your own container parser
uses
System.SysUtils, System.IOUtils, PDFium;
procedure PreviewEmbedded(const RecordFile: string);
var
Frame: TBytes; // whole container record, tens of megabytes
Offset, Size: Integer;
Pdf: TPdf;
begin
Frame := TFile.ReadAllBytes(RecordFile);
LocateEmbeddedPdf(Frame, Offset, Size); // your parser, not part of the component
Pdf := TPdf.Create(nil);
try
// No Copy(Frame, Offset, Size): the window is addressed in place
Pdf.LoadDocument(Frame, Offset, Size); // Buffered defaults to True
Frame := nil; // fine here: only the PDF window was copied
Writeln('Pages: ', Pdf.PageCount);
finally
Pdf.Free; // closes the document
end;
end;
When the global EnableV8Engine is still False and the PDFium library has not been loaded yet, the memory loaders scan the incoming bytes for XFA markers and switch EnableV8Engine on if they find any, so the V8-capable DLL gets loaded. With the byte-range call that scan sees only the window. A manifest or a second document elsewhere in the same record cannot trigger it
Why does Index plus Count overflow the bounds check?
Because Index and Count are both 32-bit signed Integer values, and adding two large positive ones does not reliably produce a large positive result. Where the offset and length come from a file header, an attacker or a corrupt record controls both. The same trap waits in your own container parser, which computes those numbers first
// A helper for your own container parser (suggested, not part of the component)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
// WRONG: Index + Count is evaluated as Integer and can wrap negative
// Result := (Index >= 0) and (Index + Count <= Length(Data));
// RIGHT: reject signs, bound Index on its own, then compare Count
// against the remainder; the only arithmetic is a subtraction that cannot wrap
Result := (Index >= 0) and (Count >= 0) and
(Index <= Length(Data)) and
(Count <= Length(Data) - Index);
end;
Work the failing case through. With Index = 2000000000 and Count = 2000000000, the true sum is four billion, but 32-bit signed arithmetic stores it as -294,967,296. That value is comfortably below any Length(Data), so the wrong check passes and @Data[Index] points far outside the array, with a two-gigabyte length attached. A crash is the good outcome. The bad one is PDFium parsing whatever process memory happens to live there
The correct order never adds. Negative values are rejected before anything is indexed. Index is then bounded against Length(Data) alone, which guarantees that Length(Data) - Index is a non-negative Integer, and only then is Count compared with that remainder. Delphi evaluates and with short-circuit logic by default ({$B-}), so each later term only runs once the earlier ones hold. Do not lean on {$Q+} as the safety net: release builds routinely ship with overflow checking off, and with it on you have only swapped a memory-safety hole for an EIntOverflow escaping from the middle of a validation routine
The byte-range LoadDocument runs the same four tests in the same order and raises EPdfError with a message naming the failed rule: "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length" and "PDF byte range exceeds data length". You can reproduce each one with a four-byte array. Index = 5, Count = 0 trips the third message, Index = 2, Count = 3 trips the fourth, and after every rejection Pdf.Active is still False. The same treatment of untrusted lengths across the native boundary is covered more broadly in hardening the PDFium VCL ABI and memory safety in Delphi
A zero-length window is still a legal call
An empty window passes a nil pointer, because @Data[Index] is not a legal expression for every Index the validation accepts. Index = Length(Data) with Count = 0 is a well-formed empty window at the tail of the buffer, and an empty TBytes gives Index = 0 on an array that has no element zero. Taking the address in either case reads past the end or dereferences a nil dynamic array, and with range checking on, Delphi raises ERangeError before the loader even runs
So the byte-range overload branches: Count = 0 yields nil, any other count yields @Data[Index]. The pointer overload accepts nil only together with a size of zero, PDFium then declines to open an empty buffer, and the call ends in an ordinary EPdfError carrying PDFium's last-error text, or "Cannot load PDF document" when PDFium reports none. A header that claims a zero-byte attachment gets the same catchable exception as any other bad input. Rejecting it in your parser, with the record number in the message, still reads better in a support log
Buffered decides who owns the bytes
Buffered is the only parameter here whose consequences outlast the call. With Buffered = True, the component copies the selected window, and only the window, into its own buffer before loading. In the 40 MB example, 312 KB are copied and the container is left alone. Once LoadDocument returns, you may free, reuse or overwrite the container immediately
With Buffered = False, @Data[Index] goes straight to FPDF_LoadMemDocument64, and PDFium keeps reading through that pointer for as long as the document is open. The entire backing TBytes becomes borrowed. It must stay alive and unmodified until you set Active := False or free the TPdf. The whole array matters, not just the window: a Delphi dynamic array is reference counted as one block, so dropping the last reference anywhere in your code frees the memory PDFium is still reading. SetLength is just as fatal. A reallocation can move the block
Two Delphi details make this easier to get wrong than it looks. Dynamic arrays have no copy-on-write, unlike strings, so Other := FFrame; Other[0] := 0; writes into the very bytes PDFium is parsing. And a receive buffer that your network code reuses for the next frame gets overwritten under the open document. Passing a function result straight in, as in Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False), leaves the array held only by a hidden temporary that dies with the calling routine, while the document lives on
Keep Buffered = True for anything that crosses a thread, sits behind a reusable buffer, or outlives the routine that loaded it. The extra cost is one copy of the PDF's own size, and it buys the freedom to drop a 40 MB record the moment the call returns. Borrowing earns its place only when the container is already a long-lived field you hold for the whole session anyway, such as a viewer that keeps an archive record open while the user pages through the attachment. In that case, make the ownership explicit in one class
type
TFrameSession = class
private
FFrame: TBytes; // owns the backing storage while FPdf has a document open
FPdf: TPdf;
public
constructor Create(const AFrame: TBytes);
destructor Destroy; override;
procedure OpenEmbedded(Offset, Size: Integer);
end;
constructor TFrameSession.Create(const AFrame: TBytes);
begin
inherited Create;
FFrame := AFrame; // shares the array; nobody else may write to it now
FPdf := TPdf.Create(nil);
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
FPdf.Active := False; // close any previous window first
FPdf.LoadDocument(FFrame, Offset, Size, False); // FFrame must outlive this document
end;
destructor TFrameSession.Destroy;
begin
FPdf.Active := False; // 1. release the borrow
FPdf.Free;
FFrame := nil; // 2. only now may the storage go
inherited;
end;
The constructor shares the caller's array rather than copying it, so the caller has to stop writing to it. If you cannot promise that, pass Copy(AFrame) to the constructor and you are back to one copy, made once per session instead of once per load
Checking that the window starts at %PDF-
A window that is off by a few bytes is the subtle failure, because the document may still open. ISO 32000-1 §7.5.4 defines every cross-reference entry as a byte offset from the beginning of the file, and §7.5.2 puts the %PDF- header at the start. When your parser miscounts a prefix and the window begins eight bytes early, those offsets are eight bytes off. PDFium looks for the header within the first 1024 bytes and compensates for leading junk; a window that starts late or ends early can push it into rebuilding the cross-reference table from the object markers it finds. Either way the page count looks right. The offset in your parser is still wrong, and the next reader of that record may be less forgiving
The overload LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) makes that visible. Report.HeaderOffset is where %PDF- was found within the first 1024 bytes of the window, or -1 when it was not found. Report.UsedRecovery is True when PDFium did not accept the cross-reference table as valid. All offsets in the report count from the first byte of the window, not from the container. A Count that is too short cuts off the trailing %%EOF, and TPdfLoadOptions.Default(plmStrict) turns on RequireFinalEndOfFileMarker, so strict mode rejects that window outright
var
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default; // plmCompatible
Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);
// LogWarning is your own logging routine
if Report.HeaderOffset > 0 then
LogWarning(Format('Window starts %d bytes before %%PDF-, check Offset',
[Report.HeaderOffset]))
else if Report.HeaderOffset < 0 then
LogWarning('No %PDF- header in the first 1024 bytes of the window');
if Report.UsedRecovery then
LogWarning('Cross-reference table was rebuilt, check Offset and Size');
end;
The audit behind those fields has a cost that matters for borrowed loads. When the window fits within Options.AuditByteLimit (256 MB by default), the audit reads a temporary copy of the window, so Buffered = False combined with a report is no longer copy-free during the load. Setting AuditByteLimit to 0 skips the audit and the copy, and leaves HeaderOffset at -1. Run the audited form while you develop the container parser and against new sources of records; switch to the plain overload once the offsets are trusted. The rest of the report is covered in catching PDF load failures with a PDFium load report
When a byte range is the wrong tool
The byte-range overload assumes the whole container is already in memory, and because Count is an Integer, one window cannot exceed MaxInt bytes. A 6 GB archive on disk, or a socket you cannot rewind, does not fit that shape, and reading everything into TBytes just to address a window inside it defeats the purpose. For those cases LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean) lets PDFium pull blocks from a seekable stream on demand. A small TStream descendant of your own that adds the PDF's starting offset to every Seek presents the embedded document as if it began at position zero, as the on-demand streaming article shows
The other mismatch is content that is not a plain PDF yet. If the embedded bytes are compressed, encrypted by the container, or wrapped in another encoding, a transformed copy is unavoidable, and loading that new array with the default Buffered = True is the straightforward answer. The byte range pays off in one shape: contiguous, unmodified PDF bytes, already resident, at an offset your parser has validated without adding
The byte-range call sits next to file, stream, raw-pointer and on-demand loading in the same TPdf component. The PDFium Component product page lists the supported Delphi and C++Builder versions, licensing options and the rest of the loading API