مقال تقني

عزل مرمّزات صور PDF في عمليات عاملة باستخدام HotPDF

يستطيع 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 ميبيبايت
    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 بايت على الأقل، لأن حداً أصغر لا يمكنه استيعاب مجموعة عمل واقعية للمفكِّك وسيؤدي إلى فشل كل مستند. يُفرض حد الذاكرة بواسطة كائن مهمة Windows بدلالة القتل عند الإغلاق، بحيث تموت العملية العاملة مع المهمة حتى لو أُنهي المضيف فجأة

الحد الثالث هو حد الإخراج، وهو مُشتق وليس مُهيَّأ. يحسب HotPDF البايتات المطلوبة من المنطقة المطلوبة، أو من هندسة الصورة المتوقعة، بضرب العرض في الارتفاع في ثلاثة بالنسبة لإخراج 24 بت، ثم يقيّد تلك القيمة عند DecodeBudgetBytes عند ضبط ميزانية. المفكِّك الذي يُبلغ عن رأس معقول ثم يحاول إصدار وحدات بكسل أكثر بكثير مما تسمح به الهندسة يُوقفه التخطيط نفسه، ويرى المضيف cwsOutputLimit. لهذا السبب تكون طبقة العزل وميزانية فك الترميز متكاملتين: تحدد الميزانية حجم الصورة المسموح به، وتضمن حدود العزل أن كذبة بشأن ذلك الحجم لا يمكن أن تتحول إلى كتابة خارج الحدود في عمليتك

أين يقع هذا في مسار استيعاب مُحصَّن

عزل العمليات هو الطبقة الخارجية لسلسلة دفاع تبدأ قبل ذلك بكثير. ترفض الحدود البنيوية المستندات غير المعقولة عند وقت التحليل. تحدّ ميزانيات المرشحات التوسّع. يحتوي العزل ما ينجو من كليهما. بالنسبة للمستندات التي تصل إلى طبقة الصور، يستحق الأمر معرفة أي مرمّز تمارسه فعلياً، إذ تختلف معالجة JPXDecode وقواميس رموز JBIG2 اختلافاً كبيراً في أنماط الفشل، ويحمل JBIG2 خصوصاً مقاطع عامة عابرة للصفحات من شأن صندوق عزل ساذج لكل صورة على حدة أن يكسرها

التكلفة صادقة وتستحق الذكر: إطلاق عملية لكل صورة معزولة يضيف مللي ثوانٍ، وسيشعر بها مستند من مئات الصفحات الممسوحة ضوئياً. قِسها مقابل ما تشتريه. في محوّل دفعي يعمل دون إشراف طوال الليل، تكون خسارة الإنتاجية غير مرئية واحتواء الأعطال هو بيت القصيد بأكمله. في عارض تفاعلي يفتح مستندات يثق بها المستخدم بالفعل، يكون cimDisabled أو cimAutomatic هو الإعداد الافتراضي المعقول. الوضع خاصية بسيطة، لذا لا شيء يمنعك من الاختيار حسب فئة المستند وقت التشغيل

يشحن HotPDF طبقة العزل وميزانيات فك الترميز وحدود المحلل البنيوية كمكون VCL أصلي واحد لـ Delphi وC++Builder، دون أي بيئة تشغيل خارجية يجب نشرها باستثناء الملف التنفيذي للعملية العاملة نفسه. التوثيق الكامل لواجهة البرمجة ونسخة تجريبية متاحان على صفحة مكون HotPDF لـ Delphi