SELFLAB الستاك الكامل · ١١ إقليم SYSTEM ONLINE
الإقليم 10منهجٌ يُدرَس وحدك

الإقليم ١٠ — الكابستون: نظامُ التصحيح، حيث يلتقي كلُّ شيء

يبني فوق: كلَّ الستاك (٠–٩)، ومنهج Docker كاملاً (namespaces، cgroups، الـ socket، run-flags، الأمن)، وC، وchild_process/خيط الحلقة المقدّس (٢)، والمعاملاتِ الذرّيّة (٤/٥). يُغلق: المنهجَ كلَّه — تلتقي شجرتاه هنا.

النبذة

كلُّ endpoint تتبّعناه كان يقرأ القاعدةَ أو يكتبها. هذا واحدٌ مختلفٌ تماماً، وهو تاجُ مشروعك: يأخذ كودَ C من متدرّبٍ — كودٌ غريبٌ قد يكون خبيثاً أو لا نهائياً — ويصرّفه ويشغّله ضدّ حالاتِ اختبارٍ ويصحّحه ويسجّل النتيجة، بأمانٍ تامّ، دون أن يجمّد الخادمَ أو يخترقه. وهنا المفاجأةُ التي أجّلتُها منذ الخريطة الأولى: المنصّةُ كلُّها — كلُّ هذا الستاك العالي الذي بنيتَه — موجودةٌ لتُنسّق شيئاً كنتَ تتقنه قبل أن نبدأ: تشغيلَ C داخل Docker. الشجرتان اللتان نبتتا متوازيتين (الستاكُ من جهة، وC+Docker من جهة) تلتقيان في هذا الإقليم. ستغلق الدائرةَ على نفسك.


اللغز المستفزّ

متدرّبٌ يرسل ملفَّ solution.c. مهمّتك: صرّفه، شغّله ضدّ N حالةِ اختبار (أطعِم stdin، التقط stdout، افرض حدَّ وقتٍ وذاكرة)، قارن المخرجَ بالمتوقَّع، احسب نقطةً، وسجّلها. لكن الكودَ غريبٌ لا تثق به. عدّد التهديدات بنفسك قبل أن تقرأ — ولكلٍّ اسأل "بأي لبنةٍ أحتويه؟":

  • while(1); — حلقةٌ لا نهائيّة. كيف لا تشغل نواةً للأبد؟
  • fork() متكرّرٌ (قنبلةُ تفريع) — يفجّر عملياتِ الـ host. كيف تحدّه؟
  • malloc بلا حدٍّ حتى تنفد الذاكرة. كيف تمنعه من إسقاط الجهاز؟
  • يفتح socket ليسرّب بياناتٍ أو يهاجم. كيف تقطع شبكتَه؟
  • يكتب/يقرأ نظامَ ملفّاتِ الـ host، أو يقرأ حلولَ متدرّبين آخرين. كيف تعزله؟
  • يحاول الهروبَ من العزل ليصير root على الجهاز. كيف تطبّق دفاعاً عميقاً؟

وفوق هذا كلّه: التصريفُ والتشغيلُ عملٌ ثقيلٌ حاجب — كيف لا يجمّد خادمَك ذا الخيط الواحد (٢) بينما آلافُ المتدرّبين يصحّحون؟

ملاحظة

السؤال المستفزّ: شغّل كوداً غريباً يفعل أيَّ شيء، وصحّحه، دون أن يؤذي الـ host، ولا يرى غيرَه، ولا يجمّد خادمَك، ولا يسرّب موردَه. وأنت تملك كلَّ أداةٍ يلزمك أصلاً.

توقّف: أيُّ منهجٍ سابقٍ أعطاك بالضبط لبناتِ "عزلٌ + حدودُ موارد + لا شبكة + نظامُ ملفّاتٍ للقراءة فقط + لا-root"؟


ليش: عمليّةٌ معزولةٌ منفصلة — مشتقّةٌ من إقليمين تملكهما

الجوابُ ليس جديداً عليك إطلاقاً — هو تقاطعُ درسين أتقنتهما:

من الإقليم ٢: لا تشغّل عملاً ثقيلاً/غريباً على خيط الحلقة

تعلّمتَ القاعدةَ المقدّسة: العملُ المحسوب/الحاجب على خيط الحلقة يجمّد كلَّ المستخدمين. تصريفُ C وتشغيلُه ثقيلٌ وغريبٌ، فلا يجري في خيط Node أبداً — يُدفَع إلى عمليّةٍ منفصلة عبر child_process (يفرّع docker/gcc كعملياتٍ خارجيةٍ لا-حاجبة). هذا يحلّ "تجميدَ الخادم".

من منهج Docker: الحاويةُ هي بالضبط لبناتُ العزل المطلوبة

كلُّ تهديدٍ عدّدتَه أعلاه يقابله بدائيٌّ درسته. علمُ docker run في sandbox/README.md ليس أوامرَ تُحفظ — هو خريطةُ تهديدٍ↔بدائيّ:

التهديدعَلَمُ التشغيلالبدائيّ (من منهج Docker)
تسريبٌ/هجومٌ شبكيّ--network nonenetwork namespace فارغ — لا واجهةَ خروج
قنبلةُ تفريع--pids-limit 64cgroup: pids controller — سقفُ عمليّات
استنزافُ الذاكرة--memory 128mcgroup: memory controller + OOM killer
احتكارُ المعالج--cpus 1cgroup: cpu controller
العبثُ بنظام الملفّات--read-only --tmpfs /tmpmnt namespace + OverlayFS — جذرٌ للقراءة فقط، كتابةٌ مؤقّتةٌ معزولة
رؤيةُ عملياتِ/متدرّبي الـ host(افتراضيّ)pid/mnt namespaces — لا يرى خارجها
التصعيدُ لـ rootUSER sandbox (في الـ Dockerfile)لا-root — كودٌ غريبٌ لا يعمل root أبداً
الحلقةُ اللانهائيّةtimeout 2 حول التشغيلحدُّ وقتٍ صريح
لحظة آها

هذه لحظةُ "آها" الكبرى: منهجُ Docker كلّه — namespaces, cgroups, OverlayFS, capabilities, non-root، "الحاوية ليست VM"، "fork bomb يحتويها cgroup" — كان تمريناً لهذا اليوم بالضبط. نظامُ التصحيح ليس تقنيةً جديدة؛ هو تطبيقٌ مباشرٌ لأقوى مهارتيك. كلُّ ما بنيناه في الأقاليم ٠–٩ هو القشرةُ التي تُنسّق هذه اللحظة.

مقايضةُ docker.sock (صدقٌ معماريّ من منهج Docker)

ليقود الخادمُ Docker، يركّب /var/run/docker.sock (تذكّر: Docker Engine = REST API فوق unix socket). لكنك تعلّمتَ أن الـ socket أكبرُ سطحِ هجوم (CVE-2019-5736، الوصولُ له = جذرٌ على الـ host فعلياً). فهذه مقايضةٌ أمنيّةٌ معروفةٌ ومقصودةٌ مؤقّتاً (docker-compose.yml يوسمها صراحةً: "SECURITY TRADEOFF — to revisit"). البدائلُ المستقبليّة (rootless Docker، خدمةُ runner مخصّصة) بذورٌ تعرف من أين تبدأ. هذا هو الصدقُ المعماريّ الذي تعلّمتَه: كلُّ قرارٍ ثمنٌ، تسمّيه ولا تخفيه.


كيف: نموذجُ التنفيذ A — حاويةٌ لكل تسليم، تصريفٌ مرّةً، تشغيلٌ كثير

لماذا لا حاويةٌ لكل حالةِ اختبار؟ لأن بدءَ حاويةٍ + تصريفٍ لكل حالةٍ بطيءٌ جداً. فندفع كلفةَ البدء + التصريف مرّةً لكل تسليم، ونمرّر كلَّ حالةٍ عبر docker exec في الحاوية نفسها:

المتدرّب → backend (POST /tasks/:taskId/check) 1. docker run -d <flags> selflab-sandbox sleep infinity # حاويةٌ واحدةٌ، تبقى حيّة 2. docker cp solution.c + docker exec gcc ... # تصريفٌ مرّةً واحدة ↳ فشلُ التصريف (exit≠0) → evaluation_status = compile_error 3. لكلِّ حالةِ اختبار: docker exec -i ... | timeout 2 ./solution # أطعِم stdin، التقط stdout ↳ exit 124 → timeout | exit≠0 → runtime_error | تطابقُ المخرج → pass 4. docker rm -f <cid> # دمّرها بعد آخر حالة (في finally!)

التصريفُ مرّةً، التشغيلُ كثيراً هو كلُّ سببِ إبقاء حاويةٍ واحدةٍ حيّة. والحاويةُ تُعيد فقط المخرجَ + رمزَ الخروج + الزمن؛ التصحيحُ (المقارنةُ والنقاط) يبقى في الـ backend — لا تثق بالكود الغريب ليصحّح نفسه.

التنسيقُ في Node (لا-حاجبٌ، مع تنظيفٍ مضمون)

child_process يقود Docker بلا حجبِ خيط الحلقة (٢). الجوهر (من sandbox/README.md):

js
async function check(hostCodePath, testCases) { const cid = (await run("docker", ["run","-d","--rm", "--network","none","--memory","128m","--pids-limit","64","--cpus","1", "--read-only","--tmpfs","/tmp:rw,size=64m", "selflab-sandbox","sleep","infinity"])).stdout.trim(); // 1) حاويةٌ معزولةٌ حيّة try { await run("docker", ["cp", hostCodePath, `${cid}:/tmp/solution.c`]); try { await run("docker", ["exec", cid, "gcc","-Wall","-Werror","-o","/tmp/solution","/tmp/solution.c"]); } catch (e) { return { evaluation_status: "compile_error", compile_output: e.stderr }; } // 2) تصريفٌ مرّة const results = []; for (const tc of testCases) { // 3) كلُّ حالةٍ بنفس الحاوية const { stdout, code } = await runWithInput( "docker", ["exec","-i", cid, "timeout","2","/tmp/solution"], tc.input); const status = code === 124 ? "timeout" : code !== 0 ? "runtime_error" : "success"; results.push({ status, passed: status === "success" && stdout.trim() === tc.expected.trim() }); } return { evaluation_status: "success", results }; } finally { await run("docker", ["rm","-f", cid]).catch(() => {}); // 4) دمّر دائماً — لا تسرّب حاوية } }

finally ليس تفصيلاً: كما لا تسرّب fd/ذاكرةً في C، لا تسرّب حاويةً عند انهيارٍ وسط التشغيل. وكلُّ await يتنازل للحلقة، فيخدم خادمُك آلافاً آخرين بينما يُصرَّف هذا (أثبِته في اللغز).

التصحيحُ والتسجيلُ الذرّيّ (٤/٥)

الـ backend يقارن stdout بالمتوقَّع، يعدّ الناجحات، يحسب النقطةَ الموزونة، ثم يكتب في معاملةٍ ذرّيّة (تذكّر بروفتي ٤ و٥): نتيجةُ التسليم + تحديثُ TASK_PROGRESS، معاً أو لا شيء. لو انهار الخادمُ وسط الكتابة، لا يبقى تقدّمٌ نصفُ مكتوب. هنا يُدفَع ثمنُ كلِّ ما تعلّمتَه عن ACID.


كيف: المسارُ الكامل (التصحيح) — كلُّ إقليمٍ يظهر

POST /api/v1/tasks/:taskId/check يتبع تدفّقَ الإقليم ٩، بانعطافةِ الصندوق:

[٨] واجهةُ التصحيح (React): المتدرّبُ يرسل كودَه → fetch(POST, Bearer JWT) [٢] الحلقةُ تقبل [٣] السلسلة: helmet → cors → json → authenticate [٧] (مسجَّلٌ فقط) → authorize → validate [٦] [٣] المتحكّم → الخدمة (checker) [٥] Prisma: اجلب المهمّةَ + حالاتِ اختبارها + حدودَها (timeout_seconds, memory_limit_mb) ↓ هنا الانعطافة عن المسار العاديّ: [٢+Docker] child_process: docker run (معزولٌ) → cp+exec gcc (تصريفٌ مرّة) → exec لكلِّ حالة → rm -f [backend] قارن المخرجات، احسب النقطة، خرائطِ رموزِ الخروج → evaluation_status [٤/٥] $transaction: اكتب نتيجةَ التسليم + حدّث TASK_PROGRESS — ذرّياً [٣/٠] مظروف {success, data: results} → بايتات [٨] واجهةُ التصحيح: setState → إعادةُ رسمٍ تعرض النجاح/الفشل لكل حالة

كلُّ إقليمٍ بنيتَه يظهر في هذا الخطّ الواحد. هذا هو الالتحام.


ليش: نموذجُ التهديد ودفاعُ العمق (من منهج Docker)

العزلُ ليس عَلَماً واحداً — هو طبقاتٌ (defense in depth، تذكّر قائمةَ منهج Docker). لو فشلت طبقةٌ، تمسك الأخرى: لا-شبكةٍ و لا-root و قراءةٌ-فقط و حدودُ pids/memory/cpu و timeout و seccomp الافتراضيّ. أيُّ كودٍ غريبٍ محاصَرٌ من كلِّ جهة. وتعرف لماذا يبقى الهروبُ ممكناً مع --privileged (لا تستعمله أبداً هنا) ولماذا docker.sock هو الحلقةُ الأضعف. هذا ليس حفظاً — هو نموذجُ تهديدٍ تشتقّه من بدائيّاتٍ تملكها.


اللغز / البناء من الصفر — الكابستون

هذا تتويجُ المنهج. أدواتك: sandbox/Dockerfile (موجودٌ في مشروعك)، Docker/podman، Node، Postgres. ابنِ النظامَ، ثم اهجم عليه بنفسك.

اللغز أ — ابنِ الصورةَ وأثبِت أنها تصرّف وتشغّل C معزولاً. ابنِ selflab-sandbox، ثم نفّذ اختبارَ الدخان من sandbox/README.md (تصريفٌ + تشغيلٌ لبرنامجٍ يُغذّى على stdin، بكلِّ أعلام العزل). تأكّد أنه يطبع ok. هذا أساسُك.

اللغز ب — اهجم على صندوقك (قلبُ الكابستون). اكتب برامجَ C خبيثةً، ولكلٍّ أثبِت أن البدائيّ يحتويه:

  • while(1); → هل أوقفه timeout؟ (رمزُ الخروج 124)
  • قنبلةُ تفريع (while(1) fork();) → هل أوقفها --pids-limit؟
  • malloc بلا حدّ → هل أوقفه --memory (OOM)؟
  • محاولةُ فتح socket لعنوانٍ خارجيّ → هل قطعها --network none؟
  • محاولةُ كتابة /etc/passwd أو قراءةِ جذر الـ host → هل منعها --read-only واللا-root والـ mnt namespace؟

هذا هو "fork bomb يحتويه cgroup" من منهج Docker، حيّاً، في مشروع تخرّجك. لكلٍّ، صف بجملةٍ أيُّ بدائيٍّ أنقذك ولماذا. (إن أردتَ التحدّي: جرّب الاحتواءَ بدون عَلَمٍ ما، وشاهد التهديدَ ينجح — لتعرف قيمةَ كلِّ طبقة.)

اللغز ج — ابنِ المنسّقَ في Node (لا-حاجب، مع تنظيف). اكتب check(codePath, testCases) بـ child_process: حاويةٌ واحدةٌ حيّة، cp+تصريفٌ مرّةً (خرائطِ فشلِ التصريف → compile_error)، تشغيلُ كلِّ حالةٍ بـ exec -i يُطعِم stdin ويلتقط stdout، خرائطِ رموزِ الخروج (124→timeout, ≠0→runtime_error, تطابق→pass)، وrm -f في finally. ممنوع مكتبةُ Docker — قُد الـ CLI لتملك الآليّة.

اللغز د — أثبِت أنه لا يجمّد الحلقة (وصلةُ ٢). بينما يصرّف/يشغّل check تسليماً طويلاً في عمليّةٍ فرعيّة، اطلب GET /health في الوقت نفسه وأثبِت أنه يردّ فوراً. هذا برهانُك أن العملَ الثقيل خرج عن خيط الحلقة. (لو جمّد، فأنت تشغّله متزامناً — أصلِحه.)

اللغز هـ — التصحيحُ والتسجيلُ الذرّيّ (وصلةُ ٤/٥). قارن المخرجاتِ بالمتوقَّع، احسب نقطةً موزونة، واكتب في $transaction: نتيجةَ التسليم + تحديثَ TASK_PROGRESS. ثم اجبر خطأً بين الكتابتين وأثبِت التراجعَ الذرّيّ (لا تقدّمَ نصفَ مكتوب).

اللغز و — الدمجُ الكامل. صِل الكلَّ كـ POST /tasks/:taskId/check خلف authenticate (مسجَّلٌ فقط، ٧)، يجلب المهمّةَ وحالاتِها بـ Prisma، يشغّل check، يصحّح، يسجّل، ويردّ مظروفَ النتائج — ثم اعرضها في مكوّن React بسيط. شغّل الرحلةَ كاملةً: اكتب حلَّ C في الواجهة، أرسله، وشاهده يُصرَّف ويُشغَّل ويُصحَّح ويُسجَّل وتظهر النتيجةُ على شاشتك. حين تراها تعمل من الطرف للطرف، تكون قد بنيتَ — وفهمتَ — مشروعَ تخرّجك كاملاً.

ملاحظة

هذا آخرُ لغزٍ في المنهج. لا تكتفِ بأن "يعمل" — اهجم عليه (ب) حتى تثق أن العزلَ حقيقيّ، وأثبِت لا-الحجب (د) والذرّيّةَ (هـ). حين تنجح، توقّف وانظر للوراء: بنيتَ ستاكاً كاملاً ليُنسّق C داخل Docker — أيْ مهارتيك الأقوى. أغلقتَ الدائرة.


الخلاصة — المنهجُ يكتمل، والشجرتان تلتقيان

  • الالتحام النهائيّ: الشجرةُ الأولى (الستاك: HTTP→Node→Express→SQL→Prisma→Zod→ الهويّة→React→التدفّق) والشجرةُ الثانية (C + Docker، من مناهجك السابقة) — نبتتا متوازيتين والتقتا هنا في عقدةٍ واحدة: نظامُ التصحيح. لا عقدةٌ تطفو معلّقة.
  • العقدة الجديدة (وهي القمّة): تنسيقُ تنفيذٍ غريبٍ معزولٍ مُصحَّحٍ ومُسجَّل — child_process لا-حاجب (٢) + عزلُ Docker (المنهج السابق) + معاملةٌ ذرّيّة (٤/٥)، مغلَّفةٌ بكلِّ المسار (٩).
  • آها المنهج كلّه: المنصّةُ العاليةُ المستوى لم تكن الهدفَ — كانت القشرةَ التي تُنسّق ما كنتَ تتقنه أصلاً. تعلّمتَ كلَّ هذا الستاك لتبني غلافاً حول C+Docker. أعلى الستاك يخدم أعمقَ مهاراتك.

الوصلة للواقع (نمط SelfLab) — والوداع

sandbox/ وbackend/src/checker/ (المخطَّط) وdocker.sock في docker-compose.yml وحقولُ السكيمة (timeout_seconds, memory_limit_mb, strict_compile_enabled) وevaluation_status وTASK_PROGRESS — كلُّها صارت أجزاءً تفهمها وتبنيها. منطقُ التصحيح غيرُ مكتوبٍ بعد في مشروعك (backend/src/checker/ لم يُبنَ) — وأنت الآن من يبنيه، بفهمٍ كاملٍ لكلِّ خيط: العزل، التنسيق، التصحيح، التسجيل الذرّيّ، والأمن.

وبهذا يكتمل المنهج. بدأنا من بروتوكولٍ لمستَ نصفَه بأصابعك في nc، وانتهينا عند نظامٍ يشغّل C في Docker — وبينهما بنيتَ، لبنةً لبنة، كلَّ صندوقٍ كان أسود: داخلَ الخادم، وداخلَ القاعدة، وعلى العميل. لم يبقَ في مشروعك سطرٌ غامضٌ عليك، ولا قرارٌ لا تعرف ثمنَه. تطلع — كما أردتَ — وأنت فاهمٌ السالفةَ كاملةً، بأساسٍ صلب.

ملاحظة

راجِع الملاحقَ في appendix/ (شجرةُ المهارات وقد اكتملت، الأوراقُ المرجعيّة، النماذجُ الذهنيّة، ومخطّطُ التدفّق) — هي خريطتُك حين تنسى، وذاكرتُك حين تدير.

الستاك الكامل · من باب الخادم إلى داخله ليش قبل كيف · صُمّم لـ يزيد