Proses yang bersiap menjalankan program lain sering memiliki batas sederhana: descriptor 0, 1, dan 2 tetap tersedia, sedangkan semua descriptor yang lebih tinggi harus hilang. Memanggil close() berulang sampai batas angka yang diperkirakan atau mengiterasi /proc/self/fd mengubah batas tersebut menjadi pemindaian di userspace.

Linux close_range() menyatakan interval itu secara langsung. Kernel menerapkan satu operasi pada setiap file descriptor terbuka dari first sampai last, termasuk kedua ujungnya. Melalui flag, interface yang sama dapat mengisolasi descriptor table bersama atau menandai interval sebagai close-on-exec alih-alih langsung menutupnya.

Interval berupa rentang angka, bukan inventaris

Bentuk dasarnya menerima dua nomor descriptor bertipe unsigned:

if (close_range(3, ~0U, 0) == -1) {
    perror("close_range");
}

~0U memberikan nilai terbesar yang dapat direpresentasikan oleh unsigned int, sehingga call tidak perlu mengetahui nomor descriptor tertinggi yang sedang dipakai proses. Descriptor yang tidak terbuka tidak memerlukan penanganan terpisah.

Ini berbeda dari enumerasi /proc/self/fd. Enumerasi lebih dulu membuat inventaris lalu bertindak pada setiap entri. close_range() menyatakan batas numerik yang diinginkan secara langsung, tanpa mensyaratkan /proc ter-mount atau dapat dibaca.

Call menghasilkan error untuk argumen tidak valid seperti first > last atau bit flag yang tidak didukung. Error saat menutup descriptor individual di dalam interval yang dipilih tidak dilaporkan secara terpisah oleh interface Linux ini.

Pembersihan rentang mempersempit permukaan inheritance

File descriptor secara normal bertahan melewati execve() kecuali flag close-on-exec aktif. Properti ini relevan bagi launcher, service manager, shell, dan proses lain yang menyusun execution environment untuk program lain.

Pembersihan tepat sebelum execve() dapat mempertahankan standard stream konvensional dan membuang semua descriptor di atasnya:

close_range(3, ~0U, 0);
execve(path, argv, envp);

Operasi tersebut tidak menentukan descriptor mana yang secara semantik aman untuk diwariskan. Caller tetap memilih batasnya. Nilainya terletak pada kemampuan merepresentasikan kebijakan interval tanpa lebih dulu menemukan setiap descriptor terbuka.

Hal ini juga berbeda dari menutup underlying open file description pada semua konteks. File descriptor adalah referensi yang disimpan dalam descriptor table; proses atau descriptor lain masih dapat merujuk open file description yang sama setelah satu descriptor dihapus.

CLOSE_RANGE_UNSHARE mengisolasi descriptor table lebih dulu

Thread yang dibuat dengan file-descriptor table bersama dapat melihat perubahan pada table tersebut. Menutup sebuah rentang ketika execution context lain berbagi table karena itu dapat berdampak lebih luas daripada calling thread.

CLOSE_RANGE_UNSHARE mengubah batas tersebut:

close_range(3, ~0U, CLOSE_RANGE_UNSHARE);

Untuk operasi yang dipilih, Linux melakukan unshare pada descriptor table milik caller sebelum menerapkan penutupan. Semantik yang didokumentasikan secara konseptual setara dengan:

unshare(CLONE_FILES);
close_range(first, last, 0);

Operasi gabungan ini dapat menghindari pekerjaan yang muncul pada urutan dua call secara literal. Dalam kasus umum ketika last melampaui descriptor table yang telah dialokasikan, kernel dapat membangun table hasil unshare sampai first dan tidak perlu menyalin entri yang akan dibuang.

Flag ini menangani sharing pada descriptor table. Flag tersebut tidak membuat aktivitas arbitrer di thread lain otomatis aman dan tidak menggantikan sinkronisasi untuk state aplikasi di luar table itu.

CLOSE_RANGE_CLOEXEC menunda penutupan aktual

CLOSE_RANGE_CLOEXEC mengubah operasi dari penghapusan langsung menjadi pemberian flag:

close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);

Descriptor dalam interval tetap dapat digunakan oleh proses saat ini, tetapi ditandai close-on-exec. execve() yang kemudian berhasil akan menutupnya sebagai bagian dari transisi eksekusi.

Pemisahan ini berguna ketika tahap pre-exec masih membutuhkan sebagian descriptor tersebut. Sebagai contoh, konfigurasi keamanan dapat berlangsung setelah interval descriptor ditandai tanpa memaksa penutupan langsung. Penghapusan descriptor akhirnya mengikuti semantik close-on-exec normal.

Flag ini tidak menjamin execve() akan terjadi. Jika eksekusi berlanjut tanpa exec yang berhasil, descriptor tetap terbuka di proses.

Race pembuatan descriptor tetap bergantung pada kebijakan

Loop seperti berikut memiliki urutan operasi individual yang terlihat:

for (int fd = 3; fd < limit; fd++) {
    close(fd);
}

Thread lain yang berbagi table dapat mengalokasikan descriptor ketika loop sedang berjalan. Pembersihan berbasis enumerasi juga memisahkan tahap penemuan dari penutupan berikutnya.

close_range() menghapus loop per-descriptor di userspace, tetapi semantik concurrency tetap bergantung pada flag dan desain proses di sekitarnya. CLOSE_RANGE_UNSHARE adalah mekanisme untuk mengisolasi caller dari descriptor table bersama sebelum rentang ditutup. Tanpa flag tersebut, call bekerja pada table yang sedang dibagi oleh caller.

Perbedaannya bersifat struktural: source code yang tampak atomik bukan dengan sendirinya jaminan concurrency. Objek kernel yang relevan adalah descriptor table dan status sharing-nya.

CLOEXEC dan penutupan langsung menangani urutan berbeda

Penutupan langsung sesuai ketika tidak ada tahap pre-exec berikutnya yang memerlukan descriptor dalam interval. Menandainya dengan CLOSE_RANGE_CLOEXEC berguna ketika urutan setup mengharuskan descriptor tetap tersedia sampai batas eksekusi.

Dua urutan ini karena itu menyatakan kebijakan berbeda:

close_range(3, ~0U, CLOSE_RANGE_UNSHARE);
execve(path, argv, envp);
close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
/* additional setup */
execve(path, argv, envp);

Urutan pertama mengisolasi dan menghapus rentang sebelum exec. Urutan kedua membiarkan rentang terbuka sementara dan meminta transisi exec menghapusnya.

CLOSE_RANGE_UNSHARE dan CLOSE_RANGE_CLOEXEC juga dapat digabungkan pada Linux. Dalam bentuk ini, descriptor table di-unshare dan descriptor terpilih pada table milik caller ditandai close-on-exec, bukan langsung ditutup.

Batasnya sengaja bersifat kasar

Interval berurutan cocok ketika proses dapat menyisakan sejumlah kecil descriptor yang sudah diketahui dan menolak inheritance untuk sisanya. Bentuk ini kurang ekspresif ketika banyak descriptor tersebar harus tetap bertahan.

Keterbatasan itu merupakan bagian dari bentuk API. close_range() tidak menerima exclusion set. Launcher yang harus mempertahankan descriptor 3, 9, dan 27 memerlukan strategi lain, misalnya memindahkan descriptor yang dipertahankan, mengatur nomor descriptor sebelum pembersihan, atau menerapkan kebijakan close-on-exec saat descriptor dibuat.

System call ini karena itu paling sesuai ketika arsitektur proses memiliki batas descriptor yang jelas. Ia mengubah batas tersebut dari prosedur inventaris-dan-loop menjadi operasi kernel, sementara CLOSE_RANGE_UNSHARE dan CLOSE_RANGE_CLOEXEC menentukan apakah sharing table dan urutan exec menjadi bagian dari transisi yang sama.