فاز 19 · درس 72

ارزیابی و اجرای امن کد در مدل‌های زبانی با زیرفرایند ایزوله

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
مبانی فاز 19 مسیر B، درس‌های 70 و 71
یک مینیاتور شیشه‌ای ایزوله که درون آن کدهای پایتون در حال آزمایش با یک زمان‌سنج دیجیتالی در بستری ایمن هستند
فهرست مطالب
  1. اهداف یادگیری
  2. چرا زیرفرایند ایزوله؟
  3. ساختار وظیفه‌ی اجرای کد
  4. فهرست سیاه واردات
  5. محدودیت زمانی واقعی
  6. سقف حجم خروجی
  7. معیار Pass-at-k
  8. کدهای خروج
  9. این درس چه کارهایی انجام نمی‌دهد
  10. راهنمای خواندن کد
  11. گام‌های بعدی

مدل‌های زبانی کد می‌نویسند، اما تا زمانی که آن کد اجرا نشود و آزمون‌ها را پاس نکند، عیار واقعی مدل مشخص نمی‌شود. اجرای مستقیم کدی که یک مدل تولید کرده با متدهایی مثل `exec`، سیستم میزبان شما را با خطرات امنیتی جدی، حلقه‌های بی‌نهایت و مصرف بی‌رویه‌ی منابع حافظه روبه‌رو می‌کند.

در بنچ‌مارک‌های مطرحی مثل HumanEval و MBPP، راه‌حل استاندارد برای سنجش خروجی‌ها اجرای هر قطعه‌کد در یک زیرفرایند (subprocess) مجزا همراه با سقف زمانی و مهار خروجی است. این درس نشان می‌دهد چگونه بدون نیاز به ابزارهای پیچیده و صرفاً با امکانات استاندارد پایتون، یک بستر ارزیابی پایدار برای اجرای کدهای تولیدی بسازید و خطاهای گوناگون را ساختاریافته مدیریت کنید.

علاوه بر ایمن‌سازی اجرا، با فرمول ریاضی برآوردگر نااریب pass-at-k نیز آشنا می‌شوید تا بتوانید عملکرد مدل را در سناریوهای تولید چندگانه به‌شکل دقیق و آماری ارزیابی و مقایسه کنید.

کد تولیدشده زمانی درست است که آزمون‌ها را با موفقیت پشت سر بگذارد. بستر ارزیابی (eval harness) باید کد را استخراج کند، آن را طوری اجرا کند که سیستم میزبان از کار نیفتد و نرخ قبولی را صادقانه محاسبه کند. این درس چنین زیرساختی را پیاده‌سازی می‌کند.

اهداف یادگیری

  • استخراج بلوک کد از خروجی متنی آزاد، به‌گونه‌ای که با قاعده‌ی پس‌پردازش درس 70 همخوانی داشته باشد.
  • اجرای کد کاندید در یک زیرفرایند (subprocess) ایزوله با محدودیت زمانی واقعی (wall-clock timeout)، سقف حجم خروجی و فهرست سیاه واردات (import denylist).
  • امتیازدهی به وظیفه بر اساس نسبت رشته‌های اعتبارسنجی (assertions) ارائه‌شده‌ای که روی کاندید با موفقیت پاس می‌شوند.
  • محاسبه‌ی معیار pass-at-k برای وظایفی که در آن‌ها چندین خروجی از یک مدل نمونه‌برداری می‌شود.
  • برخورد با کرش ساندباکس (محیط ایزوله)، خطاهای نگارشی (SyntaxError) و اتمام مهلت زمانی به‌عنوان حالت‌های شکست درجه‌یک، با کدهای خروج متمایز که اجراکننده بتواند آن‌ها را ثبت کند.
ساندباکس اجرای کداجرا، بررسی شرط، پایان مهلت زمانی
CODE-EXEC SANDBOXGenerated code is shipped on stdin to a fresh interpreter inside a subprocess with an import denylist and output cap. Each supplied assertion runs and lights pass (blue) or fail (orange); a wall-clock timeout kills runaway loops. The score is the fraction of assertions that pass, with crashes and timeouts as first-class fail modes.کاندیداکدساندباکس زیرفرایندassert 1assert 1✓assert 2assert 2✓assert 3assert 3✗assert 4assert 4✓زمان واقعیtimeout3/4 قبولی
کد تولیدشده از طریق stdin به مفسری جدید در یک زیرفرایند (subprocess) با فهرست مسدودی ایمپورت و سقف خروجی ارسال می‌شود. هر assertion اجرا شده و وضعیت قبولی (آبی) یا رد (نارنجی) را روشن می‌کند؛ یک محدودیت زمانی (wall-clock timeout) حلقه‌های بی‌پایان را متوقف می‌کند. امتیاز برابر است با کسر assertionهای قبول‌شده، که در آن خرابی‌ها و اتمام مهلت زمانی جزو حالت‌های شکست اولیه محسوب می‌شوند.

چرا زیرفرایند ایزوله؟

استفاده‌ی مستقیم از exec از نظر امنیت و پایداری خطرساز است. یک خط کد تولیدشده مثل while True: pass ارزیابی را برای همیشه مسدود می‌کند. کد تولیدشده‌ای مثل import shutil; shutil.rmtree('/') هم دقیقاً همان‌قدر فاجعه‌بار است که به نظر می‌رسد. راه‌حل این است که برای هر کاندید یک مفسر پایتون تازه راه‌اندازی کنیم، کد را از طریق stdin به آن بدهیم، نتایج اعتبارسنجی را در stdout بنویسیم و اگر فرایند از زمان تعیین‌شده فراتر رفت، آن را متوقف کنیم (kill کنیم). به این ترتیب فرایند ارزیابی میزبان همچنان به کارش ادامه می‌دهد.

ارزیابی‌های واقعی مثل HumanEval، MBPP، BigCodeBench و LiveCodeBench همگی از ساندباکس مبتنی بر زیرفرایند استفاده می‌کنند. برخی از آن‌ها لایه‌ای از Docker را هم روی آن می‌آورند. ما به یک دلیل مشخص در حد زیرفرایند متوقف می‌شویم: قابل‌حمل است، جزو کتابخانه‌ی استاندارد پایتون (stdlib) است و حالت‌های شکستی را که برای ارزیابی آموزشی مهم هستند پوشش می‌دهد. پیاده‌سازی‌های محیط عملیاتی (پروداکشن) مواردی مثل seccomp، ایزوله‌سازی شبکه و سیستم‌فایل فقط‌خواندنی را هم اضافه می‌کنند. درس بعدی درباره‌ی امن‌سازی، خارج از این مسیر آموزشی قرار دارد.

ساختار وظیفه‌ی اجرای کد

یک وظیفه‌ی code_exec شامل رشته‌های اعتبارسنجی در targets است. اجراکننده بلوک کد محصورشده را از متن تولیدشده استخراج می‌کند، یک بستر آزمایشی (test harness) پیرامون آن می‌سازد و نتیجه را اجرا می‌کند.

extract_code_blockstdout JSONtimeoutnonzero exitمتن تولیدشدهرشته کدفهرست گزاره‌های task.targetsساخت اسکریپت اجراکنندهراه‌اندازی زیرفرایند پایتونمهلت زمانی، لیست سیاه، سقف خروجینتایج به ازای هر گزارهنتیجه: اتمام مهلت زمانینتیجه: خطاامتیاز = قبولی‌ها / کل

امتیاز کسری در بازه‌ی [0, 1] است. وظیفه‌ای با سه اعتبارسنجی که دو تای آن‌ها پاس شوند، امتیاز 0.667 می‌گیرد. صرف‌نظر از اینکه چه خطایی رخ دهد، خروجی اجراکننده ساختار ثابتی دارد: کرش‌های زیرفرایند به یک کد خطای استاندارد نگاشت می‌شوند، نه اینکه ردگیری محاسبات خطای پایتون (traceback) به بستر آزمایشی نشت کند.

فهرست سیاه واردات

فهرست سیاه بر پایه‌ی دستورات import عمل می‌کند. پیش از اجرای کد کاندید، اسکریپتِ اجراکننده کدهای واردکننده‌ی ماژول‌های خطرناک را با یک تکه‌کد جایگزین (stub) بازنویسی می‌کند که خطای ImportError("denied") صادر می‌کند. این فهرست عمداً محافظه‌کارانه انتخاب شده است: os.system، subprocess، socket، requests، urllib، urllib.request، urllib.error، urllib.parse، ctypes، shutil، http.client، asyncio.subprocess.

ادعا نمی‌کنیم این روش نفوذناپذیر است. کد خرابکارانه‌ی مصمم می‌تواند از هر ساندباکسِ درون‌فرایندی در پایتون فرار کند. فهرست سیاه فقط یک لایه‌ی محافظتی پشتیبان است. کنترل‌های اصلی و بنیادین، محدودیت زمانی واقعی و سقف حجم خروجی هستند.

DENIED = {
    "os.system": True,
    "subprocess": True,
    "socket": True,
    "shutil": True,
    "requests": True,
    "urllib": True,
    "ctypes": True,
}

ما کد کاندید را با افزودن import sys در ابتدا و یک سازوکار محافظتی که os.system را با مانکی‌پچ (monkey-patch) به یک تابع خطاانداز تبدیل می‌کند، احاطه می‌کنیم. الگوی کامل در main.py موجود است.

محدودیت زمانی واقعی

به هر زیرفرایند به‌طور پیش‌فرض بودجه‌ی زمانی 3 ثانیه‌ی واقعی داده می‌شود. اجراکننده از subprocess.run(..., timeout=t) استفاده می‌کند. اگر زمان به پایان برسد، اجراکننده استثنای TimeoutExpired را می‌گیرد، فرایند را متوقف می‌کند و دلیل خروج را timeout ثبت می‌کند. امتیاز این وظیفه صفر منظور می‌شود و اجراکننده به سراغ وظیفه‌ی بعدی می‌رود.

این زمان مجاز با آرگومان timeout_s به تابع run_candidate داده می‌شود؛ اجراکننده‌ی درس 75 این مقدار را از طریق code_exec_timeout_s یک‌بار برای کل اجرا تنظیم می‌کند. آزمون‌های واحد زمان‌بر می‌توانند زمان بیشتری بخواهند، اما run_candidate این مقدار را بین 0.1 تا 30 ثانیه محدود می‌کند تا زمان اجرای مجموعه آزمون‌ها مهارشده بماند.

سقف حجم خروجی

زیرفرایند ممکن است stdout را با داده‌های متوالی پر کند و حافظه‌ی میزبان را ببلعد. اجراکننده خروجی stdout را در یک بافر جریان می‌دهد و به‌محض اینکه مجموع حجم داده‌ها از 256 کیلوبایت بگذرد، فرایند فرزند را می‌کشد. نتیجه با exit_code = error و متن تکمیلی "output overflow" ثبت می‌شود. این حالت در عمل زمانی رخ می‌دهد که کد تولیدشده اشتباهاً یک حلقه‌ی بی‌نهایت بنویسد که مدام در خروجی چاپ می‌کند.

معیار Pass-at-k

معیار Pass-at-k برآوردگر نااریبی است که در HumanEval و موارد مشابه استفاده می‌شود. اگر برای هر وظیفه n نمونه‌ی مستقل داشته باشیم و c مورد از آن‌ها پاس شوند، احتمال اینکه در یک نمونه به اندازه‌ی k از میان آن n نمونه، دست‌کم یک راه‌حل قبولی وجود داشته باشد برابر است با:

pass_at_k(n, c, k) = 1 - C(n - c, k) / C(n, k)

وقتی n - c < k باشد، طبق قاعده‌ی معمول صورت کسر یعنی C(n - c, k) صفر می‌شود (نمی‌توانید k شکست را از بین تعداد کمتری شکست انتخاب کنید)؛ بنابراین مقدار نهایی برابر 1 خواهد بود. پیاده‌سازی ما این حالت خاص را به‌طور مستقیم مدیریت می‌کند. ما تابع pass_at_k(n, c, k) را برای استفاده در لایه‌ی جدول رده‌بندی (leaderboard) در درس 74 ارائه می‌دهیم.

وظیفه با n=10 نمونهاجرای هر نمونهc نمونه قبول شدندpass_at_1 = c/npass_at_5 = 1 - C n-c, 5 / C n, 5pass_at_10 = 1 if c 0 else 0

کدهای خروج

اجراکننده برای هر وظیفه یکی از پنج نتیجه‌ی زیر را برمی‌گرداند:

  • pass: وقتی همه‌ی اعتبارسنجی‌ها پاس شوند.
  • assertion_fail: وقتی کد اجرا شود اما دست‌کم یکی از اعتبارسنجی‌ها شکست بخورد.
  • syntax_error: وقتی کد import نشود یا خطای نحوی (SyntaxError) داشته باشد.
  • timeout: وقتی مهلت زمانی تعیین‌شده به پایان برسد.
  • error: برای هر کرش دیگر، شامل موارد فهرست سیاه و سرریز خروجی (سرریز با پیام تکمیلی "output overflow" مشخص می‌شود).

امتیاز همچنان به‌صورت کسری است و کد خروج صرفاً داده‌ی توصیفی (متادیتا) به حساب می‌آید. درس‌های بعدی می‌توانند تصمیم بگیرند که اتمام زمان را صفر لحاظ کنند یا داده‌ی ازدست‌رفته.

این درس چه کارهایی انجام نمی‌دهد

این درس یک ساندباکس واقعی در اختیارتان نمی‌گذارد. کدهای غیرقابل‌اعتماد از اینترنت باز را اجرا نمی‌کند. وظایف دارای حالت مثل ورودی/خروجی فایل یا فراخوانی‌های شبکه را هم مدیریت نمی‌کند. برای این موارد به یک کانتینر یا microVM نیاز دارید. هدف این درس پیاده‌سازی قرارداد است: زیرفرایند ایزوله، فهرست سیاه، محدودیت زمانی، سقف خروجی، مجموعه‌ای مشخص از کدهای خروج و محاسبات ریاضی pass-at-k.

راهنمای خواندن کد

فایل main.py توابع extract_code، run_candidate، score_code_exec و pass_at_k را تعریف می‌کند. اسکریپت اجراکننده‌ی زیرفرایند به‌صورت یک رشته ساخته شده و با فلگ -c به مفسر تازه‌ی پایتون داده می‌شود. آزمون‌های فایل code/tests/test_exec.py چهار کد خروج و تابع pass-at-k را روی نمونه‌های برگرفته از سبک HumanEval ارزیابی می‌کنند.

فایل main.py را از بالا به پایین بخوانید. الگوی اجراکننده (runner template) بخش کلیدی کار است. به حلقه‌ی اعتبارسنجی آن‌قدر دقیق شوید تا بتوانید ساختار بسته‌ی JSON ارسالی به فرایند والد را پیش‌بینی کنید.

گام‌های بعدی

پس از اینکه سازوکار زیرفرایند درست کار کرد، مسئله‌ی بعدی قابلیت حمل است. نسخه‌های مختلف پایتون در ویندوز سیگنال SIGKILL را به شکل‌های متفاوتی مدیریت می‌کنند. تمیزترین راه‌حل قرار دادن اجراکننده داخل یک ایمیج Docker است. مرحله‌ی بعد از آن، جایگزینی رشته‌های اعتبارسنجی با فایل‌های آزمون واحدِ واقعی است تا فرایند ارزیابی با خطوط لوله‌ی CI در محیط عملیاتی همخوانی پیدا کند. از آن نقطه به بعد دیگر رشته‌های اعتبارسنجی را آزمون ننامید؛ آن‌ها آزمون‌های اسباب‌بازی هستند و حالت‌های شکست ساده‌انگارانه‌ای دارند.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا اجراکننده به‌جای فراخوانی درون‌خطی exec، برای هر کاندیدا یک زیرفرایند (subprocess) جدید پایتون ایجاد می‌کند؟
  2. پیش از درسامتیاز یک وظیفه با سه assertion که دو تای آن قبول و یکی رد می‌شود چقدر است؟
  3. حین درسدر صورتی که کد کاندیدا قابل تجزیه (parse) نباشد، اجراکننده چه کد خروجی‌ای برمی‌گرداند؟
  4. حین درسچرا مهلت زمانی واقعی (wall-clock timeout) کنترل اساسی‌تر و حیاتی‌تری نسبت به فهرست مسدودی ایمپورت است؟
  5. حین درستابع pass_at_k(n, c, k) وقتی که n - c < k باشد چه مقداری برمی‌گرداند؟
  6. پس از درساجراکننده چگونه سرریز خروجی را گزارش می‌کند و چرا سقف آن 256 کیلوبایت است؟

سؤالات متداول

چرا استفاده از exec درون فرایند اصلی برای ارزیابی کد خطرناک است؟

یک کد تولیدشده با حلقه‌ی بی‌نهایت مثل while True کل ارزیابی را مسدود می‌کند و کدهای مخربی مانند دستورات پاک‌سازی دایرکتوری سیستم، هاست را دچار آسیب جبران‌ناپذیر می‌کنند.

چگونه جلوی مصرف بیش از حد حافظه توسط حلقه‌های چاپ در زیرفرایند گرفته می‌شود؟

جریان خروجی stdout در یک بافر مانیتور می‌شود و در صورت عبور حجم داده‌ها از سقف 256 کیلوبایت، فرایند فرزند فوراً متوقف شده و خطای output overflow ثبت می‌شود.

فرمول pass-at-k در ارزیابی کد چه کاری انجام می‌دهد؟

این معیار احتمال اینکه در یک نمونه به اندازه‌ی k از میان n راه‌حل تولیدشده، دست‌کم یک کد تمام آزمون‌ها را با موفقیت پشت سر بگذارد، به روشی نااریب برآورد می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.