Sebuah proses yang bersiap memanggil execve() dapat memiliki ratusan atau ribuan file descriptor terbuka, sementara program baru hanya seharusnya mewarisi beberapa descriptor tertentu. Menutup descriptor satu per satu menimbulkan biaya pencatatan sekaligus masalah concurrency: thread lain dapat mengalokasikan descriptor ketika loop pembersihan masih berjalan.
Linux close_range() memindahkan operasi tersebut ke batas tabel descriptor. Caller menentukan rentang angka inklusif dan meminta kernel menutup descriptor dalam rentang itu atau menandainya close-on-exec. Dengan CLOSE_RANGE_UNSHARE, caller dapat lebih dahulu memisahkan tabel descriptornya dari thread atau proses yang berbagi tabel tersebut.
Mekanisme ini cocok untuk launcher, service manager, penyiapan sandbox, dan kode lain yang melewati batas execve() dengan kumpulan descriptor yang sengaja dibuat kecil.
Rentang angka menggantikan enumerasi descriptor
Interface dasarnya adalah:
#define _GNU_SOURCE
#include <linux/close_range.h>
#include <unistd.h>
int close_range(unsigned int first, unsigned int last, int flags);Kedua batas bersifat inklusif. Operasi pre-exec yang umum mempertahankan standard input, output, dan error sambil menghapus semua descriptor di atasnya:
if (close_range(3, ~0U, 0) == -1)
return -1;
execve(path, argv, envp);~0U menyatakan nilai unsigned int terbesar, sehingga caller tidak perlu membaca limit tabel descriptor saat ini atau mengiterasi /proc/self/fd. Descriptor yang tidak ada di dalam rentang tidak mengubah operasi menjadi rangkaian kegagalan per descriptor; error saat menutup descriptor individual diabaikan oleh interface ini.
Perilaku tersebut berbeda dari iterasi /proc/self/fd. Enumerasi membentuk urutan seperti snapshot di userspace, memerlukan akses ke procfs, dan membuka descriptor untuk directory yang sedang diperiksa. close_range() menyampaikan rentang yang dimaksud langsung kepada kernel.
Menutup rentang tidak mengisolasi tabel bersama
Thread Linux biasanya berbagi tabel file descriptor melalui CLONE_FILES. Dalam keadaan ini, penutupan descriptor mengubah tabel bersama, sehingga thread lain juga melihat descriptor tersebut sebagai sudah ditutup.
Perilaku itu tepat untuk kepemilikan descriptor biasa di dalam proses, tetapi kurang sesuai ketika satu thread sedang menyiapkan execution image yang berbeda. Urutan pembersihan tidak seharusnya membongkar descriptor yang masih dipakai sibling thread.
CLOSE_RANGE_UNSHARE mengubah operasi tersebut:
if (close_range(3, ~0U, CLOSE_RANGE_UNSHARE) == -1)
return -1;
execve(path, argv, envp);Secara konseptual, operasi ini menggabungkan pemisahan caller dari tabel descriptor bersama dengan penutupan rentang yang dipilih. Kernel dapat mengoptimalkan kasus umum ketika rentang berakhir pada ~0U: alih-alih menyalin tabel lengkap lalu menutup entry tinggi, kernel dapat membentuk tabel privat caller hanya sampai prefix yang dipertahankan.
Flag ini karena itu mengubah lebih dari sekadar performa. Ia menentukan tabel descriptor mana yang dimutasi dan menghilangkan satu kelas race yang berasal dari pemakai tabel bersama secara bersamaan.
Mode CLOEXEC memisahkan kebijakan dari penghancuran resource
CLOSE_RANGE_CLOEXEC tidak langsung menutup descriptor. Flag ini menetapkan state close-on-exec pada seluruh rentang yang diminta:
if (close_range(3, ~0U, CLOSE_RANGE_CLOEXEC) == -1)
return -1;
/* Setup tambahan masih dapat memakai descriptor >= 3 di sini. */
execve(path, argv, envp);Urutan ini penting ketika setup pre-exec masih memerlukan descriptor yang tidak boleh sampai ke program akhir. Launcher mungkin membutuhkan file terbuka saat memasang security policy, menyiapkan namespace, atau menyelesaikan state proses lain. Menandai rentang lebih awal menetapkan kebijakan pewarisan tanpa langsung menghancurkan resource tersebut.
Pada execve() yang berhasil, descriptor dengan close-on-exec ditutup sebagai bagian dari transisi eksekusi. Jika execve() gagal, program saat ini tetap berjalan dan descriptor tetap terbuka dengan flag tersebut. Kode penanganan kegagalan karena itu masih memiliki akses sampai descriptor ditutup secara eksplisit.
CLOSE_RANGE_CLOEXEC merupakan operasi rentang atas flag descriptor; operasi ini tidak mengubah status flag pada open file description yang mendasarinya.
Operasi rentang mengurangi satu race tetapi tidak menentukan kumpulan yang dipertahankan
Pemanggilan seperti close_range(3, ~0U, ...) sederhana ketika descriptor 0, 1, dan 2 adalah satu-satunya entry yang akan diwariskan. Launcher nyata sering membutuhkan descriptor tambahan: channel socket activation, log pipe, control socket, atau executable yang sudah dibuka.
Descriptor tersebut memerlukan penempatan atau preservasi yang disengaja. Salah satu pola menduplikasi descriptor yang dipertahankan ke interval kecil yang dicadangkan, menerapkan close_range() di luar interval tersebut, lalu melakukan pemetaan akhir dengan dup2() atau dup3().
Urutannya harus memperhitungkan alokasi descriptor. Menutup sebuah slot membuat nomornya dapat segera dipakai oleh open(), dup(), socket(), atau pemanggilan serupa. Kode yang memperlakukan nomor descriptor sebagai identitas stabil di antara langkah alokasi yang tidak terkait dapat kembali menciptakan ambiguitas setelah pembersihan rentang.
Invariant yang lebih kuat bukan bahwa suatu nomor descriptor tinggi pernah merujuk objek yang diinginkan. Invariant yang relevan adalah launcher membentuk tabel descriptor akhir dengan entry yang dipertahankan secara eksplisit, sedangkan rentang yang tidak diinginkan dihapus atau ditandai close-on-exec pada titik transisi yang jelas.
CLOEXEC saat pembuatan tetap menjadi batas containment pertama
close_range() berguna pada transisi massal, tetapi tidak menggantikan flag close-on-exec atomik ketika descriptor dibuat. Interface seperti open(..., O_CLOEXEC), pipe2(..., O_CLOEXEC), accept4(..., SOCK_CLOEXEC), dan dup3(..., O_CLOEXEC) mencegah descriptor baru menjadi inheritable selama interval sebelum operasi pembersihan berikutnya.
Dalam proses multithread, membuat descriptor lalu menetapkan FD_CLOEXEC melalui pemanggilan fcntl() terpisah meninggalkan interval saat thread lain dapat menjalankan program baru ketika descriptor tersebut masih inheritable. Flag atomik saat pembuatan menutup interval itu dari sumbernya.
Kedua mekanisme bekerja pada batas yang berbeda. CLOEXEC saat pembuatan membatasi setiap descriptor baru. close_range() menyediakan operasi kebijakan massal ketika proses akan melewati batas lifecycle.
Penanganan kegagalan merupakan bagian dari protokol launch
close_range() mengembalikan -1 untuk argumen yang tidak valid dan dapat melaporkan error resource ketika CLOSE_RANGE_UNSHARE memerlukan pembentukan tabel descriptor baru. Launcher tidak dapat memperlakukan kegagalan sebagai kondisi yang setara dengan rentang descriptor kosong.
Kode fallback juga memerlukan properti concurrency yang sama dengan jalur utama. Mengganti close_range(..., CLOSE_RANGE_UNSHARE) yang gagal dengan loop tanpa sinkronisasi atas /proc/self/fd mungkin mempertahankan fungsi dasar, tetapi kehilangan properti isolasi yang menjadi alasan penggunaan flag tersebut.
Penanganan kompatibilitas sebaiknya menyatakan invariant secara eksplisit: apakah environment menjamin hanya ada satu thread, apakah tabel descriptor privat sudah dibentuk, dan apakah enumerasi procfs dapat diterima. Pemeriksaan versi kernel saja lebih lemah daripada mencoba operasi lalu menangani ENOSYS atau flag yang tidak didukung sesuai constraint tersebut.
Tabel descriptor merupakan bagian dari interface exec
Transisi execve() mempertahankan descriptor terbuka kecuali state close-on-exec menyatakan sebaliknya. Tabel descriptor dengan demikian menjadi input bagi program baru, sebagaimana argument, environment, credential, dan namespace juga menjadi input.
close_range() memberi program Linux cara langsung untuk membentuk input tersebut sebagai operasi rentang. Penutupan langsung tepat ketika setup tidak lagi membutuhkan descriptor. CLOSE_RANGE_CLOEXEC menunda penghancuran hingga batas eksekusi. CLOSE_RANGE_UNSHARE memisahkan tabel caller sebelum mutasi ketika sharing membuat pembersihan tidak aman.
Properti pentingnya bukan sekadar jumlah pemanggilan close() yang lebih sedikit. System call ini menjadikan pewarisan descriptor sebagai transisi pada level tabel yang dapat ditempatkan secara sengaja di dalam protokol peluncuran proses.