Sebuah service membuka socket berprivilege, menjalankan program helper, dan mengharapkan helper hanya menerima standard input, output, serta error. Satu descriptor yang dibuat tanpa close-on-exec dapat melanggar batas tersebut secara diam-diam. Jika descriptor masih ada saat image program baru dipasang, helper mewarisi akses ke objek kernel meski credentials miliknya sendiri tidak akan dapat membuka objek itu.

Linux memperlakukan kondisi ini sebagai pewarisan descriptor, bukan peristiwa otorisasi baru. Keputusan keamanan ketika objek dibuka sudah melekat pada descriptor. FD_CLOEXEC mengatur apakah otoritas yang sudah terbentuk itu melintasi execve() yang berhasil.

Exec mengganti kode tetapi dapat mempertahankan otoritas descriptor

execve() yang berhasil mengganti image proses pemanggil, tetapi file descriptor yang terbuka pada umumnya tetap terbuka. Descriptor yang ditandai close-on-exec menjadi pengecualian: kernel menutupnya sebagai bagian dari transisi exec yang berhasil.

Artinya, penggantian executable tidak otomatis mereset otoritas. Program yang baru dimuat dapat mulai berjalan dengan socket, directory, pipe, device handle, atau regular file yang diperoleh kode sebelumnya. Descriptor warisan tetap dapat digunakan sesuai access mode dan semantik objek kernel yang sudah melekat padanya.

Program baru tidak memerlukan permission pathname yang dibutuhkan untuk membuka kembali file yang sama. Untuk connected socket, bahkan mungkin tidak ada operasi pathname yang bermakna untuk diulang. Pewarisan descriptor dengan demikian melintasi batas identitas memakai otoritas yang sudah terbentuk sebelum executable baru berjalan.

Close-on-exec tidak mencabut descriptor dari proses saat ini. Mekanisme ini hanya menetapkan perilaku pada exec yang berhasil. Sebelum transisi tersebut, proses tetap dapat memakai, menduplikasi, memindahkan, atau menutup descriptor sesuai aturan kernel yang berlaku.

FD_CLOEXEC melekat pada entri descriptor table

FD_CLOEXEC adalah flag file descriptor. Flag ini berbeda dari file status flags yang terkait dengan open file description. Pemisahan tersebut penting saat descriptor diduplikasi.

dup() dan dup2() membuat descriptor baru dengan flag close-on-exec dalam keadaan clear. Descriptor baru tetap merujuk open file description yang sama dengan sumbernya, tetapi kebijakan pewarisan merupakan properti entri descriptor baru. dup3() dapat menerima O_CLOEXEC, sedangkan fcntl() dengan F_DUPFD_CLOEXEC dapat membuat duplikat yang sejak awal memiliki close-on-exec.

Perbedaan ini juga mencegah kekeliruan konseptual: memasang FD_CLOEXEC pada satu descriptor tidak membuat properti global bagi semua alias dari open file description yang sama. Descriptor lain yang merujuk objek tersebut dapat tetap inheritable.

Proses yang memakai close-on-exec sebagai containment karena itu perlu memperhitungkan jalur pembuatan dan duplikasi descriptor, bukan hanya open() pertama.

Memasang flag setelah pembuatan menyisakan race

Urutan yang membuka descriptor lalu memanggil fcntl(fd, F_SETFD, FD_CLOEXEC) memiliki dua transisi state terpisah. Pada proses single-threaded dengan alur eksekusi yang dikendalikan ketat, celah itu mungkin dapat dikelola. Pada proses multithreaded, thread lain dapat memulai transisi terkait exec ketika descriptor baru masih inheritable.

Kasus berbahaya yang klasik menggabungkan pembuatan descriptor pada satu thread dengan fork() yang diikuti execve() pada thread lain. Jika fork menangkap descriptor table sebelum FD_CLOEXEC dipasang, child menerima descriptor yang inheritable. Pembaruan flag setelahnya pada parent tidak dapat mengubah descriptor table milik child secara retroaktif.

Linux menyediakan opsi close-on-exec atomik pada banyak interface pembuat descriptor. open() menerima O_CLOEXEC; socket() menerima SOCK_CLOEXEC; accept4() menerima SOCK_CLOEXEC; pipe2() menerima O_CLOEXEC. Interface lain menyediakan bentuk khusus seperti EPOLL_CLOEXEC dan MFD_CLOEXEC.

Pembuatan atomik mengubah properti keamanannya. Tidak ada state antara yang terlihat userspace ketika descriptor baru sudah tersedia tetapi belum memiliki flag close-on-exec yang diminta.

Fork dan exec adalah dua tahap pewarisan yang berbeda

fork() dan execve() sering dibahas sebagai satu rangkaian peluncuran, tetapi kebijakan descriptor mencakup dua transisi berbeda. Child hasil fork pada awalnya mewarisi salinan entri descriptor table milik parent. Close-on-exec tidak menutup descriptor hanya karena fork terjadi.

Penutupan baru terjadi ketika exec berhasil. Jika exec gagal, child tetap memiliki descriptor yang diwarisi, termasuk entri yang ditandai FD_CLOEXEC. Error path pada kode launcher karena itu tetap berada dalam domain otoritas descriptor awal sampai resource ditutup secara eksplisit atau proses berakhir.

Perbedaan ini penting bagi desain peluncuran helper yang melakukan pekerjaan di antara fork dan exec. Child pada fase antara tersebut dapat memegang descriptor sensitif meski descriptor sudah ditandai close-on-exec dengan benar. Close-on-exec membatasi otoritas pada program pengganti; mekanisme ini bukan isolasi untuk kode pre-exec yang arbitrer.

Launcher yang memerlukan batas lebih ketat perlu membatasi kode dan operasi pada interval tersebut serta menetapkan kepemilikan descriptor secara eksplisit.

Cleanup massal dan allowlist menangani bagian masalah yang berbeda

Sebagian launcher sengaja meneruskan sejumlah kecil descriptor ke child. Launcher lain menginginkan program pengganti menerima hampir tidak ada descriptor. Close-on-exec mendukung kedua model, tetapi hanya jika setiap descriptor relevan membawa flag yang dimaksud.

Fasilitas penutupan descriptor secara massal dan API peluncuran dengan file actions eksplisit dapat mengurangi ketergantungan pada state proses ambient. Keduanya melengkapi pembuatan close-on-exec atomik. Cleanup pass dapat menetapkan allowlist pada sisi child, sedangkan pembuatan atomik mencegah descriptor baru menjadi inheritable untuk sementara sebelum kebijakan cleanup dijalankan.

Kedua kontrol bekerja pada titik waktu berbeda. Satu membatasi state descriptor ketika dibuat; yang lain membatasi kumpulan yang dipertahankan untuk peluncuran. Menganggap salah satunya sebagai pengganti universal dapat menyisakan celah di sekitar pembuatan descriptor secara concurrent, duplikasi, atau aktivitas pre-exec.

Pewarisan descriptor adalah keputusan propagasi capability

Descriptor warisan dapat membawa otoritas yang lebih besar daripada yang tampak dari identitas filesystem program pengganti. Descriptor dapat mewakili file yang sudah diotorisasi, listening socket, connected peer, directory untuk operasi relatif, atau objek kernel lain yang aksesnya telah terbentuk lebih awal.

Hal itu menjadikan close-on-exec sebagai kontrol propagasi capability. Nilainya bukan pada pemeriksaan permission tambahan. Nilainya terletak pada pencegahan reference tertentu yang sudah terbentuk agar tidak melintasi batas executable.

Pola deployment yang paling kuat bersifat mekanis: minta close-on-exec dalam operasi yang sama dengan pembuatan atau duplikasi descriptor, ubah kebijakan itu secara sengaja hanya untuk descriptor yang memang ditujukan bagi child, dan pertahankan interval pre-exec sesempit mungkin. Dengan kondisi tersebut, descriptor table menjadi interface peluncuran yang eksplisit, bukan kanal tidak sengaja bagi otoritas warisan.