오버플로 검사가 꺼져 있으면 2000000000 + 2000000000은 두 피연산자가 모두 Delphi Integer일 때 -294,967,296으로 평가됩니다. 이 숫자가 잡지식에서 벗어나는 순간은 컨테이너 파서가 레코드 헤더에서 오프셋과 길이를 읽어 더해 임베디드 PDF가 버퍼 안에 들어맞는지 확인하려 할 때입니다. 합은 음수로 감기고, Length(Data)와의 비교는 통과하며, 다음 줄은 배열 끝에서 약 2기가바이트 너머의 바이트 주소를 취합니다. 더 큰 버퍼 한가운데서 PDF를 여는 일은 절대 더하지 않는 범위 검사와, 문서가 열린 뒤 바이트를 누가 소유하는지에 대한 분명한 답을 필요로 합니다
이 글은 이미 RAM에 있고 다른 무언가 안의 알려진 오프셋에 자리한 문서를 다룹니다. 메모리에 담기엔 너무 큰 문서는 다른 문제로, PDFium VCL로 대용량 PDF를 요청 시 스트리밍하기에서 설명하듯 디스크에서 필요할 때 블록을 끌어와 처리합니다. 마지막 절이 경계를 그려줍니다
알려진 오프셋의 임베디드 PDF
메일 저장소는 메시지 본문과 첨부를 하나의 레코드에 보관합니다. 아카이브 컨테이너는 매니페스트와 이미지 몇 장, PDF를 이어 붙입니다. 사용자 정의 와이어 프로토콜은 길이 접두 헤더 뒤에 문서를 담습니다. 레코드가 40 MB이고 PDF가 바이트 1,182,336에서 시작해 312 KB라고 칩시다. 습관적인 코드는 Copy(Data, Index, Count)로 슬라이스를 자르고 그 슬라이스를 자기 버퍼로 한 번 더 복사하는 로더에 넘깁니다. 컨테이너까지 세면 같은 312 KB가 세 번 존재하고, 메일함 스캔은 메시지마다 그것을 반복합니다
PDFium Component는 창을 직접 주소 지정합니다. TPdf는 LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True)를 선언하는데, Index는 0 기반 바이트 위치, Count는 바이트 길이입니다. 메서드는 두 숫자를 검증하고 포인터 하나를 계산해 포인터 오버로드 LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean)로 전달하며, 그것은 FPDF_LoadMemDocument64로 문서를 엽니다. 배열 전체 형태인 LoadDocument(const Data: TBytes; Buffered: Boolean)는 바이트 범위 형태에 0과 Length(Data)를 넘기는 한 줄 호출이므로 모든 TBytes 로드가 같은 검증을 지납니다
코드에서는 슬라이스가 그저 사라집니다. LocateEmbeddedPdf는 여러분 컨테이너 파서의 자리표시자입니다
uses
System.SysUtils, System.IOUtils, PDFium;
procedure PreviewEmbedded(const RecordFile: string);
var
Frame: TBytes; // 컨테이너 레코드 전체, 수십 메가바이트
Offset, Size: Integer;
Pdf: TPdf;
begin
Frame := TFile.ReadAllBytes(RecordFile);
LocateEmbeddedPdf(Frame, Offset, Size); // 여러분 파서, 컴포넌트 일부 아님
Pdf := TPdf.Create(nil);
try
// Copy(Frame, Offset, Size) 없음: 창은 제자리에서 주소 지정된다
Pdf.LoadDocument(Frame, Offset, Size); // Buffered 기본값은 True
Frame := nil; // 여기선 안전: 복사된 것은 PDF 창뿐이다
Writeln('Pages: ', Pdf.PageCount);
finally
Pdf.Free; // 문서를 닫는다
end;
end;
전역 EnableV8Engine이 아직 False이고 PDFium 라이브러리가 아직 로드되지 않았을 때, 메모리 로더들은 들어오는 바이트에서 XFA 마커를 스캔해 발견하면 EnableV8Engine을 켜 V8 대응 DLL이 로드되게 합니다. 바이트 범위 호출에서 그 스캔은 창만 봅니다. 같은 레코드의 다른 곳에 있는 매니페스트나 두 번째 문서가 그것을 촉발할 수 없습니다
Index 더하기 Count는 왜 경계 검사를 오버플로하는가?
Index와 Count가 둘 다 32비트 부호 Integer이고, 큰 양수 둘을 더한 결과가 큰 양수로 나온다는 보장이 없기 때문입니다. 오프셋과 길이가 파일 헤더에서 온다면 공격자나 깨진 레코드가 둘 모두를 통제합니다. 그 숫자들을 먼저 계산하는 여러분 컨테이너 파서에도 같은 함정이 기다립니다
// 여러분 컨테이너 파서용 헬퍼(제안이며 컴포넌트 일부가 아님)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
// 틀림: Index + Count는 Integer로 평가되며 음수로 감길 수 있음
// Result := (Index >= 0) and (Index + Count <= Length(Data));
// 옳음: 부호를 거부하고 Index를 자체적으로 경계한 뒤 Count를
// 나머지와 비교한다. 유일한 산술은 감길 수 없는 뺄셈 하나다
Result := (Index >= 0) and (Count >= 0) and
(Index <= Length(Data)) and
(Count <= Length(Data) - Index);
end;
실패 사례를 끝까지 따라가 봅시다. Index = 2000000000이고 Count = 2000000000이면 참 합은 40억이지만 32비트 부호 산술은 -294,967,296으로 저장합니다. 그 값은 어떤 Length(Data)보다도 편하게 아래이므로 잘못된 검사는 통과하고 @Data[Index]는 2기가바이트 길이를 달고 배열 밖 먼 곳을 가리킵니다. 크래시가 좋은 결과입니다. 나쁜 결과는 PDFium이 그 자리에 살던 프로세스 메모리 무엇이든 파싱하는 것입니다
올바른 순서는 절대 더하지 않습니다. 음수는 어떤 것도 인덱싱되기 전에 거부됩니다. Index는 그다음 Length(Data)만으로 경계 지어지는데, 이는 Length(Data) - Index가 음수가 아닌 Integer임을 보장하고, 그제서야 Count가 그 나머지와 비교됩니다. Delphi는 기본값({$B-})에서 and를 단락 평가하므로 뒤의 항은 앞의 항이 성립할 때만 돌아갑니다. {$Q+}를 안전망으로 삼지 마십시오. 릴리스 빌드는 오버플로 검사를 끈 채 출하되고, 켜 놓으면 메모리 안전 구멍을 검증 루틴 한가운데서 튀어나오는 EIntOverflow로 바꾼 것에 불과합니다
바이트 범위 LoadDocument는 같은 네 검사를 같은 순서로 돌리고 실패한 규칙을 이름 짓는 메시지로 EPdfError를 raise합니다. "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length", "PDF byte range exceeds data length"입니다. 네 바이트짜리 배열만으로도 각각을 재현할 수 있습니다. Index = 5, Count = 0은 세 번째 메시지를, Index = 2, Count = 3은 네 번째를 건드리고, 거부마다 Pdf.Active는 여전히 False입니다. 네이티브 경계를 넘는 신뢰할 수 없는 길이의 동일한 처리는 Delphi에서 PDFium VCL ABI와 메모리 안전 강화하기가 더 넓게 다룹니다
길이 0짜리 창도 합법적인 호출이다
빈 창은 nil 포인터를 넘깁니다. 검증이 받아들이는 모든 Index에 대해 @Data[Index]가 합법적인 표현식은 아니기 때문입니다. Index = Length(Data)에 Count = 0은 버퍼 꼬리의 잘 구성된 빈 창이고, 빈 TBytes는 요소 0이 없는 배열에 Index = 0을 줍니다. 어느 쪽이든 주소를 취하면 끝을 넘어 읽거나 nil 동적 배열을 역참조하고, 범위 검사가 켜져 있으면 로더가 돌기도 전에 Delphi가 ERangeError를 raise합니다
그래서 바이트 범위 오버로드는 갈라집니다. Count = 0이면 nil, 그 외의 개수면 @Data[Index]입니다. 포인터 오버로드는 nil을 크기 0과 함께할 때만 받아들이고, PDFium은 빈 버퍼 열기를 사양하며, 호출은 PDFium의 마지막 오류 텍스트를 실은 평범한 EPdfError나 보고가 없을 때 "Cannot load PDF document"로 끝납니다. 0바이트 첨부를 주장하는 헤더는 다른 나쁜 입력과 같은 잡을 수 있는 예외를 받습니다. 레코드 번호를 메시지에 넣어 파서에서 그것을 거부하는 편이 여전히 지원 로그에서 더 읽기 좋습니다
Buffered가 바이트의 소유자를 정한다
Buffered는 이 자리에서 호출을 넘어서는 결과를 가진 유일한 매개변수입니다. Buffered = True이면 컴포넌트가 선택된 창, 그리고 창만을 자기 버퍼로 복사한 뒤 로드합니다. 40 MB 사례에서는 312 KB가 복사되고 컨테이너는 그대로 둘려집니다. LoadDocument가 돌아온 뒤에는 컨테이너를 즉시 해제하거나 재사용하거나 덮어써도 됩니다
Buffered = False이면 @Data[Index]가 FPDF_LoadMemDocument64로 곧장 가고, PDFium은 문서가 열려 있는 동안 그 포인터 너머를 계속 읽습니다. 백업 TBytes 전체가 빌려집니다. Active := False를 설정하거나 TPdf를 해제할 때까지 살아 있고 수정되지 않은 채로 있어야 합니다. 창만이 아니라 배열 전체가 문제입니다. Delphi 동적 배열은 한 블록으로 참조 카운트되므로, 코드 어디에서든 마지막 참조를 내려놓으면 PDFium이 아직 읽고 있는 메모리가 해제됩니다. SetLength도 똑같이 치명적입니다. 재할당은 블록을 옮길 수 있습니다
Delphi 세부 두 가지가 겉보기보다 실수를 부릅니다. 동적 배열은 문자열과 달리 copy-on-write가 없어서 Other := FFrame; Other[0] := 0;은 PDFium이 파싱 중인 바이트 그 자리에 씁니다. 그리고 네트워크 코드가 다음 프레임에 재사용하는 수신 버퍼는 열려 있는 문서 아래에서 덮어씁니다. Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False)처럼 함수 결과를 곧바로 넘기면 배열은 호출 루틴과 함께 죽는 숨은 임시 객체만이 쥐고, 문서는 살아 남습니다
스레드를 넘는 것, 재사용 버퍼 뒤에 숨는 것, 로드한 루틴보다 오래 사는 것에는 Buffered = True를 유지하십시오. 추가 비용은 PDF 자체 크기의 복사 한 번이고, 그 대가로 호출이 돌아오는 순간 40 MB 레코드를 내려놓을 자유를 삽니다. 빌리기가 제 자리를 얻는 경우는 컨테이너가 이미 세션 내내 쥐고 있던 오래 사는 필드일 때뿐입니다. 사용자가 첨부를 넘기는 동안 아카이브 레코드를 열어 두는 뷰어 같은 경우입니다. 그럴 때는 소유권을 한 클래스 안에서 명시적으로 만드십시오
type
TFrameSession = class
private
FFrame: TBytes; // FPdf가 문서를 열어 둔 동안 백업 저장소를 소유한다
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; // 배열을 공유한다. 이제 다른 누구도 쓰면 안 된다
FPdf := TPdf.Create(nil);
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
FPdf.Active := False; // 이전 창이 있다면 먼저 닫는다
FPdf.LoadDocument(FFrame, Offset, Size, False); // FFrame은 이 문서보다 오래 살아야 한다
end;
destructor TFrameSession.Destroy;
begin
FPdf.Active := False; // 1. 빌린 것을 돌려준다
FPdf.Free;
FFrame := nil; // 2. 이제서야 저장소가 떠날 수 있다
inherited;
end;
생성자는 복사 대신 호출자의 배열을 공유하므로 호출자는 더 이상 그것에 쓰면 안 됩니다. 그렇게 약속할 수 없다면 생성자에 Copy(AFrame)를 넘기십시오. 복사 한 번으로 돌아오되, 로드마다가 아니라 세션마다 한 번입니다
창이 %PDF-에서 시작하는지 확인하기
몇 바이트 어긋난 창이 미묘한 실패입니다. 문서가 여전히 열릴 수 있기 때문입니다. ISO 32000-1 §7.5.4는 모든 교차 참조 항목을 파일 시작점으로부터의 바이트 오프셋으로 정의하고 §7.5.2는 %PDF- 헤더를 시작점에 둡니다. 파서가 접두어를 잘못 세어 창이 여덟 바이트 일찍 시작하면 그 오프셋들은 여덟 바이트 틀어집니다. PDFium은 처음 1024바이트 안에서 헤더를 찾고 앞의 잡동사니를 보정하지만, 늦게 시작하거나 일찍 끝나는 창은 발견한 객체 마커들로부터 교차 참조 테이블을 다시 짜는 지경에 이르게 할 수 있습니다. 어느 쪽이든 페이지 수는 멀쩡해 보입니다. 파서의 오프셋은 여전히 틀렸고, 그 레코드의 다음 독자는 덜 관대할 수 있습니다
오버로드 LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True)는 이를 드러내게 합니다. Report.HeaderOffset는 창의 처음 1024바이트 안에서 %PDF-를 찾은 위치이고 못 찾으면 -1입니다. Report.UsedRecovery는 PDFium이 교차 참조 테이블을 유효한 것으로 받아들이지 않았을 때 True입니다. 보고서의 모든 오프셋은 컨테이너가 아니라 창의 첫 바이트부터 셉니다. 짧은 Count는 꼬리의 %%EOF를 잘라내고, TPdfLoadOptions.Default(plmStrict)는 RequireFinalEndOfFileMarker를 켜므로 strict 모드는 그런 창을 그 자리에서 거부합니다
var
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default; // plmCompatible
Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);
// LogWarning은 여러분의 로깅 루틴이다
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;
그 필드들 뒤의 감사에는 빌린 로드에서 비용이 문제가 됩니다. 창이 Options.AuditByteLimit(기본값 256 MB) 안에 들어맞으면 감사는 창의 임시 복사본을 읽으므로, Buffered = False와 보고서의 조합은 로드 동안 더 이상 복사 없음이 아닙니다. AuditByteLimit를 0으로 두면 감사와 복사를 건너뛰고 HeaderOffset를 -1로 남깁니다. 컨테이너 파서를 개발하는 동안과 새 레코드 출처를 상대할 때는 감사 붙은 형태로 돌리고, 오프셋이 신뢰되면 평범한 오버로드로 바꾸십시오. 보고서의 나머지는 PDFium 로드 보고서로 PDF 로드 실패 잡기가 다룹니다
바이트 범위가 잘못된 도구일 때
바이트 범위 오버로드는 컨테이너 전체가 이미 메모리에 있다고 가정하고, Count가 Integer이므로 한 창은 MaxInt 바이트를 넘을 수 없습니다. 디스크의 6 GB 아카이브나 되감을 수 없는 소켓은 그 모양에 들어맞지 않고, 그 안의 창을 주소 지정하려고 전부 TBytes로 읽어들이는 일은 목적을 무너뜨립니다. 그런 경우 LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean)이 PDFium이 seekable 스트림에서 필요할 때 블록을 끌어오게 합니다. 모든 Seek에 PDF 시작 오프셋을 더하는 작은 TStream 하위 클래스 하나면 요청 시 스트리밍 문서가 보여주듯 임베디드 문서가 위치 0에서 시작하는 것처럼 보입니다
다른 맞지 않음은 아직 평범한 PDF가 아닌 콘텐츠입니다. 임베디드 바이트가 압축되어 있거나 컨테이너가 암호화했거나 다른 인코딩에 감싸여 있다면 변환된 복사본은 피할 수 없고, 그 새 배열을 기본값 Buffered = True로 로드하는 것이 솔직한 답입니다. 바이트 범위가 값을 내는 형태는 하나입니다. 인접하고, 수정되지 않은, 이미 상주하는, 파서가 더하기 없이 검증한 오프셋의 PDF 바이트
바이트 범위 호출은 같은 TPdf 컴포넌트의 파일, 스트림, 날 포인터, 요청 시 로딩 곁에 자리합니다. PDFium Component 제품 페이지에 지원 Delphi와 C++Builder 버전, 라이선스 옵션, 나머지 로딩 API가 정리되어 있습니다