user_namespaces(7)

الاسم
user_namespaces - نظرة عامة على مساحات أسماء المستخدمين في لينكس

الوصف
للحصول على نظرة عامة على مساحات الأسماء، انظر namespaces(7).

مساحات أسماء المستخدمين تعزل المعرفات والسمات المتعلقة بالأمان، وتحديدًا معرفات المستخدم والمجموعة (انظر credentials(7))، والدليل الجذر، والمفاتيح (انظر keyrings(7))، والصلاحيات (انظر capabilities(7)). يمكن أن تكون معرفات المستخدم والمجموعة لعملية ما مختلفة داخل مساحة اسم المستخدم وخارجها. على وجه الخصوص، يمكن أن يكون لعملية ما معرف مستخدم عادي غير مميز خارج مساحة اسم المستخدم بينما يكون لديها في نفس الوقت معرف مستخدم بقيمة 0 داخل المساحة؛ بمعنى آخر، تمتلك العملية صلاحيات كاملة للعمليات داخل مساحة اسم المستخدم، ولكنها غير مميزة للعمليات خارج المساحة.

  مساحات الأسماء المتداخلة، العضوية في مساحة الاسم
يمكن أن تكون مساحات أسماء المستخدمين متداخلة؛ أي أن كل مساحة اسم مستخدم —باستثناء المساحة الأولية ("الجذر")— لها مساحة اسم مستخدم أب، ويمكن أن تحتوي على صفر أو أكثر من مساحات أسماء المستخدمين الفرعية. مساحة اسم المستخدم الأب هي مساحة اسم المستخدم للعملية التي تنشئ مساحة اسم المستخدم عبر استدعاء unshare(2) أو clone(2) مع العلم CLONE_NEWUSER.

يفرض النواة (منذ لينكس 3.11) حدًا أقصى يبلغ 32 مستوى متداخلًا من مساحات أسماء المستخدمين. تفشل استدعاءات unshare(2) أو clone(2) التي قد تتسبب في تجاوز هذا الحد مع الخطأ EUSERS.

كل عملية هي عضو في مساحة اسم مستخدم واحدة بالضبط. العملية المنشأة عبر fork(2) أو clone(2) بدون العلم CLONE_NEWUSER هي عضو في نفس مساحة اسم المستخدم مثل أبويها. يمكن لعملية أحادية الخيط الانضمام إلى مساحة اسم مستخدم أخرى باستخدام setns(2) إذا كانت تمتلك CAP_SYS_ADMIN في تلك المساحة؛ عند القيام بذلك، تكتسب مجموعة كاملة من الصلاحيات في تلك المساحة.

استدعاء clone(2) أو unshare(2) مع العلم CLONE_NEWUSER يجعل العملية الفرعية الجديدة (لـ clone(2)) أو المستدعي (لـ unshare(2)) عضوًا في مساحة اسم المستخدم الجديدة المنشأة بواسطة الاستدعاء.

يمكن استخدام عملية NS_GET_PARENT ioctl(2) لاكتشاف العلاقة الأبوية بين مساحات أسماء المستخدمين؛ انظر ioctl_nsfs(2).

المهمة التي تغير أحد معرفاتها الفعالة ستعيد تعيين قابلية التفريغ الخاصة بها إلى القيمة في /proc/sys/fs/suid_dumpable. قد يؤثر هذا على ملكية ملفات proc للعمليات الفرعية وقد يتسبب بالتالي في افتقار العملية الأب للأذونات اللازمة للكتابة إلى ملفات التعيين للعمليات الفرعية التي تعمل في مساحة اسم مستخدم جديدة. في مثل هذه الحالات، جعل العملية الأب قابلة للتفريغ، باستخدام PR_SET_DUMPABLE في استدعاء prctl(2)، قبل إنشاء عملية فرعية في مساحة اسم مستخدم جديدة قد يصحح هذه المشكلة. انظر prctl(2) و proc(5) للحصول على تفاصيل حول كيفية تأثر الملكية.

  القدرات
العملية الفرعية المنشأة بواسطة clone(2) مع العلم CLONE_NEWUSER تبدأ بمجموعة كاملة من الصلاحيات في مساحة اسم المستخدم الجديدة. وبالمثل، العملية التي تنشئ مساحة اسم مستخدم جديدة باستخدام unshare(2) أو تنضم إلى مساحة اسم مستخدم موجودة باستخدام setns(2) تكتسب مجموعة كاملة من الصلاحيات في تلك المساحة. من ناحية أخرى، لا تمتلك تلك العملية أي صلاحيات في مساحة اسم المستخدم الأب (في حالة clone(2)) أو السابقة (في حالة unshare(2) و setns(2))، حتى لو تم إنشاء المساحة الجديدة أو الانضمام إليها بواسطة المستخدم الجذر (أي عملية بمعرف مستخدم 0 في مساحة الاسم الجذر).

لاحظ أن استدعاء execve(2) سيتسبب في إعادة حساب صلاحيات العملية بالطريقة المعتادة (انظر capabilities(7)). وبالتالي، ما لم يكن للعملية معرف مستخدم بقيمة 0 داخل مساحة الاسم، أو كان للملف القابل للتنفيذ قناع صلاحيات قابل للتوريث غير فارغ، فستفقد العملية جميع الصلاحيات. انظر مناقشة تعيينات معرف المستخدم والمجموعة، أدناه.

استدعاء clone(2) أو unshare(2) باستخدام العلم CLONE_NEWUSER أو استدعاء setns(2) الذي ينقل المستدعي إلى مساحة اسم مستخدم أخرى يضبط أعلام "securebits" (انظر capabilities(7)) على قيمها المبدئية (جميع الأعلام معطلة) في الفرع (لـ clone(2)) أو المستدعي (لـ unshare(2) أو setns(2)). لاحظ أنه نظرًا لأن المستدعي لم يعد يمتلك صلاحيات في مساحة اسم المستخدم الأصلية بعد استدعاء setns(2)، فليس من الممكن لعملية إعادة تعيين أعلام "securebits" الخاصة بها مع الاحتفاظ بعضويتها في مساحة اسم المستخدم باستخدام زوج من استدعاءات setns(2) للانتقال إلى مساحة اسم مستخدم أخرى ثم العودة إلى مساحة اسم المستخدم الأصلية.

قواعد تحديد ما إذا كانت العملية تمتلك صلاحية في مساحة اسم مستخدم معينة هي كما يلي:

•   تمتلك العملية صلاحية داخل مساحة اسم مستخدم إذا كانت عضوًا في تلك المساحة وكانت تمتلك الصلاحية في مجموعة صلاحياتها الفعالة. يمكن للعملية اكتساب صلاحيات في مجموعة صلاحياتها الفعالة بطرق مختلفة. على سبيل المثال، قد تنفذ برنامج set-user-ID أو ملفًا قابلاً للتنفيذ مع صلاحيات ملف مرتبطة. بالإضافة إلى ذلك، قد تكتسب العملية صلاحيات عبر تأثير clone(2)، unshare(2)، أو setns(2)، كما هو موصوف بالفعل.

•   إذا كانت العملية تمتلك صلاحية في مساحة اسم مستخدم، فإنها تمتلك تلك الصلاحية في جميع مساحات الأسماء الفرعية (والسليلة البعيدة) أيضًا.

•   عند إنشاء مساحة اسم مستخدم، تسجل النواة معرف المستخدم الفعال للعملية المنشئة على أنه "مالك" المساحة. العملية التي تقع في أب مساحة اسم المستخدم والتي يتطابق معرف المستخدم الفعال الخاص بها مع مالك المساحة تمتلك جميع الصلاحيات في المساحة. بموجب القاعدة السابقة، هذا يعني أن العملية تمتلك جميع الصلاحيات في جميع مساحات أسماء المستخدمين السليلة البعيدة أيضًا. يمكن استخدام عملية NS_GET_OWNER_UID ioctl(2) لاكتشاف معرف المستخدم لمالك المساحة؛ انظر ioctl_nsfs(2).

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

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

امتلاك CAP_SYS_ADMIN داخل مساحة اسم المستخدم التي تمتلك مساحة اسم التوصيل الخاصة بعملية ما يسمح لتلك العملية بإنشاء توصيلات ربط وتوصيل الأنواع التالية من أنظمة الملفات:

    •   /proc (منذ لينكس 3.8)
    •   /sys (منذ لينكس 3.8)
    •   devpts (منذ لينكس 3.9)
    •   tmpfs(5) (منذ لينكس 3.9)
    •   ramfs (منذ لينكس 3.9)
    •   mqueue (منذ لينكس 3.9)
    •   bpf (منذ لينكس 4.4)
    •   overlayfs (منذ لينكس 5.11)

امتلاك CAP_SYS_ADMIN داخل مساحة اسم المستخدم التي تملك مساحة اسم cgroup لعملية يسمح (منذ لينكس 4.6) لتلك العملية بوصل نظام ملفات cgroup الإصدار 2 والتسلسلات الهرمية المسماة لـ cgroup الإصدار 1 (أي أنظمة ملفات cgroup الموصولة مع الخيار "none,name=").

امتلاك CAP_SYS_ADMIN داخل مساحة اسم المستخدم التي تملك مساحة اسم PID لعملية يسمح (منذ لينكس 3.8) لتلك العملية بوصل أنظمة ملفات /proc.

مع ذلك، وصل أنظمة الملفات القائمة على الكتل يمكن أن يُنجز فقط بواسطة عملية تمتلك CAP_SYS_ADMIN في مساحة اسم المستخدم الأولية.

  تفاعل مساحات اسم المستخدم وأنواع أخرى من مساحات الأسماء
منذ لينكس 3.8، العمليات غير المميزة يمكنها إنشاء مساحات اسم المستخدم، وأنواع مساحات الأسماء الأخرى يمكن إنشاؤها فقط بقابلية CAP_SYS_ADMIN في مساحة اسم المستخدم للمتصل.

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

إذا تم تحديد CLONE_NEWUSER مع أعلام CLONE_NEW* أخرى في استدعاء واحد clone(2) أو unshare(2)، يتم ضمان إنشاء مساحة اسم المستخدم أولاً، مما يعطي الطفل (clone(2)) أو المتصل (unshare(2)) امتيازات على مساحات الأسماء المتبقية المنشأة بواسطة الاستدعاء. وبالتالي، من الممكن لمتصل غير مميز تحديد هذه المجموعة من الأعلام.

عند إنشاء مساحة اسم جديدة (غير مساحة اسم المستخدم) عبر clone(2) أو unshare(2)، يسجل النواة مساحة اسم المستخدم للعملية المنشئة كمالك لمساحة الاسم الجديدة. (لا يمكن تغيير هذا الارتباط.) عندما تقوم عملية في مساحة الاسم الجديدة لاحقًا بعمليات مميزة تعمل على موارد عالمية معزولة بواسطة مساحة الاسم، يتم إجراء فحوصات الإذن وفقًا لقابليات العملية في مساحة اسم المستخدم التي ربطتها النواة بمساحة الاسم الجديدة. على سبيل المثال، افترض أن عملية تحاول تغيير اسم المضيف (sethostname(2))، وهو مورد محكوم بمساحة اسم UTS. في هذه الحالة، ستحدد النواة أي مساحة اسم مستخدم تملك مساحة اسم UTS للعملية، وتتحقق مما إذا كانت العملية تمتلك القابلية المطلوبة (CAP_SYS_ADMIN) في تلك المساحة.

عملية NS_GET_USERNS ioctl(2) يمكن استخدامها لاكتشاف مساحة اسم المستخدم التي تملك مساحة اسم غير مستخدم؛ انظر ioctl_nsfs(2).

  تعيينات معرف المستخدم ومعرف المجموعة: uid_map و gid_map
عند إنشاء مساحة اسم المستخدم، تبدأ بدون تعيين لمعرفات المستخدم (معرفات المجموعة) إلى مساحة اسم المستخدم الأم. الملفات /proc/pid/uid_map و /proc/pid/gid_map (المتاحة منذ لينكس 3.5) تكشف التعيينات لمعرفات المستخدم والمجموعة داخل مساحة اسم المستخدم للعملية pid. هذه الملفات يمكن قراءتها لعرض التعيينات في مساحة اسم المستخدم والكتابة إليها (مرة واحدة) لتعريف التعيينات.

الوصف في الفقرات التالية يشرح التفاصيل لـ uid_map؛ gid_map هو نفسه تمامًا، لكن كل حالة من "معرف المستخدم" تُستبدل بـ "معرف المجموعة".

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

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

(1) بداية نطاق معرفات المستخدم في مساحة اسم المستخدم للعملية pid.

(2) بداية نطاق معرفات المستخدم التي تُعيّن إليها معرفات المستخدم المحددة بواسطة الحقل الأول. كيفية تفسير الحقل الثاني تعتمد على ما إذا كانت العملية التي فتحت uid_map والعملية pid في نفس مساحة اسم المستخدم، كالتالي:

    (أ) إذا كانت العمليتان في مساحتي اسم مستخدم مختلفتين: الحقل الثاني هو بداية نطاق معرفات المستخدم في مساحة اسم المستخدم للعملية التي فتحت uid_map.

    (ب) إذا كانت العمليتان في نفس مساحة اسم المستخدم: الحقل الثاني هو بداية نطاق معرفات المستخدم في مساحة اسم المستخدم الأم للعملية pid. هذه الحالة تمكن فاتح uid_map (الحالة الشائعة هنا هي فتح /proc/self/uid_map) من رؤية تعيين معرفات المستخدم إلى مساحة اسم المستخدم للعملية التي أنشأت هذه المساحة.

(3) حجم نطاق معرفات المستخدم الذي يُعيّن بين مساحتي اسم المستخدم.

استدعاءات النظام التي تُرجع معرفات المستخدم (معرفات المجموعة)—على سبيل المثال، getuid(2)، getgid(2)، وحقول الاعتماد في البنية المُعادة بواسطة stat(2)—تُرجع معرف المستخدم (معرف المجموعة) المُعيّن إلى مساحة اسم المستخدم للمتصل.

عند وصول عملية إلى ملف، تُعيّن معرفات المستخدم والمجموعة الخاصة بها إلى مساحة اسم المستخدم الأولية لغرض فحص الإذن وتعيين المعرفات عند إنشاء ملف. عندما تسترجع عملية معرفات المستخدم والمجموعة للملف عبر stat(2)، تُعيّن المعرفات في الاتجاه المعاكس، لإنتاج قيم نسبية لتعيينات معرف المستخدم والمجموعة للعملية.

مساحة اسم المستخدم الأولية ليس لها مساحة اسم أم، لكن، من أجل الاتساق، توفر النواة ملفات تعيين معرف مستخدم ومجموعة وهمية لهذه المساحة. النظر إلى ملف uid_map (gid_map هو نفسه) من شل في المساحة الأولية يُظهر:

    $ cat /proc/$$/uid_map;
             0          0 4294967295

يخبرنا هذا التعيين أن النطاق الذي يبدأ بمعرف المستخدم 0 في هذا النطاق الاسمي يُعيّن إلى نطاق يبدأ بـ 0 في النطاق الاسمي الأصلي (غير الموجود)، وحجم النطاق هو أكبر عدد صحيح غير موقّع 32 بت. هذا يترك 4294967295 (القيمة -1 الموقّعة 32 بت) غير معيّنة. هذا متعمد: (uid_t) -1 يُستخدم في عدة واجهات (مثل setreuid(2)) كطريقة لتحديد "لا يوجد معرف مستخدم". ترك (uid_t) -1 غير معيّنة وغير قابلة للاستخدام يضمن عدم حدوث ارتباك عند استخدام هذه الواجهات.

  تعريف تعيينات معرف المستخدم والمجموعة: الكتابة إلى uid_map و gid_map
بعد إنشاء نطاق اسمي جديد للمستخدم، يمكن الكتابة إلى ملف uid_map الخاص بـ واحد من العمليات في النطاق الاسمي مرة واحدة لتعريف تعيين معرفات المستخدم في النطاق الاسمي الجديد للمستخدم. أي محاولة للكتابة أكثر من مرة إلى ملف uid_map في نطاق اسمي للمستخدم تفشل مع الخطأ EPERM. تنطبق قواعد مماثلة على ملفات gid_map.

يجب أن تتوافق الأسطر المكتوبة إلى uid_map (gid_map) مع قواعد الصلاحية التالية:

•   يجب أن تكون الحقول الثلاثة أرقامًا صالحة، ويجب أن يكون الحقل الأخير أكبر من 0.

•   تُنهى الأسطر بأحرف السطر الجديد.

•   يوجد حد لعدد الأسطر في الملف. حتى لينكس 4.14، كان هذا الحد (بشكل تعسفي) مضبوطًا على 5 أسطر. منذ لينكس 4.16، أصبح الحد 340 سطرًا. بالإضافة إلى ذلك، يجب أن يكون عدد البايتات المكتوبة إلى الملف أقل من حجم صفحة النظام، ويجب إجراء الكتابة في بداية الملف (أي لا يمكن استخدام lseek(2) و pwrite(2) للكتابة إلى إزاحات غير صفرية في الملف).

•   لا يمكن أن يتداخل نطاق معرفات المستخدم (معرفات المجموعة) المحدد في كل سطر مع النطاقات في أي أسطر أخرى. في التطبيق الأولي (لينكس 3.8)، تم تلبية هذا المطلب من خلال تطبيق مبسط فرض مطلبًا إضافيًا بأن القيم في كل من الحقل 1 والحقل 2 من الأسطر المتتالية يجب أن تكون بترتيب تصاعدي عددي، مما منع إنشاء بعض الخرائط الصالحة بخلاف ذلك. منذ لينكس 3.9، تمت إزالة هذا القيد، مما يسمح بأي مجموعة صالحة من الخرائط غير المتداخلة.

•   يجب كتابة سطر واحد على الأقل إلى الملف.

تفشل عمليات الكتابة التي تخالف القواعد المذكورة أعلاه مع الخطأ EINVAL.

لكي تتمكن عملية من الكتابة إلى ملف /proc/pid/uid_map (/proc/pid/gid_map)، يجب استيفاء جميع متطلبات الإذن التالية:

•   يجب أن تمتلك عملية الكتابة القدرة CAP_SETUID (CAP_SETGID) في النطاق الاسمي للمستخدم للعملية pid.

•   يجب أن تكون عملية الكتابة إما في النطاق الاسمي للمستخدم للعملية pid أو في النطاق الاسمي للمستخدم الأصلي للعملية pid.

•   يجب أن يكون لمعرفات المستخدم (معرفات المجموعة) المعيّنة بدورها تعيين في النطاق الاسمي للمستخدم الأصلي.

•   إذا تم تحديث /proc/pid/uid_map لإنشاء تعيين يُعيّن UID 0 في النطاق الاسمي الأصلي، فيجب أن يكون أحد ما يلي صحيحًا:

    (أ) إذا كانت عملية الكتابة في النطاق الاسمي للمستخدم الأصلي، فيجب أن تمتلك القدرة CAP_SETFCAP في ذلك النطاق الاسمي للمستخدم؛ أو

    (ب) إذا كانت عملية الكتابة في النطاق الاسمي للمستخدم الفرعي، فيجب أن تكون العملية التي أنشأت النطاق الاسمي للمستخدم قد امتلكت القدرة CAP_SETFCAP عند إنشاء النطاق الاسمي.

    هذه القاعدة سارية منذ لينكس 5.12. إنها تزيل خطأ أمني سابق حيث يمكن لعملية UID 0 التي تفتقر إلى القدرة CAP_SETFCAP، اللازمة لإنشاء ملف ثنائي بقدرات ملفات ذات نطاق اسمي (كما هو موصوف في capabilities(7))، مع ذلك إنشاء مثل هذا الملف الثنائي، من خلال الخطوات التالية:

    (1) إنشاء نطاق اسمي جديد للمستخدم مع تعيين الهوية (أي UID 0 في النطاق الاسمي الجديد للمستخدم يُعيّن إلى UID 0 في النطاق الاسمي الأصلي)، بحيث يكون UID 0 في كلا النطاقين الاسميين مكافئًا لنفس معرف المستخدم الجذر.

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

•   ينطبق أحد الحالتين التاليتين:

    (أ) إما أن تمتلك عملية الكتابة القدرة CAP_SETUID (CAP_SETGID) في النطاق الاسمي للمستخدم الأصلي.

        •   لا تنطبق قيود إضافية: يمكن للعملية إجراء تعيينات لمعرفات مستخدم (معرفات مجموعة) عشوائية في النطاق الاسمي للمستخدم الأصلي.

    (ب) أو بخلاف ذلك تنطبق جميع القيود التالية:

        •   يجب أن تتكون البيانات المكتوبة إلى uid_map (gid_map) من سطر واحد يُعيّن معرف المستخدم الفعال (معرف المجموعة) لعملية الكتابة في النطاق الاسمي للمستخدم الأصلي إلى معرف مستخدم (معرف مجموعة) في النطاق الاسمي للمستخدم.

        •   يجب أن تمتلك عملية الكتابة نفس معرف المستخدم الفعال لعملية أنشأت مساحة اسم المستخدم.

        •   في حالة gid_map، يجب أولاً رفض استخدام استدعاء النظام setgroups(2) بكتابة "deny" إلى ملف /proc/pid/setgroups (انظر أدناه) قبل الكتابة إلى gid_map.

تفشل عمليات الكتابة التي تخالف القواعد أعلاه مع الخطأ EPERM.

  تعيينات معرف المشروع: projid_map
على غرار تعيينات معرف المستخدم والمجموعة، من الممكن إنشاء تعيينات معرف مشروع لمساحة اسم المستخدم. (تُستخدم معرفات المشروع لحصص القرص؛ انظر setquota(8) وquotactl(2).)

تُعرّف تعيينات معرف المشروع بالكتابة إلى ملف /proc/pid/projid_map (موجود منذ Linux 3.7).

قواعد الصلاحية للكتابة إلى ملف /proc/pid/projid_map هي نفسها للكتابة إلى ملف uid_map؛ يؤدي انتهاك هذه القواعد إلى فشل write(2) مع الخطأ EINVAL.

قواعد الإذن للكتابة إلى ملف /proc/pid/projid_map هي كما يلي:

•   يجب أن تكون عملية الكتابة إما في النطاق الاسمي للمستخدم للعملية pid أو في النطاق الاسمي للمستخدم الأصلي للعملية pid.

•   يجب أن تمتلك معرفات المشروع المعيّنة بدورها تعيينًا في مساحة اسم المستخدم الأصل.

يؤدي انتهاك هذه القواعد إلى فشل write(2) مع الخطأ EPERM.

  التفاعل مع استدعاءات النظام التي تغير معرفات المستخدم أو معرفات المجموعة للعملية
في مساحة اسم المستخدم حيث لم يُكتب ملف uid_map، ستفشل استدعاءات النظام التي تغير معرفات المستخدم. وبالمثل، إذا لم يُكتب ملف gid_map، ستفشل استدعاءات النظام التي تغير معرفات المجموعة. بعد كتابة ملفي uid_map وgid_map، يمكن استخدام القيم المعيّنة فقط في استدعاءات النظام التي تغير معرفات المستخدم والمجموعة.

لمعرفات المستخدم، تتضمن استدعاءات النظام ذات الصلة setuid(2) وsetfsuid(2) وsetreuid(2) وsetresuid(2). ولمعرفات المجموعة، تتضمن استدعاءات النظام ذات الصلة setgid(2) وsetfsgid(2) وsetregid(2) وsetresgid(2) وsetgroups(2).

كتابة "deny" إلى ملف /proc/pid/setgroups قبل الكتابة إلى /proc/pid/gid_map ستعطل setgroups(2) بشكل دائم في مساحة اسم المستخدم وتسمح بالكتابة إلى /proc/pid/gid_map دون امتلاك القدرة CAP_SETGID في مساحة اسم المستخدم الأصل.

  ملف /proc/pid/setgroups
يعرض ملف /proc/pid/setgroups السلسلة "allow" إذا سُمح للعمليات في مساحة اسم المستخدم التي تحتوي على العملية pid باستخدام استدعاء النظام setgroups(2)؛ ويعرض "deny" إذا لم يُسمح بـ setgroups(2) في مساحة اسم المستخدم تلك. لاحظ أنه بغض النظر عن القيمة في ملف /proc/pid/setgroups (وبغض النظر عن قدرات العملية)، لا يُسمح أيضًا باستدعاءات setgroups(2) إذا لم يُعيّن /proc/pid/gid_map بعد.

يمكن لعملية متميزة (تلك التي تمتلك القدرة CAP_SYS_ADMIN في مساحة الاسم) كتابة أي من السلسلتين "allow" أو "deny" إلى هذا الملف قبل كتابة تعيين معرف مجموعة لمساحة اسم المستخدم هذه إلى ملف /proc/pid/gid_map. تمنع كتابة السلسلة "deny" أي عملية في مساحة اسم المستخدم من استخدام setgroups(2).

جوهر القيود الموصوفة في الفقرة السابقة هو أنه يُسمح بالكتابة إلى /proc/pid/setgroups فقط طالما أن استدعاء setgroups(2) غير مسموح به لأن /proc/pid/gid_map لم يُعيّن. يضمن هذا ألا تستطيع العملية الانتقال من حالة يُسمح فيها بـ setgroups(2) إلى حالة يُرفض فيها setgroups(2)؛ يمكن للعملية الانتقال فقط من حالة عدم السماح بـ setgroups(2) إلى حالة السماح بـ setgroups(2).

القيمة المبدئية لهذا الملف في مساحة اسم المستخدم الأولية هي "allow".

بمجرد الكتابة إلى /proc/pid/gid_map (والذي له تأثير تمكين setgroups(2) في مساحة اسم المستخدم)، لم يعد من الممكن منع setgroups(2) بكتابة "deny" إلى /proc/pid/setgroups (تفشل الكتابة مع الخطأ EPERM).

ترث مساحة اسم المستخدم الفرعية إعداد /proc/pid/setgroups من أصلها.

إذا كان ملف setgroups يحتوي على القيمة "deny"، فلا يمكن إعادة تمكين استدعاء النظام setgroups(2) لاحقًا (بكتابة "allow" إلى الملف) في مساحة اسم المستخدم هذه. (تفشل محاولات القيام بذلك مع الخطأ EPERM.) ينتشر هذا القيد أيضًا إلى جميع مساحات اسم المستخدم الفرعية لمساحة اسم المستخدم هذه.

أُضيف ملف /proc/pid/setgroups في Linux 3.19، لكنه نُقل إلى العديد من سلاسل النواة المستقرة السابقة، لأنه يعالج مشكلة أمنية. تعلقت المشكلة بالملفات ذات الأذونات مثل "rwx---rwx". تمنح هذه الملفات أذونات أقل لـ "group" مقارنة بـ "other". يعني هذا أن إسقاط المجموعات باستخدام setgroups(2) قد يسمح لعملية بالوصول إلى ملف لم يكن لديها سابقًا. قبل وجود مساحات اسم المستخدم، لم يكن هذا مصدر قلق، لأن عملية متميزة فقط (تلك التي تمتلك القدرة CAP_SETGID) يمكنها استدعاء setgroups(2). ومع ذلك، مع إدخال مساحات اسم المستخدم، أصبح من الممكن لعملية غير متميزة إنشاء مساحة اسم جديدة يمتلك فيها المستخدم جميع الامتيازات. سمح هذا بعد ذلك للمستخدمين غير المتميزين سابقًا بإسقاط المجموعات وبالتالي الحصول على وصول إلى ملفات لم يمتلكوها سابقًا. أُضيف ملف /proc/pid/setgroups لمعالجة هذه المشكلة الأمنية، برفض أي مسار لعملية غير متميزة لإسقاط المجموعات باستخدام setgroups(2).

  معرفات المستخدم والمجموعة غير المعيّنة
توجد أماكن مختلفة قد يُعرض فيها معرف مستخدم غير معيّن (معرف مجموعة) إلى مساحة المستخدم. على سبيل المثال، قد تستدعي العملية الأولى في مساحة اسم مستخدم جديدة getuid(2) قبل تعريف تعيين معرف مستخدم لمساحة الاسم. في معظم هذه الحالات، يُحوّل معرف المستخدم غير المعيّن إلى معرف المستخدم الفائض (معرف المجموعة)؛ القيمة المبدئية لمعرف المستخدم الفائض (معرف المجموعة) هي 65534. انظر أوصاف /proc/sys/kernel/overflowuid و/proc/sys/kernel/overflowgid في proc(5).

الحالات التي تُرسم فيها المعرفات غير المُعيّنة بهذه الطريقة تشمل استدعاءات النظام التي تُرجع معرفات المستخدم (getuid(2)، getgid(2) وما شابه)، بيانات الاعتماد المُمرّرة عبر مقبس نطاق UNIX، بيانات الاعتماد المُعادة بواسطة stat(2)، waitid(2)، وعمليات "ctl" IPC_STAT لنظام V IPC، بيانات الاعتماد المُكشوفة بواسطة /proc/pid/status والملفات في /proc/sysvipc/*، بيانات الاعتماد المُعادة عبر حقل si_uid في siginfo_t المُستقبَل مع إشارة (انظر sigaction(2))، بيانات الاعتماد المُكتوبة في ملف محاسبة العملية (انظر acct(5))، وبيانات الاعتماد المُعادة مع إشعارات قائمة انتظار رسائل POSIX (انظر mq_notify(3)).

هناك حالة بارزة واحدة حيث لا تُحوّل معرفات المستخدم والمجموعة غير المُعيّنة إلى قيمة معرف الفائض المقابلة. عند عرض ملف uid_map أو gid_map الذي لا يوجد فيه رسم للحقل الثاني، يُعرض ذلك الحقل كـ 4294967295 (-1 كعدد صحيح غير مُوقّع).

  الوصول إلى الملفات
لتحديد الأذونات عندما يصل عملية غير مُمتازة إلى ملف، تُرسم بيانات اعتماد العملية (UID، GID) وبيانات اعتماد الملف فعليًا إلى ما ستكون عليه في مساحة المستخدم الأولية ثم تُقارَن لتحديد الأذونات التي تمتلكها العملية على الملف. ينطبق الشيء نفسه على الكائنات الأخرى التي تستخدم نموذج الوصول لبيانات الاعتماد بالإضافة إلى قناع الأذونات، مثل كائنات System V IPC.

  تشغيل القدرات المتعلقة بالملفات
تسمح قدرات معينة لعملية بتجاوز قيود مختلفة يُفرضها النواة عند تنفيذ عمليات على ملفات مملوكة لمستخدمين أو مجموعات أخرى. هذه القدرات هي: CAP_CHOWN، CAP_DAC_OVERRIDE، CAP_DAC_READ_SEARCH، CAP_FOWNER، و CAP_FSETID.

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

•   تمتلك العملية القدرة الفعالة ذات الصلة في مساحة المستخدم الخاصة بها؛ و

•   كل من معرف المستخدم ومعرف المجموعة للملف لهما رسوم صالحة في مساحة المستخدم.

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

  برامج set-user-ID و set-group-ID
عندما تنفذ عملية داخل مساحة مستخدم برنامج set-user-ID (set-group-ID)، يُغيَّر معرف المستخدم (المجموعة) الفعال للعملية داخل المساحة إلى أي قيمة مُرسَمة لمعرف المستخدم (المجموعة) للملف. ومع ذلك، إذا كان إما معرف المستخدم أو معرف المجموعة للملف ليس له رسم داخل المساحة، يُتجاهل بت set-user-ID (set-group-ID) بصمت: يُنفَّذ البرنامج الجديد، لكن يبقى معرف المستخدم (المجموعة) الفعال للعملية دون تغيير. (يعكس هذا دلالات تنفيذ برنامج set-user-ID أو set-group-ID الموجود على نظام ملفات تم وصله مع علامة MS_NOSUID، كما هو موصوف في mount(2).)

  متفرقات
عندما تُمرَّر معرفات المستخدم والمجموعة لعملية عبر مقبس نطاق UNIX إلى عملية في مساحة مستخدم مختلفة (انظر وصف SCM_CREDENTIALS في unix(7))، تُترجَم إلى القيم المقابلة وفقًا لرسوم معرفات المستخدم والمجموعة للعملية المُستقبِلة.

المعايير
لينكس.

ملاحظات
على مر السنين، أُضيفت العديد من الميزات إلى نواة Linux والتي أصبحت متاحة فقط للمستخدمين المُمتازين بسبب قدرتها على إرباك تطبيقات set-user-ID-root. بشكل عام، يصبح من الآمن السماح للمستخدم الجذر في مساحة مستخدم باستخدام تلك الميزات لأنه من المستحيل، أثناء وجودك في مساحة مستخدم، الحصول على امتياز أكثر مما يمتلكه المستخدم الجذر لمساحة المستخدم.

  الجذر العام
يُستخدم المصطلح "جذر عام" أحيانًا كاختصار لمعرف المستخدم 0 في مساحة المستخدم الأولية.

  التوفر
يتطلب استخدام مساحات المستخدم نواة مُهيأة بخيار CONFIG_USER_NS. تتطلب مساحات المستخدم دعمًا في مجموعة من الأنظمة الفرعية عبر النواة. عندما يُهيأ نظام فرعي غير مدعوم في النواة، لا يمكن تهيئة دعم مساحات المستخدم.

اعتبارًا من Linux 3.8، دعمت معظم الأنظمة الفرعية ذات الصلة مساحات المستخدم، لكن عددًا من أنظمة الملفات لم تمتلك البنية التحتية اللازمة لرسم معرفات المستخدم والمجموعة بين مساحات المستخدم. أضاف Linux 3.9 دعم البنية التحتية المطلوبة للعديد من أنظمة الملفات غير المدعومة المتبقية (Plan 9 (9P)، Andrew File System (AFS)، Ceph، CIFS، CODA، NFS، و OCFS2). أضاف Linux 3.12 دعمًا لآخر أنظمة الملفات الرئيسية غير المدعومة، XFS.

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

أولاً، ننظر إلى بيئة وقت التشغيل:

    $ uname -rs;     # Need Linux 3.8 or later
    Linux 3.8.0
    $ id -u;         # Running as unprivileged user
    1000
    $ id -g;
    1000

الآن ابدأ صدفة جديدة في مساحات مستخدم جديدة (-U)، وصل (-m)، و PID (-p)، مع معرف مستخدم (-M) ومعرف مجموعة (-G) 1000 مرسوم إلى 0 داخل مساحة المستخدم:

    $ ./userns_child_exec -p -m -U -M '0 1000 1' -G '0 1000 1' bash;

تمتلك الصدفة PID 1، لأنها أول عملية في مساحة PID الجديدة:

    bash$ echo $$;
    1

وصل نظام ملفات /proc جديد وسرد جميع العمليات المرئية في فضاء PID الجديد يُظهر أن الصدفة لا تستطيع رؤية أي عمليات خارج فضاء PID:

    bash$ mount -t proc proc /proc;
    bash$ ps ax;
      PID TTY      STAT   TIME COMMAND
        1 pts/3    S      0:00 bash
       22 pts/3    R+     0:00 ps ax

داخل فضاء المستخدم، تمتلك الصدفة معرف المستخدم والمجموعة 0، ومجموعة كاملة من القدرات المسموح بها والفعالة:

    bash$ cat /proc/$$/status | egrep '^[UG]id';
    Uid:	0	0	0	0
    Gid:	0	0	0	0
    bash$ cat /proc/$$/status | egrep '^Cap(Prm|Inh|Eff)';
    CapInh:	0000000000000000
    CapPrm:	0000001fffffffff
    CapEff:	0000001fffffffff

  مصدر البرنامج
    /* userns_child_exec.c

       Licensed under GNU General Public License v2 or later

       Create a child process that executes a shell command in new
       namespace(s); allow UID and GID mappings to be specified when
       creating a user namespace.
    */
    #define _GNU_SOURCE
    #include <err.h>
    #include <sched.h>
    #include <unistd.h>
    #include <stdint.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <signal.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <limits.h>
    #include <errno.h>

    struct child_args {
        char **argv;        /* Command to be executed by child, with args */
        int    pipe_fd[2];  /* Pipe used to synchronize parent and child */
    };

    static int verbose;

    static void
    usage(char *pname)
    {
        fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname);
        fprintf(stderr, "Create a child process that executes a shell "
                "command in a new user namespace,\n"
                "and possibly also other new namespace(s).\n\n");
        fprintf(stderr, "Options can be:\n\n");
    #define fpe(str) fprintf(stderr, "    %s", str);
        fpe("-i          New IPC namespace\n");
        fpe("-m          New mount namespace\n");
        fpe("-n          New network namespace\n");
        fpe("-p          New PID namespace\n");
        fpe("-u          New UTS namespace\n");
        fpe("-U          New user namespace\n");
        fpe("-M uid_map  Specify UID map for user namespace\n");
        fpe("-G gid_map  Specify GID map for user namespace\n");
        fpe("-z          Map user's UID and GID to 0 in user namespace\n");
        fpe("            (equivalent to: -M '0 <uid> 1' -G '0 <gid> 1')\n");
        fpe("-v          Display verbose messages\n");
        fpe("\n");
        fpe("If -z, -M, or -G is specified, -U is required.\n");
        fpe("It is not permitted to specify both -z and either -M or -G.\n");
        fpe("\n");
        fpe("Map strings for -M and -G consist of records of the form:\n");
        fpe("\n");
        fpe("    ID-inside-ns   ID-outside-ns   size\n");
        fpe("\n");
        fpe("A map string can contain multiple records, separated"
            " by commas;\n");
        fpe("the commas are replaced by newlines before writing"
            " to map files.\n");

        exit(EXIT_FAILURE);
    }

    /* Update the mapping file 'map_file', with the value provided in
       'mapping', a string that defines a UID or GID mapping.  A UID or
       GID mapping consists of one or more newline-delimited records
       of the form:

           ID_inside-ns    ID-outside-ns   size

       Requiring the user to supply a string that contains newlines is
       of course inconvenient for command-line use.  Thus, we permit the
       use of commas to delimit records in this string, and replace them
       with newlines before writing the string to the file.  */

    static void
    update_map(char *mapping, char *map_file)
    {
        int fd;
        size_t map_len;     /* Length of 'mapping' */

        /* Replace commas in mapping string with newlines.  */

        map_len = strlen(mapping);
        for (size_t j = 0; j < map_len; j++)
            if (mapping[j] == ',')
                mapping[j] = '\n';

        fd = open(map_file, O_RDWR);
        if (fd == -1) {
            fprintf(stderr, "ERROR: open %s: %s\n", map_file,
                    strerror(errno));
            exit(EXIT_FAILURE);
        }

        if (write(fd, mapping, map_len) != map_len) {
            fprintf(stderr, "ERROR: write %s: %s\n", map_file,
                    strerror(errno));
            exit(EXIT_FAILURE);
        }

        close(fd);
    }

    /* Linux 3.19 made a change in the handling of setgroups(2) and
       the 'gid_map' file to address a security issue.  The issue
       allowed *unprivileged* users to employ user namespaces in
       order to drop groups.  The upshot of the 3.19 changes is that
       in order to update the 'gid_maps' file, use of the setgroups()
       system call in this user namespace must first be disabled by
       writing "deny" to one of the /proc/PID/setgroups files for
       this namespace.  That is the purpose of the following function.  */

    static void
    proc_setgroups_write(pid_t child_pid, char *str)
    {
        char setgroups_path[PATH_MAX];
        int fd;

        snprintf(setgroups_path, PATH_MAX, "/proc/%jd/setgroups",
                (intmax_t) child_pid);

        fd = open(setgroups_path, O_RDWR);
        if (fd == -1) {

            /* We may be on a system that doesn't support
               /proc/PID/setgroups.  In that case, the file won't exist,
               and the system won't impose the restrictions that Linux 3.19
               added.  That's fine: we don't need to do anything in order
               to permit 'gid_map' to be updated.

               However, if the error from open() was something other than
               the ENOENT error that is expected for that case, let the
               user know.  */

            if (errno != ENOENT)
                fprintf(stderr, "ERROR: open %s: %s\n", setgroups_path,
                    strerror(errno));
            return;
        }

        if (write(fd, str, strlen(str)) == -1)
            fprintf(stderr, "ERROR: write %s: %s\n", setgroups_path,
                strerror(errno));

        close(fd);
    }

    static int              /* Start function for cloned child */
    childFunc(void *arg)
    {
        struct child_args *args = arg;
        char ch;

        /* Wait until the parent has updated the UID and GID mappings.
           See the comment in main().  We wait for end of file on a
           pipe that will be closed by the parent process once it has
           updated the mappings.  */

        close(args->pipe_fd[1]);    /* Close our descriptor for the write
                                       end of the pipe so that we see EOF
                                       when parent closes its descriptor.  */
        if (read(args->pipe_fd[0], &ch, 1) != 0) {
            fprintf(stderr,
                    "Failure in child: read from pipe returned != 0\n");
            exit(EXIT_FAILURE);
        }

        close(args->pipe_fd[0]);

        /* Execute a shell command.  */

        printf("About to exec %s\n", args->argv[0]);
        execvp(args->argv[0], args->argv);
        err(EXIT_FAILURE, "execvp");
    }

    #define STACK_SIZE (1024 * 1024)

    static char child_stack[STACK_SIZE];    /* Space for child's stack */

    int
    main(int argc, char *argv[])
    {
        int flags, opt, map_zero;
        pid_t child_pid;
        struct child_args args;
        char *uid_map, *gid_map;
        const int MAP_BUF_SIZE = 100;
        char map_buf[MAP_BUF_SIZE];
        char map_path[PATH_MAX];

        /* Parse command-line options.  The initial '+' character in
           the final getopt() argument prevents GNU-style permutation
           of command-line options.  That's useful, since sometimes
           the 'command' to be executed by this program itself
           has command-line options.  We don't want getopt() to treat
           those as options to this program.  */

        flags = 0;
        verbose = 0;
        gid_map = NULL;
        uid_map = NULL;
        map_zero = 0;
        while ((opt = getopt(argc, argv, "+imnpuUM:G:zv")) != -1) {
            switch (opt) {
            case 'i': flags |= CLONE_NEWIPC;        break;
            case 'm': flags |= CLONE_NEWNS;         break;
            case 'n': flags |= CLONE_NEWNET;        break;
            case 'p': flags |= CLONE_NEWPID;        break;
            case 'u': flags |= CLONE_NEWUTS;        break;
            case 'v': verbose = 1;                  break;
            case 'z': map_zero = 1;                 break;
            case 'M': uid_map = optarg;             break;
            case 'G': gid_map = optarg;             break;
            case 'U': flags |= CLONE_NEWUSER;       break;
            default:  usage(argv[0]);
            }
        }

        /* -M or -G without -U is nonsensical */

        if (((uid_map != NULL || gid_map != NULL || map_zero) &&
                    !(flags & CLONE_NEWUSER)) ||
                (map_zero && (uid_map != NULL || gid_map != NULL)))
            usage(argv[0]);

        args.argv = &argv[optind];

        /* We use a pipe to synchronize the parent and child, in order to
           ensure that the parent sets the UID and GID maps before the child
           calls execve().  This ensures that the child maintains its
           capabilities during the execve() in the common case where we
           want to map the child's effective user ID to 0 in the new user
           namespace.  Without this synchronization, the child would lose
           its capabilities if it performed an execve() with nonzero
           user IDs (see the capabilities(7) man page for details of the
           transformation of a process's capabilities during execve()).  */

        if (pipe(args.pipe_fd) == -1)
            err(EXIT_FAILURE, "pipe");

        /* Create the child in new namespace(s).  */

        child_pid = clone(childFunc, child_stack + STACK_SIZE,
                          flags | SIGCHLD, &args);
        if (child_pid == -1)
            err(EXIT_FAILURE, "clone");

        /* Parent falls through to here.  */

        if (verbose)
            printf("%s: PID of child created by clone() is %jd\n",
                    argv[0], (intmax_t) child_pid);

        /* Update the UID and GID maps in the child.  */

        if (uid_map != NULL || map_zero) {
            snprintf(map_path, PATH_MAX, "/proc/%jd/uid_map",
                    (intmax_t) child_pid);
            if (map_zero) {
                snprintf(map_buf, MAP_BUF_SIZE, "0 %jd 1",
                        (intmax_t) getuid());
                uid_map = map_buf;
            }
            update_map(uid_map, map_path);
        }

        if (gid_map != NULL || map_zero) {
            proc_setgroups_write(child_pid, "deny");

            snprintf(map_path, PATH_MAX, "/proc/%jd/gid_map",
                    (intmax_t) child_pid);
            if (map_zero) {
                snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1",
                        (intmax_t) getgid());
                gid_map = map_buf;
            }
            update_map(gid_map, map_path);
        }

        /* Close the write end of the pipe, to signal to the child that we
           have updated the UID and GID maps.  */

        close(args.pipe_fd[1]);

        if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */
            err(EXIT_FAILURE, "waitpid");

        if (verbose)
            printf("%s: terminating\n", argv[0]);

        exit(EXIT_SUCCESS);
    }

انظر أيضًا
newgidmap(1)، newuidmap(1)، clone(2)، ptrace(2)، setns(2)، unshare(2)، proc(5), subgid(5)، subuid(5)، capabilities(7)، cgroup_namespaces(7)، credentials(7)، namespaces(7)، pid_namespaces(7)

ملف مصدر النواة Documentation/admin-guide/namespaces/resource-control.rst.

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

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

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