محمدرضا عبدی
محمدرضا عبدی
خواندن ۳ دقیقه·۴ سال پیش

بررسی ویژگی بروزرسانی kernel لینوکس 5.2: افزودن CLONE_PIDFD به فراخوانی سیستمی clone

هر پردازه‌ (process) ای که در kernel تولید می‌شود، تا لحظه‌ای که توسط پردازه دیگری kill شود یا خود exit کند یک شناسه یکتایی به نام pid دارد که می‌توان با استفاده از آن به پردازه مورد نظر دسترسی داشت. مقدار pid یک عدد صحیح بدون علامت از 0 تا 32767 می‌تواند باشد. از آنجایی که این مقدار در kernel محدود می‌باشد. kernel مجبور به استفاده چندین باره از یک pid می‌باشد. (pid recycling)

اگر تنها از pid برای مدیریت پردازه ها استفاده شود ممکن است خطا‌های پیچیده و انواع شرایط مسابقه (race condition) به وجود آیند. به عنوان مثال فرض کنید که پردازه A با pid برابر با 100 در kernel وجود دارد، یک پردازه B نیز وجود دارد که pid A را داشته و می‌خواهد برای او سیگنالی بفرستد (مثلا SIGKILL). حال اگر پردازه A قبل از رسیدن سیگنال پردازه B خود exit کند یا توسط پردازه دیگری kill شود و در همان حال پردازه C ای در حال ساخته شدن باشد و pid برابر با 100 را بگیرد، سیگنال پردازه B به پردازه C فرستاده می‌شود و به اشتباه kill می‌شود. بنابراین یکی از مشکلات اصلی این است، یک پردازه غیر پدری که به pid پردازه دیگر دسترسی دارد نمی‌تواند صد در صد مطمئن باشد که آن pid، به پردازه مدنظر اشاره می‌کند. این مشکل می‌تواند به عنوان یک حمله امنیتی منجر شود که یکی از نمونه‌های آن در این پست قابل مشاهده است. (پردازه‌ای که پردازه دیگر را می‌سازد، پدر آن محسوب می‌شود.)

به صورت کلی توصیفگر پرونده (file descriptor) ها در kernel همانند دستگیره (handle) برای دسترسی به یک فایل یا سایر منابع IO (مثل pipe یا socket های شبکه) که از قبل باز (open) شده‌اند، عمل می‌کنند. توصیفگر پرونده‌ها یک عدد صحیح می‌باشند و به صورت یکتا در یک جدول در فضای kernel ذخیره می‌شوند.

برای حل مشکل ذکر شده به همراه سایر مسائل دیگر در مدیریت پردازه‌ها در kernel، از pidfd استفاده می‌شود. pidfd یک توصیفگر پرونده می‌باشد که به طور کلی به یک پردازه اشاره می‌کند (در واقع به thread group leader اشاره می‌کند) و زمانی مقدار دهی می‌شود که پوشه مربوط به یک پردازه با pid مورد نظر در مسیر <proc/<pid/ در proc file system (یا به اختصار procfs) باز شود. تمامی اطلاعات مورد نیاز برای یک پردازه در حال اجرا، در همین پوشه قرار دارد.

دقت کنید که برای ساختن pidfd جدول توصیفگر پرونده ها در kernel را به حالت unshared می‌بریم تا هنگام ساختن pidfd، شرایط مسابقه دیگری فراهم نشود و pidfd مخصوص برای پردازه مورد نظر ساخته شود.

بنابراین pidfd یک دستگیره پایدار و خصوصی به یک پردازه می‌باشد و تا زمانی اعتبار دارد که پردازه مدنظر زنده باشد و به محض exit کردن یا kill شدن آن پردازه، آن pidfd مربوطه نیز بسته می‌شود.

پیاده سازی واسط برنامه‌نویسی (API) pidfd در kernel نسخه 5، در 4 نسخه 5.1 تا 5.4 صورت گرفته که به ترتیب شامل موارد زیر می‌شود:

1) 5.1: استفاده از pidfd برای ارسال سیگنال به یک پردازه:

به صورت خاص فراخوانی‌ سیستمی pidfd_send_signal پیاده‌سازی شد تا از pidfd به عنوان یک دستگیره قابل اطمینان برای ارسال سیگنال به یک پردازه‌ای که وجود دارد استفاده شود. نمونه استفاده از آن را در این لینک می‌توان مشاهده نمود.

2) 5.2: تابع clone مقدار pidfd را برگرداند:

فراخوانی clone یکی از فراخوانی‌های سیستمی مهم پیاده‌سازی شده در kernel می‌باشد که به نوعی یک واسط‌ (interface) برای سایر فراخوانی‌های سیستمی مربوط به ساختن یک پردازه از پردازه دیگری محسوب می‌شود. به عنوان مثال می‌توان با استفاده از این تابع، یک پردازه‌ای از پردازه دیگری ساخت که تنها توصیفگر پرونده آن متفاوت باشد یا pid یکسانی با پردازه پدر داشته باشد. این تابع تعدادی پرچم (flag) به عنوان ورودی می‌گیرد که با استفاده از آن‌ها می توان یک پردازه، thread یا چیزی بین این دو ساخت. لازم به ذکر است که در نسخه‌های امروزی kernel، توابع fork و pthread_create نیز به وسیله clone پیاده‌سازی شده‌اند.

در این نسخه پرچم CLONE_PIDFD به پرچم‌های clone اضافه شده است که با استفاده از آن، این تابع در هنگام ساخت یک پردازه از پردازه دیگری، توصیفگر پرونده‌ای می‌سازد که به آن پردازه اشاره می‌کند و به کاربر برمی‌گرداند. (در واقع توصیفگر پرونده به مسیر <proc/<pid/ اشاره می‌کند.)

دلایل این گونه تغییر در ساختار clone به صورت زیر می‌باشد:

1- بلافاصله بعد از ساختن پردازه مدنظر، pidfd آن در اختیار پردازه پدر قرار گرفته و در استفاده از pidfd_send_signal بسیار کارمان را راحت می‌کند.

2- می‌توانستیم به‌جای ساختن توصیفگر پرونده مربوط به پردازه در clone، آن را به صورت جدا و با فراخوانی‌ سیستمی pidfd_open انجام دهیم (این فراخوانی با pid گرفته شده، توصیفگر پرونده مربوط به پردازه مورد نظر را می‌سازد که به مسیر <proc/<pid/ اشاره می‌کند). ولی باز هم امکان بروز شرایط مسابقه وجود داشت. فرض کنید که پردازه A وجود دارد و بخواهد pidfd مربوط به پردازه B را دریافت کند، در هنگام صدا زدن و اجرای pidfd_open توسط پردازه A، چندین بار context switch رخ داده و پردازه C ای، پردازه B را kill کند و pidfd B بلافاصله توسط پردازه دیگری مورد استفاده قرار گیرد. لذا ممکن است pidfd ای به A برگردانده شود که مربوط به پردازه B نباشد. بنابراین تنها جایی که واقعا می‌توان مطمئن بود که توصیفگر پرونده مربوط به پردازه مورد نظر دریافت شود و هیچ شرایط مسابقه‌ای نباشد، زمان ساختن خود پردازه می‌باشد. بنابراین ساختار فراخوانی سیستمی clone بدین نحو تغییر کرد. (در context switch وضعیت thread در حال اجرا روی پردازنده در حافظه ذخیره می‌شود و اجرای آن متوقف می‌شود. سپس وضعیت thread دیگری از حافظه بازیابی شده و اجرای آن روی پردازنده ادامه می‌یابد.)

3) 5.3: استفاده از pidfd برای حل مشکلات مربوط به pid reuse در kernel:

با توجه به روز رسانی‌ 5.2، هنوز هم بسیاری از پردازه‌ها در کد kernel وجود دارند که با fork یا clone ساخته می‌شوند، بدون اینکه از قابلیت CLONE_PIDFD استفاده کنند. در نتیجه پردازه‌های غیر پدر نمی‌توانند در مورد وضعیت دقیق یک پردازه دیگر، تنها با داشتن pid آن مطمئن باشند. در این نسخه pidfd_open پیاده‌سازی شد تا بتوان با آن pidfd پردازه‌هایی که با پرچم CLONE_PIDFD ساخته نشده‌اند را دریافت کرد. همچنین در این نسخه polling support برای pidfd نیز پیاده‌سازی شد. در نتیجه پردازه های غیر پدر می‌توانند همانند پردازه‌های پدر از وضعیت زنده بودن یا نبودن یک پردازه دیگر با خبر شوند.

4) 5.4: قابلیت wait کردن روی pidfd:

به طور خاص به تابع waitid یک پرچم P_PIDFD اضافه شد که امکان منتظر ماندن برای یک پردازه با استفاده از pidfd آن فراهم می‌شد. حال با استفاده از ویژگی این نسخه و ویژگی‌های نسخه‌های قبلی، می‌توان به طور کامل پردازه‌ها را تنها با استفاده از pidfd مدیریت کرد.

نحوه پیاده‌سازی

در این پست ما به تغییرات جدید clone در kernel 5.2 می‌پردازیم. برای این امر 5 commit در git.kernel صورت گرفته است که به بررسی بخش‌های مهم کد در هر یک می‌پردازیم:

توضیحات commit 1: در این commit برخی از پرونده‌های config به منظور پیاده‌سازی تغییر کرده‌اند که در این پست به بررسی آن نمی‌پردازیم.

توضیحات commit 2: شالوده اصلی پیاده‌سازی در این commit می‌باشد. پرچم CLONE_PIDFD به clone اضافه شده است. به بررسی کد‌های زده شده در این بخش خواهیم پرداخت:

static int pidfd_release(struct inode *inode, struct file *file) { struct pid *pid = file->private_data; file->private_data = NULL; put_pid(pid); return 0; }

تابع release از عملیات‌های مربوط به کار با ساختار (struct) file در سطح kernel می‌باشد.(این ساختار متفاوت با FILE در سمت برنامه‌های کاربری می‌باشد. این ساختار به نوعی دید kernel از یک device را نشان می‌دهد و از آن برای ارتباط با driver ها استفاده می‌شود.) این تابع می‌بایست هر آنچه در فیلد private_data ی مربوط به file وجود دارد آزاد کرده و از تعداد دفعات استفاده (usage count) آن file یکی کم کند. در نهایت نیز اگر تعداد دفعات استفاده صفر بود، device مربوط به file را خاموش (shut down) می‌کند. حال در تابع بالا، release برای pidfd په صورت اختصاصی پیاده‌سازی شده است. فیلد private_data در واقع معادل با pidfd می‌باشد. همچنین تابع put_pid تقریبا همان کار کم کردن استفاده‌ها از pidfd را انجام می‌دهد‌ (علاوه بر آن کارهای دیگری نیز انجام می‌دهد).

#ifdef CONFIG_PROC_FS static void pidfd_show_fdinfo(struct seq_file *m, struct file *f) { struct pid_namespace *ns = proc_pid_ns(file_inode(m->file)); struct pid *pid = f->private_data; seq_put_decimal_ull(m, &quotPid:\t&quot, pid_nr_ns(pid, ns)); seq_putc(m, '\n'); } #ifdef

در kernel تابع show_fdinfo وجود دارد که به ازای هر توصیفگر پرونده، اطلاعات مخصوص به آن را در پرونده دیگری ذخیره کرد. حال با تعریف پرچم kernel ،CONFIG_PROC_FS هایی که از procfs پشتیبانی می‌کنند می‌توانند با استفاده از تابع بالا اطلاعات اضافه‌ای را در ارتباط با pidfd در پرونده دیگری ذخیره کنند.

static int pidfd_create(struct pid *pid) { int fd; fd = anon_inode_getfd(&quot[pidfd]&quot, &pidfd_fops, get_pid(pid), O_RDWR | O_CLOEXEC); if (fd < 0) put_pid(pid); return fd; }

تابع بالا یک pidfd می‌سازد. دقت کنید که pidfd ها بر اساس anonymous inode ها در ساختارهای kernel پیاده‌سازی شده‌اند. (indode در kernel ساختاری است که اطلاعاتی در ارتباط با پوشه‌ها و پرونده‌های باز شده در دیسک فراهم می‌سازد. فرق anonymous inode با inode این است که به هیچ directory entry‌ ای متصل نمی‌باشد. لذا توسط file system قابل شناسایی نمی‌باشد ولی می‌توان روی آن خواندن و نوشتن را انجام داد). تابع anon_inode_getfd نیز توصیفگر پرونده متناسب با anonymous inode مربوط به پردازه مورد نظر می‌سازد. وجود pidfd_fops در آرگومان‌های صدا زده شده در anon_inode_getfd به این معنی است که می‌بایست از عملیات‌هایی که pidfd پشتیبانی می‌کند برای توصیفگر پرونده ساخته شده استفاده شود. getpid نیز مقدار صحیح pid در ساختار pid را برمی‌گرداند. همچنین اگر به پرچم های استفاده شده هنگام صدا زدن anon_inode_getfd دقت کنیم، متوجه خواهیم شد که با پرچم O_CLOEXEC، pidfd بعد از exit شدن پردازه بسته می‌شود و همچنین با پرچم O_RDWR، قابلیت خواندن و نوشتن در inode مورد نظر وجود دارد. اگر عملیات با خطا مواجه شود، یک عدد صحیح منفی را به عنوان خطا باز گردانده می‌شود. دقت کنید زمانی این تابع به درستی کار می‌کند که جدول توصیفگر پرونده‌ها در حالت unshared قرار گیرد تا pidfd مورد نظر به پردازه جدیدی تخصیص داده نشود.

static __latent_entropy struct task_struct *copy_process( ... int __user *parent_tidptr, ... ) { int pidfd = -1, retval; ... if (clone_flags & CLONE_PIDFD) { /*section [1]*/ int reserved; if (clone_flags & (CLONE_DETACHED | CLONE_PARENT_SETTID | CLONE_THREAD)) return ERR_PTR(-EINVAL); if (get_user(reserved, parent_tidptr)) return ERR_PTR(-EFAULT); if (reserved != 0) return ERR_PTR(-EINVAL); } ... if (clone_flags & CLONE_PIDFD) { /*section [2]*/ retval = pidfd_create(pid); if (retval < 0) goto bad_fork_free_pid; pidfd = retval; retval = put_user(pidfd, parent_tidptr); if (retval) goto bad_fork_put_pidfd; } ... bad_fork_put_pidfd: if (clone_flags & CLONE_PIDFD) ksys_close(pidfd); }

تابع copy_process در kernel توسط توابعی مانند _do_fork و init_idle_pids که به صورت مستقیم در clone استفاده می‌شوند، صدا زده می‌شود. این تابع از یک پردازه بر اساس پرچم های داده شده به آن، یک پردازه جدید می‌سازد ولی آن را اجرا نمی‌کند. به آرگومان‌های ورودی این تابع یک parent_tidptr اضافه می‌کنیم که برای قرار دادن pidfd در آن استفاده می‌شود و user__ نشان می‌دهد که این متغیر به متغیر دیگری در فضای کاربر اشاره می‌کند. retval مقداری است که این تابع برمی‌گرداند و برابر با کد وضعیت اتمام تابع (موفقیت آمیز یا با خطا) می‌باشد.

در بخش (1) در صورت وجود داشتن پرچم CLONE_PIDFD، پرچم های CLONE_DETACHED ،CLONE_PARENT_SETTID و CLONE_THREAD بلوکه شده‌اند. اولین پرچم زمانی استفاده می‌شود که بخواهیم pid پردازه فرزند در محلی که متغیر parent_tid به آن اشاره می‌کند، ذخیره شود. استفاده از این پرچم در کنار CLONE_PIDFD معنی ندارد، زیرا pidfd توسط parent_tidptr به محیط کاربر بازگردانده می‌شود. دومین پرچم زمانی استفاده می‌شود که نخواهیم پردازه فرزند موقع خاتمه یافتن (terminate) شدن به پردازه پدر سیگنالی را ارسال کند. لزومی ندازد که این قابلیت هنگام استفاده از CLONE_PIDFD فعال باشد. سومین پرچم زمانی استفاده می‌شود که بخواهیم پردازه فرزند در یک thread group با پردازه پدر قرار گیرد که در اینجا استفاده‌ای ندارد. قسمت بعدی در کل برای چک کردن سالم بودن و قابل استفاده بودن متغیری که parent_tidptr به آن در فضای کاربر اشاره می‌کند، می‌باشد. تابع get_user برای گرفتن مقدار یک متغیر در فضای کاربر می‌باشد. بنابراین مقدار جایی که parent_tidptr به آن اشاره می‌کند را دریافت کرده و در reserved قرار می‌دهد. get_user در صورت اتمام موفقیت آمیز، صفر خروجی می‌دهد.

در بخش (2) در صورت وجود داشتن پرچم CLONE_PIDFD، یک pidfd جدید با تابع pidfd_create می‌سازیم، سپس آن را در فضای کاربر با دستور put_user قرار می‌دهیم. تابع put_user در صورت اتمام موفقیت آمیز، صفر خروجی می‌دهد. اگر در مرحله ذخیره سازی در فضای کاربر با خطا مواجه شویم، به bad_fork_put_pidfd پرش می‌کنیم که در آنجا pidfd باز شده را با فراخوانی سیستمی ksys_close می‌بندیم. دقت کنید که این بخش بعد از unshare کردن جدول توصیفگر پرونده‌ها می‌باشد، برای این که pidfd حاصل به پردازه جدیدی تخصیص داده نشود.

توضیحات commit 3: یک مثال که در آن ابتدا یک پردازه فرزند با دستور clone و پرچم CLONE_PIDFD کپی می‌شود، سپس با دستور sys_pidfd_send_signal به آن سیگنالی فرستاده می‌شود. همچنین بعد از ارسال سیگنال مطمئن می‌شویم که سیگنال مورد نظر به دست پردازه فرزند ساخته شده می‌رسد. در این پست از تحلیل آن صرف نظر می‌کنیم.

توضیحات commit 4: در این commit در فراخوانی‌ سیستمی pidfd_send_signal تغییری ایجاد می‌کنیم تا از تغییرات ایجاد شده در commit‌ های قبلی استفاده کند. این فراخوانی‌ سیستمی در 5.1 kernel به منظور فرستادن سیگنال به پردازه مدنظر بدون شرایط مسابقه تنها با pidfd آن پردازه قرار داده شده بود. در این سیگنال تابع tgid_pidfd_to_pid صدا زده شده است که pidfd را از طریق file ی در procfs به صورت مستقیم دریافت می‌کند. می‌خواهیم این تابع را به گونه‌ای تغییر دهیم که دیگر این تابع وابسته به procfs نباشد و به زبان دیگر کاربران بتوانند بدون اینکه procfs را mount کنند یا اینکه kernel آنها از procfs پشتیبانی کند، از آن استفاده کنند. دقت کنید، تغییراتی که تا به این commit داده‌ایم، نیازی به mount شدن procfs توسط کاربر نداشت. چرا که pidfd ها در ساختار file ذخیره شده اند و بر اساس anonymous inode ها پیاده‌سازی شده‌اند. حال کافی است که تابع tgid_pidfd_to_pid را که در فراخوانی‌ سیستمی pidfd_send_signal استفاده شده است، با تابع زیر جایگزین کنیم:

static struct pid *pidfd_to_pid(const struct file *file) { if (file->f_op == &pidfd_fops) return file->private_data; return tgid_pidfd_to_pid(file); }

در kernel هر ساختار file یک f_op دارد که به ساختار file_operations اشاره می‌کند و نمایانگر عملیات‌هایی است که با آن file می‌توان انجام داد. حال به طور ویژه، f_op می‌تواند نوع (type) یک file را نیز در kernel مشخص کند. از آنجایی که ساختار pidfd_fops معادل با عملیات‌های مربوط به یک pidfd می‌باشد، معادل با نوع pidfd بودن یک file نیز می‌باشد. بنابراین اگر file ی که به این تابع داده می‌باشد از نوع pidfd باشد، می‌توان به جای ارجاع به procfs، مقدار pidfd را از فیلد private_data ی خود بخواند. بنابراین با این روش، وابستگی pidfd_send_signal به procfs از خواهد رفت.

توضیحات commit 5: در این commit تمامی تغییرات داده شده در commit‌ های قبلی در branch دیگری merge می‌شوند.

امیدوارم مطالب بالا برای شما مفید بوده باشد. به طور کلی درک نحوه پیاده سازی kernel لینوکس دشوار می‌باشد، چرا که پیچیدگی‌های بسیاری در آن پیاده‌سازی شده‌اند. در این پست سعی شده مفاهیم بیان شده، مختصر و مفید باشند.

منابع:

https://kernelnewbies.org/Linux_5.1#Safe_signal_delivery_in_presence_of_PID_reuse

https://kernelnewbies.org/Linux_5.2#Let_clone.282.29_return_pidfs

https://kernelnewbies.org/Linux_5.3#New_.27pidfd.27_functionality_to_help_service_managers_to_deal_with_PID_reuse_problems

https://kernelnewbies.org/Linux_5.4#Core_.28various.29

https://kernel-recipes.org/en/2019/talks/pidfds-process-file-descriptors-on-linux/

https://lwn.net/Articles/784831/

https://lwn.net/Articles/773459/

https://lwn.net/ml/linux-kernel/CAG48ez2gb94SqS30Ai4+VBHhnzBp5Po9_u00nMrvUW6Wqq6hPA@mail.gmail.com/

https://asciinema.org/a/IQjuCHew6bnq1cr78yuMv16cy

https://man7.org/linux/man-pages/man2/clone.2.html

https://man7.org/linux/man-pages/man2/pidfd_open.2.html

https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html

https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch03s04.html

https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch03s05.html

https://www.kernel.org/doc/html/v4.13/core-api/kernel-api.html

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=dadd2299ab61fc2b55b95b7b3a8f674cdd3b69c9

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b3e5838252665ee4cfa76b82bdf1198dca81e5be

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=43c6afee48d4d866d5eb984d3a5dbbc7d9b4e7bf

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2151ad1b067275730de1b38c7257478cae47d29e

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=eac7078a0fff1e72cf2b641721e3f55ec7e5e21e

oskernelpidfdlinuxclone
شاید از این پست‌ها خوشتان بیاید