PDFium 컴포넌트는 라이브러리의 모든 PDF 파서에 대해 재귀 깊이, 토큰 수, 객체 수, 디코딩된 바이트, 확장 비율, 할당, 벽시계 데드라인을 제한하는 단일 레코드 TPdfParserResourceBudget을 노출합니다. ConfigurePdfParserResourceBudget은 그것을 프로세스에 원자적으로 설정하고, 각 파싱은 스냅샷을 복사하므로 작업 도중에 한계가 흔들릴 수 없습니다
파서 여섯 개, 사적 상한 여섯 개
이것을 강제한 상황은 평범하고 아마 익숙할 것입니다. sparse xref 순회는 자체 객체 상한이 있었습니다. 스트림 검사기는 자체 디코딩 바이트 상한이 있었습니다. XMP는 하나, XFDF와 FDF는 또 하나, 콘텐츠 정규화는 세 번째, preflight 기본값은 네 번째가 있었습니다. 그 숫자들은 각각 그 자체로 방어 가능했지만 외부에서 도달할 수 있는 것은 하나도 없었습니다. 그래서 "이 수집 경로는 익명 업로드를 처리합니다. 여기서 64 MiB를 넘게 할당하거나 4초를 넘게 실행해서는 안 됩니다"라고 말하고 싶은 호출자에게 말할 곳이 없었습니다. 두 번째 문제는 조용해서 더 나빴습니다. 한계가 가변 전역에 살고 있는 곳에서는 한 정책 아래 시작된 파싱이 다른 정책 아래 끝날 수 있었습니다. 한 스레드가 setter를 호출하는 동안 다른 스레드가 객체 스트림을 절반쯤 처리하고 있으면, 그 순회의 전반부는 옛 상한으로, 후반부는 새 상한으로 제한됩니다. 아무것도 크래시하지 않고, 아무것도 로그되지 않으며, 이후에 그 작업이 실제로 어떤 한계 아래 실행됐는지 말할 수 없습니다. 그것이 바로 여러분이 한계를 필요로 했던 바로 그 속성입니다
TPdfParserResourceBudget이 실제로 제한하는 것은 무엇인가
다른 것들이 통과시키는 입력 부류를 각각 하나씩 잡도록 선택된 일곱 차원입니다. MaxRecursionDepth는 기본 1024로, 중첩 사전과 배열이 ISO 32000-1 §7.3 객체 구문을 스택 오버플로로 만들지 못하게 합니다. MaxTokens는 기본 10,000,000으로 파일 크기와 무관하게 렉싱 작업을 제한합니다. MaxObjects는 기본 4,000,000으로 xref 서브섹션 수, 객체 스트림 /N 값, 그래프 순회를 커버합니다. MaxDecodedBytes는 기본 256 MiB이고 MaxAllocationBytes는 512 MiB라서, 할당 후가 아니라 할당 전에 파일이 거부될 수 있습니다. MaxExpansionRatio는 기본 1000.0이며 사람들이 과소평가하는 바로 그것입니다. ISO 32000-1 §7.4 스트림 필터는 범용 압축이고, 몇 킬로바이트의 Flate 데이터가 정당하게 수백 메가바이트로 확장될 수 있으며, 비율 상한이 압축 해제 폭탄에 대한 표준 방어입니다. 그것은 절대 바이트 상한과 직교합니다. 200 MiB가 되고 싶어 하는 200 KiB 스트림은 256 MiB 상한 아래 있으면서도 여전히 터무니없기 때문입니다. DeadlineMilliseconds는 기본 0, 즉 데드라인 없음을 뜻하므로 그 차원은 순전히 옵트인입니다. 기본값 집합은 의도적으로 관대하게, 일반 업무 문서가 아무것도 건드리지 않게 하는 크기로 되어 있으며 강화된 프로필이 아닙니다. 입력이 공개 업로드 폼에서 온다면 Delphi PDF 미리보기 창 강화에 설명된 규칙과 함께 아래 숫자에 더 가까운 것을 원할 것입니다
uses
FPdfPdfCommon;
procedure ApplyUntrustedIntakeBudget;
var
Budget: TPdfParserResourceBudget;
begin
Budget := TPdfParserResourceBudget.Default;
// 유지되는 기본값: MaxRecursionDepth 1024, MaxTokens 10000000,
// MaxObjects 4000000
Budget.MaxDecodedBytes := Int64(32) * 1024 * 1024; // 기본 256 MiB
Budget.MaxAllocationBytes := Int64(64) * 1024 * 1024; // 기본 512 MiB
Budget.MaxExpansionRatio := 50.0; // 기본 1000.0
Budget.DeadlineMilliseconds := 4000; // 기본 0 = 꺼짐
ConfigurePdfParserResourceBudget(Budget);
end;
전역을 읽는 대신 스냅샷을 찍는 이유
강제되는 동안 바뀔 수 있는 한계는 한계가 아니기 때문입니다. 이것이 기능 전체의 핵심 설계 지점이며, 모든 진입점에서 한 줄의 규율입니다. 작업 시작 시 정책을 로컬 TPdfParserResourceBudget으로 한 번 읽고, 모든 체크포인트가 그 로컬 복사본과 비교하게 합니다. GetPdfParserResourceBudget은 크리티컬 섹션을 잡고 레코드의 값 복사본을 돌려주며, 이후 파싱은 다시는 가변 전역 상태를 건드리지 않습니다. 결과적으로 다른 스레드의 ConfigurePdfParserResourceBudget 호출은 다음 작업에만 영향을 주고, 진행 중인 작업에는 절대 영향을 주지 않습니다. 동시 파싱은 간섭 없이 서로 다른 정책 아래 실행되고, 리소스 한계로 실패한 작업은 단일하고 일관된 임계값 집합을 상대로 실패했으며, 잠금은 토큰당이 아니라 파싱당 한 번 잡히므로 핫 경로에 잠금 경합이 없습니다
type
TCustomScanState = record
Budget: TPdfParserResourceBudget;
StartTick: Cardinal;
TokenCount: Int64;
Depth: Integer;
end;
procedure BeginScan(var S: TCustomScanState);
begin
S.Budget := GetPdfParserResourceBudget; // 스냅샷을 시작 시 한 번 찍습니다
S.StartTick := PdfParserStartTick;
S.TokenCount := 0;
S.Depth := 0;
end;
function ScanCheckpoint(var S: TCustomScanState): Boolean;
begin
Result := False;
if PdfParserDeadlineReached(S.StartTick, S.Budget) then
Exit;
Inc(S.TokenCount);
if S.TokenCount > S.Budget.MaxTokens then
Exit;
if S.Depth > S.Budget.MaxRecursionDepth then
Exit;
Result := True;
end;
라이브러리 위에 자신의 파서를 쓴다면, 토큰 루프 안에서 GetPdfParserResourceBudget을 호출하는 대신 그 모양을 복사하세요. 토큰마다 호출하는 것은 정확하지만 무의미합니다. 매 반복마다 크리티컬 섹션을 지불하고, 스냅샷이 막으려던 작업 중 표류를 다시 들여오기 때문입니다
바이트 상한이 왜 병리적 파일을 막지 못하는가
PDF에서 크기와 비용은 느슨하게만 연관되기 때문입니다. /Index와 /W 배열이 그럴듯하지만 적대적인 항목 수를 기술하는 ISO 32000-1 §7.5.8 xref 스트림, 같은 오프셋을 다시 방문하는 객체 스트림 체인, 또는 각각이 보기보다 비싼 연산자들로 된 콘텐츠 스트림을 가진 400 KiB 파일은, 설정한 모든 바이트와 객체 상한 아래에 편안히 앉아 있으면서도 워커 스레드를 몇 분 동안 점유합니다. 바이트 수는 메모리를 제한하지, 시간을 제한하지 않습니다. 그것이 PdfParserStartTick과 PdfParserDeadlineReached의 용도입니다. PdfParserStartTick은 작업 시작 시 틱 값을 잡고, PdfParserDeadlineReached는 경과 시간을 DeadlineMilliseconds와 비교하며 데드라인이 0이면 언제나 False를 반환합니다. 뺄셈은 부호 없는 산술로 실행되므로 틱 카운터 롤오버가 가짜 타임아웃을 만들지 않습니다. xref 파싱, 객체 스트림 디코딩, 콘텐츠 토큰 정규화, FDF와 XFDF 리더가 모두 루프 헤드에서 그것을 검사하므로, 예산을 초과한 파싱은 외부에서 죽임을 당하는 대신 체크포인트에서 멈춥니다
정직한 주의 사항 두 가지. 그것은 CPU 시간이 아니라 벽시계 시간이라서, 스와핑이 일어나는 머신이나 부하가 걸린 빌드 서버는 조용한 머신이라면 통과할 데드라인을 넘어뜨릴 수 있습니다. 이는 중앙값에 가깝게 두지 말고 관찰된 최악의 경우보다 훨씬 위로 설정하라는 뜻입니다. 그리고 기본적으로 꺼져 있습니다. 월말 배치 처리 중 정당하게 큰 문서에서 발화하는 데드라인은 그것이 막는 실패보다 더 나쁜 실패이기 때문입니다. 전역이 아니라 수집 경로별로 옵트인하고, xref 스트림 검증의 구조적 검사와 짝지으세요
예산은 상한이지 형식 한계의 대체물이 아닙니다
각 파서는 자체 형식 특정 상수를 유지하고 둘 다 적용합니다. XFDF가 가장 명확한 예입니다. 필드 리더는 스냅샷 값과 함께 XfdfMaxTopLevelFields와 100000인 MaxFieldNodes 상수를 강제하며, 둘 중 하나라도 파싱을 멈출 수 있습니다
const
XfdfMaxTopLevelFields = 100000; // 형식 로컬, 항상 적용
...
// 두 상한 모두 유효하며, 더 낮은 쪽이 이깁니다
if (FieldCount >= XfdfMaxTopLevelFields) or
(FieldCount >= Budget.MaxObjects) then
Exit(False);
이것은 의도적이며, 존재하지 않는 노브를 찾으러 가기 전에 내면화할 가치가 있습니다. 전역 예산 4,000,000 객체가 XFDF 파일에 4,000,000 필드를 허용하지는 않습니다. 100000 필드는 이미 실제 주석 내보내기가 생산하는 어떤 것보다 훨씬 밖이기 때문이고, 전역 상한을 올리는 것이 형식 특정 온전성 검사를 조용히 넓혀서는 안 되기 때문입니다. 콘텐츠 정규화도 반대 방향에서 같은 방식으로 동작합니다. TPdfContentNormalizeOptions.Default는 자체 페이지별 한계에서 시작해 각각을 스냅샷으로 클램프하므로, MaxTokensPerPage는 예산에 의해 낮아질 수만 있고 결코 높아질 수 없습니다. 전역 정책은 조입니다. 풀지 않습니다
잘못된 설정은 설정할 때 실패합니다
ConfigurePdfParserResourceBudget은 할당하기 전에 검증합니다. 여섯 숫자 차원 중 하나라도 양수가 아니면 즉시 예외를 던지고, High(Integer)를 넘는 MaxDecodedBytes는 디코더 어딘가 깊숙이에서 조용히 잘리는 대신 플랫폼 한계 초과로 거부됩니다. 이것은 들리는 것보다 중요합니다. 키가 빠진 설정 파일에서 만든 예산은 0을 주고, 시작 시 받아들여진 객체 상한 0은 세 시간 뒤 첫 문서에서 수수께끼 같은 파싱 실패로 변하며, 그 시점에는 아무도 설정을 의심하지 않습니다. 같은 주의는 테스트를 포함해 예산을 일시적으로 조이는 모든 코드에 적용됩니다. 정책은 프로세스 전역이므로, 픽스처가 끝난 뒤 4초 데드라인을 설치해 둔 채로 두면 이후의 모든 것을 오염시킵니다. 변경을 중심으로 저장하고 복원하세요
var
Saved: TPdfParserResourceBudget;
begin
Saved := GetPdfParserResourceBudget;
try
ApplyUntrustedIntakeBudget;
ProcessInboundDocument(FileName);
finally
ConfigurePdfParserResourceBudget(Saved);
end;
end;
ResetPdfParserResourceBudget은 더 무딘 버전으로 TPdfParserResourceBudget.Default를 통째로 복원합니다. 알려진 상태를 원하는 테스트 해체에서 그것을 쓰고, 프로덕션 코드에서는 저장-복원을 쓰세요. 프로덕션에서는 호스트 애플리케이션이 시작 시 설정한 것이 돌아가야 할 상태입니다
예산이 더는 도움이 되지 않는 곳
그것은 단일 프로세스 전역 정책이며, 이것이 주요 한계입니다. 한 워커 스레드에 엄격한 프로필을, 다른 워커에 관대한 프로필을 줄 수 없습니다. 하나의 크리티컬 섹션 뒤에 레코드가 하나뿐이기 때문입니다. 테넌트별 한계가 정말로 필요하다면, 작동하는 형태는 각 정책 클래스를 자신의 프로세스로 격리하거나 저장-복원으로 작업을 감싸고, 다른 어떤 것도 동시에 파싱하지 않을 때만 그 괄호가 안전하다는 것을 받아들이는 것입니다. 또한 예산은 렌더링이 아니라 파싱을 제한합니다. 래스터화 메모리, 폰트 캐시 성장, 이미지 디코딩은 이 일곱 차원 밖에 있으며 자체 통제가 필요합니다. 그리고 예산은 작업이 너무 비쌌다고 알려줄 뿐, 왜인지는 알려주지 않습니다. 발화한 상한은 증상이고, 그 뒤의 잘못된 /Index 배열이나 통제 불능 필터 체인은 여전히 손으로 진단해야 합니다. 그래서 숫자를 움직이기 전에 객체 스트림과 프레딕터 디코딩에 설명된 디코딩 경로를 이해할 가치가 있습니다. 예산을 나쁜 문서가 프로세스를 함께 끌어내리지 못하게 하는 것으로 취급하고, 문서가 무엇을 하고 있었는지 알아낼 수 있을 만큼 로깅을 잘 유지하세요
전체 파서 API 표면, 예산 레코드, Delphi, C++Builder, Lazarus용 소스 코드 빌드는 제품 페이지에 있습니다: PDFium Component