Proses Linux multithread dapat mencapai batas yang rumit tepat sebelum execve(): satu thread hendak membuang setiap file descriptor di atas standard input, output, dan error, sementara thread lain masih dapat membuat descriptor pada tabel yang sama. Loop pemanggilan close() menangani nomor descriptor satu per satu, tetapi tidak dengan sendirinya mengubah fakta bahwa tabel tersebut dibagi.
close_range() dengan CLOSE_RANGE_UNSHARE menangani race spesifik itu. Kernel terlebih dahulu memberi caller tabel file descriptor yang tidak lagi dibagi dengan pengguna lain dari tabel lama, lalu menerapkan penutupan massal yang diminta pada tabel milik caller. Properti keamanannya berkaitan dengan kepemilikan tabel selama pembersihan, bukan sekadar pengurangan jumlah system call.
Tabel descriptor dapat menjadi state bersama
File descriptor Linux adalah indeks integer ke dalam tabel file descriptor proses. Thread yang dibuat dengan model pthread biasa berbagi tabel tersebut karena memakai state CLONE_FILES yang sama. Linux juga mengizinkan susunan task lain yang berbagi tabel serupa.
Pembagian itu berguna selama eksekusi normal. Descriptor yang dibuka satu thread dapat langsung dipakai thread lain. Konsekuensinya, pembersihan descriptor juga merupakan mutasi terhadap state bersama.
Pertimbangkan urutan pra-exec yang mencoba menutup descriptor satu per satu:
thread A thread B
close(3)
close(4)
open(...) -> 4
close(5)
execve(...)Alokasi descriptor yang tepat bergantung pada state proses, tetapi masalah strukturalnya tetap sama: thread B dapat mengubah tabel yang sama ketika thread A sedang membersihkannya. Enumerasi /proc/self/fd lebih dahulu tidak mengubah rangkaian close() berikutnya menjadi batas atomik.
CLOSE_RANGE_UNSHARE mengubah batas kepemilikan
Antarmuka Linux close_range(first, last, flags) beroperasi pada rentang nomor descriptor yang inklusif. Dengan flags bernilai nol, descriptor pada rentang itu ditutup dari tabel caller saat ini.
CLOSE_RANGE_UNSHARE menambahkan properti terpisah. Sebelum menutup rentang tersebut, kernel memisahkan tabel descriptor caller dari task lain yang membaginya. Dokumentasi Linux menggambarkan operasi ini secara konseptual serupa dengan:
unshare(CLONE_FILES);
close_range(first, last, 0);System call gabungan dapat menerapkan transisi tersebut secara lebih efisien. Pada kasus umum ketika last melampaui descriptor tertinggi yang dialokasikan, kernel dapat membentuk tabel baru hanya sampai first, alih-alih menyalin tabel besar lalu menutup entri di dalamnya.
Batas pra-exec dapat dinyatakan sebagai:
close_range(3, ~0U, CLOSE_RANGE_UNSHARE);
execve(path, argv, envp);Setelah close_range() berhasil, descriptor 3 dan seterusnya tidak ada pada tabel caller yang sudah terpisah. Peer yang masih memakai tabel bersama lama dapat membuka atau menutup descriptor di sana tanpa mengisi kembali tabel milik caller.
Batas rentang tetap menentukan hasil
Pemisahan tabel tidak berarti setiap descriptor ditutup. Argumen first dan last tetap menentukan rentang inklusif yang dipengaruhi pemanggilan.
Jika proses memanggil:
close_range(10, 20, CLOSE_RANGE_UNSHARE);caller memperoleh tabel terpisah dan operasi menargetkan descriptor 10 sampai 20. Descriptor di luar rentang tersebut tidak dipilih untuk ditutup hanya karena tabel telah dipisahkan.
Hal ini penting ketika descriptor bernomor rendah membawa otoritas yang memang dipertahankan. Mempertahankan descriptor 0 sampai 2 sambil membuang semua descriptor di atasnya merupakan batas exec yang umum, tetapi aplikasi dapat memilih cutoff lain. Pilihan itu merupakan kebijakan deployment; system call tidak mengklasifikasikan descriptor sebagai aman atau sensitif.
Pemisahan tabel berbeda dari penandaan close-on-exec
CLOSE_RANGE_CLOEXEC mengubah dimensi lain. Flag tersebut menetapkan close-on-exec pada seluruh rentang terpilih alih-alih langsung menutup descriptor. Descriptor tetap dapat dipakai dalam executable image saat ini dan ditutup oleh transisi exec yang berhasil.
CLOSE_RANGE_UNSHARE berkaitan dengan pembagian tabel descriptor. CLOSE_RANGE_CLOEXEC berkaitan dengan keberlangsungan descriptor melewati exec. Linux mengizinkan flag untuk menyatakan properti ini, tetapi keduanya bukan kontrol yang dapat saling menggantikan.
Sebuah service dapat memilih penutupan langsung sebelum mengeksekusi program dengan tingkat kepercayaan lebih rendah. Urutan lain mungkin masih memerlukan descriptor selama setup pra-exec dan menandainya untuk penutupan kemudian. Pilihan yang tepat bergantung pada otoritas yang harus tetap tersedia sebelum execve() dan race pembagian tabel yang mungkin terjadi selama interval tersebut.
Penutupan massal tidak mencabut otoritas duplikat di tempat lain
Pemisahan tabel caller tidak mencabut referensi file yang sudah dipegang task lain. Peer yang mempertahankan tabel bersama lama tetap memegang descriptor di dalamnya. Descriptor yang sudah diduplikasi ke proses lain melalui inheritance atau descriptor passing juga berada di luar tabel baru milik caller.
Batasnya bersifat lokal:
before:
caller ----+
+--> shared fd table --> open file descriptions
peer ----+
after CLOSE_RANGE_UNSHARE:
caller ------> private fd table
peer ------> old fd table -----> open file descriptionsMenutup entri pada tabel private caller menghapus referensi tersebut dari caller. Operasi ini bukan primitive pencabutan global untuk file, socket, pipe, atau objek kernel lain yang mendasarinya.
Kegagalan harus menghentikan transisi sensitif
close_range() mengembalikan nol saat berhasil dan -1 saat error. Dengan CLOSE_RANGE_UNSHARE, pembentukan tabel baru dapat gagal, termasuk karena memori kernel tidak mencukupi atau kondisi batas descriptor yang didokumentasikan Linux.
Kode yang menjadikan pembersihan sebagai batas keamanan tidak boleh melanjutkan ke jalur execve() sensitif setelah kegagalan tersebut. Tetap melanjutkan akan mengubah kondisi isolasi yang diwajibkan menjadi langkah pembersihan best-effort.
System call ini juga mengabaikan error yang terkait dengan penutupan masing-masing descriptor dalam rentang. Perilaku tersebut merupakan bagian dari antarmuka Linux dan berbeda dari loop yang mengamati hasil setiap close(). Aplikasi yang memerlukan pencatatan per descriptor membutuhkan kontrak operasional yang berbeda.
Efek keamanan berakhir pada isolasi tabel descriptor
CLOSE_RANGE_UNSHARE menghapus kelas race yang sempit: pengguna lain dari tabel descriptor yang sebelumnya dibagi tidak dapat mengisi kembali atau mengubah tabel caller yang sudah terpisah setelah transisi unshare. Flag ini tidak memeriksa otoritas yang diwakili descriptor yang tetap berada di luar rentang terpilih, serta tidak membatasi operasi open(), socket(), dup(), atau penerimaan descriptor yang dilakukan caller setelahnya.
Batas proses yang lengkap dapat memerlukan kontrol tambahan untuk akses filesystem, system call, credential, namespace, dan state environment yang diwariskan. Penutupan descriptor massal menyumbang satu properti pada batas tersebut: caller dapat memisahkan tabel descriptornya dan menghapus rentang terpilih tanpa peer mengembalikan entri secara bersamaan ke tabel yang sama.