Proses yang bersiap mengeksekusi program lain sering memerlukan invariant descriptor yang sederhana: standard input, output, dan error tetap tersedia, sedangkan descriptor yang tidak terkait tidak melewati batas eksekusi. Menutup descriptor satu per satu dapat mengubah invariant tersebut menjadi persoalan enumerasi. Linux close_range() menerapkan operasi pada interval numerik inklusif di tabel file descriptor milik task pemanggil.
Antarmukanya kecil, tetapi semantiknya menjangkau sharing tabel descriptor, pewarisan melalui execve(), alokasi descriptor secara concurrent, dan transisi privilege. Flag yang dipilih menentukan lebih dari strategi implementasi: flag tersebut menetapkan apakah descriptor langsung hilang, menjadi close-on-exec, atau terlebih dahulu dipisahkan dari tabel yang dibagi dengan task lain.
Interval menargetkan nomor descriptor, bukan open file description
close_range(first, last, flags) memilih nomor file descriptor dari first sampai last, secara inklusif. Dengan flags bernilai nol, descriptor pada interval tersebut ditutup. Nomor descriptor yang tidak sedang terbuka di dalam interval tidak membuat operasi gagal hanya karena entri itu tidak ada.
Operasi ini bekerja pada lapisan tabel descriptor. Sebuah descriptor adalah entri yang merujuk pada open file description atau objek kernel lain yang direpresentasikan melalui antarmuka descriptor. Menutup satu entri tidak selalu menghancurkan objek yang dirujuk. Descriptor lain dapat tetap merujuk pada open file description yang sama, dan proses lain dapat memegang referensi yang diwarisi melalui fork() atau diterima melalui descriptor passing.
Pemisahan tersebut menjaga cakupan operasi tetap presisi. close_range() menghapus referensi terpilih dari satu tabel descriptor; operasi ini bukan primitive bulk shutdown untuk file, socket, pipe, atau objek event yang mendasarinya.
Batas yang umum dimulai di atas descriptor standar konvensional:
if (close_range(3, ~0U, 0) == -1) {
/* tangani error */
}Di sini ~0U memasok nilai unsigned terbesar yang diterima antarmuka, sehingga interval dinyatakan dari descriptor 3 hingga ruang descriptor bagian atas implementasi tanpa lebih dulu meminta batas saat ini dan mengiterasinya.
Range closure mengeluarkan enumerasi userspace dari kontrak
Loop berbasis batas descriptor memiliki dua urusan terpisah: memilih ruang pencarian numerik dan menerbitkan satu operasi close untuk setiap kandidat. Pemindaian /proc/self/fd mengganti pencarian numerik dengan enumerasi direktori, tetapi tetap memerlukan perhatian pada descriptor direktori yang dipakai untuk pemindaian serta perubahan concurrent pada tabel.
close_range() memindahkan pemilihan interval ke satu system call. Kernel dapat bekerja langsung pada representasi tabel descriptornya tanpa meminta userspace merekonstruksi entri mana yang ada.
Manfaat semantiknya lebih penting daripada mengasumsikan hasil performa tertentu. Kode aplikasi menyatakan postcondition yang dituju sebagai operasi rentang. Kode tersebut tidak memerlukan snapshot setiap descriptor terbuka hanya untuk meminta penghapusan entri-entri itu.
Hal ini tidak membuat seluruh kebijakan descriptor concurrent menjadi otomatis. Jika thread lain dapat mengalokasikan atau menduplikasi descriptor saat pembersihan berlangsung, relasi sharing tabel descriptor tetap menentukan perilaku.
Tabel descriptor bersama membuat close langsung terlihat oleh task lain
Task Linux yang dibuat dengan CLONE_FILES berbagi tabel file descriptor. Thread pada proses multithreaded konvensional biasanya memiliki relasi sharing ini. Ketika satu thread menutup entri pada tabel bersama, entri tersebut juga hilang bagi thread lain yang memakai tabel yang sama.
Pemanggilan close_range() biasa dari salah satu thread tersebut karena itu dapat memengaruhi penggunaan descriptor di bagian proses lain. Ini bukan efek samping khusus API rentang; perilaku tersebut berasal dari operasi terhadap tabel descriptor bersama.
CLOSE_RANGE_UNSHARE mengubah batas itu. Kernel terlebih dahulu mengatur agar pemanggil memiliki tabel descriptor yang tidak lagi dibagi sebagai bagian dari operasi, lalu menerapkan semantik rentang yang diminta pada tabel milik pemanggil. Task lain yang mempertahankan tabel bersama sebelumnya tidak terkena penghapusan descriptor melalui tabel baru milik pemanggil.
if (close_range(3, ~0U, CLOSE_RANGE_UNSHARE) == -1) {
/* tangani error */
}Flag tersebut dengan demikian berkaitan dengan kepemilikan mutasi tabel descriptor. Semantik ini berguna saat pembersihan merupakan bagian dari transisi satu task dan tidak semestinya memutasi tabel yang masih digunakan task sejawat.
Unsharing tidak mencabut referensi eksternal. Task sejawat yang sudah memiliki entri descriptor sendiri tetap memegang referensi itu sesuai aturan lifetime normal objek kernel yang dirujuk.
CLOSE_RANGE_CLOEXEC menunda penghapusan hingga batas eksekusi
CLOSE_RANGE_CLOEXEC tidak langsung menutup descriptor yang dipilih. Flag ini menandainya sebagai close-on-exec, dengan efek setara pengaturan flag descriptor FD_CLOEXEC pada interval terpilih. Descriptor tetap dapat digunakan sebelum transisi keluarga execve() berhasil dan ditutup oleh semantik eksekusi ketika transisi tersebut mengganti process image.
if (close_range(3, ~0U, CLOSE_RANGE_CLOEXEC) == -1) {
/* descriptor tetap terbuka jika operasi gagal */
}
execve(path, argv, envp);Ini membentuk kontrak kegagalan yang berbeda dari penutupan langsung. Jika execve() gagal, descriptor yang ditandai masih ada pada proses saat ini dan mempertahankan flag close-on-exec. Dengan range closure langsung, kegagalan eksekusi terjadi setelah descriptor tersebut sudah dihapus.
Perbedaan itu dapat berpengaruh pada jalur error. Launcher yang harus melaporkan kegagalan eksekusi melalui descriptor khusus perlu menempatkan descriptor tersebut di luar rentang terdampak atau mempertahankannya dengan cara lain sampai protokol pelaporan selesai.
Bentuk close-on-exec juga menyediakan batas yang berguna ketika langkah setup berikutnya masih memerlukan descriptor yang tidak boleh bertahan setelah eksekusi berhasil. Tabel tetap operasional selama setup, sedangkan kebijakan pewarisan dikodekan dalam flag descriptor.
Flag close-on-exec dan alokasi descriptor adalah urusan sinkronisasi terpisah
FD_CLOEXEC melekat pada entri descriptor. Descriptor baru yang dibuat setelah sebuah rentang ditandai tidak secara retroaktif memperoleh flag tersebut hanya karena nilai numeriknya berada di dalam interval sebelumnya.
Artinya, close_range(..., CLOSE_RANGE_CLOEXEC) bukan kebijakan permanen atas nomor descriptor. Operasi itu memutasi entri yang ada sesuai semantik tabel descriptor system call pada saat tersebut. Kode yang mengizinkan pembuatan descriptor secara concurrent tetap memerlukan model sinkronisasi atau kepemilikan untuk periode menjelang execve().
Banyak API pembuat descriptor menyediakan flag pembuatan close-on-exec atomik, seperti O_CLOEXEC, SOCK_CLOEXEC, EPOLL_CLOEXEC, dan antarmuka terkait. Flag tersebut menangani race yang berbeda: descriptor dibuat dengan pembatasan pewarisan yang sudah melekat, sehingga tidak ada jendela create-then-fcntl() terpisah.
Penandaan rentang melengkapi API tersebut ketika proses perlu menerapkan batas pewarisan pada kumpulan entri yang sudah ada secara lebih luas. Operasi ini tidak menggantikan flag pembuatan atomik untuk descriptor yang dapat dibuat secara concurrent.
Unshare dan close-on-exec dapat membentuk kebijakan eksekusi privat
Flag dapat digabungkan ketika kombinasi tersebut didukung antarmuka. CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC memisahkan tabel descriptor milik pemanggil dan menandai entri terpilih sebagai close-on-exec pada tabel privat itu, alih-alih langsung menutupnya.
Kombinasi ini memisahkan dua keputusan yang mudah tercampur:
batas kepemilikan: tabel descriptor task mana yang diubah
batas pewarisan: entri mana yang bertahan setelah exec berhasilKeputusan pertama ditetapkan melalui unsharing. Keputusan kedua direpresentasikan oleh state close-on-exec. Pemisahan keduanya bernilai dalam kode process-launch karena kepemilikan descriptor sebelum eksekusi dan pewarisan descriptor setelah eksekusi merupakan properti berbeda.
Model proses yang sebenarnya tetap menentukan hasil. Task yang tidak berbagi tabel descriptor tidak memperoleh isolasi baru dari sejawat yang memang sudah terpisah, sedangkan task yang berbagi tabel mengubah cakupan mutasi dengan meminta unsharing.
Error berlaku pada operasi rentang, bukan pada setiap descriptor yang tidak ada
Kode bulk cleanup sering menangani error close(fd) satu descriptor demi satu. close_range() memiliki bentuk berbeda: pemanggil menerima satu hasil untuk operasi rentang yang diminta.
Interval dengan first lebih besar daripada last tidak valid. Bit flag yang tidak didukung juga tidak valid. Operasi yang memerlukan alokasi atau unsharing state tabel descriptor dapat gagal karena kondisi resource atau batas sistem yang didokumentasikan antarmuka kernel.
Nomor descriptor yang tidak ada di dalam interval valid bukan error individual yang perlu dikumpulkan. Properti ini sesuai dengan kode pembersihan yang hanya menginginkan tidak ada entri terbuka terpilih yang tersisa, bukan kode yang memerlukan audit trail per descriptor.
Aplikasi yang memerlukan perilaku shutdown spesifik objek tetap membutuhkan operasi spesifik objek sebelum range cleanup. Menutup descriptor socket, misalnya, bukan pengganti protocol-level drain atau acknowledgement aplikasi ketika perilaku tersebut merupakan bagian dari kontrak sekelilingnya.
Kebijakan seccomp dapat membuat urutan operasi terlihat
Sebuah proses dapat memasang filter seccomp yang memblokir pemanggilan close_range() berikutnya. Jika pewarisan descriptor harus dibatasi sebelum filter restriktif mulai berlaku, urutan operasi menjadi bagian dari protokol process-launch.
CLOSE_RANGE_CLOEXEC dapat relevan pada urutan semacam ini karena proses dapat menandai interval descriptor yang dituju sebelum memasang filter yang mencegah pemanggilan rentang, lalu melanjutkan setup dengan descriptor yang masih ada hingga eksekusi. Ini adalah properti pengurutan antarmuka, bukan jaminan bahwa kebijakan seccomp sembarang kompatibel dengan urutan tersebut.
Filter itu sendiri, pemanggilan setup descriptor yang masih diperlukan setelahnya, dan pemanggilan eksekusi akhir semuanya perlu diizinkan sesuai kebijakan aktual. close_range() hanya menyediakan transisi tabel descriptor; operasi ini tidak mengoordinasikan pemasangan kebijakan keamanan atas nama aplikasi.
Descriptor hygiene adalah kontrak batas proses
Pewarisan descriptor yang tidak diharapkan dapat mempertahankan resource melampaui pemilik yang dimaksud. Endpoint pipe yang bocor dapat mencegah peserta lain menerima end-of-file. Socket yang diwariskan dapat memperpanjang lifetime koneksi di kernel. Directory atau file descriptor yang diwariskan juga dapat mengekspos capability yang tidak dimaksudkan untuk program baru.
close_range() memberi kode process-launch Linux cara langsung untuk menyatakan batas tabel descriptor yang luas. Penutupan biasa menghapus entri saat ini. CLOSE_RANGE_CLOEXEC mempertahankan penggunaan saat ini sambil mengecualikan entri dari transisi eksekusi yang berhasil. CLOSE_RANGE_UNSHARE mengubah tabel bersama mana yang menerima mutasi.
Mode-mode tersebut menyelesaikan persoalan yang berkaitan tetapi berbeda. Penggunaan yang tepat berawal dari state descriptor yang dibutuhkan sebelum dan sesudah eksekusi, ditambah model sharing task selama setup. Setelah batas tersebut eksplisit, operasi rentang dapat mengodekannya tanpa mengubah pembersihan descriptor menjadi prosedur inventarisasi userspace.