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:

Demo dasar Granary

Pada contoh penggunaan di atas, saya melakukan:

  1. Penambahan data baru, yakni foo
  2. Melihat daftar semua key yang disimpan

Sekarang saya ingin menghapus data foo dari penyimpanan:

Demo penghapusan data Granary

Masih oke. Ketika diperiksa return code-nya pun tidak terindikasi terdapat error.

Tetapi, ketika saya ingin kembali melihat daftar dari semua key:

Demo bug Granary

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:

  1. Bug terjadi setelah subcommand remove dijalankan (meskipun tidak ada kode error yang dikembalikan oleh program)
  2. 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:

Tampilan isi file penyimpanan Granary

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🥲

Tampilan perbedaan isi file penyimpanan Granary

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

  1. Pada subcommand set:
func (sc *subCommandSet) handle(c *cli.Context) error {
  // ...
  data[c.Args().Get(0)] = c.Args().Get(1)
  // ...
}
  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:

Tampilan isi file penyimpanan Granary yang corrupt

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👋