Sebuah pathname yang dimulai di dalam directory tepercaya dapat berakhir pada objek di luar directory tersebut sebelum open() selesai. Parent component, symbolic link, magic link, mount point, dan perubahan namespace secara concurrent semuanya ikut menentukan pathname lookup Linux. Memeriksa string sebelum membukanya karena itu tidak menetapkan lokasi akhir resolusi yang dilakukan kernel.

Linux openat2() menempatkan pembatasan di dalam operasi lookup itu sendiri. Pemanggil memberikan directory file descriptor, open flag biasa, dan kebijakan resolve melalui struct open_how. Kernel kemudian menerapkan batas tersebut saat menelusuri setiap path component yang relevan. Security boundary berpindah dari validasi awal atas teks pathname ke operasi yang benar-benar melakukan resolusi.

Directory descriptor menjadi anchor lookup berbasis objek

Relative path yang diberikan ke openat2() di-resolve dari dirfd, sama seperti openat(). Perbedaan ini penting karena directory file descriptor mengacu pada objek filesystem yang sudah dibuka, bukan meminta proses mencari directory itu lagi berdasarkan pathname.

Sebuah service dapat membuka directory yang mewakili tree yang diizinkan, mempertahankan descriptor tersebut, lalu me-resolve request path dari sana:

struct open_how how = {
    .flags = O_RDONLY | O_CLOEXEC,
    .resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
};

int fd = syscall(SYS_openat2, rootfd, request_path,
                 &how, sizeof(how));

RESOLVE_BENEATH mensyaratkan resolusi yang berhasil tetap berada di bawah directory yang direpresentasikan oleh rootfd. Input berupa absolute path ditolak, demikian pula absolute symbolic link yang akan mengembalikan lookup ke root milik proses.

Descriptor anchor dan aturan resolusi menangani dua persoalan berbeda. dirfd memilih objek awal. RESOLVE_BENEATH membatasi lokasi yang dapat dicapai penelusuran berikutnya.

RESOLVE_BENEATH menolak escape, bukan meniru root

RESOLVE_BENEATH sesuai ketika input diharapkan menunjuk descendant dari directory awal. Component yang akan di-resolve ke luar subtree tersebut menyebabkan operasi gagal.

Semantik ini berbeda dari RESOLVE_IN_ROOT. Dengan RESOLVE_IN_ROOT, directory yang direpresentasikan dirfd bertindak sebagai root sementara hanya untuk lookup tersebut. Absolute path diinterpretasikan relatif terhadap directory itu, absolute symbolic link juga diinterpretasikan di dalamnya, dan .. pada root tidak bergerak ke atas.

Perbedaannya memengaruhi semantik aplikasi:

RESOLVE_BENEATH  -> bentuk yang melakukan escape ditolak
RESOLVE_IN_ROOT  -> lookup memperlakukan dirfd sebagai root

Kedua mode tidak mengubah root untuk seluruh proses. Pembatasan melekat pada satu operasi openat2(), sehingga thread lain tidak mewarisi state namespace sementara darinya.

Saat ini kedua flag juga memblokir resolusi magic link sebagai efek samping, tetapi interface terdokumentasi tidak menjadikan efek tersebut pengganti permanen untuk RESOLVE_NO_MAGICLINKS. Kode yang kebijakan keamanannya mewajibkan magic link diblokir perlu meminta flag itu secara eksplisit.

Tetap berada di bawah sebuah directory tidak berarti symbolic link harus dilarang. Relative symbolic link dapat secara sah menunjuk objek lain di dalam subtree yang diizinkan. RESOLVE_BENEATH dapat menerima kasus tersebut sambil tetap menolak escape.

RESOLVE_NO_SYMLINKS menetapkan aturan yang lebih ketat: traversal symbolic link dilarang pada semua path component. Flag ini juga mengimplikasikan pembatasan magic link. Cakupannya lebih luas daripada O_NOFOLLOW, yang mengatur penanganan symbolic link pada component terakhir dan bukan melarang traversal symlink di seluruh path.

Perbedaan itu terlihat pada path seperti:

assets/current/config.json

Jika current adalah symbolic link, O_NOFOLLOW pada final open tidak dengan sendirinya melarang traversal melalui current. RESOLVE_NO_SYMLINKS berlaku sepanjang lookup.

Service perlu memilih perilaku symlink berdasarkan object model-nya. Package store yang mensyaratkan direct directory entry dapat melarang symlink sepenuhnya. Deployment tree yang sengaja memakai internal symlink dapat mempertahankannya sambil membatasi resolusi dengan RESOLVE_BENEATH.

Mount boundary memerlukan constraint terpisah

Sebuah path dapat tetap berada di bawah directory secara hierarkis tetapi menyeberang ke filesystem lain yang di-mount. Bind mount membuat batas ini penting karena sebuah mount dapat mengekspos objek dari lokasi lain pada titik di dalam tree yang diizinkan.

RESOLVE_NO_XDEV menolak traversal melewati mount point, termasuk bind mount. Flag ini terpisah dari batas subtree karena “di bawah directory ini” dan “pada mount ini” merupakan dua kebijakan berbeda.

Flag tersebut juga dapat menolak layout yang sah. Sistem umum memakai bind mount dan nested mount sebagai mekanisme deployment biasa. Menerapkan RESOLVE_NO_XDEV secara global dapat mengubah layout filesystem yang valid menjadi application error. Flag ini paling tepat ketika tetap berada pada mount awal memang merupakan persyaratan keamanan atau konsistensi.

Linux mengekspos magic link melalui interface seperti /proc/<pid>/fd/* dan /proc/<pid>/exe. Dari userspace bentuknya menyerupai symbolic link, tetapi link tersebut dapat membuat kernel berpindah ke sebuah objek tanpa mengikuti semantik symlink biasa.

RESOLVE_NO_MAGICLINKS menolak perpindahan tersebut. Batas ini relevan ketika directory tidak tepercaya dapat mengekspos entry procfs atau ketika lookup dapat bertemu magic link yang mengacu pada objek di luar konteks filesystem yang dimaksud.

Flag ini lebih sempit daripada RESOLVE_NO_SYMLINKS: symbolic link biasa tetap dapat digunakan sementara magic link dilarang. Kebijakan dapat mempertahankan layout berbasis symlink yang umum tanpa menerima perilaku perpindahan objek khusus milik procfs magic link.

Kernel lookup dapat gagal ketika escape tidak dapat dipastikan aman

Path resolution berjalan pada namespace yang dapat diubah proses lain. Rename directory dan perubahan mount dapat berpacu dengan lookup. Untuk RESOLVE_BENEATH dan RESOLVE_IN_ROOT, Linux dapat mengembalikan EAGAIN ketika kernel tidak dapat memastikan secara aman bahwa component .. tetap berada dalam boundary yang diwajibkan.

Error tersebut merupakan bagian dari kontrak keamanan, bukan tanda bahwa kernel diam-diam melonggarkan pembatasan. Pemanggil dapat mencoba ulang operasi. Jika escape benar-benar terdeteksi, failure yang terdokumentasi adalah EXDEV.

Perilaku ini lebih kuat daripada urutan yang melakukan canonicalization pathname lalu membuka hasil canonical tersebut. Dua operasi terpisah menyisakan interval ketika state namespace dapat berubah. openat2() mengevaluasi pembatasan sebagai bagian dari lookup yang menghasilkan descriptor.

RESOLVE_CACHED menyatakan batas latensi, bukan trust boundary

RESOLVE_CACHED meminta kernel menyelesaikan lookup hanya dari informasi yang sudah berada di cache. Jika revalidation atau I/O diperlukan, pemanggilan gagal dengan EAGAIN. Aplikasi dapat memakainya sebagai fast path lalu mengalihkan operasi ke execution path yang lebih lambat ketika cache-only lookup tidak tersedia.

Flag ini tidak membuat pathname lebih aman hanya karena component-nya berada di cache. Ia membatasi pekerjaan yang diizinkan selama resolusi. Security restriction seperti RESOLVE_BENEATH, RESOLVE_IN_ROOT, atau kebijakan tanpa link tetap merupakan properti terpisah dan dapat dikombinasikan ketika semantiknya sesuai dengan operasi.

Hasil EAGAIN yang sama juga berarti pemanggil perlu menafsirkan kegagalan berdasarkan resolve flag yang dipakai. Cache miss dan ketidakmampuan membuktikan parent traversal yang aman sama-sama dapat memerlukan retry, tetapi keduanya berasal dari constraint berbeda.

Descriptor hasil open menjadi objek yang relevan bagi security boundary

Setelah openat2() berhasil, descriptor yang dikembalikan mengacu pada objek yang dicapai di bawah kebijakan lookup yang diminta. Kode sensitif terhadap keamanan dapat beroperasi pada descriptor tersebut tanpa membangun kembali trust dari pathname awal.

Batasnya tetap sengaja sempit. openat2() tidak mengautentikasi isi file, membekukan namespace di sekitarnya, atau membuktikan bahwa objek yang dibuka berasal dari publisher yang disetujui. Fungsinya membatasi pathname resolution. Integritas konten, authorization, pemeriksaan ownership, dan mutasi setelahnya tetap merupakan persoalan terpisah.

Pemisahan ini berguna pada sistem yang menerima fragmen path dari input dengan tingkat kepercayaan lebih rendah. String pathname tetap hanya sebuah request. Kernel melakukan penelusuran dari objek directory tepercaya, menerapkan pembatasan eksplisit selama penelusuran, dan mengembalikan descriptor hanya ketika kebijakan resolusi yang diminta terpenuhi.