خطاهای مسیر و دسترسی؛ ENOENT، ENOTDIR و EACCES
در این درس ۸ بخش
پیام «فایل پیدا نشد» همیشه به این معنی نیست که همان نام نهایی را باید دوباره بسازیم. یک پوشهٔ میانی ممکن است مفقود باشد، یک نام میانی ممکن است فایل معمولی باشد یا برنامه از پوشهٔ کاری دیگری آغاز شده باشد. در این درس این حالتها را با خطای دسترسی مقایسه میکنیم. هدف، کاربرد دقیق دانستههای قبلی دربارهٔ مسیر و مجوز است؛ جدول chmod را از ابتدا تکرار نمیکنیم. هر بازتولید داخل پوشهٔ موقت خود برنامه انجام میشود و دادهٔ واقعی کاربر در آزمایش نیست.
مسیر از دید کدام فرایند حل میشود؟
مسیر نسبی از پوشهٔ کاری جاری همان فرایند آغاز میشود. این پوشه لزوماً جایی نیست که فایل اسکریپت ذخیره شده است. مسیر مطلق از ریشهٔ قابل مشاهدهٔ فرایند شروع میشود؛ در محیط دارای namespace یا chroot، این دید هم ممکن است با شل مدیر فرق کند. برای تمرین فعلی در یک محیط عادی لینوکس، cwd را صریح میسازیم. #linux-working-directory را با نام فایل و مسیر کد یکی ندانید؛ سه نام نزدیک میتوانند به سه موقعیت متفاوت اشاره کنند.
حل مسیر، عبور از مؤلفههای آن است. در /lab/reports/today.txt ابتدا باید lab و reports در جای درست قابل پیمایش باشند. نبود مؤلفه میتواند ENOENT بدهد؛ مؤلفهٔ میانیِ غیرپوشه میتواند ENOTDIR بسازد؛ نداشتن اختیار جستوجو در یک پوشه میتواند EACCES بدهد. جزئیات عملیات نهایی هم مهم است: خواندن یک فایل با ساختن فایل تازه قرارداد یکسانی ندارد. بنابراین پیام را کنار نام عملیات و پرچمهای مربوط بخوانید، نه کنار نام مسیر بهتنهایی.
مثال اجراشدنی؛ فایل ثابت، پوشهٔ کاری متفاوت
فایل examples/03-working-directory.py دو پوشه و یک فایل کوچکِ training میسازد. نخست از والد به نام settings.txt مراجعه میکند؛ سپس cwd را به app میبرد و همان نام نسبی را دوباره میخواند. مسیر مطلقِ ذخیرهشده به همان فایل اشاره دارد. در finally، پوشهٔ کاری همان فرایند Python به مقدار اولیه برمیگردد، حتی اگر آزمون شکست بخورد؛ این کار برای ادامهٔ درست مراحل داخل همان فرایند مهم است. تغییر cwd در فرزند Python اصولاً پوشهٔ کاری پوستهٔ والد را عوض نمیکند و بازگشت finally برای جلوگیری از تغییر پوستهٔ والد نیست.
import errno
import os
from pathlib import Path
from tempfile import TemporaryDirectory
original = Path.cwd()
with TemporaryDirectory(prefix="dars-path-") as folder:
root = Path(folder)
app = root / "app"
app.mkdir()
target = app / "settings.txt"
target.write_text("training", encoding="utf-8")
try:
os.chdir(root)
try:
Path("settings.txt").read_text(encoding="utf-8")
except OSError as error:
assert error.errno == errno.ENOENT
print(errno.errorcode[error.errno])
else:
raise AssertionError("Unexpected relative-path success")
os.chdir(app)
assert Path("settings.txt").read_text(encoding="utf-8") == "training"
assert target.read_text(encoding="utf-8") == "training"
print("same absolute file; different cwd")
finally:
os.chdir(original)
assert Path.cwd() == original
خروجی ENOENT و سپس same absolute file; different cwd است. فایل در فاصلهٔ دو خواندن ساخته نشده؛ فقط مبنای تفسیر نام نسبی تغییر کرده است. این شاهد، فرضیهٔ «فایل همیشه مفقود است» را رد میکند. در برنامهٔ تولیدی بهتر است مبنای مسیر داده را طبق قرارداد روشن انتخاب کنید؛ تبدیل تصادفی تمام مسیرها به مطلق، بهتنهایی سیاست استقرار و جابهجایی برنامه را درست نمیکند. اگر نام ورودی از کاربر میآید، مسئلهٔ محدودکردن مسیر و پیوندها بررسی امنیتی جدا میخواهد.
سه شکست کنترلشده در بستهٔ اصلی
در پوشهٔ project فرمان زیر را با حساب معمولی اجرا کنید. سناریو برای هر شکست عملیات جداگانه دارد: مسیر ناموجود، تلاش برای عبور از فایل معمولی و عبور از پوشهای که اختیار جستوجویش برای این کاربر برداشته شده است. تغییر مجوز فقط روی همان پوشهٔ موقت تازه انجام میشود و پیش از پاکسازی بازگردانده میشود. این آزمایش قرار نیست مجوز خانه، فایل برنامه یا تنظیم سرویس دیگری را تغییر دهد.
python3 lab.py pathدر JSON موفق، case برابر path و observed شامل ENOENT، ENOTDIR و EACCES است. same_file_after_chdir برابر true شاهد بخش پوشهٔ کاری است. شمارهٔ تصادفی مسیر معیار قبولی نیست و لازم نیست با خروجی شخص دیگری برابر شود. اگر خطای دیگری دیده شد، نام عملیات و reason یا پیام تشخیصی را نگه دارید؛ با chmod روی یک پوشهٔ واقعی سعی نکنید خروجی را به زور شبیه نمونه کنید. این تفاوت میتواند نشانهٔ محیط، هویت یا محدودیت مشاهده باشد.
اجازهٔ خواندن فایل با اجازهٔ عبور از مسیر فرق دارد
داشتن read روی نام نهایی کافی نیست؛ فرایند باید در مؤلفههای والد اختیار لازم برای پیمایش را هم داشته باشد. در پوشهها، معنای search یا بیت اجرا با اجرای یک فایل برنامه یکسان نیست. ACL و ماسک مؤثر نیز میتوانند نتیجه را از یک نگاه کوتاه به بیتها پیچیدهتر کنند. برای همین، در بررسی واقعی، هویت مؤثر، گروهها و تمام مسیر را با ابزارهای مناسب همان محیط میخوانیم. #linux-file-permissions یک نقطهٔ مرور است، نه مجوز پیشنهاد دسترسی کامل برای همه.
کاربر root و قابلیتهای مؤثر میتوانند بخشی از کنترل معمول دسترسی یا DAC را دور بزنند. آزمایشی که فقط chmod000 میکند و بعد با root میخواند، شاهد ردِ مدل مجوز برای کاربر عادی نیست. بستهٔ اصلی این درس اجرای سناریو با UID مؤثر صفر را با ۷۷ رد میکند. برای انجام تمرین، به حساب معمولیِ آزمایشگاه خود برگردید؛ اضافهکردن sudo درست بر خلاف پیششرط است. در محیط واقعی، SELinux، AppArmor یا sandbox سرویس نیز میتوانند محدودیت دیگری بسازند و نباید برای رسیدن به پاسخ دلخواه خاموش شوند.
الگوی تشخیص؛ هر شاهد کدام فرضیه را محدود میکند؟
عملیات خواندن مسیر شکست خورد
ENOENT: نام یا مؤلفهٔ مورد نیاز در دید فرایند پیدا نشد
ENOTDIR: مؤلفهای که باید پوشه باشد، پوشه نیست
EACCES: دسترسی لازم برای این عملیات یا پیمایش رد شده است
پرسشهای بعدی: cwd چیست؟ هویت چیست؟ کدام مؤلفه؟ کدام عملیات؟این فهرست راهنمای انتخاب آزمایش بعدی است، نه موتور تشخیص علت قطعی. ENOENT بهتنهایی نمیگوید خطای نامگذاری در بسته رخ داده یا mount مورد انتظار حاضر نیست. EACCES نیز همیشه به یک بیت ساده تقلیل پیدا نمیکند. صورت مشاهده را دقیق نگه دارید: «در این هویت و cwd، خواندن این مسیر با این خطا رد شد». سپس فرضیهٔ کوچکتر را بیازمایید. عوضکردن همزمان مالکیت، مجوز، مسیر و هویت، بیشتر شواهد تشخیصی را از بین میبرد.
چرا exists قبل از open، تضمین نیست؟
میان بررسی وجود یک مسیر و بازکردن آن، وضعیت میتواند تغییر کند. تولیدکنندهٔ دیگری ممکن است نام را جابهجا کند، فایل را حذف کند یا پیوند را تغییر دهد. حتی یک بررسی access نیز مجوز موفقیت عملیات بعدی را برای همیشه صادر نمیکند. در کد معمول، خودِ عملیات لازم را انجام و خطای مشخص آن را مدیریت میکنیم. طراحی امن برای مسیرهای نامطمئن، پیوندهای نمادین و کار همزمان موضوعی عمیقتر است؛ وجود این مثال ساده نباید بهعنوان حل کامل آن مسئله معرفی شود.
برای پیکربندی یک سرویس، ابتدا مسیر مورد انتظار و پوشهٔ کاری را از تعریف همان سرویس بخوانید. با محیط شل تعاملی خود قیاس بیدلیل نکنید. اگر مسیر درست است، مرحلهٔ بعد خواندن مجوزها و محدودیت مؤثر در هویت برنامه است. برای انتشار اصلاح، تغییر کوچک و قابل بررسی انتخاب کنید و همان عملیاتِ شکستخورده را دوباره بسنجید. موفقیت cat با root، جای موفقیت خواندن در فرایند محدود برنامه را نمیگیرد. یک پاسخ HTTP سبز نیز الزاماً همهٔ فایلهای مورد نیازِ مسیرهای دیگر را نخوانده است.
تمرینها و پاسخ تشریحی
پرسش — مسیر /lab/reports/today.txt به ENOTDIR میرسد و reports یک فایل معمولی است. آیا ساخت today.txt زیر آن راهحل ممکن است؟
پاسخ تشریحی — خیر. مؤلفهٔ میانی باید پوشهای قابل پیمایش باشد. تا وقتی reports فایل معمولی است، نمیتوان today.txt را فرزند آن دانست. قرارداد مسیر یا نوع reports را بررسی کنید؛ حذف و تبدیل فایل بدون شناخت صاحب داده کار درستی نیست. این شاهد را با ENOENT که یک مؤلفه یافت نشده فرق بگذارید.
پرسش — یک فایل با نام نسبی در ترمینال باز میشود، اما همان کد در سرویس شکست میخورد. چه چیزی را پیش از تغییر مجوز بررسی میکنید؟
پاسخ تشریحی — پوشهٔ کاری مؤثر فرایند، هویت اجرا و مسیر کامل مورد انتظار را ثبت میکنیم. نام نسبی نسبت به cwd همان فرایند تفسیر میشود؛ پوشهٔ فایل کد الزاماً cwd نیست. اگر هدف درست پیدا نشده باشد، chmod مسئله را حل نمیکند. پس از تثبیت مسیر، مجوز پیمایش والدها و خواندن فایل را در همان هویت میسنجیم.
پرسش — آزمایش chmod000 با root هنوز فایل را میخواند. چرا نتیجه نمیگیریم EACCES غیرواقعی است؟
پاسخ تشریحی — root و قابلیتهای مؤثر میتوانند بعضی بررسیهای معمول DAC را دور بزنند. آزمایش باید با هویت غیرریشهٔ هدف انجام شود و شرایط والدها هم معلوم باشد. بستهٔ این دوره اجرای سناریو با root را رد میکند، چون چنین اجرا شاهد مناسبی برای حالت معمول Permission denied نیست. خاموشکردن حفاظتهای دیگر نیز روش اثبات این مثال نیست.
پیشنیازهای قبلی و منابع
مبنای مسیر را در درس نقشهٔ فایلسیستم مرور کنید:
https://www.darsavand.com/courses/linux-server-from-zero?lesson=9ea841e0-0cd7-4232-9059-54c638a5f78b
برای مجوز مؤثر فراتر از بیتهای ساده، درس ACL را بخوانید:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=a1591f8c-95af-44a4-8d04-5692abd73e5e
برای اثر WorkingDirectory و هویت سرویس، درس استقرار systemd مثال کاربردی دارد:
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=78a3a92a-f850-4744-9a6f-0b4fade2d5a6
در این درس، آن مفاهیم را به خطای عملیات مشخص وصل کردیم.
منابع رسمی برای مطالعهٔ تکمیلی:
Linux man-pages — path_resolution(7): https://man7.org/linux/man-pages/man7/path_resolution.7.html
Linux man-pages — open(2): https://man7.org/linux/man-pages/man2/open.2.html
Python 3.12 — os: https://docs.python.org/3.12/library/os.html
Python 3.12 — tempfile: https://docs.python.org/3.12/library/tempfile.html
تألیف، مثالها و پرسشهای اختصاصی: تحریریه درسآوند.
فایل آزمایشگاه، راهنمای اجرا، آزمونها و فرم گزارش رخداد:
https://www.darsavand.com/resources/linux-application-errors-practice-kit
برای دیدن ترتیب دورههای مقدماتی، پیشرفته و این مسیر، نقشهٔ راه لینوکس را بخوانید:
https://www.darsavand.com/resources/linux-learning-roadmap
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.