Path yang diberikan ke openat() diresolusikan oleh kernel, tetapi caller hanya memiliki kontrol terbatas atas traversal komponen perantara. Linux openat2() menambahkan field resolve untuk menerapkan batas pada keseluruhan operasi lookup. Batas tersebut diperiksa ketika komponen path sedang ditelusuri, bukan melalui validasi path di userspace yang dilanjutkan dengan open terpisah.

Perbedaan ini relevan saat komponen path dapat berubah secara concurrent. Urutan yang memeriksa path lalu membukanya menghasilkan dua observasi terpisah terhadap state filesystem yang mutable. openat2() menempatkan policy lookup pada system call yang sama dengan operasi yang menghasilkan file descriptor.

RESOLVE_BENEATH menahan traversal di bawah dirfd

RESOLVE_BENEATH mensyaratkan hasil resolusi tetap berada di bawah directory yang dirujuk oleh dirfd. Input berupa absolute path ditolak, begitu juga absolute symbolic link yang ditemui selama traversal.

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

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

Komponen relatif seperti .. tidak otomatis dilarang. Komponen itu menjadi masalah ketika resolusinya akan keluar dari subtree yang diizinkan. Kernel melacak lookup terhadap directory yang diberikan dan menolak escape dengan EXDEV.

Interface ini juga dapat menghasilkan EAGAIN jika kernel tidak dapat memastikan traversal .. tetap aman akibat concurrent rename atau race terkait. Caller dapat mencoba operasi kembali. Mekanisme ini merupakan pemeriksaan containment saat runtime, bukan normalisasi lexical terhadap string input.

RESOLVE_IN_ROOT mengganti root lookup untuk satu operasi

RESOLVE_IN_ROOT memberi dirfd semantik seperti root selama lookup tersebut. Absolute path diinterpretasikan relatif terhadap directory itu, absolute symbolic link juga diresolusikan relatif terhadapnya, dan .. pada boundary tetap berada di boundary.

process root: /
dirfd:       /srv/image
path:        /etc/app.conf

Target lookup RESOLVE_IN_ROOT:
/srv/image/etc/app.conf

Efeknya menyerupai penggantian sementara root untuk resolusi pathname, tetapi scope-nya hanya satu pemanggilan openat2(). Flag ini tidak mengubah root directory milik process dan tidak memengaruhi thread lain yang berjalan bersamaan.

Karena itu, RESOLVE_BENEATH dan RESOLVE_IN_ROOT menyatakan policy yang berbeda. Yang pertama menolak resolusi yang keluar dari directory dan menolak absolute path. Yang kedua menjadikan directory sebagai root lookup sehingga absolute path memiliki arti di dalam root tersebut.

O_NOFOLLOW berlaku pada komponen terakhir pathname. Flag itu tidak mencegah komponen perantara berupa symbolic link. RESOLVE_NO_SYMLINKS memiliki cakupan lebih luas: resolusi symbolic link dilarang pada semua komponen dan flag ini juga mengimplikasikan RESOLVE_NO_MAGICLINKS.

Magic link adalah mekanisme Linux yang berbeda dan terutama muncul pada entry procfs seperti /proc/pid/fd/*. RESOLVE_NO_MAGICLINKS memblokir resolusi magic link tanpa melarang symbolic link biasa.

Perilaku kernel saat ini untuk RESOLVE_BENEATH dan RESOLVE_IN_ROOT juga menonaktifkan resolusi magic link, tetapi dokumentasi interface tidak menetapkannya sebagai properti permanen kedua flag tersebut. Policy yang secara spesifik harus menolak magic link sebaiknya menetapkan RESOLVE_NO_MAGICLINKS secara eksplisit.

Pemisahan ini membuat caller dapat menyatakan boundary yang memang dibutuhkan. Sebuah service dapat mengizinkan symbolic link biasa di dalam tree terkontrol sambil menolak magic link bergaya procfs, atau melarang traversal symlink sepenuhnya.

Perpindahan mount adalah batas yang terpisah

RESOLVE_NO_XDEV menolak traversal melewati mount point, termasuk bind mount. Sebuah path dapat tetap berada di bawah dirfd secara struktur tetapi masuk ke filesystem lain melalui mount. Karena itu, containment subtree dan containment mount adalah dua properti berbeda.

Saat RESOLVE_NO_XDEV mendeteksi perpindahan tersebut, openat2() menghasilkan EXDEV. Flag ini dapat terlalu ketat pada sistem yang memakai bind mount atau mount point sebagai bagian normal dari hierarchy directory. Semantiknya tepat ketika policy memang mensyaratkan lookup tetap berada pada satu mount, bukan sebagai hardening universal.

RESOLVE_CACHED menjadikan state cache sebagai hasil eksplisit

RESOLVE_CACHED mensyaratkan lookup selesai menggunakan path cache kernel tanpa revalidation atau I/O filesystem. Jika syarat itu tidak dapat dipenuhi, pemanggilan menghasilkan EAGAIN.

Flag ini tidak menyatakan target harus selalu berada di cache dan tidak mengubah semantik filesystem target. Cache-only menjadi syarat untuk satu lookup tersebut. Aplikasi dapat memakai hasilnya untuk mempertahankan fast path yang tidak memerlukan operasi lebih lambat dan mengalihkan fallback ke jalur lain.

Karakter ini berbeda dari flag containment. RESOLVE_BENEATH, RESOLVE_IN_ROOT, dan kelompok NO_* membatasi lokasi atau cara traversal berjalan. RESOLVE_CACHED membatasi pekerjaan yang boleh dilakukan kernel untuk menyelesaikan lookup.

open_how adalah struktur ABI yang extensible

openat2() menerima struktur open_how beserta ukurannya. Struktur tersebut membawa flag open biasa, mode pembuatan file, dan flag resolusi:

struct open_how how = {
    .flags = O_RDONLY | O_CLOEXEC,
    .mode = 0,
    .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS,
};

Struktur harus diisi nol pada awalnya agar field yang ditambahkan kernel pada masa depan tetap bernilai nol kecuali caller secara eksplisit memakainya. Argumen size menjadi bagian dari kontrak versioning untuk struktur extensible ini.

Berbeda dari openat(), openat2() menolak nilai flag yang tidak dikenal atau saling bertentangan, bukan diam-diam mengabaikan bit yang tidak dikenal. Nilai tidak valid pada how.flags, how.mode, atau how.resolve dapat muncul sebagai EINVAL, sehingga caller tidak berjalan dengan semantik yang lebih lemah tanpa indikasi error.

Boundary keamanan berada pada lookup kernel, bukan string yang dibersihkan

Sanitasi path sering bekerja pada teks: menghapus .., menolak prefix absolut, atau memeriksa target symlink sebelum open. Pemeriksaan semacam itu tidak membekukan namespace antara inspeksi dan penggunaan. Rename, perubahan mount, dan penggantian symlink dapat membuat lookup berikutnya melihat state berbeda.

openat2() sendiri tidak membuat setiap operasi filesystem otomatis aman. Caller tetap memerlukan directory file descriptor yang sesuai, access control yang benar, open flag yang tepat, dan policy yang cocok dengan objek filesystem yang diizinkan. Properti pentingnya lebih sempit: batas resolusi path yang dipilih diterapkan kernel selama lookup yang menghasilkan descriptor.

Dengan model tersebut, containment berpindah dari konvensi pemeriksaan pathname sebelum open ke operasi yang benar-benar meresolusikan path. Untuk kode yang menerima path relatif terhadap directory tepercaya, boundary itu menjadi bagian dari API kernel, bukan sekadar aturan pemrosesan string.

Referensi