
BPF-LSM تخفيف لثغرة CVE-2026-31431 (Copy Fail) — يمنع إنشاء مقابس AF_ALG على مستوى المجموعة
تخفيف BPF-LSM لـ CVE-2026-31431 ("Copy Fail") و
متغير RxRPC من Dirty Frag،
ولثغرات تصعيد الامتيازات المماثلة التي تعتمد على وصول مساحة المستخدم
إلى مسار تشفير داخل النواة قابل للوصول عبر عائلتي مآخذ AF_ALG أو
AF_RXRPC.
تقوم DaemonSet صغيرة بإرفاق برنامج BPF-LSM واحد بخطاف socket_create
على كل عقدة. يُرجع البرنامج -EPERM لأي استدعاء من مساحة المستخدم
socket(AF_ALG, ...) أو socket(AF_RXRPC, ...)، بغض النظر عن
صلاحيات العملية أو النطاق (namespace) أو ملف تعريف seccomp. يُسمح
للمستدعين داخل النواة sock_create_kern() (مثل fs/afs، حزمة IPsec)
بالمرور، لذا تستمر الاستخدامات المشروعة داخل النواة في العمل.
تم اختباره على Talos Linux (الذي يأتي مع CONFIG_BPF_LSM=y و bpf في
حزمة LSM الافتراضية منذ v1.10)، ويعمل على أي توزيعة بنفس إعدادات النواة.
Copy Fail (CVE-2026-31431) هو خلل منطقي في algif_aead يسمح
لمستخدم محلي غير مميز بإجراء كتابة بحجم 4 بايت في ذاكرة التخزين المؤقت للصفحات (page-cache) لأي ملف ثنائي setuid، محققًا صلاحيات الجذر بسكربت Python بحجم 732 بايت. لا يحتاج الاستغلال سوى AF_ALG + splice()، وكلاهما قابل للوصول من أي عملية غير مميزة افتراضيًا. الإصلاح الرئيسي هو a664bf3d603d.
Dirty Frag هو فئة ثغرات لاحقة تم الكشف عنها في مايو 2026 بواسطة
نفس الخط البحثي. يربط خللين "يلوّثان" عضو frag في
sk_buff — xfrm-ESP Page-Cache Write و RxRPC Page-Cache Write. يقوم
متغير RxRPC بفك تشفير pcbc(fcrypt) في المكان على صفحة ذاكرة تخزين مؤقت مثبتة بـ splice() داخل rxkad_verify_packet_1() ويصل إلى الجذر دون
الحاجة إلى إنشاء نطاق مستخدم، مما يجعله النصف الأكثر قابلية للاستغلال عالميًا في السلسلة على التوزيعات المحصّنة. تم إصدار إصلاح xfrm-ESP في netdev كـ f4c50a4034e6 (2026-05-07)؛ ولا تزال التوزيعات
تقوم بالترحيل الخلفي وقت كتابة هذا النص، ولا يوجد إصلاح عام لـ RxRPC حتى الآن — انظر
التقرير العلني
للجدول الزمني للكشف.
يعتمد كلا الاستغلالين على فتح مأخذ في العائلة المتأثرة. حتى تصل إصلاحات النواة إلى توزيعتك، يمكن إزالة سطح الهجوم
بمنع مساحة المستخدم من إنشاء مآخذ AF_ALG أو AF_RXRPC على الإطلاق.
مقارنة بالبدائل:
هذا المشروع هو الخيار بدون إعادة تشغيل. شغّله على مستوى الكتلة، ثم خطط لإصلاحات النواة الدائمة في جدول التصحيح المعتاد.
ملاحظة حول متغير ESP من Dirty Frag. نصف
xfrm-ESP Page-Cache Writeمن Dirty Frag لا يُغلق بواسطة هذه DaemonSet — يتم تشغيله عبر XFRM netlink +UDP_ENCAP_ESPINUDP، وليس عبر عائلة مآخذ مخصصة، ومرشح BPF-LSM نظيف له إما سيكسر IPsec المشروع على المضيف أو يتطلب منطقًا واعيًا بنطاقات المستخدم. غير متتبع حاليًا هنا — المساهمات مرحب بها. على التوزيعات المحصّنة التي تمنع نطاقات المستخدم غير المميزة (مثل سياسة AppArmor الافتراضية في Ubuntu)، يكون متغير ESP غير قابل للوصول من الأساس ويكون حظر RxRPC هنا كافيًا.
bpf/blocker.c هو برنامج BPF-LSM قصير:
SEC("lsm/socket_create")
int BPF_PROG(block_socket_family, int family, int type, int protocol,
int kern, int ret)
{
if (ret)
return ret;
/* kern != 0 يعني sock_create_kern() — اسمح للمستدعين داخل النواة بالمرور. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
محمّل Go (main.go، حوالي 40 سطرًا) يحمّل البرنامج ويرفقه عبر
bpf(BPF_LINK_CREATE). يتم الاحتفاظ بالرابط طوال عمر البود. عند
SIGTERM، يُغلق الرابط وينفصل الخطاف.
يتطلب نواة مبنية مع CONFIG_BPF_LSM=y و bpf في حزمة LSM النشطة
(lsm=...,bpf في سطر أوامر النواة). يأتي Talos Linux مع
كلاهما مفعّلًا افتراضيًا منذ v1.10.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
لأحدث commit على main (قد يتضمن تغييرات غير منشورة):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
الـ chart غير منشور كأداة OCI (مسار السجل مشترك مع صورة الحاوية). ثبّت من checkout موسوم:
git clone --branch v0.3.0 https://github.com/cozystack/copy-fail-blocker
cd copy-fail-blocker
helm upgrade --install copy-fail-blocker charts/copy-fail-blocker \
--namespace kube-system
أو عبر اختصارات Makefile:
make apply # helm upgrade --install إلى kube-system
make diff # معاينة التغييرات مقابل الكتلة
make delete # إلغاء التثبيت
make manifest # إعادة توليد manifests/copy-fail-blocker.yaml
يجب أن تعمل DaemonSet بصلاحيات مميزة (تحمّل برامج BPF وتكتب إلى
bpffs). ضعها في نطاق مع معيار أمان Pod المميز،
أو في kube-system، وهو مميز افتراضيًا.
من أي بود على عقدة مغطاة:
python3 -c '
import errno, socket
# مرر كل عائلة بنوع يدعمه create() الخاص بالعائلة فعليًا
# (AF_ALG → SOCK_SEQPACKET، AF_RXRPC → SOCK_DGRAM) بحيث على
# عقدة بدون هذا الخطاف سينجح الاستدعاء (FAIL: تم إنشاء المأخذ)
# أو يفشل برمز خطأ غير EPERM — كلاهما يظهر كـ FAIL أدناه.
# مع تفعيل الخطاف، يُرجع security_socket_create() -EPERM قبل
# تشغيل pf->create()، لذا لا يهم النوع؛ ما زلنا نمرر
# النوع الصحيح لإبقاء تشخيص FAIL غير غامض.
for name, family, stype in [("AF_ALG", 38, socket.SOCK_SEQPACKET),
("AF_RXRPC", 33, socket.SOCK_DGRAM)]:
try:
socket.socket(family, stype, 0)
print(f"FAIL: {name} socket created")
except OSError as e:
if e.errno == errno.EPERM:
print(f"OK ({name}): blocked with EPERM")
else:
print(f"FAIL: {name} got {e.errno} ({e.strerror}), expected EPERM")'
المخرجات المتوقعة:
OK (AF_ALG): blocked with EPERM
OK (AF_RXRPC): blocked with EPERM
أي رمز خطأ آخر (مثل ESOCKTNOSUPPORT 94، EAFNOSUPPORT 97) يعني أن
الخطاف غير نشط على تلك العقدة — حقّق قبل افتراض أنك
مغطى.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # وسم مخصص
make image PUSH=0 LOAD=1 # بناء محليًا بدون push
make image يحدّث charts/copy-fail-blocker/values.yaml بـ
digest الصورة المُحلّ بحيث يثبّت الـ chart دائمًا بالـ digest.
تبعيات البناء موجودة في Containerfile (clang، libbpf-dev، Go). يحتاج
المضيف المحلي فقط docker buildx، helm، yq (mikefarah)، kubectl، و
helm-diff.
charts/copy-fail-blocker/values.yaml:
AF_ALG و AF_RXRPC من مساحة المستخدم
قابلين للوصول مجددًا. لمعظم نماذج التهديد هذا مقبول؛ إذا لم يكن كذلك،
فكر في تثبيت رابط BPF إلى bpffs (غير منفذ حاليًا —
المساهمات مرحب بها).CAP_BPF و CAP_SYS_ADMIN على المضيف يمكنه فصل
الخطاف. هذا ليس بديلًا عن قيود الصلاحيات على مستوى الكتلة.algif_skcipher / algif_hash / إلخ. يرفض البرنامج
عائلة AF_ALG بأكملها، لكن algif_aead فقط معروف حاليًا
بأنه قابل للاستغلال. إذا تطلب CVE مستقبلي مرشحًا أدق (مثل خطاف
bind() وفحص salg_type)، فمن السهل إضافته.AF_ALG أو AF_RXRPC
مفتوحًا. تستمر المآخذ الموجودة في العمل حتى تُغلق.رخصة Apache 2.0 — انظر LICENSE.
| التخفيف | التغطية | إعادة تشغيل؟ | دائم؟ |
|---|
سطر أوامر النواة module_blacklist=af_alg,rxrpc (معالجات العائلة، وليس فقط algif_aead) | على مستوى المضيف | نعم | نعم |
/etc/modprobe.d/*.conf مع install af_alg /bin/false + install rxrpc /bin/false و rmmod للوحدات المحمّلة بالفعل (يطابق إرشادات Dirty Frag العلنية — لاحظ أن blacklist العادي لا يوقف التحميل التلقائي request_module() داخل النواة، فقط install … /bin/false يفعل ذلك) | على مستوى المضيف | لا | نعم (طالما الملف موجود) |
نواة مخصصة بدون CRYPTO_USER_API / AF_RXRPC | على مستوى المضيف | نعم | نعم |
| ملف تعريف seccomp مخصص لكل بود | فقط أحمال العمل الموسومة | لا | نعم |
| copy-fail-blocker (هذا المشروع) | مساحة مستخدم على مستوى المضيف | لا | طالما تعمل DS |
| المفتاح | الافتراضي | ملاحظات |
|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | يتم تحديثه تلقائيًا بواسطة make image |
image.tag | vX.Y.Z@sha256:... | مثبّت بالـ digest، القيمة الحالية في values.yaml |
priorityClassName | system-node-critical | يضمن بقاء الـ daemon عند عمليات الإخلاء |
tolerations | [{operator: Exists}] | يعمل على كل عقدة، بما فيها الملوّثة |
resources.requests | 5m CPU / 16Mi memory | البصمة الخاملة بعد الإرفاق |
UDP_ENCAP_ESPINUDP، وليس عبر عائلة مآخذ
مخصصة — غير متتبع حاليًا، المساهمات مرحب بها. انظر الملاحظة في
لماذا.AF_RXRPC من مساحة المستخدم محظورون. RxRPC هو بروتوكول
شبكة AFS. حارس !kern يعني أن وحدة fs/afs (kAFS) داخل الشجرة
تستمر في العمل لأنها تفتح مآخذها عبر sock_create_kern().
أدوات AFS من مساحة المستخدم (مثل خوادم OpenAFS من مساحة المستخدم) التي تفتح
مأخذ AF_RXRPC مباشرة عبر socket(2) سيُرفض طلبها — لا
تنشر هذه DaemonSet على عقد تشغّل مثل هذه الأدوات.