SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-21

ما الذي تثبته فعلياً عمليات البناء القابلة للتكرار؟

تثبت قيمة التحقق أن الملف سليم، لكن البناء القابل للتكرار يضمن مطابقة الملف للكود المصدري. اكتشف الفرق الجوهري بين حماية النقل والتحقق من سلامة الكود المصدري.

ما الذي تثبته عمليات البناء القابلة للتكرار

تثبت عمليات البناء القابلة للتكرار حقيقة واحدة محددة: الملف الثنائي الذي حصلت عليه هو نفسه الملف الثنائي الذي ينتجه هذا الكود المصدري بعينه. يمكن لأي شخص أخذ الكود المصدري نفسه، وبناؤه مرة أخرى، ومقارنة البايتات. تتوقف عملية التحقق عن كونها أمراً لا يستطيع القيام به سوى الناشر.

يُعرّف مشروع البناء القابل للتكرار (Reproducible Builds) هذا المفهوم على النحو التالي: "تعتبر عملية البناء قابلة للتكرار إذا كان بإمكان أي طرف، عند توفير الكود المصدري نفسه وبيئة البناء وتعليمات البناء، إعادة إنشاء نسخ متطابقة بتاتاً (bit-by-bit) من جميع المخرجات المحددة". عملية المقارنة بحد ذاتها هي عبارة عن دالة تجزئة (hash). تكمن الصعوبة بأكملها في تثبيت البيئة والتعليمات بدقة كافية لضمان توافق جهازين مختلفين على النتيجة ذاتها.

لماذا لا تجيب قيمة التحقق (checksum) على هذا السؤال

تُثبت قيمة التحقق المنشورة أن الملف وصل دون تلف. وتُثبت التوقيعات الرقمية على تلك القيمة أن الملف جاء من الشخص الذي يمتلك المفتاح. لكن لا توفر أي منهما معلومات عما حدث قبل وجود الملف (artifact). إذا تم اختراق جهاز البناء الخاص بالناشر، فسيتم حساب قيمة التحقق وتوقيع الملف الضار تماماً كما يُفعل مع الملف السليم، وبالتالي ستنجح كل عمليات التحقق في المراحل اللاحقة. وينطبق الأمر نفسه إذا قام أحد المطورين بالبناء من شجرة عمل (working tree) لم يتم دفعها (push) إلى المستودع.

هذه هي الفجوة. يمكنك قراءة الكود المصدري، والتحقق من التوقيع، والتحقق من قيمة التحقق، ومع ذلك تظل تشغل كوداً لم يظهر أبداً في المستودع. لذا، فإن قابلية إعادة الإنتاج (reproducibility) هي موضوع مختلف عن التحقق من التنزيل مقابل قيمة التحقق المنشورة. تحمي قيمة التحقق عملية النقل، بينما تحمي عملية إعادة البناء كل ما حدث قبل النقل.

هذا الهجوم ليس نظرياً. ففي اختراق SolarWinds Orion عام 2020، كان الوضع مطابقاً تماماً لهذا السيناريو: أصدر نظام البناء ملفات موقعة لا تتطابق مع الكود المصدري الذي راجعه أي شخص. لقد نجحت كل عمليات التحقق من التوقيع، لأن التوقيعات تبدأ من الملف النهائي نفسه.

ما لا يثبته البناء القابل للتكرار

هذا هو الجانب الذي يُبالغ في تقديره، لذا كن دقيقاً بشأن الحدود.

  • لا يعني أن المصدر آمن. الباب الخلفي الملتزم به علناً يُبنى بشكل قابل للتكرار، ويؤكد كل من يعيد البناء ذلك، لأنهم جميعاً بنوا نفس المصدر الضار. تنقل القابلية للتكرار الهدف إلى شجرة المصدر. لا يزال يتعين على شخص ما قراءة تلك الشجرة، ولهذا السبب تظل سياسة المراجعة، بما في ذلك سياسات الكود المساعد بالذكاء الاصطناعي في مشاريع المصدر المفتوح، عنصراً تحكمياً منفصلاً.
  • لا يعني أن مدخلاتك آمنة. التبعيات جزء مما تبنيه. الحزمة الضارة التي يتم حلها في وقت البناء تُجمع داخل الأداة الناتجة، وكل من يعيد البناء ويحل نفس التبعية يتفق معك. هكذا يصل هجوم سلسلة توريد npm إلى الخادم، ويقوم البناء القابل للتكرار بإعادة إنتاجه بأمانة.
  • لا يعني أن سلسلة الأدوات نزيهة. إذا تم اختراق المترجم، فإن كل من يعيد البناء باستخدام ذلك المترجم يُصدر نفس المخرجات المخترقة، وتتفق جميع الأحكام. تزيد القابلية للتكرار من تكلفة ذلك الهجوم، لكنها لا تكتشفه.
  • لا تقول شيئاً عن الثغرات الأمنية. المكتبة القديمة التي أُعيد إنتاجها بتطابق تام لا تزال مكتبة قديمة بها عيوبها المعلنة، لذا استمر في فحص خادمك بحثاً عن ثغرات CVE المعروفة وفق جدولها الزمني الخاص.

ما تزيله القابلية للتكرار هو موقع هجوم محدد: جهاز البناء، والمسار الكامل من المصدر إلى الملف الثنائي. إلى أن تصبح الحزمة قابلة للتكرار، لا يمكن لأي شخص خارج الناشر فحص ذلك المسار على الإطلاق.

لماذا ينتج المصدر نفسه بايتات مختلفة

معظم البرمجيات ليست قابلة للتكرار (reproducible) افتراضياً، وأسباب ذلك روتينية. فالمترجمات وصيغ الأرشفة تسجل حقائق حول الجهاز الذي نفذها.

  • الطابع الزمني. تخزن الصيغ tar وar وzip أوقات تعديل الملفات، لذا فإن البناء في ثانية مختلفة يغير ملف المخرجات.
  • المسار. تسجل معلومات التصحيح (debug information) مسار دليل البناء المطلق، لذا يختلف البناء في /home/alice/src عن البناء في /build/pkg حتى لو كان الكود متطابقاً.
  • الترتيب. تُرجع قراءة الدليل المدخلات بترتيب نظام الملفات، لذا يتغير ترتيب سطر الربط أو أعضاء الأرشيف بين الأجهزة.
  • الهوية. تضمن نصوص البناء البرمجية اسم المستخدم، أو اسم المضيف، أو الإعدادات المحلية (locale) للشخص الذي قام بالبناء.
  • قرار متخذ وقت البناء. إن اكتشاف ميزات المعالج، أو وضع بذور عشوائية (seeding)، يجعل المخرجات تعتمد على الجهاز بدلاً من المصدر.

يمكنك مراقبة السبب الأول وهو يحدث في حوالي عشر ثوانٍ:

mkdir -p /tmp/rb && cd /tmp/rb
echo hello > a.txt && tar cf one.tar a.txt
sleep 2
echo hello > a.txt && tar cf two.tar a.txt
sha256sum one.tar two.tar

تختلف قيمتا الـhash لأن ترويسة tar تخزن وقت تعديل a.txt، وإعادة كتابة الملف قدمت ذلك الوقت ثانيتين. المحتوى متطابق بايت مقابل بايت. تثبيت البيانات الوصفية (metadata) يحل المشكلة:

export SOURCE_DATE_EPOCH=1700000000
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner -cf three.tar a.txt
touch a.txt
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner -cf four.tar a.txt
sha256sum three.tar four.tar

الآن تتطابق قيم الـhash، لأنه لا يوجد حقل في ترويسة الأرشيف يأتي من الحالة الحالية للجهاز. يصلح --sort=name الترتيب، ويصلح --mtime الساعة، وتمنع أعلام الملكية تسجيل معرف المستخدم الخاص بك.

قراءة الفروقات باستخدام diffoscope

عندما يختلف بناآن، يخبرك sha256sum بأنهما مختلفان لا أكثر. وُجد diffoscope ليخبرك بالسبب بصيغة مقروءة للبشر. يقوم الأداة بفك ضغط كلا الطرفين بشكل متكرر، وتحويل الصيغ الثنائية إلى نصوص، ثم مقارنة النصوص. تتعامل الأداة مع حزم Debian، وملفات ELF الثنائية، وأرشيفات tar وZIP، وملفات PDF، وقواعد بيانات SQLite، وأكثر من مئة صيغة أخرى.

sudo apt install -y diffoscope
diffoscope one.tar two.tar

بالنسبة لزوج ملفات tar أعلاه، يكون التقرير قصيراً. بعد تقليصه، يبدو هكذا:

--- one.tar
+++ two.tar
├── file list
│ @@ -1 +1 @@
│ -rw-r--r-- 0/0  6 2026-08-18 10:14:02.000000 a.txt
│ +rw-r--r-- 0/0  6 2026-08-18 10:14:04.000000 a.txt

هذا هو التشخيص الكامل: نفس الحجم، نفس المسار، نفس الصلاحيات، وقت تعديل مختلف. تنتج الحزمة الحقيقية تقريراً أطول بكثير، لذا اكتبه في ملف وافتحه في متصفح:

diffoscope --html report.html build1.changes build2.changes

يخرج diffoscope برمز 0 عندما تكون المدخلات متطابقة، و1 عندما تختلف، و2 عند مواجهة مشكلة، لذا فهو يعمل مباشرة ضمن مهام CI دون الحاجة إلى سكربت وسيط. على خادم VPS صغير، ثبّت diffoscope-minimal بدلاً من diffoscope: فالحزمة الكاملة تجلب مجموعة كبيرة من مساعدات الصيغ التي لن تحتاجها غالباً.

ما الذي يصححه SOURCE_DATE_EPOCH، وأين يتوقف تأثيره

SOURCE_DATE_EPOCH هو متغير بيئة يحمل رقماً واحداً: وقت التعديل الأخير للمصدر، محسوباً بالثواني منذ 1 يناير 1970 بالتوقيت العالمي المنسق (UTC). أداة البناء التي تدعم هذا المتغير تستخدم ذلك الرقم بدلاً من طلب الوقت الحالي من نظام التشغيل. اضبط القيمة من نظام التحكم في الإصدار، لكي تتبع القيمة المصدر بدلاً من عملية البناء:

export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)

في حزم Debian، يقوم debhelper بتصدير القيمة من سجل التغييرات (changelog) نيابة عنك. ضبط المتغير يدوياً في debian/rules يبدو كالتالي:

export SOURCE_DATE_EPOCH ?= $(shell dpkg-parsechangelog -STimestamp)

الدعم يعتمد على الأداة، وليس على مستوى النظام. تدعم cmake الإصدار 3.8 وما يليه، وgcc الإصدار 7 وما يليه، وrpm الإصدار 4.13 وما يليه، وDocker buildx الإصدار 0.10 وما يليه قراءة هذا المتغير. سكربتاتك الخاصة لن تدعمه ما لم تبرمجها للقيام بذلك. إذا كان السكربت يستدعي date، مرر المتغير إليه:

BUILD_DATE="$(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"

توجد قاعدة واحدة مهمة عند التنفيذ. إذا كان المتغير مضبوطاً مسبقاً، فإن تلك القيمة هي الوقت الحالي بالنسبة لعملية البناء، لذا لا تقم أبداً بالكتابة فوق القيمة التي قدمها المستدعي.

صور الحاويات تواجه المشكلة نفسها في غلاف مختلف. يقوم Docker buildx الإصدار 0.10 وما يليه بتمرير SOURCE_DATE_EPOCH من الصدفة (shell) إلى عملية البناء كمعامل بناء. الطوابع الزمنية للملفات داخل الطبقات تتطلب من المصدّر إعادة كتابتها، وهو ما أضافه BuildKit في الإصدار 0.13، والصيغة الموثقة تقوم بدفع النتيجة إلى سجل (registry):

export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
docker buildx build --output type=image,name=registry.example.com/app:1.0,push=true,rewrite-timestamp=true .

اختبار بنيتك الخاصة باستخدام reprotest

تقوم أداة reprotest ببناء المصدر نفسه مرتين مع تغيير بيئة البناء عمداً بين المرتين، ثم تقارن النتائج. هذه الاختلافات هي جوهر عمل الأداة. افتراضياً، تقوم الأداة بتغيير مسار البناء، والوقت، والمنطقة الزمنية، والإعدادات المحلية (locale)، وumask، واسم المضيف، والمستخدم والمجموعة، وعدد وحدات المعالجة المركزية، والمجلد الرئيسي، وترتيب الملفات.

sudo apt install -y reprotest
reprotest . -- null

كل ما يأتي بعد -- يحدد نظام تشغيل بيئة البناء، بينما تعني null النظام الذي تعمل عليه حالياً. أضف -vv -d للاحتفاظ بالمجلدات المؤقتة من أجل فحصها، كما في reprotest . -vv -- null -d. استخدم reprotest auto -- null للسماح للأداة بتحديد نوع شجرة المصدر التي تتعامل معها تلقائياً.

تحتاج بعض الاختلافات إلى صلاحيات مرتفعة أو حزم إضافية، وتفشل هذه الاختلافات بوضوح عند تعذر تشغيلها. قم بإيقاف تشغيل هذه الاختلافات بدلاً من تشغيل العملية بالكامل بصلاحيات root:

reprotest --vary=-user_group,-domain_host,-fileordering auto -- null

أي شيء تبلّغ عنه reprotest هو أمر كان سيبلغ عنه أي نظام إعادة بناء لاحقاً، وبشكل علني، مع إدراج اسم مشروعك.

ماذا يعني حكم أداة إعادة البناء (rebuilder)

أداة إعادة البناء هي جهاز لا يتبع للناشر. تأخذ هذه الأداة المصدر المنشور وبيئة البناء المسجلة، ثم تعيد بناء الحزمة، وتقارن مخرجاتها بالملف الموجود في الأرشيف. تكتسب النتيجة قيمتها فقط لأن الجهاز مستقل.

يسجّل Debian البيئة في ملف .buildinfo الذي يكتبه dpkg-buildpackage بجانب .deb. الحقول هي الجزء المهم. يسرد Installed-Build-Depends كل حزمة مثبتة قد تؤثر على عملية البناء، مع إصداراتها الدقيقة. يسجّل Build-Path مكان تنفيذ البناء. يسجّل Environment متغيرات البيئة التي يُعرف أنها مؤثرة. يغطي Checksums-Sha256 المخرجات. ذلك الملف هو الوصفة لمحاولة ثانية:

sudo apt install -y devscripts mmdebstrap
debrebuild --buildresult=./artifacts --builder=mmdebstrap hello_2.10-2_amd64.buildinfo

يقرأ debrebuild ملف buildinfo ويسحب إصدارات التبعيات الدقيقة المذكورة فيه من snapshot.debian.org، بحيث يمكن لإعادة البناء اليوم استخدام إصدارات الحزم التي كانت موجودة يوم البناء الأصلي. لا يحتاج الباني mmdebstrap إلى إعداد chroot ولا إلى صلاحيات المستخدم الجذر (superuser). قارن المخرجات التي ينتجها بنسخة الأرشيف باستخدام diffoscope.

يشغّل Arch Linux أداة rebuilderd، التي تقوم بهذا العمل بشكل مستمر وتنشر الأحكام:

rebuildctl -H https://reproducible.archlinux.org pkgs ls --name rebuilderd

الحالات هي GOOD وBAD وUNKWN، والقراءة المباشرة لكل منها خاطئة في اتجاه مختلف. تعني GOOD أن طرفاً مستقلاً حصل على نفس البايتات، وهو تصريح قوي حول عملية البناء ولا يعني شيئاً حول المصدر. نادراً ما تكون حالة BAD هجوماً: السبب المعتاد هو طابع زمني أو مسار فشلت الحزمة في تثبيته، ولهذا السبب يمكن لـ rebuilderd إرفاق تقرير diffoscope بالفشل. تعني UNKWN أنه لم يختبره أحد، والحزمة غير المختبرة ليست حزمة مجتازة للاختبار.

لذا، القاعدة التشغيلية مختصرة. حكم BAD هو سبب لقراءة التقرير. إذا أظهر التقرير طوابع زمنية، أو مسارات بناء، أو ترتيب أعضاء، أرسل بلاغ خطأ في الحزمة. إذا أظهر كوداً تنفيذياً مختلفاً دون هذا التفسير، توقف عن نشر ذلك البناء وقم بتصعيد الأمر.

ما مدى قابلية Debian للتكرار حالياً؟

ChartDebian packages reproducible in the test framework, amd64
The data behind this chart
[
  {
    "label": "unstable",
    "percent_reproducible": 94.2,
    "tested_count": "41,163"
  },
  {
    "label": "forky",
    "percent_reproducible": 93.4,
    "tested_count": "39,059"
  },
  {
    "label": "experimental",
    "percent_reproducible": 67.0,
    "tested_count": "588"
  }
]

في يوم كتابة هذا المنشور، بلغت نسبة الحزم القابلة للتكرار في فرع unstable على معمارية amd64 ما نسبته 94.2% من أصل 41,163 حزمة تم اختبارها. أما في فرع experimental، فقد بلغت النسبة 67.0%، وذلك ضمن عينة أصغر بكثير وأحدث تضم 588 حزمة، وهو أمر متوقع للحزم التي لم ينتهِ أحد من إصلاحها بعد.

تأتي هذه الأرقام من صفحة Debian على موقع tests.reproducible-builds.org، والتي تمت قراءتها بتاريخ 2026-08-18، حيث كانت الصفحة تحمل ختم "آخر تحديث: 2026-08-18 16:02 بالتوقيت العالمي المنسق". هذه الأرقام متغيرة. لذا، طالع أداة التتبع بدلاً من اقتباس هذه الفقرة بعد ستة أشهر.

هناك تحذير واحد أكثر أهمية من النسبة المئوية. يقوم هذا الإطار ببناء كل حزمة مرتين على أجهزته الخاصة، مع تغيير البيئة بين عمليتي البناء، ثم يقارن بين النتيجتين. هو يقيس ما إذا كانت الحزمة قادرة على البناء بشكل قابل للتكرار. إنه ليس فحصاً للتأكد من أن .deb الموجود في الأرشيف يطابق الحزمة، فهذه مهمة منفصلة يقوم بها نظام إعادة البناء (rebuilder) عبر المقارنة مع الملف المنشور. كلا الرقمين مفيدان. إنهما يجيبان على أسئلة مختلفة، لكن الناس يقتبسون الرقم الأول وكأنه الرقم الثاني.

ما يجب فعله على خادمك الخاص

أنت لن تقوم بإعادة بناء توزيعة برمجية. الأجزاء التي تُنقل إلى خادم عادي أصغر حجماً وأقل تكلفة.

  • ثبّت سلسلة الأدوات (toolchain). الصورة الأساسية التي يُشار إليها بوسم (tag) قد تتغير دون سابق إنذار. أشر إليها باستخدام الـdigest، وسجّل هذا الـdigest مع الإصدار.
  • سجّل المدخلات. احتفظ بملف القفل (lockfile)، وdigest الصورة، وإصدار المترجم (compiler) بجانب الأداة الناتجة. البناء الذي لا يمكنك إعادة تكوين بيئته لا يمكن إعادة بنائه، وبالتالي لا يمكن التحقق منه أبداً.
  • ابنِ المشروع مرتين في بيئة CI وأوقف المهمة إذا اختلفت المخرجات. هذا يكلف عملية بناء إضافية واحدة، لكنه يكشف عدم الحتمية (nondeterminism) في اليوم الذي يضيفها فيه أحدهم، بدلاً من اكتشافها بعد عام أثناء وقوع حادثة.
  • احذف المسارات التي يضمّنها المترجم. بالنسبة لـGo، يقوم go build -trimpath -buildvcs=false بإزالة دليل البناء ودمغة التحكم في الإصدار، ويقوم go version -m ./app بطباعة ما انتهى به المطاف فعلياً داخل الملف الثنائي.
  • احتفظ بـhash لما قمت بنشره. عندما تحتاج إلى معرفة ما إذا كان الملف الثنائي قيد التشغيل يطابق مراجعة برمجية معينة، فإن هذا السجل هو الشيء الوحيد الذي يمكنه الإجابة.

فحص الـCI يتكون من أربعة أسطر:

set -eu
./build.sh && mv dist/app app.1
./build.sh && mv dist/app app.2
diffoscope --text - app.1 app.2

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

FAQ

هل يعني البناء القابل للتكرار (Reproducible Build) أن البرنامج آمن؟

لا. فهو يثبت فقط أن الملف الثنائي يطابق الكود المصدري، ولا شيء أكثر من ذلك. يمكن بناء باب خلفي (backdoor) تم دمجه في شجرة المصدر العامة بشكل قابل للتكرار، وسيؤكد كل من يعيد البناء ذلك، لأن كلاً منهم بنى نفس المصدر الضار. الحزمة التي تحتوي على ثغرة أمنية معروفة (CVE) تُبنى بشكل مثالي وتظل عرضة للاختراق. تزيل القابلية للتكرار موقعاً واحداً للمهاجم، وهو جهاز البناء والمسار من المصدر إلى الملف الثنائي. قراءة المصدر وتتبع الثغرات هي مهام منفصلة لا تقوم القابلية للتكرار بها نيابة عنك.

لماذا تختلف عمليتا البناء اللتان أجريتهما رغم عدم تغير أي شيء في المصدر؟

السبب دائماً تقريباً هو طابع زمني، أو مسار، أو ترتيب. تخزن تنسيقات الأرشفة مثل tar وzip أوقات تعديل الملفات، لذا فإن عملية سحب (checkout) تمت في ثانية مختلفة تنتج بايتات مختلفة. تسجل معلومات التصحيح مسار دليل البناء المطلق، لذا فإن /home/alice/src و/build/pkg تنتجان ملفات ثنائية مختلفة من نفس الكود. تُرجع عمليات قراءة الدليل الإدخالات بترتيب نظام الملفات، لذا يمكن ترتيب ملفات الكائنات (object files) في سطر الربط بشكل مختلف على جهاز آخر. شغّل diffoscope build1 build2 وسيوضح التقرير أي هذه الأسباب هو المسؤول بدلاً من تركك في حيرة.

ما هو SOURCE_DATE_EPOCH وهل يجب عليّ ضبطه؟

هو متغير بيئة قياسي يحتوي على رقم واحد: وقت آخر تعديل للمصدر، بالثواني منذ 1 يناير 1970 بالتوقيت العالمي المنسق (UTC). الأدوات التي تدعمه تستخدم هذه القيمة بدلاً من قراءة ساعة النظام. اضبطه من نظام التحكم في الإصدار باستخدام export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). هذا الإجراء ليس تلقائياً وليس حلاً عاماً. الأدوات التي تنفذه فقط هي التي تقرأه، ويجب أن تقرأه نصوص البناء (build scripts) الخاصة بك بنفسها، لذا فإن النص الذي يستدعي date سيستمر في ختم الوقت الحالي حتى تقوم بتغييره.

ماذا أفعل عندما يبلغ نظام إعادة البناء عن حالة BAD؟

اقرأ التقرير قبل القيام بأي شيء آخر. تعني نتيجة BAD أن عملية إعادة بناء مستقلة واحدة لم تنتج نفس البايتات، والسبب المعتاد هو عدم الحتمية (nondeterminism) في التغليف وليس هجوماً. يمكن لـ rebuilderd إنشاء تقرير diffoscope لهذا السبب تحديداً. إذا كانت الاختلافات تتعلق بالطوابع الزمنية، أو مسارات البناء، أو ترتيب الملفات، فهذا خطأ في التغليف يستحق الإبلاغ عنه. إذا كان الاختلاف في كود قابل للتنفيذ دون أي تفسير من هذا النوع، توقف عن نشر ذلك البناء، واحتفظ بالملفات الناتجة، وصعّد الأمر إلى الناشر.

#reproducible-builds#supply-chain#diffoscope#debian#verification