Proses Linux multithread dapat memakai satu tabel file descriptor secara bersama. Pola itu praktis selama eksekusi normal: descriptor yang dibuka satu thread tersedia bagi thread lain. Masalah muncul ketika satu thread sedang menyiapkan konteks eksekusi terbatas dan ingin membuang range descriptor yang luas, sementara thread lain masih dapat mengalokasikan descriptor pada tabel yang sama.

close_range() menyediakan operasi berbasis range untuk batas tersebut. Dengan CLOSE_RANGE_UNSHARE, kernel lebih dulu memisahkan caller dari tabel descriptor bersama, lalu menerapkan penutupan yang diminta pada tabel hasil pemisahan milik caller. Secara konsep, operasi ini mendekati gabungan unshare(CLONE_FILES) dan penutupan range, tetapi kernel dapat menghindari pekerjaan yang tidak diperlukan pada kasus umum.

Tabel descriptor dapat dibagi meski alur eksekusi terpisah

Thread yang dibuat dengan CLONE_FILES merujuk tabel descriptor yang sama. Nomor descriptor merupakan entry pada tabel tersebut, sehingga membuka, menduplikasi, atau menutup entry mengubah tabel yang terlihat oleh setiap task yang membaginya.

Kondisi ini membentuk race pada cleanup berbasis pemanggilan close() berulang:

thread A                         thread B

close(fd 7)
close(fd 8)                     open(...) -> fd 8
close(fd 9)

Alokasi nomor descriptor yang tepat bergantung pada aktivitas concurrent. Iterasi snapshot /proc/self/fd memiliki persoalan struktural serupa: snapshot dan penutupan berikutnya merupakan operasi terpisah, sedangkan thread lain dapat mengubah tabel bersama di antaranya.

Satu close_range(first, last, 0) menutup interval numerik dalam satu system call, tetapi tanpa unsharing operasi tersebut tetap bekerja pada tabel bersama. Thread lain ikut melihat entry tersebut ditutup.

UNSHARE mengubah batas kepemilikan sebelum cleanup

CLOSE_RANGE_UNSHARE mengubah operasi dari mutasi tabel bersama menjadi cleanup lokal untuk caller. Secara konsep, urutannya:

tabel descriptor bersama
        |
        +-- thread A
        +-- thread B

thread A: close_range(3, ~0U, CLOSE_RANGE_UNSHARE)

tabel private A                 tabel bersama awal
fd 0,1,2                       fd 0,1,2,3,4,...
   |                                |
thread A                         thread B

Caller tidak lagi bergantung pada thread lain untuk diam sementara descriptor yang tidak diperlukan dihapus. Thread lain tetap terhubung ke tabel bersama awal.

Sifat ini relevan di sekitar transisi eksekusi, setup sandbox, dan persiapan helper process, ketika satu konteks memerlukan himpunan descriptor yang ketat sementara bagian lain dari proses multithread masih aktif.

Kernel dapat menghindari penyalinan entry yang akan dibuang

Model konseptualnya adalah unshare(CLONE_FILES) lalu close_range(first, last, 0), tetapi implementasi tidak selalu perlu membuat salinan private lengkap lalu menutup sebagian besar isinya.

Ketika range melampaui descriptor tertinggi yang dialokasikan, seperti last = ~0U, kernel dapat membangun tabel hasil unshare hanya sampai batas yang perlu dipertahankan. Untuk request umum yang mempertahankan standard input, output, dan error sambil membuang semua descriptor mulai nomor 3, tidak ada manfaat menyalin entry bernomor tinggi hanya untuk segera menghapusnya.

#define _GNU_SOURCE
#include <unistd.h>
#include <linux/close_range.h>

int rc = close_range(3, ~0U, CLOSE_RANGE_UNSHARE);

Pemanggilan yang sukses mempertahankan descriptor di bawah first pada caller dan menghapus range lebih tinggi yang diminta dari tabel private miliknya. Error penutupan descriptor individual tidak dilaporkan secara terpisah.

Tahap unshare sendiri dapat gagal. Linux mendokumentasikan ENOMEM ketika memori kernel tidak cukup, sedangkan EMFILE dapat muncul pada kasus unshare yang terkait dengan batas tabel descriptor sistem di /proc/sys/fs/nr_open.

CLOEXEC membentuk batas transisi yang berbeda

CLOSE_RANGE_CLOEXEC tidak langsung menutup descriptor terpilih. Flag ini menandainya sebagai close-on-exec. Pola tersebut berguna ketika kode setup masih membutuhkan descriptor sebelum execve(), tetapi program image berikutnya tidak boleh menerimanya.

Perbedaannya berada pada waktu penutupan:

flags = 0
range ditutup saat close_range()

CLOSE_RANGE_CLOEXEC
range masih dapat dipakai sekarang
range ditutup saat exec berhasil

Hal ini dapat membantu ketika tahap setup berikutnya, termasuk konfigurasi keamanan, masih membutuhkan descriptor yang tidak boleh melewati batas eksekusi final.

CLOSE_RANGE_UNSHARE dan CLOSE_RANGE_CLOEXEC menangani persoalan berbeda. Flag pertama memisahkan tabel descriptor caller dari thread lain; flag kedua menunda penghapusan descriptor sampai penggantian eksekusi. Pemilihan flag perlu memperhitungkan sharing dan lifetime, bukan menganggap semua cleanup range setara.

Range numerik adalah kebijakan, bukan identitas objek

close_range() bekerja pada nomor descriptor. System call ini tidak memilih socket, file, pipe, atau objek kernel lain berdasarkan peran semantiknya. Request dari 3 sampai ~0U berarti setiap descriptor pada wilayah numerik tersebut boleh dibuang dari caller.

Kebijakan itu tepat hanya setelah descriptor yang harus bertahan ditempatkan di bawah batas atau sudah diperhitungkan dengan cara lain. Jika control socket pada descriptor 12 masih diperlukan, range menyeluruh mulai 3 akan menghapusnya, kecuali transisi memakai close-on-exec dan socket tersebut selesai digunakan sebelum eksekusi.

Sifat numerik ini juga memisahkan close_range() dari mekanisme kepemilikan yang melekat pada open file description. System call mengubah entry tabel descriptor; dampak pada open file description di bawahnya mengikuti reference counting biasa setelah entry dihapus.

Cleanup range tidak menggantikan disiplin saat membuat descriptor

Batas cleanup yang sudah di-unshare mencegah aktivitas thread lain mengisi kembali tabel baru milik caller melalui tabel bersama. Hal tersebut tidak menghapus kebutuhan akan kebijakan inheritance descriptor.

Descriptor yang dibuat sebelum batas transisi tetap perlu ditangani secara sengaja. Library dapat menyimpan descriptor internal. Beberapa descriptor mungkin perlu bertahan sampai operasi setup terakhir. Pembuatan descriptor dengan O_CLOEXEC, SOCK_CLOEXEC, pipe2(O_CLOEXEC), dan fasilitas close-on-exec atomik serupa tetap bernilai karena kebijakan inheritance ditetapkan saat descriptor dibuat.

Kedua teknik menangani persoalan yang berdekatan. Pembuatan close-on-exec secara atomik mencegah descriptor ikut terbawa tanpa sengaja melalui race eksekusi. close_range() menyediakan transisi bulk ketika sebuah konteks perlu membersihkan tabel yang sudah ada.

Jaminan pentingnya adalah isolasi mutasi tabel

Properti utama CLOSE_RANGE_UNSHARE bukan sekadar kemampuan menutup banyak descriptor dengan satu pemanggilan. Cleanup dipindahkan ke tabel descriptor yang tidak lagi dibagi dengan thread lain.

Perubahan ini mengubah model concurrency. Aktivitas thread lain dapat terus berjalan pada tabel bersama lama tanpa membuka kembali entry pada tabel private caller, sementara caller dapat menetapkan batas descriptor yang ringkas sebelum fase eksekusi berikutnya.

Untuk kode yang memperlakukan state descriptor sebagai bagian dari batas keamanan atau peluncuran proses, isolasi tersebut lebih penting daripada sekadar mengganti loop close() dengan satu system call.