Seal Linux memfd Mengubah Status File Mutable Menjadi Kontrak Terbatas

File yang dikembalikan memfd_create() bermula sebagai status file mutable pada penyimpanan berorientasi memori, tetapi Linux dapat menghapus operasi mutasi dari file tersebut secara bertahap. Saat pembuatan memakai MFD_ALLOW_SEALING, fcntl() dengan F_ADD_SEALS dapat melarang penyusutan, pertumbuhan, penulisan, penulisan pada masa berikutnya, atau perubahan lanjutan terhadap kumpulan seal. Pembatasan itu melekat pada inode, bukan pada satu descriptor, sehingga pemindahan descriptor lain untuk objek yang sama tidak memulihkan operasi yang telah dihapus oleh seal.

Mekanisme ini membentuk batas yang berguna untuk transfer data antarproses. Produsen dapat mengalokasikan dan mengisi file anonim, membatasi mutasi berikutnya, lalu mengirim descriptor melalui UNIX domain socket. Konsumen menerima objek serupa file biasa dengan permukaan mutasi yang sudah dipersempit oleh kernel.

Penamaan anonim tidak membuat objek privat bagi satu proses

memfd_create() mengembalikan descriptor untuk file anonim. Nama yang diberikan terutama berguna untuk diagnostik dan tidak membuat pathname biasa yang dapat dipakai aplikasi untuk membuka ulang objek. Descriptor tetap mengikuti semantik descriptor normal: fork() dapat mewarisinya, execve() dapat mempertahankannya kecuali close-on-exec aktif, dan pengiriman descriptor dapat memindahkan akses ke proses lain.

Ukuran file dapat diubah dengan ftruncate(), file dapat dipetakan dengan mmap(), dan operasi file biasa dapat mengaksesnya sesuai izin serta seal yang kemudian diterapkan. Masa hidupnya karena itu berorientasi pada descriptor dan mapping, bukan terikat pada directory entry persisten.

Perbedaan tersebut penting pada rancangan IPC. Penempatan anonim menghilangkan publikasi pathname dari pertukaran data, tetapi tidak otomatis membuat byte bersifat immutable. Penerima yang memegang descriptor writable masih dapat mengubah objek yang belum diberi seal. Sealing merupakan mekanisme terpisah yang mempersempit kewenangan itu.

Seal adalah status inode yang monoton

F_ADD_SEALS menambahkan bit ke kumpulan seal. Seal yang berhasil ditambahkan tidak dapat dihapus. F_GET_SEALS melaporkan kumpulan saat ini, dan semua descriptor yang merujuk inode yang sama menerima pembatasan yang sama.

Sifat monoton tersebut mengubah model koordinasi. Produsen tidak sekadar menyerahkan janji berbasis konvensi. Objek dapat dipindahkan melalui status dengan operasi yang diizinkan terus berkurang:

dibuat
  |
  v
ditetapkan ukuran dan diisi
  |
  v
F_SEAL_SHRINK | F_SEAL_GROW
  |
  v
F_SEAL_WRITE
  |
  v
F_SEAL_SEAL

Urutan ini bersifat ilustratif, bukan kewajiban. Aplikasi memilih seal sesuai mutasi yang perlu dilarang. Setelah F_SEAL_SEAL terpasang, upaya berikutnya untuk menambahkan seal gagal dengan EPERM, sehingga bit tersebut menutup konfigurasi seal itu sendiri.

Kernel memberlakukan pembatasan setelah F_ADD_SEALS berhasil; kerja sama dari proses yang memegang descriptor lain tidak diperlukan.

Ukuran dan isi merupakan dimensi mutasi yang berbeda

F_SEAL_SHRINK mencegah operasi yang mengurangi ukuran file, sedangkan F_SEAL_GROW mencegah operasi yang memperbesar ukuran. Tidak satu pun dari kedua seal itu, jika berdiri sendiri, membuat byte yang sudah ada menjadi read-only.

Pemisahan tersebut penting bagi data yang dipetakan. Jika konsumen mengasumsikan layout berukuran tetap, penyusutan tak terduga dapat menghilangkan bagian file di luar batas baru dan membuat akses berikutnya bermasalah. Pencegahan shrink melindungi batas ukuran, sedangkan pencegahan write melindungi isi. Payload tetap dan immutable umumnya memerlukan pembatasan ukuran sekaligus pembatasan write.

F_SEAL_WRITE memblokir perubahan isi melalui operasi seperti write() dan mode fallocate() yang relevan. Seal ini juga mencegah pembuatan shared writable mapping baru. Linux menolak penambahan F_SEAL_WRITE dengan EBUSY selama masih ada writable shared mapping, sebab mapping tersebut akan mempertahankan jalur untuk mengubah file setelah seal dinyatakan terpasang.

Produsen yang mengisi objek melalui MAP_SHARED | PROT_WRITE karena itu perlu melepas writable shared mapping tersebut sebelum dapat menerapkan F_SEAL_WRITE dengan sukses.

F_SEAL_FUTURE_WRITE mempertahankan shared writer yang sudah ada

F_SEAL_FUTURE_WRITE menetapkan batas yang berbeda. Seal ini memblokir pemanggilan write() berikutnya dan pembuatan writable mapping berikutnya, tetapi shared writable mapping yang dibuat sebelum seal tetap dapat mengubah file.

Perilaku tersebut mendukung produsen yang masih harus memperbarui region hasil mapping sambil mencegah konsumen baru memperoleh jalur write yang setara. Seal ini tidak dapat dipertukarkan dengan F_SEAL_WRITE: mapping produsen yang sudah ada tetap menjadi kanal mutasi aktif.

Kontrak yang dapat diamati harus memperhitungkan perbedaan itu. Konsumen yang memerlukan byte stabil tidak dapat menganggap F_SEAL_FUTURE_WRITE saja sebagai bukti immutability. Shared writable mapping yang sudah ada masih dapat mengubah payload.

Transfer descriptor tidak melemahkan seal

UNIX domain socket dapat membawa file descriptor melalui SCM_RIGHTS. Proses penerima memperoleh descriptor yang merujuk objek file terbuka atau status inode yang sama sesuai semantik pengiriman descriptor. File seal tetap melekat pada inode, sehingga penerima tidak dapat melewatinya hanya dengan memperoleh nomor descriptor baru.

Urutan publikasi yang ringkas dapat memakai status kernel sebagai bagian dari handoff:

int fd = memfd_create("payload", MFD_ALLOW_SEALING | MFD_CLOEXEC);

ftruncate(fd, payload_size);
/* populate fd */

int seals = F_SEAL_SHRINK |
            F_SEAL_GROW |
            F_SEAL_WRITE |
            F_SEAL_SEAL;

if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
    /* do not publish the descriptor */
}

/* transfer fd only after sealing succeeds */

Penanganan error merupakan bagian dari protokol. Jika sealing gagal, tetap mengirim descriptor akan memublikasikan objek dengan kontrak mutasi yang lebih lemah daripada ekspektasi penerima. Batas handoff karena itu semestinya ditempatkan setelah seal yang diperlukan berhasil diterapkan.

Seal membatasi mutasi, bukan interpretasi

memfd yang telah diberi seal tidak memvalidasi data di dalamnya. Kernel dapat menegakkan larangan perubahan byte atau ukuran file melalui operasi yang dilarang, tetapi kernel tidak menetapkan bahwa header konsisten secara internal, offset berada dalam rentang, checksum benar, atau objek serial sesuai dengan schema aplikasi.

Ada dua kontrak yang berbeda. Seal membatasi mutasi file yang masih mungkin dilakukan. Validasi aplikasi menentukan apakah payload tetap tersebut dapat diterima oleh parser atau protokol tertentu.

Urutannya dapat berpengaruh. Produsen dapat mengisi dan memvalidasi representasinya sebelum sealing. Konsumen yang menerima objek dengan write seal penuh dapat memvalidasi byte tanpa writer konkuren mengubahnya melalui jalur write biasa atau shared writable mapping yang keberadaannya akan menggagalkan pemasangan F_SEAL_WRITE.

Sealing adalah status khusus Linux, bukan jaminan file portabel

memfd_create() dan operasi sealing yang dibahas di sini merupakan interface Linux. Kode yang memakainya perlu memperlakukan sealing sebagai kontrak sistem operasi, bukan properti bahasa C atau jaminan file POSIX umum.

Bahkan di Linux, kumpulan seal yang tepat lebih penting daripada label “sealed”. File dengan hanya F_SEAL_SHRINK tetap writable. File dengan F_SEAL_FUTURE_WRITE masih mungkin memiliki shared writable mapping yang dibuat sebelumnya. File dengan F_SEAL_SEAL hanya mencegah perubahan kumpulan seal; properti mutasi lainnya bergantung pada seal tambahan yang terpasang.

Abstraksi yang tepat adalah pengurangan kapabilitas secara eksplisit. Produsen memulai dengan file yang dapat dibentuk, menghapus operasi mutasi tertentu pada batas publikasi yang disengaja, lalu mengirim descriptor dengan perilaku tersisa yang ditegakkan terhadap setiap pemegang inode tersebut.