HotPDF יכול לפענח את שלושת מסנני התמונה המסוכנים ביותר ב-PDF, DCTDecode, JPXDecode ו-JBIG2Decode, בתוך תהליך עבודה נפרד וקצר-חיים במקום בתוך היישום שלך. המאפיין שמפעיל זאת הוא CodecIsolationMode, וההשפעה המעשית היא שקובץ JPEG 2000 פגום שהיה קורס את יישום ה-VCL שלך כעת ממית תהליך ילד חד-פעמי בעוד המארח מדווח קוד סטטוס וממשיך הלאה
ההבדל הזה חשוב ביותר בדיוק במקומות שמהם קבצי PDF באמת מגיעים: טופס העלאה, שער דואר, מכשיר סריקה, נקודת מסירת FTP של שותף. אין לך שליטה על הבייטים האלה, ושם, בקודקי התמונה, יושב הנזק ההיסטורי
מדוע תמונה פגומה אחת מפילה את כל היישום?
מפני שקודק תמונה הוא החלק היחיד בקורא PDF שמריץ מכונת מצבים מורכבת על נתונים בשליטת תוקף כמעט ללא בדיקות מבניות שנותרו לגיבוי. בזמן שהבייטים מגיעים למפענח JPEG 2000 או JBIG2, טבלת ההפניות הצולבות כבר נותחה, האובייקט כבר נפתר, שרשרת המסננים כבר נפרסה, ומה שנותר הוא קוד-זרם גולמי שאומר כמה אריחים, כמה רכיבים, כמה סיביות לדגימה. מספר שגוי שם אינו שגיאת ניתוח. זו הקצאה שגויה או אינדקס מחוץ לטווח בתוך לולאת פענוח הדוקה
תקציבים עוזרים, וכבר כדאי שיהיו לך כאלה. HotPDF מגביל התרחבות עם DecodeBudgetBytes ו-DocumentDecodeBudgetBytes, ומגביל שרשרות מסננים עם DecodeFilterLimit ו-DecodePipelineDepthLimit; ההיגיון מאחורי התקרות האלה מכוסה בפענוח מוגבל למסננים מקוננים ולפצצות PDF. אבל תקציב בייטים עונה רק על שאלה אחת, כמה פלט מותר. הוא לא יכול לענות מה קורה כשהמפענח נכשל לפני שהוא מפיק פלט כלשהו. תקלת גישה בתוך לולאת פענוח אינה הפרת מדיניות שאפשר לדחות; זהו אירוע ברמת תהליך, והדרך היחידה האמינה להכיל אירוע ברמת תהליך היא תהליך אחר
מה HotPDF מבודד, ומה לא
HotPDF מבודד בדיוק שלושה סוגי קודק, המנויים כ-hckDCT, hckJPX ו-hckJBIG2 ביחידת HPDFCodecIsolation. כל השאר, Flate, LZW, RunLength, ASCII85, CCITT, נשארים בתוך התהליך, מפני שהמפענחים האלה פשוטים מספיק כדי להגביל בתקציבים ואינם המקום שממנו מגיעים הכשלים המעניינים
שכבת ההעברה צרה בכוונה. המארח מקצה מיפוי זיכרון משותף מוגבל אחד, כותב THPDFCodecSharedHeader קבוע בתוספת הקלט הדחוס וכל מקטעי JBIG2 גלובליים, מפעיל את תהליך העבודה, וממתין. תהליך העבודה כותב פיקסלים מפוענחים בחזרה לאותו מיפוי וקובע מילת סטטוס. אין פרוטוקול צנרת שיכול לצאת מסנכרון, אין פורמט סריאליזציה לפאזז, והכותרת נושאת ערך קסם וגרסה כך שבינארי תהליך עבודה לא תואם נדחה במקום להיקרא שגוי
uses
HPDFDoc, HPDFCodecIsolation;
var
Pdf: THotPDF;
Info: THPDFCodecWorkerInfo;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
// כשל-סגור: לעולם אל תפענח קודקים אלה בתוך התהליך
Pdf.CodecIsolationMode := cimRequired;
Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
Pdf.CodecWorkerTimeoutMilliseconds := 5000; // 1..600000
Pdf.CodecWorkerMemoryLimitBytes := 268435456; // 0 או >= 64 MiB
Pdf.DecodeBudgetBytes := 134217728;
if Pdf.LoadFromFile('untrusted-upload.pdf') = 1 then
if Pdf.GetLoadedImageCount > 0 then
begin
Bmp := Pdf.ExtractLoadedImage(0);
try
if Pdf.GetLastCodecWorkerInfo(Info) then
LogCodecOutcome(Info);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
השאר את CodecWorkerExecutable ריק ו-HotPDF יפתור את תהליך העבודה ליד ההרצה שלך עצמה, בתור HotPDFCodecWorker.exe בתיקייה של ParamStr(0). קבע אותו במפורש כאשר הפריסה שלך שמה את תהליך העבודה במקום אחר; הערך מורחב דרך ExpandFileName, כך שנתיב יחסי נפתר ביחס לתיקייה הנוכחית ולא לתיקיית היישום, מה שנדיר שזה מה שרוצים בשירות
אוטומטי או חובה: איזה כשל אתה מעדיף?
שלושת הערכים של THPDFCodecIsolationMode מקודדים שלוש תשובות שונות לשאלה אחת, מה צריך לקרות כשתהליך העבודה בכלל לא יכול לרוץ. cimDisabled מדלג על הבידוד לחלוטין ומפענח בתוך התהליך, ההתנהגות שלפני גרסה 3.x. cimAutomatic, ברירת המחדל, מנסה את תהליך העבודה ונופל בחזרה בשקט לפענוח בתוך התהליך כשקובץ ההרצה של תהליך העבודה חסר או לא מצליח לעלות, מה שמדווח כסטטוס cwsUnavailable. cimRequired מסרב לנפילה הזו: תהליך עבודה לא זמין מסמן את הפענוח כמטופל ונכשל, כך שאף קוד-זרם לא מהימן לעולם לא מגיע למרחב הכתובות שלך
בחר לפי מודל האיום, לא לפי נוחות. צופה שולחני שפותח מסמכים שכבר יש למשתמש על הדיסק בסדר גמור עם cimAutomatic, שם תהליך עבודה חסר נסוג להתנהגות הקלאסית במקום לשבור את המוצר. שירות קליטה שמנתח קבצים מהאינטרנט צריך לרוץ עם cimRequired, מפני שטעות פריסה שמשמיטה בשקט את שכבת הבידוד היא בדיוק הסוג של רגרסיה שאף אחד לא שם לב אליה עד שהיא חשובה. שים לב לאסימטריה: רק cwsUnavailable מפעיל נפילה חזרה. תהליך עבודה שעלה ואז קרס, פג זמנו, או פגע במגבלה הוא כשל פענוח בשני המצבים, לעולם לא ניסיון חוזר שקט בתוך התהליך
קריאת הפסק מתוך THPDFCodecWorkerStatus
GetLastCodecWorkerInfo מחזיר את התוצאה של הפענוח המבודד האחרון, ומניית הסטטוס ספציפית מספיק כדי להניע החלטות תפעוליות אמיתיות ולא רק שורת יומן גנרית של "תמונה נכשלה". הערכים הם cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError ו-cwsOutputLimit
התייחס אליהם כשלוש קבוצות. בעיות פריסה הן cwsUnavailable ו-cwsLaunchFailed: מישהו שלח בלי תהליך העבודה, או שמוצר אנטי-וירוס חוסם יצירת תהליכים. בעיות מסמך הן cwsDecodeFailed ו-cwsOutputLimit: הקובץ פגום או גדול יותר ממה שהמדיניות שלך מאפשרת, ודחייתו היא התשובה הנכונה. הקבוצה המעניינת היא cwsTimedOut ו-cwsCrashed, מפני שאלה האירועים שקודם לכן היו תולים או ממיתים את תהליך המארח. כשזה קורה, השדות הנלווים ProcessId, ExitCode ו-ElapsedMilliseconds נותנים לך מספיק כדי להתאים לרשומת Windows Error Reporting ולהחליט אם קובץ אחד של לקוח פתולוגי או שמישהו בודק אותך
procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
case Info.Status of
cwsSucceeded:
; // אין מה לדווח
cwsUnavailable, cwsLaunchFailed:
Alert('Codec worker not deployed: ' + Info.ErrorMessage);
cwsTimedOut, cwsCrashed:
Quarantine(Format('pid %d exit %d after %d ms',
[Info.ProcessId, Info.ExitCode, Info.ElapsedMilliseconds]));
else
RejectDocument(Info.ErrorMessage);
end;
end;
התקרות שבאמת מגבילות
שלוש תקרות נפרדות חלות על כל פענוח מבודד, וידיעה איזו מהן הופעלה חוסכת אחר צהריים שלם של ניחושים. CodecWorkerTimeoutMilliseconds ברירת מחדל 10,000 ומאומת לטווח 1 עד 600,000; ערך מחוץ לטווח מעלה חריגה במקום לחתוך בשקט. CodecWorkerMemoryLimitBytes ברירת מחדל 536,870,912 בייטים וחייב להיות אפס, כלומר ללא הגבלה, או לפחות 67,108,864 בייטים, מפני שתקרה קטנה יותר לא יכולה להחזיק סט עבודה ריאלי של מפענח ותכשיל כל מסמך. תקרת הזיכרון נאכפת על ידי אובייקט Job של Windows עם סמנטיקת קטל-בסגירה, כך שתהליך העבודה מת יחד עם ה-Job גם אם המארח מסתיים בפתאומיות
התקרה השלישית היא תקרת הפלט, והיא נגזרת ולא מוגדרת ידנית. HotPDF מחשב את הבייטים הנדרשים מהאזור המבוקש, או מגיאומטריית התמונה הצפויה, כרוחב כפול גובה כפול שלוש עבור פלט 24-ביט, ואז חותך את הערך הזה בהתאם ל-DecodeBudgetBytes כשתקציב מוגדר. מפענח שמדווח כותרת סבירה ואז מנסה להפיק הרבה יותר פיקסלים ממה שהגיאומטריה מאפשרת נעצר על ידי המיפוי עצמו, והמארח רואה cwsOutputLimit. זו הסיבה שכבת הבידוד ותקציב הפענוח הם משלימים: התקציב מגדיר כמה גדולה תמונה מותר להיות, וגבול הבידוד מוודא ששקר לגבי הגודל הזה לא יכול להפוך לכתיבה מחוץ לגבולות בתוך התהליך שלך
איפה זה משתלב בנתיב קליטה מוקשח
בידוד תהליכים הוא השכבה החיצונית ביותר בשרשרת הגנה שמתחילה הרבה קודם. מגבלות מבניות דוחות מסמכים בלתי סבירים בזמן הניתוח. תקציבי מסננים מגבילים התרחבות. הבידוד מכיל את מה ששרד את שניהם. עבור מסמכים שמגיעים לשכבת התמונה, כדאי לדעת באיזה קודק בעצם משתמשים, מפני שלטיפול ב-JPXDecode ולמילוני סמלים של JBIG2 יש פרופילי כשל שונים מאוד, ו-JBIG2 בפרט נושא מקטעים גלובליים חוצי-עמוד ששיטת ארגז חול נאיבית לכל תמונה הייתה שוברת
העלות כנה וכדאי לומר אותה: הפעלת תהליך לכל תמונה מבודדת מוסיפה מילישניות, ומסמך עם מאות עמודים סרוקים ירגיש בזה. מדוד את זה מול מה שזה קונה. במעביר-אצווה שרץ ללא השגחה בלילה, אובדן התפוקה בלתי נראה ובלימת הקריסה היא כל הסיפור. בצופה אינטראקטיבי שפותח מסמכים שהמשתמש כבר סומך עליהם, cimDisabled או cimAutomatic הם ברירת המחדל הסבירה. המצב הוא מאפיין רגיל, כך שדבר לא מונע ממך לבחור לפי מחלקת מסמך בזמן ריצה
HotPDF משלח את שכבת הבידוד, תקציבי הפענוח ומגבלות המנתח המבניות כרכיב VCL מקורי אחד ל-Delphi ול-C++Builder, ללא ריצה חיצונית לפרוס מעבר לקובץ ההרצה של תהליך העבודה עצמו. תיעוד API מלא וגרסת ניסיון זמינים בדף רכיב HotPDF ל-Delphi