Sebuah proses layanan dapat mengumpulkan socket, pipe, handle direktori, file log, dan descriptor kontrol jauh sebelum menjalankan helper. Jika descriptor tersebut tetap terbuka pada program baru, helper menerima kapabilitas yang tidak tampak dari argumen command line maupun environment. Socket yang sudah terhubung dapat membawa akses terautentikasi; direktori yang terbuka dapat mempertahankan akses ke lokasi filesystem; pipe dapat membuka jalur data komponen lain.

Linux close_range() memberi kode sebelum exec operasi berbasis rentang atas file descriptor. Nilai keamanannya bukan karena descriptor menjadi tidak berbahaya. Mekanisme ini memungkinkan proses mempersempit kumpulan descriptor yang melewati batas execve() tanpa menginventarisasi /proc/self/fd atau memanggil close() satu per satu untuk setiap descriptor kandidat.

Objek yang diwariskan adalah descriptor terbuka, bukan pathname

File descriptor adalah integer lokal proses yang merujuk melalui descriptor table ke open file description atau objek kernel lain. Setelah descriptor tersedia, pembatasan pathname yang diterapkan kemudian tidak mencabut otoritas yang sudah direpresentasikan oleh referensi tersebut.

Saat execve() berlangsung, descriptor tetap terbuka kecuali flag close-on-exec telah dipasang. Karena itu, pewarisan descriptor merupakan batas kepercayaan yang berbeda dari pemilihan executable. Helper dapat berupa binary yang memang dituju tetapi tetap menerima akses yang tidak direncanakan karena proses peluncur mempertahankan descriptor di luar desain helper.

Bentuk kebijakan yang umum cukup sederhana: pertahankan sekumpulan kecil descriptor yang sengaja disiapkan untuk child, lalu hapus semua descriptor di atas kumpulan tersebut. Untuk proses yang hanya ingin mempertahankan standard input, output, dan error, rentang dimulai dari descriptor 3.

if (close_range(3, ~0U, CLOSE_RANGE_UNSHARE) == -1) {
    /* handle failure before exec */
}
execve(path, argv, envp);

Batas atas bersifat inklusif. Nilai ~0U menyatakan sisa rentang descriptor unsigned tanpa perlu lebih dahulu meminta limit proses.

Penutupan rentang memisahkan kebijakan dari inventaris descriptor

Pola lama dapat memindai /proc/self/fd, mengidentifikasi descriptor yang terbuka, lalu menutupnya satu per satu. Pendekatan itu menjadikan penemuan descriptor sebagai bagian mekanisme pembersihan dan bergantung pada ketersediaan procfs. Loop yang dibatasi oleh limit descriptor juga menghabiskan kerja pada integer yang mungkin tidak merujuk ke descriptor terbuka.

close_range(first, last, 0) meminta kernel menutup descriptor terbuka dalam interval inklusif tersebut. Error dari penutupan descriptor individual dalam rentang saat ini diabaikan; syscall melaporkan error yang terkait dengan operasi rentang itu sendiri.

Interface ini tidak menentukan descriptor mana yang aman dipertahankan. Kebijakan tersebut tetap menjadi tanggung jawab aplikasi. Syscall hanya membuat aturan pengecualian kontigu dapat dinyatakan langsung pada batas descriptor table.

Descriptor table bersama mengubah persoalan konkurensi

Thread biasanya berbagi file descriptor table. Penutupan descriptor pada table bersama karena itu dapat memengaruhi thread lain yang masih mengandalkannya. Jalur pembersihan sebelum exec yang berpacu dengan aktivitas descriptor secara bersamaan juga dapat membuat perilaku aplikasi bergantung pada urutan waktu.

CLOSE_RANGE_UNSHARE mengubah batas tersebut. Linux lebih dahulu memisahkan descriptor table milik task pemanggil dari task lain yang membagikannya, lalu menerapkan operasi penutupan pada rentang yang diminta. Secara konseptual, operasi ini setara dengan memisahkan CLONE_FILES lalu menutup rentang, meski kernel dapat mengimplementasikan operasi gabungan itu secara lebih efisien.

Flag tersebut relevan ketika pemanggil sedang menyiapkan status descriptor miliknya sendiri untuk exec berikutnya. Thread lain mempertahankan referensi melalui table bersama yang lama, sedangkan pemanggil memperoleh table privat tempat pembatasan rentang diterapkan.

Mekanisme ini tidak membekukan state proses lain. Pemisahan berlaku khusus pada file descriptor table. Memori, credential, konteks filesystem, dan resource bersama lain mengikuti aturan sharing Linux masing-masing.

CLOEXEC menunda penghapusan sampai batas eksekusi

CLOSE_RANGE_CLOEXEC memberi efek berbeda. Alih-alih langsung menutup descriptor dalam rentang, flag ini memasang close-on-exec. Descriptor tetap dapat dipakai program saat ini sampai exec yang berhasil mengganti process image; pada saat itu aturan close-on-exec menghapusnya.

if (close_range(3, ~0U, CLOSE_RANGE_CLOEXEC) == -1) {
    /* handle failure */
}

/* setup that may still need existing descriptors */

execve(path, argv, envp);

Urutan ini penting ketika langkah setup berikutnya masih memerlukan descriptor yang terbuka. Kebijakan descriptor juga dapat ditetapkan sebelum memasang filter seccomp yang mungkin membatasi pilihan syscall berikutnya.

Flag tersebut tidak membuat descriptor privat sebelum exec. Kode yang berjalan dalam proses saat ini masih dapat memakainya, dan execve() yang gagal meninggalkan process image tetap berjalan dengan descriptor yang masih terbuka tetapi sudah ditandai close-on-exec. Penanganan error perlu memperhitungkan state tersebut.

Close-on-exec saat pembuatan tetap menutup race yang berbeda

close_range() bukan pengganti flag saat pembuatan seperti O_CLOEXEC, SOCK_CLOEXEC, atau opsi pembuatan descriptor atomik yang setara. Dalam proses multithreaded, membuat descriptor lalu baru memasang FD_CLOEXEC dapat berpacu dengan thread lain yang melakukan fork lalu exec. Descriptor dapat melewati batas eksekusi sebelum pembaruan flag berikutnya terjadi.

Close-on-exec saat pembuatan mencegah jendela tersebut untuk descriptor yang sedang dibuat. Operasi rentang memiliki peran lain: menetapkan kebijakan luas atas descriptor yang sudah ada ketika proses menyiapkan batas eksekusi.

Desain yang kuat sering memakai kedua lapisan. Descriptor dibuat non-inheritable secara default, kemudian jalur peluncuran menerapkan kebijakan rentang terakhir agar kelalaian tidak diam-diam memperluas otoritas program child.

Descriptor yang dipertahankan memerlukan penempatan eksplisit

Kebijakan penutupan kontigu paling mudah diaudit ketika descriptor untuk child menempati nomor yang telah ditentukan di bawah rentang penutupan atau diduplikasi ke allowlist eksplisit sebelum pembersihan. Remapping descriptor juga harus menghindari collision dan mempertahankan state close-on-exec yang dimaksud.

Di titik ini batas penolakan berbasis rentang terlihat jelas. close_range() tidak menerima allowlist sparse, tidak memberi label tujuan pada descriptor, dan tidak memverifikasi bahwa descriptor 4 masih menunjuk objek yang diharapkan peluncur. Properti tersebut berada pada logika aplikasi.

Klaim keamanannya karena itu harus tetap sempit: operasi rentang yang berhasil membatasi entri descriptor table pada interval tertentu sesuai flag yang diberikan. Operasi tersebut tidak mengautentikasi objek yang dipertahankan atau membuktikan bahwa kumpulan descriptor tersisa sesuai dengan kebijakan tingkat aplikasi.

Kebersihan exec adalah kebersihan kapabilitas

Pewarisan descriptor penting karena referensi kernel yang terbuka membawa otoritas yang dapat digunakan, terpisah dari konfigurasi tekstual program baru. Helper yang mewarisi socket berprivilege dan sudah terhubung dapat melewati kontrol yang seharusnya berlaku jika helper harus membuat koneksi baru sendiri.

close_range() menyediakan titik enforcement ringkas untuk batas tersebut. CLOSE_RANGE_UNSHARE memisahkan descriptor table pemanggil sebelum penutupan; CLOSE_RANGE_CLOEXEC menunda penutupan sampai exec; flag close-on-exec saat pembuatan mencegah race terpisah ketika descriptor pertama kali muncul.

Mekanisme tersebut mencakup tahap yang berbeda dalam masa hidup descriptor. Memakainya sebagai lapisan yang saling melengkapi menjaga klaim operasional tetap presisi: jalur peluncuran dapat mengurangi referensi yang diwariskan tanpa sengaja, tetapi hanya kebijakan aplikasi yang dapat menentukan referensi tersisa yang sah sebagai kapabilitas bagi process image baru.