seccomp_unotify(2)

الاسم
seccomp_unotify - آلية إشعار seccomp في فضاء المستخدم

المكتبة
مكتبة سي المعيارية (libc، -lc)

موجز
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>

int seccomp(unsigned int operation, unsigned int flags, void *args);

#include <sys/ioctl.h>

int ioctl(int fd, SECCOMP_IOCTL_NOTIF_RECV,
          struct seccomp_notif *req);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_SEND,
          struct seccomp_notif_resp *resp);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_ID_VALID, __u64 *id);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_ADDFD,
          struct seccomp_notif_addfd *addfd);

الوصف
تصف هذه الصفحة آلية إشعار فضاء المستخدم التي توفرها وسيلة الحوسبة الآمنة (seccomp). بالإضافة إلى استخدام علامة SECCOMP_FILTER_FLAG_NEW_LISTENER، وقيمة الإجراء SECCOMP_RET_USER_NOTIF، وعملية SECCOMP_GET_NOTIF_SIZES الموصوفة في seccomp(2)، تتضمن هذه الآلية استخدام عدد من عمليات ioctl(2) ذات الصلة (الموصوفة أدناه).

  نظرة عامة
في الاستخدام التقليدي لمرشح seccomp، يُتخذ القرار بشأن كيفية التعامل مع استدعاء النظام بواسطة المرشح نفسه. على النقيض من ذلك، تسمح آلية إشعار فضاء المستخدم لمرشح seccomp بتفويض معالجة استدعاء النظام إلى عملية أخرى في فضاء المستخدم. لاحظ أن هذه الآلية ليست مخصصة صراحةً كطريقة لتنفيذ سياسة أمنية؛ انظر الملاحظات (NOTES).

في المناقشة التي تلي، يُشار إلى الخيط (أو الخيوط) التي ثُبت عليها مرشح seccomp باسم الهدف (target)، ويُشار إلى العملية التي تُخطر بواسطة آلية إشعار فضاء المستخدم باسم المشرف (supervisor).

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

نظرة عامة على الخطوات التي يقوم بها كل من الهدف والمشرف هي كما يلي:

(1) ينشئ الهدف مرشح seccomp بالطريقة المعتادة، ولكن مع اختلافين:

    •   تتضمن وسيطة flags في seccomp(2) العلامة SECCOMP_FILTER_FLAG_NEW_LISTENER. وبناءً على ذلك، تكون القيمة المعادة من استدعاء seccomp(2) (الناجح) هي واصف ملف "استماع" جديد يمكن استخدامه لتلقي الإشعارات. يمكن تثبيت مرشح seccomp "مستمع" واحد فقط لكل خيط.

    •   في الحالات المناسبة، يعيد مرشح seccomp قيمة الإجراء SECCOMP_RET_USER_NOTIF. ستؤدي هذه القيمة المعادة إلى إطلاق حدث إشعار.

(2) لكي يتمكن المشرف من الحصول على الإشعارات باستخدام واصف ملف الاستماع، يجب تمرير (نسخة من) واصف الملف هذا من الهدف إلى المشرف. إحدى الطرق التي يمكن من خلالها القيام بذلك هي تمرير واصف الملف عبر اتصال مقبس نطاق يونكس بين الهدف والمشرف (باستخدام نوع الرسالة الملحقة SCM_RIGHTS الموصوفة في unix(7)). طريقة أخرى للقيام بذلك هي من خلال استخدام pidfd_getfd(2).

(3) سيتلقى المشرف أحداث الإشعار على واصف ملف الاستماع. تُعاد هذه الأحداث في هيئة هياكل من نوع seccomp_notif. ولأن هذا الهيكل وحجمه قد يتطوران عبر إصدارات النواة، يجب على المشرف أولاً تحديد حجم هذا الهيكل باستخدام عملية SECCOMP_GET_NOTIF_SIZES في seccomp(2)، والتي تعيد هيكلًا من نوع seccomp_notif_sizes. يخصص المشرف مخزنًا مؤقتًا بحجم seccomp_notif_sizes.seccomp_notif بايت لتلقي أحداث الإشعارات. بالإضافة إلى ذلك، يخصص المشرف مخزنًا مؤقتًا آخر بحجم seccomp_notif_sizes.seccomp_notif_resp بايت للاستجابة (هيكل struct seccomp_notif_resp) التي سيقدمها للنواة (وبالتالي للهدف).

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

(5) يمكن للمشرف الآن مراقبة واصف ملف الاستماع بشكل متكرر للأحداث التي تطلقها SECCOMP_RET_USER_NOTIF. للقيام بذلك، يستخدم المشرف عملية SECCOMP_IOCTL_NOTIF_RECV في ioctl(2) لقراءة معلومات حول حدث إشعار؛ تُحجب هذه العملية حتى يتوفر حدث. تعيد العملية هيكل seccomp_notif يحتوي على معلومات حول استدعاء النظام الذي يحاول الهدف تنفيذه. (كما هو موضح في الملاحظات (NOTES)، يمكن أيضًا مراقبة واصف الملف باستخدام select(2) أو poll(2) أو epoll(7).)

(6) يتضمن هيكل seccomp_notif الذي تعيده عملية SECCOMP_IOCTL_NOTIF_RECV نفس المعلومات (هيكل seccomp_data) التي مُررت إلى مرشح seccomp. تسمح هذه المعلومات للمشرف باكتشاف رقم استدعاء النظام ووسائط استدعاء نظام الهدف. بالإضافة إلى ذلك، يحتوي حدث الإشعار على معرف الخيط الذي أطلق الإشعار وقيمة "كعكة" (cookie) فريدة تُستخدم في عمليتي SECCOMP_IOCTL_NOTIF_ID_VALID و SECCOMP_IOCTL_NOTIF_SEND اللاحقتين.

    يمكن استخدام المعلومات الواردة في الإشعار لاكتشاف قيم وسائط المؤشر لاستدعاء نظام الهدف. (هذا شيء لا يمكن القيام به من داخل مرشح seccomp.) إحدى الطرق التي يمكن للمشرف من خلالها القيام بذلك هي فتح ملف /proc/tid/mem المقابل (انظر proc(5)) وقراءة البايتات من الموقع الذي يقابل أحد وسائط المؤشر التي زُودت قيمتها في حدث الإشعار. (يجب على المشرف توخي الحذر لتجنب حالة التسابق التي يمكن أن تحدث عند القيام بذلك؛ انظر وصف عملية SECCOMP_IOCTL_NOTIF_ID_VALID في ioctl(2) أدناه.) بالإضافة إلى ذلك، يمكن للمشرف الوصول إلى معلومات النظام الأخرى التي تظهر في فضاء المستخدم ولكن لا يمكن الوصول إليها من مرشح seccomp.

(7) بعد الحصول على المعلومات كما في الخطوة السابقة، قد يختار المشرف بعد ذلك تنفيذ إجراء استجابةً لاستدعاء نظام الهدف (والذي، كما لوحظ أعلاه، لا يُنفذ عندما يعيد مرشح seccomp قيمة الإجراء SECCOMP_RET_USER_NOTIF).

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

(8) يرسل المشرف بعد ذلك استجابة للإشعار. تستخدم النواة المعلومات الواردة في هذه الاستجابة لبناء قيمة معادة لاستدعاء نظام الهدف وتوفير قيمة ستُسند إلى متغير errno الخاص بالهدف.

    تُرسل الاستجابة باستخدام عملية SECCOMP_IOCTL_NOTIF_SEND في ioctl(2)، والتي تُستخدم لنقل هيكل seccomp_notif_resp إلى النواة. يتضمن هذا الهيكل قيمة الكعكة التي حصل عليها المشرف في هيكل seccomp_notif الذي أعادته عملية SECCOMP_IOCTL_NOTIF_RECV. تسمح قيمة الكعكة هذه للنواة بربط الاستجابة بالهدف. يجب أن يتضمن هذا الهيكل قيمة الكعكة التي حصل عليها المشرف في هيكل seccomp_notif الذي أعادته عملية SECCOMP_IOCTL_NOTIF_RECV؛ تسمح الكعكة للنواة بربط الاستجابة بالهدف.

(9) بمجرد إرسال الإشعار، يُلغى حجب استدعاء النظام في خيط الهدف، ويعيد المعلومات التي قدمها المشرف في استجابة الإشعار.

كبديل للخطوتين الأخيرتين، يمكن للمشرف إرسال استجابة تخبر النواة بوجوب تنفيذ استدعاء نظام خيط الهدف؛ انظر مناقشة SECCOMP_USER_NOTIF_FLAG_CONTINUE أدناه.

عمليات IOCTL
عمليات ioctl(2) التالية مدعومة من قبل واصف ملف إشعار فضاء مستخدم seccomp. لكل من هذه العمليات، تكون الوسيطة الأولى (واصف الملف) لـ ioctl(2) هي واصف ملف الاستماع الذي أعاده استدعاء seccomp(2) مع علامة SECCOMP_FILTER_FLAG_NEW_LISTENER.

  SECCOMP_IOCTL_NOTIF_RECV
تُستخدم عملية SECCOMP_IOCTL_NOTIF_RECV (متوفرة منذ لينكس 5.0) للحصول على حدث إشعار في فضاء المستخدم. إذا لم يكن هناك مثل هذا الحدث معلقًا حاليًا، تُحجب العملية حتى يقع حدث. الوسيطة الثالثة لـ ioctl(2) هي مؤشر لهيكل بالشكل التالي يحتوي على معلومات حول الحدث. يجب تصفير هذا الهيكل قبل الاستدعاء.

    struct seccomp_notif {
        __u64  id;              /* كعكة */
        __u32  pid;             /* TID لخيط الهدف */
        __u32  flags;           /* غير مستخدم حاليا (0) */
        struct seccomp_data data;   /* انظر seccomp(2) */
    };

الحقول في هذا الهيكل هي كما يلي:

id  هذه هي كعكة للإشعار. يُضمن أن تكون كل كعكة من هذا القبيل فريدة لمرشح seccomp المقابل.

    •   يمكن استخدام الكعكة مع عملية SECCOMP_IOCTL_NOTIF_ID_VALID في ioctl(2) الموصوفة أدناه.

    •   عند إرجاع استجابة إشعار إلى النواة، يجب على المشرف تضمين قيمة الكعكة في هيكل seccomp_notif_resp المحدد كوسيطة لعملية SECCOMP_IOCTL_NOTIF_SEND.

pid هذا هو معرف الخيط لخيط الهدف الذي أطلق حدث الإشعار.

flags
    هذا قناع بتات للعلامات التي توفر معلومات إضافية حول الحدث. في التنفيذ الحالي، هذا الحقل دائمًا صفر.

data
    هذا هيكل seccomp_data يحتوي على معلومات حول استدعاء النظام الذي أطلق الإشعار. هذا هو نفس الهيكل الذي يُمرر إلى مرشح seccomp. انظر seccomp(2) للحصول على تفاصيل حول هذا الهيكل.

عند النجاح، تعيد هذه العملية 0؛ وعند الفشل، تُعاد -1، ويُضبط errno للإشارة إلى الخطأ. يمكن أن تفشل هذه العملية بالأخطاء التالية:

EINVAL (منذ لينكس 5.5)
    هيكل seccomp_notif الذي مُرر إلى الاستدعاء احتوى على حقول غير صفرية.

ENOENT
    قُتل خيط الهدف بواسطة إشارة أثناء إنشاء معلومات الإشعار، أو قوطع استدعاء نظام الهدف (المحجوب) بواسطة معالج إشارة.

  SECCOMP_IOCTL_NOTIF_ID_VALID
تُستخدم عملية SECCOMP_IOCTL_NOTIF_ID_VALID (متوفرة منذ لينكس 5.0) للتحقق من أن معرف الإشعار الذي أعادته عملية SECCOMP_IOCTL_NOTIF_RECV سابقة لا يزال صالحًا (أي أن الهدف لا يزال موجودًا واستدعاء نظامه لا يزال محجوبًا في انتظار استجابة).

الوسيطة الثالثة لـ ioctl(2) هي مؤشر للكعكة (id) التي أعادتها عملية SECCOMP_IOCTL_NOTIF_RECV.

هذه العملية ضرورية لتجنب حالات التسابق التي يمكن أن تحدث عندما ينتهي الـ pid الذي أعادته عملية SECCOMP_IOCTL_NOTIF_RECV، ويُعاد استخدام معرف العملية هذا بواسطة عملية أخرى. مثال على هذا النوع من التسابق هو ما يلي

(1) يُنشأ إشعار على واصف ملف الاستماع. يحتوي هيكل seccomp_notif المُرجع على معرف الخيط (TID) للخيط الهدف (في حقل pid الخاص بالهيكل).

(2) ينتهي الهدف.

(3) يُنشأ خيط أو عملية أخرى على النظام تُعيد استخدام TID الذي حُرّر عند انتهاء الهدف عن طريق المصادفة.

(4) يفتح المشرف عبر open(2) ملف /proc/tid/mem لـ TID الذي حُصل عليه في الخطوة 1، بقصد فحص مواقع الذاكرة التي تحتوي على وسيط (أو وسائط) استدعاء النظام الذي أطلق الإشعار في الخطوة 1.

في السيناريو أعلاه، يكمن الخطر في أن المشرف قد يحاول الوصول إلى ذاكرة عملية أخرى غير الهدف. يمكن تجنب حالة السباق هذه بإتباع استدعاء open(2) بعملية SECCOMP_IOCTL_NOTIF_ID_VALID للتحقق من أن العملية التي أنشأت الإشعار لا تزال حية. (لاحظ أنه إذا انتهى الهدف بعد الخطوة الأخيرة، فإن أي read(2) لاحقة من واصف الملف قد تُرجع 0، مما يشير إلى نهاية الملف).

انظر الملاحظات لمناقشة الحالات الأخرى التي يجب فيها إجراء فحوصات SECCOMP_IOCTL_NOTIF_ID_VALID.

عند النجاح (أي أن معرف الإشعار لا يزال صالحاً)، تُرجع هذه العملية 0. وعند الفشل (أي أن معرف الإشعار لم يعد صالحاً)، يُرجع -1، ويُضبط errno على ENOENT.

  SECCOMP_IOCTL_NOTIF_SEND
تُستخدم عملية SECCOMP_IOCTL_NOTIF_SEND (المتوفرة منذ لينكس 5.0) لإرسال رد إشعار إلى النواة. وسيط ioctl(2) الثالث لهذا الهيكل هو مؤشر إلى هيكل بالشكل التالي:

    struct seccomp_notif_resp {
        __u64 id;           /* قيمة الكوكيز */
        __s64 val;          /* قيمة النجاح المرجعة */
        __s32 error;        /* 0 (نجاح) أو رقم خطأ سالب */
        __u32 flags;        /* انظر أدناه */
    };

حقول هذا الهيكل هي كالتالي:

id  هذه هي قيمة الكوكيز التي حُصل عليها باستخدام عملية SECCOMP_IOCTL_NOTIF_RECV. تسمح قيمة الكوكيز هذه للنواة بربط هذا الرد بشكل صحيح مع استدعاء النظام الذي أطلق إشعار مساحة المستخدم.

val هذه هي القيمة التي ستُستخدم لرد نجاح زائف لاستدعاء نظام الهدف؛ انظر أدناه.

error
    هذه هي القيمة التي ستُستخدم كرقم خطأ (errno) لرد خطأ زائف لاستدعاء نظام الهدف؛ انظر أدناه.

flags
    هذا قناع بتات يتضمن صفراً أو أكثر من اللصائق التالية:

    SECCOMP_USER_NOTIF_FLAG_CONTINUE (منذ لينكس 5.5)
        إخبار النواة بتنفيذ استدعاء نظام الهدف.

يوجد نوعان ممكنان من الردود:

•   رد للنواة يخبرها بتنفيذ استدعاء نظام الهدف. في هذه الحالة، يتضمن حقل flags العلامة SECCOMP_USER_NOTIF_FLAG_CONTINUE ويجب أن يكون الحقلان error و val صفرين.

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

    يجب استخدام وسم SECCOMP_USER_NOTIF_FLAG_CONTINUE بحذر؛ انظر الملاحظات.

•   قيمة مرجعة زائفة لاستدعاء نظام الهدف. في هذه الحالة، لا تنفذ النواة استدعاء نظام الهدف، وبدلاً من ذلك تجعل استدعاء النظام يُرجع قيمة زائفة كما هو محدد في حقول هيكل seccomp_notif_resp. يجب على المشرف ضبط حقول هذا الهيكل كما يلي:

    •   لا يحتوي flags على SECCOMP_USER_NOTIF_FLAG_CONTINUE.

    •   يُضبط error إما على 0 لرد "نجاح" زائف أو على رقم خطأ سالب لرد "فشل" زائف. في الحالة الأولى، تجعل النواة استدعاء نظام الهدف يُرجع القيمة المحددة في حقل val. وفي الحالة الثانية، تجعل النواة استدعاء نظام الهدف يُرجع -1، ويُسند إلى errno قيمة error المنفية.

    •   يُضبط val على قيمة ستُستخدم كقيمة مرجعة لرد "نجاح" زائف لاستدعاء نظام الهدف. تُتجاهل القيمة في هذا الحقل إذا احتوى حقل error على قيمة غير صفرية.

عند النجاح، تعيد هذه العملية 0؛ وعند الفشل، تُعاد -1، ويُضبط errno للإشارة إلى الخطأ. يمكن أن تفشل هذه العملية بالأخطاء التالية:

EINPROGRESS
    أُرسل رد على هذا الإشعار بالفعل.

EINVAL
    حُددت قيمة غير صالحة في حقل flags.

EINVAL
    احتوى حقل flags على SECCOMP_USER_NOTIF_FLAG_CONTINUE، ولم يكن حقل error أو val صفراً.

ENOENT
    قوطع استدعاء النظام المحجوب في الهدف بواسطة معالج إشارة أو أن الهدف قد انتهى.

  SECCOMP_IOCTL_NOTIF_ADDFD
تسمح عملية SECCOMP_IOCTL_NOTIF_ADDFD (المتوفرة منذ لينكس 5.9) للمشرف بتثبيت واصف ملف في جدول واصفات ملفات الهدف. تشبه هذه العملية إلى حد كبير استخدام رسائل SCM_RIGHTS الموصوفة في unix(7)، وهي مكافئة دلالياً لنسخ واصف ملف من جدول واصفات ملفات المشرف إلى جدول واصفات ملفات الهدف.

تسمح عملية SECCOMP_IOCTL_NOTIF_ADDFD للمشرف بمحاكاة استدعاء نظام للهدف (مثل socket(2) أو openat(2)) الذي يُولد واصف ملف. يمكن للمشرف تنفيذ استدعاء النظام الذي يُولد واصف الملف (ووصف الملف المفتوح المرتبط به) ثم استخدام هذه العملية لتخصيص واصف ملف يشير إلى نفس وصف الملف المفتوح في الهدف. (لشرح أوصاف الملفات المفتوحة، انظر open(2)).

بمجرد تنفيذ هذه العملية، يمكن للمشرف إغلاق نسخته من واصف الملف.

في الهدف، يخضع واصف الملف المستلم لنفس فحوصات وحدة أمن لينكس (LSM) التي تُطبق على واصف ملف مستلم في رسالة SCM_RIGHTS إضافية. إذا كان واصف الملف يشير إلى مقبس، فإنه يرث إعدادات متحكم الشبكة للمجموعة التقسيمية (cgroup) الإصدار 1 (classid و netprioidx) الخاصة بالهدف.

وسيط ioctl(2) الثالث هو مؤشر إلى هيكل بالشكل التالي:

    struct seccomp_notif_addfd {
        __u64 id;           /* قيمة الكوكيز */
        __u32 flags;        /* اللصائق */
        __u32 srcfd;        /* رقم واصف الملف المحلي */
        __u32 newfd;        /* 0 أو رقم واصف الملف المطلوب
                               في الهدف */
        __u32 newfd_flags;  /* اللصائق المراد ضبطها على واصف ملف
                               الهدف */
    };

الحقول في هذا الهيكل هي كما يلي:

id  يجب ضبط هذا الحقل على معرف الإشعار (قيمة الكوكيز) التي حُصل عليها عبر SECCOMP_IOCTL_NOTIF_RECV.

flags
    هذا الحقل عبارة عن قناع بتات من اللصائق التي تعدل سلوك العملية. حالياً، تُدعم لصيقة واحدة فقط:

    SECCOMP_ADDFD_FLAG_SETFD
        عند تخصيص واصف الملف في الهدف، استخدم رقم واصف الملف المحدد في حقل newfd.

    SECCOMP_ADDFD_FLAG_SEND (منذ لينكس 5.14)
        تنفيذ ما يعادل SECCOMP_IOCTL_NOTIF_ADDFD بالإضافة إلى SECCOMP_IOCTL_NOTIF_SEND كعملية ذرية. عند الاستدعاء الناجح، سيكون errno لعملية الهدف 0 وستكون القيمة المرجعة هي رقم واصف الملف الذي خُصص في الهدف. إذا فشل تخصيص واصف الملف في الهدف، يستمر حجب استدعاء نظام الهدف حتى يُرسل رد ناجح.

srcfd
    يجب ضبط هذا الحقل على رقم واصف الملف في المشرف المراد نسخه.

newfd
    يحدد هذا الحقل رقم واصف الملف الذي يُخصص في الهدف. إذا ضُبطت الوسم SECCOMP_ADDFD_FLAG_SETFD، فإن هذا الحقل يحدد رقم واصف الملف الذي يجب تخصيصه. إذا كان رقم واصف الملف هذا مفتوحاً بالفعل في الهدف، فسيُغلق ويُعاد استخدامه ذرياً. إذا فشل نسخ الواصف بسبب فحص LSM، أو إذا لم يكن srcfd واصف ملف صالحاً، فلن يُغلق واصف الملف newfd في عملية الهدف.

    إذا لم تُضبط الوسم SECCOMP_ADDFD_FLAG_SETFD، فيجب أن يكون هذا الحقل 0، وتقوم النواة بتخصيص أقل رقم واصف ملف غير مستخدم في الهدف.

newfd_flags
    هذا الحقل عبارة عن قناع بتات يحدد اللصائق التي يجب ضبطها على واصف الملف المستلم في عملية الهدف. حالياً، تُنفذ اللصيقة التالية فقط:

    O_CLOEXEC
        ضبط وسم الغلق عند التنفيذ على واصف الملف المستلم.

عند النجاح، يُرجع استدعاء ioctl(2) هذا رقم واصف الملف الذي خُصص في الهدف. بافتراض أن استدعاء النظام الذي تمت محاكاته هو استدعاء يُرجع واصف ملف كتيجة لدالته (مثل socket(2))، يمكن استخدام هذه القيمة كقيمة مرجعة (resp.val) التي تُوفر في الرد الذي يُرسل لاحقًا بعملية SECCOMP_IOCTL_NOTIF_SEND.

عند حدوث خطأ، يُرجع -1 ويُضبط errno للإشارة إلى الخطأ.

يمكن أن تفشل هذه العملية بالأخطاء التالية:

EBADF
    سيؤدي تخصيص واصف الملف في الهدف إلى تجاوز حد RLIMIT_NOFILE للهدف (انظر getrlimit(2)).

EBUSY
    إذا استُخدمت اللصيقة SECCOMP_IOCTL_NOTIF_SEND، فهذا يعني أن العملية لا يمكن أن تستمر حتى تُعالج طلبات SECCOMP_IOCTL_NOTIF_ADDFD الأخرى.

EINPROGRESS
    إشعار مساحة المستخدم المحدد في حقل id موجود ولكنه لم يُجلب بعد (بواسطة SECCOMP_IOCTL_NOTIF_RECV) أو تم الرد عليه بالفعل (بواسطة SECCOMP_IOCTL_NOTIF_SEND).

EINVAL
    حُددت لصيقة غير صالحة في حقل flags أو newfd_flags، أو أن حقل newfd غير صفري ولم تُحدد اللصيقة SECCOMP_ADDFD_FLAG_SETFD في حقل flags.

EMFILE
    يتجاوز رقم واصف الملف المحدد في newfd الحد المحدد في /proc/sys/fs/nr_open.

ENOENT
    قوطع استدعاء النظام المحجوب في الهدف بواسطة معالج إشارة أو أن الهدف قد انتهى.

إليك بعض التعليمات البرمجية النموذجية (مع حذف معالجة الأخطاء) التي تستخدم عملية SECCOMP_ADDFD_FLAG_SETFD (هنا، لمحاكاة استدعاء لـ openat(2)):

    int fd, removeFd;

    fd = openat(req->data.args[0], path, req->data.args[2],
                    req->data.args[3]);

    struct seccomp_notif_addfd addfd;
    addfd.id = req->id;  /* الكوكيز من SECCOMP_IOCTL_NOTIF_RECV */
    addfd.srcfd = fd;
    addfd.newfd = 0;
    addfd.flags = 0;
    addfd.newfd_flags = O_CLOEXEC;

    targetFd = ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd);

    close(fd);          /* لم تعد هناك حاجة إليه في المشرف */

    struct seccomp_notif_resp *resp;
        /* كود تخصيص 'resp' محذوف */
    resp->id = req->id;
    resp->error = 0;        /* "نجاح" */
    resp->val = targetFd;
    resp->flags = 0;
    ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_SEND, resp);

ملاحظات
أحد الأمثلة على حالة استخدام آلية إشعار مساحة المستخدم هو السماح لمدير الحاوية (عملية تعمل عادةً بامتيازات أكثر من العمليات داخل الحاوية) بوصل الأجهزة الكتلية أو إنشاء عقد الأجهزة للحاوية. توفر حالة استخدام الوصل مثالاً على فائدة عملية SECCOMP_USER_NOTIF_FLAG_CONTINUE ioctl(2). عند استلام إشعار لاستدعاء النظام mount(2)، يمكن لمدير الحاوية ("المشرف") التمييز بين طلب لوصل نظام ملفات كتلي (والذي لن يكون ممكناً لعملية "هدف" داخل الحاوية) ووصل نظام الملفات هذا. ومن ناحية أخرى، إذا اكتشف مدير الحاوية أن العملية يمكن تنفيذها بواسطة العملية داخل الحاوية (على سبيل المثال، وصل نظام ملفات tmpfs(5))، فيمكنه إخطار النواة بأن استدعاء نظام mount(2) لعملية الهدف يمكن أن يستمر.

  دلالات select()/poll()/epoll
يمكن مراقبة واصف الملف المعاد عندما يُوظف seccomp(2) مع العلم SECCOMP_FILTER_FLAG_NEW_LISTENER باستخدام poll(2) و epoll(7) و select(2). تشير هذه الواجهات إلى أن واصف الملف جاهز كما يلي:

•   عندما يكون الإشعار معلقاً، تشير هذه الواجهات إلى أن واصف الملف قابل للقراءة. وعقب هذا المؤشر، لن تحظر عملية SECCOMP_IOCTL_NOTIF_RECV ioctl(2) اللاحقة، حيث ستعيد إما معلومات حول الإشعار أو تفشل مع الخطأ EINTR إذا قُتل الهدف بواسطة إشارة أو قوطع استدعاء النظام الخاص به بواسطة معالج إشارة.

•   بعد تلقي الإشعار (أي بواسطة عملية SECCOMP_IOCTL_NOTIF_RECV ioctl(2))، تشير هذه الواجهات إلى أن واصف الملف قابل للكتابة، مما يعني أنه يمكن إرسال استجابة إشعار باستخدام عملية SECCOMP_IOCTL_NOTIF_SEND ioctl(2).

•   بعد إنهاء آخر خيط يستخدم المرشح وحصده باستخدام waitpid(2) (أو ما شابه)، يشير واصف الملف إلى حالة نهاية الملف (قابل للقراءة في select(2)؛ و POLLHUP/EPOLLHUP في poll(2)/ epoll_wait(2)).

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

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

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

لاحظ علاوة على ذلك أنه يمكن تجاوز مُخطِر مساحة المستخدم إذا سمحت المرشحات الحالية باستخدام seccomp(2) أو prctl(2) لتثبيت مرشح يعيد قيمة إجراء ذات أسبقية أعلى من SECCOMP_RET_USER_NOTIF (راجع seccomp(2)).

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

  تحذيرات بشأن استخدام
أشار النقاش أعلاه في /proc/tid/mem إلى الحاجة لاستخدام ioctl(2) SECCOMP_IOCTL_NOTIF_ID_VALID عند فتح ملف /proc/tid/mem الخاص بالهدف لتجنب إمكانية الوصول إلى ذاكرة العملية الخاطئة في حال انتهاء الهدف وإعادة تدوير معرفه بواسطة خيط آخر (غير ذي صلة). ومع ذلك، فإن استخدام عملية ioctl(2) هذه ضروري أيضًا في مواقف أخرى، كما هو موضح في الفقرات التالية.

تأمل السيناريو التالي، حيث يحاول المشرف قراءة وسيط مسار استدعاء نظام mount(2) المحظور للهدف:

(1) من إحدى وظائفه (func())، يستدعي الهدف mount(2)، مما يطلق إشعار مساحة مستخدم ويتسبب في حظر الهدف.

(2) يتلقى المشرف الإشعار، ويفتح /proc/tid/mem، ويجري فحص SECCOMP_IOCTL_NOTIF_ID_VALID (بنجاح).

(3) يتلقى الهدف إشارة، مما يتسبب في إجهاض mount(2).

(4) يُنفذ معالج الإشارة في الهدف، ثم يعود.

(5) عند العودة من المعالج، يُستأنف تنفيذ func())، وتعود (وربما تُستدعى وظائف أخرى، مما يعيد الكتابة فوق الذاكرة التي كانت تُستخدم لإطار مكدس func()).

(6) باستخدام العنوان الموفر في معلومات الإشعار، يقرأ المشرف من موقع ذاكرة الهدف الذي كان يحتوي على المسار.

(7) يستدعي المشرف الآن mount(2) مع بعض البايتات العشوائية التي حُصل عليها في الخطوة السابقة.

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

لمنع مثل هذه السيناريوهات، يجب فصل كل عملية قراءة من ذاكرة الهدف عن استخدام البايتات التي حُصل عليها بفحص SECCOMP_IOCTL_NOTIF_ID_VALID. وفي المثال أعلاه، سيوضع الفحص بين الخطوتين الأخيرتين. يظهر مثال على هذا الفحص في قسم "أمثلة".

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

  تحذيرات بشأن حظر استدعاءات النظام
افترض أن الهدف يجري استدعاء نظام حاصراً (مثل accept(2)) ينبغي أن يعالجه المشرف. قد ينفذ المشرف بدوره نفس استدعاء النظام الحاصر.

في هذا السيناريو، من المهم ملاحظة أنه إذا قوطع استدعاء نظام الهدف الآن بواسطة إشارة، فإن المشرف لا يُخطر بذلك. وإذا لم يتخذ المشرف خطوات مناسبة لاكتشاف إلغاء استدعاء نظام الهدف بنشاط، فقد تحدث صعوبات مختلفة. وبالأخذ بمثال accept(2)، قد يظل المشرف محظوراً في accept(2) الخاص به ممسكاً برقم منفذ قد يتوقع الهدف (الذي ربما أغلق مقبس الاستماع الخاص به بعد مقاطعته من معالج الإشارة) أن يتمكن من إعادة استخدامه في استدعاء bind(2).

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

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

  التفاعل مع معالجات إشارة SA_RESTART
تأمل السيناريو التالي:

(1) استخدمت العملية الهدف sigaction(2) لتثبيت معالج إشارة مع العلم SA_RESTART.

(2) أجرى الهدف استدعاء نظام أطلق إشعار مساحة مستخدم seccomp، والهدف محظور حالياً حتى يرسل المشرف استجابة إشعار.

(3) سُلّمت إشارة إلى الهدف ونُفذ معالج الإشارة.

(4) عندما (إذا) حاول المشرف إرسال استجابة إشعار، ستفشل عملية ioctl(2) SECCOMP_IOCTL_NOTIF_SEND مع الخطأ ENOENT.

في هذا السيناريو، ستعيد النواة تشغيل استدعاء نظام الهدف. وبناءً على ذلك، سيتلقى المشرف إشعاراً آخر في مساحة المستخدم. وهكذا، اعتماداً على عدد مرات مقاطعة استدعاء النظام المحظور بواسطة معالج إشارة، قد يتلقى المشرف إشعارات متعددة لنفس نسخة استدعاء النظام في الهدف.

من الأمور الغريبة أن إعادة تشغيل استدعاء النظام كما هو موضح في هذا السيناريو ستحدث حتى بالنسبة لاستدعاءات النظام الحاصرة المدرجة في signal(7) والتي لا يُعاد تشغيلها أبداً في العادة بواسطة العلم SA_RESTART.

علاوة على ذلك، إذا كانت استجابة المشرف هي واصف ملف أُضيف باستخدام SECCOMP_IOCTL_NOTIF_ADDFD، فيمكن حينئذٍ استخدام العلم SECCOMP_ADDFD_FLAG_SEND لإضافة واصف الملف وإعادة تلك القيمة ذرياً، مما يضمن عدم تسرب أي واصفات ملفات عن غير قصد إلى الهدف.

العلل
إذا أُجريت عملية SECCOMP_IOCTL_NOTIF_RECV ioctl(2) بعد انتهاء الهدف، فإن استدعاء ioctl(2) يحظر ببساطة (بدلاً من إعادة خطأ يشير إلى أن الهدف لم يعد موجوداً).

أمثلة
يوضح البرنامج الموضح أدناه (المصمم لغرض العرض) استخدام الواجهات الموصوفة في هذه الصفحة. ينشئ البرنامج عملية ابنة تعمل كعملية "هدف". تثبت العملية الابنة مرشح seccomp يعيد قيمة إجراء SECCOMP_RET_USER_NOTIF إذا أُجري استدعاء لـ mkdir(2). ثم تستدعي العملية الابنة mkdir(2) مرة واحدة لكل وسيط من وسائط سطر الأوامر الموفرة، وتبلغ عن النتيجة التي أعادها الاستدعاء. وبعد معالجة كافة الوسائط، تنتهي العملية الابنة.

تعمل العملية الأب كمشرف، حيث تستمع للإشعارات التي تُولّد عندما تستدعي العملية الهدف mkdir(2). وعند حدوث مثل هذا الإشعار، يفحص المشرف ذاكرة العملية الهدف (باستخدام /proc/pid/mem) لاكتشاف وسيط المسار الذي وُفر لاستدعاء mkdir(2)، وينفذ أحد الإجراءات التالية:

•   إذا بدأ المسار بالبادئة "/tmp/"، يحاول المشرف إنشاء الدليل المحدد، ثم يزيف عودة للعملية الهدف بناءً على القيمة المعادة من استدعاء mkdir(2) الخاص بالمشرف. وفي حال نجاح ذلك الاستدعاء، تكون قيمة العودة الناجحة المزيفة هي طول المسار.

•   إذا بدأ المسار بـ "./" (أي أنه مسار نسبي)، يرسل المشرف استجابة SECCOMP_USER_NOTIF_FLAG_CONTINUE إلى النواة للإشارة إلى أنه يجب على النواة تنفيذ استدعاء mkdir(2) للعملية الهدف.

•   إذا بدأ المسار ببادئة أخرى، يزيف المشرف عودة خطأ للعملية الهدف، لكي يبدو أن استدعاء mkdir(2) للعملية الهدف قد فشل مع الخطأ EOPNOTSUPP ("العملية غير مدعومة"). بالإضافة إلى ذلك، إذا كان المسار المحدد هو "/bye" بالضبط، فينتهي المشرف.

يمكن استخدام هذا البرنامج لتوضيح جوانب مختلفة من سلوك آلية إشعار مساحة مستخدم seccomp. وللمساعدة في مثل هذه العروض التوضيحية، يسجل البرنامج رسائل متنوعة لإظهار عملية العملية الهدف (السطور المسبوقة بـ "T:") والمشرف (السطور المزاحة والمسبوقة بـ "S:").

في المثال التالي، يحاول الهدف إنشاء الدليل /tmp/x. وعند تلقي الإشعار، ينشئ المشرف الدليل نيابة عن الهدف، ويزيف عودة ناجحة ليتلقاها استدعاء mkdir(2) للعملية الهدف.

    $ ./seccomp_unotify /tmp/x;
    T: PID = 23168

    T: على وشك إجراء mkdir("/tmp/x")
            S: جُلب إشعار (ID 0x17445c4a0f4e0e3c) لـ PID 23168
            S: جارِ تنفيذ: mkdir("/tmp/x", 0700)
            S: نجاح! عودة مزيفة = 6
            S: جارِ إرسال استجابة (flags = 0; val = 6; error = 0)
    T: نجاح: mkdir(2) أعاد 6

    T: جارِ الإنهاء
            S: انتهى الهدف؛ وداعاً

في المخرجات أعلاه، لاحظ أن قيمة العودة المزيفة التي تراها العملية الهدف هي 6 (طول المسار /tmp/x)، بينما يعيد استدعاء mkdir(2) العادي 0 عند النجاح.

في المثال التالي، يحاول الهدف إنشاء دليل باستخدام المسار النسبي ./sub. وبما أن هذا المسار يبدأ بـ "./"، يرسل المشرف استجابة SECCOMP_USER_NOTIF_FLAG_CONTINUE إلى النواة، وتنفذ النواة حينئذٍ (بنجاح) استدعاء mkdir(2) للعملية الهدف.

    $ ./seccomp_unotify ./sub;
    T: PID = 23204

    T: على وشك إجراء mkdir("./sub")
            S: جُلب إشعار (ID 0xddb16abe25b4c12) لـ PID 23204
            S: يمكن للهدف تنفيذ استدعاء النظام
            S: جارِ إرسال استجابة (flags = 0x1; val = 0; error = 0)
    T: نجاح: mkdir(2) أعاد 0

    T: جارِ الإنهاء
            S: انتهى الهدف؛ وداعاً

إذا حاولت العملية الهدف إنشاء دليل بمسار لا يبدأ بـ "." ولا يبدأ بالبادئة "/tmp/"، فإن المشرف يزيف عودة خطأ (EOPNOTSUPP، "العملية غير مدعومة") لاستدعاء mkdir(2) للهدف (الذي لا يُنفذ):

    $ ./seccomp_unotify /xxx;
    T: PID = 23178

    T: على وشك إجراء mkdir("/xxx")
            S: جُلب إشعار (ID 0xe7dc095d1c524e80) لـ PID 23178
            S: جارِ تزييف استجابة خطأ (العملية غير مدعومة)
            S: جارِ إرسال استجابة (flags = 0; val = 0; error = -95)
    T: خطأ: mkdir(2): العملية غير مدعومة

    T: جارِ الإنهاء
            S: انتهى الهدف؛ وداعاً

في المثال التالي، تحاول العملية الهدف إنشاء دليل بالمسار /tmp/nosuchdir/b. وعند تلقي الإشعار، يحاول المشرف إنشاء ذلك الدليل، ولكن يفشل استدعاء mkdir(2) لأن الدليل /tmp/nosuchdir غير موجود. وبناءً على ذلك، يزيف المشرف عودة خطأ تمرر الخطأ الذي تلقاه مرة أخرى إلى استدعاء mkdir(2) للعملية الهدف.

    $ ./seccomp_unotify /tmp/nosuchdir/b;
    T: PID = 23199

    T: على وشك إجراء mkdir("/tmp/nosuchdir/b")
            S: جُلب إشعار (ID 0x8744454293506046) لـ PID 23199
            S: جارِ تنفيذ: mkdir("/tmp/nosuchdir/b", 0700)
            S: فشل! (errno = 2; لا يوجد ملف أو دليل كهذا)
            S: جارِ إرسال استجابة (flags = 0; val = 0; error = -2)
    T: خطأ: mkdir(2): لا يوجد ملف أو دليل كهذا

    T: جارِ الإنهاء
            S: انتهى الهدف؛ وداعاً

إذا تلقى المشرف إشعارًا ورأى أن وسيط mkdir(2) الخاص بالهدف هو السلسلة "/bye"، فحينئذٍ ينهي المشرف عمله (بالإضافة إلى تزييف خطأ EOPNOTSUPP). وإذا نفذت عملية الهدف لاحقًا mkdir(2) أخرى تحفز مرشح seccomp الخاص بها ليعيد قيمة الإجراء SECCOMP_RET_USER_NOTIF، فتتسبب النواة في فشل استدعاء النظام لعملية الهدف بخطأ ENOSYS ("الوظيفة غير مفعلة"). ويُوضح ذلك في المثال التالي:

    $ ./seccomp_unotify /bye /tmp/y;
    T: PID = 23185

    T: على وشك إجراء mkdir("/bye")
            S: جُلب إشعار (ID 0xa81236b1d2f7b0f4) لـ PID 23185
            S: جارِ تزييف استجابة خطأ (العملية غير مدعومة)
            S: جارِ إرسال استجابة (flags = 0; val = 0; error = -95)
            S: جارِ الإنهاء **********
    T: خطأ: mkdir(2): العملية غير مدعومة

    T: على وشك إجراء mkdir("/tmp/y")
    T: خطأ: mkdir(2): الوظيفة غير منفذة

    T: جارِ الإنهاء

  مصدر البرنامج
    #define _GNU_SOURCE
    #include <err.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <limits.h>
    #include <linux/audit.h>
    #include <linux/filter.h>
    #include <linux/seccomp.h>
    #include <signal.h>
    #include <stdbool.h>
    #include <stdcountof.h>
    #include <stddef.h>
    #include <stdint.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/ioctl.h>
    #include <sys/prctl.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <sys/un.h>
    #include <unistd.h>

    #define streq(...)  (strcmp(__VA_ARGS__) == 0)

    /* Send the file descriptor 'fd' over the connected UNIX domain socket
       'sockfd'.  Returns 0 on success, or -1 on error.  */

    static int
    sendfd(int sockfd, int fd)
    {
        int             data;
        struct iovec    iov;
        struct msghdr   msgh;
        struct cmsghdr  *cmsgp;

        /* Allocate a char array of suitable size to hold the ancillary data.
           However, since this buffer is in reality a 'struct cmsghdr', use a
           union to ensure that it is suitably aligned.  */
        union {
            char   buf[CMSG_SPACE(sizeof(int))];
                            /* Space large enough to hold an 'int' */
            struct cmsghdr align;
        } controlMsg;

        /* The 'msg_name' field can be used to specify the address of the
           destination socket when sending a datagram.  However, we do not
           need to use this field because 'sockfd' is a connected socket.  */

        msgh.msg_name = NULL;
        msgh.msg_namelen = 0;

        /* On Linux, we must transmit at least one byte of real data in
           order to send ancillary data.  We transmit an arbitrary integer
           whose value is ignored by recvfd().  */

        msgh.msg_iov = &iov;
        msgh.msg_iovlen = 1;
        iov.iov_base = &data;
        iov.iov_len = sizeof(int);
        data = 12345;

        /* Set 'msghdr' fields that describe ancillary data */

        msgh.msg_control = controlMsg.buf;
        msgh.msg_controllen = sizeof(controlMsg.buf);

        /* Set up ancillary data describing file descriptor to send */

        cmsgp = CMSG_FIRSTHDR(&msgh);
        cmsgp->cmsg_level = SOL_SOCKET;
        cmsgp->cmsg_type = SCM_RIGHTS;
        cmsgp->cmsg_len = CMSG_LEN(sizeof(int));
        memcpy(CMSG_DATA(cmsgp), &fd, sizeof(int));

        /* Send real plus ancillary data */

        if (sendmsg(sockfd, &msgh, 0) == -1)
            return -1;

        return 0;
    }

    /* Receive a file descriptor on a connected UNIX domain socket.  Returns
       the received file descriptor on success, or -1 on error.  */

    static int
    recvfd(int sockfd)
    {
        int            data, fd;
        ssize_t        nr;
        struct iovec   iov;
        struct msghdr  msgh;

        /* Allocate a char buffer for the ancillary data.  See the comments
           in sendfd() */
        union {
            char   buf[CMSG_SPACE(sizeof(int))];
            struct cmsghdr align;
        } controlMsg;
        struct cmsghdr *cmsgp;

        /* The 'msg_name' field can be used to obtain the address of the
           sending socket.  However, we do not need this information.  */

        msgh.msg_name = NULL;
        msgh.msg_namelen = 0;

        /* Specify buffer for receiving real data */

        msgh.msg_iov = &iov;
        msgh.msg_iovlen = 1;
        iov.iov_base = &data;       /* Real data is an 'int' */
        iov.iov_len = sizeof(int);

        /* Set 'msghdr' fields that describe ancillary data */

        msgh.msg_control = controlMsg.buf;
        msgh.msg_controllen = sizeof(controlMsg.buf);

        /* Receive real plus ancillary data; real data is ignored */

        nr = recvmsg(sockfd, &msgh, 0);
        if (nr == -1)
            return -1;

        cmsgp = CMSG_FIRSTHDR(&msgh);

        /* Check the validity of the 'cmsghdr' */

        if (cmsgp == NULL
            || cmsgp->cmsg_len != CMSG_LEN(sizeof(int))
            || cmsgp->cmsg_level != SOL_SOCKET
            || cmsgp->cmsg_type != SCM_RIGHTS)
        {
            errno = EINVAL;
            return -1;
        }

        /* Return the received file descriptor to our caller */

        memcpy(&fd, CMSG_DATA(cmsgp), sizeof(int));
        return fd;
    }

    static void
    sigchldHandler(int sig)
    {
        char msg[] = "\tS: target has terminated; bye\n";

        write(STDOUT_FILENO, msg, sizeof(msg) - 1);
        _exit(EXIT_SUCCESS);
    }

    static int
    seccomp(unsigned int operation, unsigned int flags, void *args)
    {
        return syscall(SYS_seccomp, operation, flags, args);
    }

    /* The following is the x86-64-specific BPF boilerplate code for checking
       that the BPF program is running on the right architecture + ABI.  At
       completion of these instructions, the accumulator contains the system
       call number.  */

    /* For the x32 ABI, all system call numbers have bit 30 set */

    #define X32_SYSCALL_BIT         0x40000000

    #define X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR \
            BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
                     (offsetof(struct seccomp_data, arch))), \
            BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 2), \
            BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
                     (offsetof(struct seccomp_data, nr))), \
            BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, X32_SYSCALL_BIT, 0, 1), \
            BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)

    /* installNotifyFilter() installs a seccomp filter that generates
       user-space notifications (SECCOMP_RET_USER_NOTIF) when the process
       calls mkdir(2); the filter allows all other system calls.

       The function return value is a file descriptor from which the
       user-space notifications can be fetched.  */

    static int
    installNotifyFilter(void)
    {
        int notifyFd;

        struct sock_filter filter[] = {
            X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR,

            /* mkdir() triggers notification to user-space supervisor */

            BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_mkdir, 0, 1),
            BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_USER_NOTIF),

            /* Every other system call is allowed */

            BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        };

        struct sock_fprog prog = {
            .len = countof(filter),
            .filter = filter,
        };

        /* Install the filter with the SECCOMP_FILTER_FLAG_NEW_LISTENER flag;
           as a result, seccomp() returns a notification file descriptor.  */

        notifyFd = seccomp(SECCOMP_SET_MODE_FILTER,
                           SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
        if (notifyFd == -1)
            err(EXIT_FAILURE, "seccomp-install-notify-filter");

        return notifyFd;
    }

    /* Close a pair of sockets created by socketpair() */

    static void
    closeSocketPair(int sockPair[2])
    {
        if (close(sockPair[0]) == -1)
            err(EXIT_FAILURE, "closeSocketPair-close-0");
        if (close(sockPair[1]) == -1)
            err(EXIT_FAILURE, "closeSocketPair-close-1");
    }

    /* Implementation of the target process.  Create a child process that:

       (1) installs a seccomp filter with the
           SECCOMP_FILTER_FLAG_NEW_LISTENER flag;
       (2) writes the seccomp notification file descriptor returned from
           the previous step onto the UNIX domain socket, 'sockPair[0]';
       (3) calls mkdir(2) for each element of 'argv'.

       The function return value in the parent is the PID of the child
       process; the child does not return from this function.  */

    static pid_t
    targetProcess(int sockPair[2], char *argv[])
    {
        int    notifyFd, s;
        pid_t  targetPid;

        targetPid = fork();

        if (targetPid == -1)
            err(EXIT_FAILURE, "fork");

        if (targetPid > 0)          /* In parent, return PID of child */
            return targetPid;

        /* Child falls through to here */

        printf("T: PID = %ld\n", (long) getpid());

        /* Install seccomp filter(s) */

        if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
            err(EXIT_FAILURE, "prctl");

        notifyFd = installNotifyFilter();

        /* Pass the notification file descriptor to the tracing process over
           a UNIX domain socket */

        if (sendfd(sockPair[0], notifyFd) == -1)
            err(EXIT_FAILURE, "sendfd");

        /* Notification and socket FDs are no longer needed in target */

        if (close(notifyFd) == -1)
            err(EXIT_FAILURE, "close-target-notify-fd");

        closeSocketPair(sockPair);

        /* Perform a mkdir() call for each of the command-line arguments */

        for (char **ap = argv; *ap != NULL; ap++) {
            printf("\nT: about to mkdir(\"%s\")\n", *ap);

            s = mkdir(*ap, 0700);
            if (s == -1)
                perror("T: ERROR: mkdir(2)");
            else
                printf("T: SUCCESS: mkdir(2) returned %d\n", s);
        }

        printf("\nT: terminating\n");
        exit(EXIT_SUCCESS);
    }

    /* Check that the notification ID provided by a SECCOMP_IOCTL_NOTIF_RECV
       operation is still valid.  It will no longer be valid if the target
       process has terminated or is no longer blocked in the system call that
       generated the notification (because it was interrupted by a signal).

       This operation can be used when doing such things as accessing
       /proc/PID files in the target process in order to avoid TOCTOU race
       conditions where the PID that is returned by SECCOMP_IOCTL_NOTIF_RECV
       terminates and is reused by another process.  */

    static bool
    cookieIsValid(int notifyFd, uint64_t id)
    {
        return ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == 0;
    }

    /* Access the memory of the target process in order to fetch the
       pathname referred to by the system call argument 'argNum' in
       'req->data.args[]'.  The pathname is returned in 'path',
       a buffer of 'size' bytes allocated by the caller.

       Returns true if the pathname is successfully fetched, and false
       otherwise.  For possible causes of failure, see the comments below.  */

    static bool
    getTargetPathname(struct seccomp_notif *req, int notifyFd,
                      int argNum, char *path, size_t size)
    {
        int      procMemFd;
        char     procMemPath[PATH_MAX];
        ssize_t  nread;

        snprintf(procMemPath, sizeof(procMemPath), "/proc/%d/mem", req->pid);

        procMemFd = open(procMemPath, O_RDONLY | O_CLOEXEC);
        if (procMemFd == -1)
            return false;

        /* Check that the process whose info we are accessing is still alive
           and blocked in the system call that caused the notification.
           If the SECCOMP_IOCTL_NOTIF_ID_VALID operation (performed in
           cookieIsValid()) succeeded, we know that the /proc/PID/mem file
           descriptor that we opened corresponded to the process for which we
           received a notification.  If that process subsequently terminates,
           then read() on that file descriptor will return 0 (EOF).  */

        if (!cookieIsValid(notifyFd, req->id)) {
            close(procMemFd);
            return false;
        }

        /* Read bytes at the location containing the pathname argument */

        nread = pread(procMemFd, path, size, req->data.args[argNum]);

        close(procMemFd);

        if (nread <= 0)
            return false;

        /* Once again check that the notification ID is still valid.  The
           case we are particularly concerned about here is that just
           before we fetched the pathname, the target's blocked system
           call was interrupted by a signal handler, and after the handler
           returned, the target carried on execution (past the interrupted
           system call).  In that case, we have no guarantees about what we
           are reading, since the target's memory may have been arbitrarily
           changed by subsequent operations.  */

        if (!cookieIsValid(notifyFd, req->id)) {
            perror("\tS: notification ID check failed!!!");
            return false;
        }

        /* Even if the target's system call was not interrupted by a signal,
           we have no guarantees about what was in the memory of the target
           process.  (The memory may have been modified by another thread, or
           even by an external attacking process.) We therefore treat the
           buffer returned by pread() as untrusted input.  The buffer should
           contain a terminating null byte; if not, then we will trigger an
           error for the target process.  */

        if (strnlen(path, nread) < nread)
            return true;

        return false;
    }

    /* Allocate buffers for the seccomp user-space notification request and
       response structures.  It is the caller's responsibility to free the
       buffers returned via 'req' and 'resp'.  */

    static void
    allocSeccompNotifBuffers(struct seccomp_notif **req,
                             struct seccomp_notif_resp **resp,
                             struct seccomp_notif_sizes *sizes)
    {
        size_t  resp_size;

        /* Discover the sizes of the structures that are used to receive
           notifications and send notification responses, and allocate
           buffers of those sizes.  */

        if (seccomp(SECCOMP_GET_NOTIF_SIZES, 0, sizes) == -1)
            err(EXIT_FAILURE, "seccomp-SECCOMP_GET_NOTIF_SIZES");

        *req = malloc(sizes->seccomp_notif);
        if (*req == NULL)
            err(EXIT_FAILURE, "malloc-seccomp_notif");

        /* When allocating the response buffer, we must allow for the fact
           that the user-space binary may have been built with user-space
           headers where 'struct seccomp_notif_resp' is bigger than the
           response buffer expected by the (older) kernel.  Therefore, we
           allocate a buffer that is the maximum of the two sizes.  This
           ensures that if the supervisor places bytes into the response
           structure that are past the response size that the kernel expects,
           then the supervisor is not touching an invalid memory location.  */

        resp_size = sizes->seccomp_notif_resp;
        if (sizeof(struct seccomp_notif_resp) > resp_size)
            resp_size = sizeof(struct seccomp_notif_resp);

        *resp = malloc(resp_size);
        if (*resp == NULL)
            err(EXIT_FAILURE, "malloc-seccomp_notif_resp");

    }

    /* Handle notifications that arrive via the SECCOMP_RET_USER_NOTIF file
       descriptor, 'notifyFd'.  */

    static void
    handleNotifications(int notifyFd)
    {
        bool                        pathOK;
        char                        path[PATH_MAX];
        struct seccomp_notif        *req;
        struct seccomp_notif_resp   *resp;
        struct seccomp_notif_sizes  sizes;

        allocSeccompNotifBuffers(&req, &resp, &sizes);

        /* Loop handling notifications */

        for (;;) {

            /* Wait for next notification, returning info in '*req' */

            memset(req, 0, sizes.seccomp_notif);
            if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_RECV, req) == -1) {
                if (errno == EINTR)
                    continue;
                err(EXIT_FAILURE, "\tS: ioctl-SECCOMP_IOCTL_NOTIF_RECV");
            }

            printf("\tS: got notification (ID %#llx) for PID %d\n",
                   req->id, req->pid);

            /* The only system call that can generate a notification event
               is mkdir(2).  Nevertheless, we check that the notified system
               call is indeed mkdir() as kind of future-proofing of this
               code in case the seccomp filter is later modified to
               generate notifications for other system calls.  */

            if (req->data.nr != SYS_mkdir) {
                printf("\tS: notification contained unexpected "
                       "system call number; bye!!!\n");
                exit(EXIT_FAILURE);
            }

            pathOK = getTargetPathname(req, notifyFd, 0, path, sizeof(path));

            /* Prepopulate some fields of the response */

            resp->id = req->id;     /* Response includes notification ID */
            resp->flags = 0;
            resp->val = 0;

            /* If getTargetPathname() failed, trigger an EINVAL error
               response (sending this response may yield an error if the
               failure occurred because the notification ID was no longer
               valid); if the directory is in /tmp, then create it on behalf
               of the supervisor; if the pathname starts with '.', tell the
               kernel to let the target process execute the mkdir();
               otherwise, give an error for a directory pathname in any other
               location.  */

            if (!pathOK) {
                resp->error = -EINVAL;
                printf("\tS: spoofing error for invalid pathname (%s)\n",
                       strerror(-resp->error));
            } else if (strncmp(path, "/tmp/", strlen("/tmp/")) == 0) {
                printf("\tS: executing: mkdir(\"%s\", %#llo)\n",
                       path, req->data.args[1]);

                if (mkdir(path, req->data.args[1]) == 0) {
                    resp->error = 0;            /* "Success" */
                    resp->val = strlen(path);   /* Used as return value of
                                                   mkdir() in target */
                    printf("\tS: success! spoofed return = %lld\n",
                           resp->val);
                } else {

                    /* If mkdir() failed in the supervisor, pass the error
                       back to the target */

                    resp->error = -errno;
                    printf("\tS: failure! (errno = %d; %s)\n", errno,
                           strerror(errno));
                }
            } else if (strncmp(path, "./", strlen("./")) == 0) {
                resp->error = resp->val = 0;
                resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
                printf("\tS: target can execute system call\n");
            } else {
                resp->error = -EOPNOTSUPP;
                printf("\tS: spoofing error response (%s)\n",
                       strerror(-resp->error));
            }

            /* Send a response to the notification */

            printf("\tS: sending response "
                   "(flags = %#x; val = %lld; error = %d)\n",
                   resp->flags, resp->val, resp->error);

            if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1) {
                if (errno == ENOENT)
                    printf("\tS: response failed with ENOENT; "
                           "perhaps target process's syscall was "
                           "interrupted by a signal?\n");
                else
                    perror("ioctl-SECCOMP_IOCTL_NOTIF_SEND");
            }

            /* If the pathname is just "/bye", then the supervisor breaks out
               of the loop and terminates.  This allows us to see what happens
               if the target process makes further calls to mkdir(2).  */

            if (streq(path, "/bye"))
                break;
        }

        free(req);
        free(resp);
        printf("\tS: terminating **********\n");
        exit(EXIT_FAILURE);
    }

    /* Implementation of the supervisor process:

       (1) obtains the notification file descriptor from 'sockPair[1]'
       (2) handles notifications that arrive on that file descriptor.  */

    static void
    supervisor(int sockPair[2])
    {
        int notifyFd;

        notifyFd = recvfd(sockPair[1]);

        if (notifyFd == -1)
            err(EXIT_FAILURE, "recvfd");

        closeSocketPair(sockPair);  /* We no longer need the socket pair */

        handleNotifications(notifyFd);
    }

    int
    main(int argc, char *argv[])
    {
        int               sockPair[2];
        struct sigaction  sa;

        setbuf(stdout, NULL);

        if (argc < 2) {
            fprintf(stderr, "At least one pathname argument is required\n");
            exit(EXIT_FAILURE);
        }

        /* Create a UNIX domain socket that is used to pass the seccomp
           notification file descriptor from the target process to the
           supervisor process.  */

        if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockPair) == -1)
            err(EXIT_FAILURE, "socketpair");

        /* Create a child process--the "target"--that installs seccomp
           filtering.  The target process writes the seccomp notification
           file descriptor onto 'sockPair[0]' and then calls mkdir(2) for
           each directory in the command-line arguments.  */

        (void) targetProcess(sockPair, &argv[optind]);

        /* Catch SIGCHLD when the target terminates, so that the
           supervisor can also terminate.  */

        sa.sa_handler = sigchldHandler;
        sa.sa_flags = 0;
        sigemptyset(&sa.sa_mask);
        if (sigaction(SIGCHLD, &sa, NULL) == -1)
            err(EXIT_FAILURE, "sigaction");

        supervisor(sockPair);

        exit(EXIT_SUCCESS);
    }

انظر أيضًا
ioctl(2)، pidfd_getfd(2)، pidfd_open(2)، seccomp(2)

يمكن العثور على مزيد من الأمثلة في ملف مصدر النواة samples/seccomp/user-trap.c.

ترجمة
تُرجمت هذه الصفحة من الدليل بواسطة زايد السعيدي <zayed.alsaidi@gmail.com>

هذه الترجمة هي وثيقة مجانية؛ راجع رخصة جنو العامة الإصدار 3 (https://www.gnu.org/licenses/gpl-3.0.html) أو ما بعده للاطلاع على شروط حقوق النشر. لا توجد أي ضمانات.

إذا وجدت أي أخطاء في ترجمة صفحة الدليل هذه، يرجى إرسال بريد إلكتروني إلى قائمة بريد المترجمين: kde-l10n-ar@kde.org.