Sebuah Bug Dalam File Handling
• 3 menit
Beberapa waktu lalu saya menemukan sebuah bug yang cukup aneh tetapi sepele di dalam Granary.
Saya menghabiskan waktu beberapa jam untuk menemukan sumber dari bug tersebut🥲
Setelah penyebabnya ditemukan, solusi untuk mengatasi masalah ini ternyata sangat sederhana: hanya membutuhkan 1 baris kode tambahan!
Permasalahan
Mari kita lihat contoh penggunaan dasar Granary:
Pada contoh penggunaan di atas, saya melakukan:
- Penambahan data baru, yakni
foo
- Melihat daftar semua key yang disimpan
Sekarang saya ingin menghapus data foo
dari penyimpanan:
Masih oke. Ketika diperiksa return code-nya pun tidak terindikasi terdapat error.
Tetapi, ketika saya ingin kembali melihat daftar dari semua key:
Kini ada error yang berbunyi “cipher: message authentication failed”. Dari mana datangnya error ini?
Padahal sebelumnya tidak ada yang aneh ketika perintah gran list
pertama kali dijalankan!
Proses debugging
Terdapat beberapa fakta terkait bug ini:
- Bug terjadi setelah subcommand
remove
dijalankan (meskipun tidak ada kode error yang dikembalikan oleh program) - Pesan “cipher: message authentication failed” mengindikasikan bahwa error ini melibatkan proses enkripsi (cipher)
Kemungkinan #1: Format file yang corrupt
Tebakan pertama saya adalah: format file penyimpanan menjadi corrupt setelah subcommand remove
dijalankan.
Tetapi ketika saya mencoba memeriksa isi file penyimpanan itu sendiri, format filenya masih sesuai dengan spesifikasi yang saya gunakan:
Isi konten terbagi menjadi 3 segmen yang dipisahkan oleh karakter “:”. Pada segmen kedua, kunci terbagi lagi menjadi 2 bagian yang dipisahkan oleh karakter “$”.
Format isi file pada gambar di atas masih sesuai dengan ketentuan-ketentuan tersebut.
Sehingga saya mencoret kemungkinan yang pertama ini.
Kemungkinan #2: Hash kunci yang corrupt
Saya memikirkan kemungkinan kedua: hash kunci yang disimpan berubah. Kemudian saya coba langsung memeriksa hal tersebut.
Saya lalu menemukan bahwa… hash keduanya juga masih sama🥲
Bagian segmen kedua dari isi file pada gambar menunjukkan hasil yang sama, sehingga kunci tidak berubah.
Maka kita juga mencoret kemungkinan yang satu ini.
Kemungkinan lain
Saya mencoba melihat perbedaan fungsi yang menghandle subcommand set
dengan subcommand remove
.
Perbedaan yang terdapat di antara mereka hanyalah
- Pada subcommand
set
:
func (sc *subCommandSet) handle(c *cli.Context) error {
// ...
data[c.Args().Get(0)] = c.Args().Get(1)
// ...
}
- Sedangkan pada subcommand
remove
:
func (sc *subCommandRemove) handle(c *cli.Context) error {
// ...
delete(data, c.Args().First())
// ...
}
Itu saja! Hanya satu baris yang berbeda, dan itupun hanya sebuah operasi map dasar milik Golang.
Dari mana sebenarnya bug ini berasal?
Saya bahkan telah memeriksa beberapa release note dari Go sejak versi yang saya gunakan saat ini (1.22) hingga versi yang terbaru. Saya berpikir, barangkali terdapat bug di dalam fungsi json.Marshal
atau semacamnya.
(disclaimer: sayangnya tidak ada🙂)
Titik terang
Kemudian terbesit di dalam pikiran saya:.
Panjang isi file penyimpanan Granary harusnya berkurang setelah subcommand
remove
dijalankan.
Lalu saya memeriksa kembali isi konten dari file penyimpanan.
Ternyata benar saja, panjang konten ternyata masih sama antara sebelum maupun sesudah data dihapus!
Dan jika dilihat, ternyata beberapa karakter terakhir juga tidak berubah:
Ekspektasi saya adalah seharusnya Granary akan me-replace semua isi konten yang ada di dalam file penyimpanan setiap kali terdapat data yang berubah.
Sedangkan, berdasarkan gambar di atas, ternyata bagian file yang di-overwrite hanya mencapai bagian tertentu saja, dan tidak sampai menyentuh EOF.
Oleh karenanya, proses dekripsi selalu gagal untuk dilakukan, karena data ciphertext-nya itu sendiri sudah menjadi tidak valid.
Solusi?
Saya harus memastikan bahwa seluruh isi file penyimpanan Granary dihapus terlebih dulu sebelum data disimpan kembali pada saat subcommand remove
dijalankan.
Saya menggunakan method func (*File) Truncate
untuk melakukan ini.
Sehingga jumlah kode yang saya tambahkan untuk menyelesaikan bug ini hanyalah: 1 baris kode saja (tidak termasuk comment).
Penutup
Dapat kita lihat, sebuah permasalahan sepele dalam programming terkadang membutuhkan usaha dan waktu yang lumayan banyak untuk bisa diselesaikan.
Sekian dan terima kasih sudah membaca👋