Memory leak adalah bug di mana program mengalokasikan memori dengan malloc/calloc/realloc tetapi lupa memanggil free sebelum pointer hilang. Dampaknya bertumpuk: program yang berjalan lama (server, daemon, aplikasi desktop) perlahan mengonsumsi memori sampai sistem kehabisan dan membunuh proses. LeakSanitizer (LSan) adalah tool yang otomatis terintegrasi dengan ASan pada platform Linux dan mendeteksi blok memori yang masih teralokasi saat program keluar. Pada chapter ini kita akan mempelajari cara LSan bekerja, menguji dua skenario leak (sederhana dan bertingkat), serta membaca laporan direct leak vs indirect leak. Dokumentasi resmi LSan tersedia di clang.llvm.org/docs/LeakSanitizer.html.
Cara Kerja LSan #
LSan berjalan otomatis saat program berakhir. Saat fungsi exit() dipanggil, runtime LSan memindai seluruh memori heap yang masih teralokasi, lalu memeriksa apakah alamat blok tersebut masih direferensikan oleh pointer yang reachable dari stack, global, atau register. Bila tidak ditemukan pointer yang menunjuk ke blok tersebut, blok diklasifikasikan sebagai leak.
Karena LSan terintegrasi dengan ASan, kita tidak perlu flag tambahan untuk mengaktifkannya. Build dengan -fsanitize=address di Linux sudah otomatis memakai LSan.
gcc -fsanitize=address -g -O0 -std=c11 program.c -o programLSan hanya aktif secara default di Linux. Pada macOS, kita perlu menambahkan -fsanitize=leak eksplisit. Pada Windows, LSan belum didukung sepenuhnya.
Leak Sederhana #
Mulai dari kasus paling sederhana: sebuah fungsi salin_nama yang mengalokasikan buffer string. Pemanggil membebaskan nama1 tetapi lupa membebaskan nama2.
/* File: /tmp/test_leak1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *salin_nama(const char *src) {
size_t n = strlen(src) + 1;
char *out = (char *)malloc(n);
if (out) memcpy(out, src, n);
return out;
}
int main(void) {
char *nama1 = salin_nama("Budi");
char *nama2 = salin_nama("Siti");
printf("nama1 = %s\n", nama1);
free(nama1);
/* nama2 sengaja di-leak untuk demo */
return 0;
}gcc -fsanitize=address -g -O0 -std=c11 /tmp/test_leak1.c -o /tmp/test_leak1
/tmp/test_leak1Output yang Dihasilkan:
nama1 = Budi
=================================================================
==9495==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 5 byte(s) in 1 object(s) allocated from:
#0 0x7f7600d7cc57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x5574688f81e4 in salin_nama /tmp/test_leak1.c:6
#2 0x5574688f8236 in main /tmp/test_leak1.c:12
#3 0x7f7600abdca7 (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7)
SUMMARY: AddressSanitizer: 5 byte(s) leaked in 1 allocation(s).Mari kita bedah elemen kunci pada laporan LSan di atas:
==9495==ERROR: LeakSanitizer: detected memory leaks: nomor dalam==...==adalah PID proses. LSan mengumumkan bahwa leak ditemukan. Pesan ini muncul setelah program selesai menjalankanmaindan kembali ke runtime—bukan saat leak terjadi.Direct leak of 5 byte(s) in 1 object(s): klasifikasi leak adalahDirect, yaitu blok memori yang tidak direferensikan pointer manapun yang reachable dari stack/global/register. Ukuran 5 byte (string"Siti"+ null terminator) dalam 1 alokasi.#0 ... in malloc ... #1 ... in salin_nama /tmp/test_leak1.c:6: backtrace alokasi menunjukkan bahwa blok dialokasikan olehmallocyang dipanggil darisalin_namapada baris 6 (char *out = (char *)malloc(n);).#2 ... in main /tmp/test_leak1.c:12: pemanggil fungsisalin_namaada di baris 12main, yaituchar *nama2 = salin_nama("Siti");. Dengan backtrace ini, kita tahu persis blok mana yang bocor dan siapa pemanggil akar.SUMMARY: AddressSanitizer: 5 byte(s) leaked in 1 allocation(s): ringkasan akhir—total 5 byte dalam 1 alokasi yang bocor. Kode keluar program menjadi 1 (bukan 0) sehingga CI bisa mendeteksi kegagalan.
Leak Bertingkat (Direct vs Indirect) #
Pada kasus nyata, leak sering kali terjadi pada struct yang berisi pointer ke struct lain. LSan membedakan dua jenis leak:
- Direct leak: blok yang tidak direferensikan oleh pointer manapun yang reachable.
- Indirect leak: blok yang hanya direferensikan oleh pointer dalam blok yang juga bocor (direct leak). Bila direct leak diperbaiki, indirect leak otomatis ikut teratasi.
/* File: /tmp/test_leak2.c */
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
char *nama;
} Pengguna;
static Pengguna *buat_pengguna(int id, const char *nama) {
Pengguna *p = (Pengguna *)malloc(sizeof(Pengguna));
if (!p) return NULL;
p->id = id;
p->nama = (char *)malloc(50);
if (p->nama) {
snprintf(p->nama, 50, "%s", nama);
}
return p;
}
int main(void) {
Pengguna *u1 = buat_pengguna(1, "Budi");
Pengguna *u2 = buat_pengguna(2, "Siti");
printf("pengguna 1: %d %s\n", u1->id, u1->nama);
/* sengaja tidak free u1 dan u2 */
return 0;
}gcc -fsanitize=address -g -O0 -std=c11 /tmp/test_leak2.c -o /tmp/test_leak2
/tmp/test_leak2Output yang Dihasilkan:
pengguna 1: 1 Budi
=================================================================
==9503==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 16 byte(s) in 1 object(s) allocated from:
#0 0x7ff3524c6c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55810f1e41f1 in buat_pengguna /tmp/test_leak2.c:8
#2 0x55810f1e42e7 in main /tmp/test_leak2.c:19
Direct leak of 16 byte(s) in 1 object(s) allocated from:
#0 0x7ff3524c6c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55810f1e41f1 in buat_pengguna /tmp/test_leak2.c:8
#2 0x55810f1e42cf in main /tmp/test_leak2.c:18
Indirect leak of 50 byte(s) in 1 object(s) allocated from:
#0 0x7ff3524c6c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55810f1e4248 in buat_pengguna /tmp/test_leak2.c:11
#2 0x55810f1e42e7 in main /tmp/test_leak2.c:19
SUMMARY: AddressSanitizer: 82 byte(s) leaked in 3 allocation(s).Mari kita uraikan tiga blok leak yang dilaporkan LSan pada output di atas:
Direct leak of 16 byte(s) ... #1 in buat_pengguna /tmp/test_leak2.c:8 ... #2 in main /tmp/test_leak2.c:19: ini adalah structPenggunakedua (u2) yang dialokasikan pada baris 8buat_pengguna. Pemanggilnya adalahmainbaris 19 (Pengguna *u2 = buat_pengguna(2, "Siti");). Ukuran 16 byte sesuaisizeof(Pengguna)(4 byteint+ padding + 8 byte pointer, total 16 byte pada sistem 64-bit).Direct leak of 16 byte(s) ... #2 in main /tmp/test_leak2.c:18: ini adalah structPenggunapertama (u1) yang dialokasikan darimainbaris 18. Klasifikasi direct karena setelahmainselesai, pointeru1danu2hilang dari stack—tidak ada yang menunjuk ke struct tersebut.Indirect leak of 50 byte(s) ... #1 in buat_pengguna /tmp/test_leak2.c:11 ... #2 in main /tmp/test_leak2.c:19: ini adalah fieldnama(alokasi 50 byte) miliku2. Klasifikasi indirect karena satu-satunya pointer ke blok ini ada di dalam structu2yang juga bocor. Bila kita membebaskanu2(yang juga otomatis membebaskanu2->nama), indirect leak ini akan hilang.
Total: 16 + 16 + 50 = 82 byte dalam 3 alokasi. LSan menampilkan ringkasan ini pada baris SUMMARY.
Perbaikan Leak Bertingkat #
Strategi perbaikan leak bertingkat selalu dimulai dari direct leak. Pada contoh di atas, perbaikan adalah menambah pembebasan u1 dan u2 lengkap dengan field-nya:
free(u1->nama);
free(u1);
free(u2->nama);
free(u2);Urutan penting: bebaskan field nama terlebih dahulu, lalu struct Pengguna. Membalik urutan (free(u1) duluan) menyebabkan use-after-free saat kita mengakses u1->nama setelah struct dibebaskan.
Mengontrol LSan #
LSan dikendalikan oleh variabel lingkungan LSAN_OPTIONS. Beberapa opsi penting:
| Variabel | Fungsi |
|---|---|
LSAN_OPTIONS=detect_leaks=1 | aktifkan LSan (default: on di Linux) |
LSAN_OPTIONS=detect_leaks=0 | matikan LSan (berguna saat debugging ASan tanpa laporan leak) |
LSAN_OPTIONS=exitcode=23 | kode keluar saat leak ditemukan (default: 23) |
LSAN_OPTIONS=suppressions=file.txt | file berisi pola suppress untuk leak tertentu |
LSAN_OPTIONS=print_stats=1 | cetak statistik sebelum laporan leak |
LSAN_OPTIONS=detect_leaks=0 /tmp/test_leak1Output yang Dihasilkan:
nama1 = BudiDengan detect_leaks=0, LSan dimatikan dan program keluar dengan kode 0. Hal ini berguna saat kita sedang fokus pada bug ASan (misalnya use-after-free) dan tidak ingin laporan leak mengganggu output.
File Suppression #
Pada proyek yang menggunakan pustaka pihak ketiga dengan leak internal yang tidak bisa kita perbaiki, LSan menyediakan mekanisme suppression. File suppression berisi pola yang dicocokkan dengan backtrace alokasi; alokasi yang cocok tidak dilaporkan sebagai leak.
# File: /tmp/lsan_suppress.txt
leak:libpurple.*
leak:buat_penggunaLSAN_OPTIONS=suppressions=/tmp/lsan_suppress.txt /tmp/test_leak2Format setiap baris suppression adalah leak:<pattern>, di mana <pattern> adalah regex yang dicocokkan dengan nama fungsi pada backtrace. Baris leak:buat_pengguna akan menyembunyikan seluruh leak yang melibatkan fungsi buat_pengguna.
Strategi Membebaskan Memori #
Untuk memastikan tidak ada leak, praktik terbaik adalah memakai pola berikut:
- Setiap alokasi harus memiliki pemilik yang jelas: pemilik adalah pihak yang bertanggung jawab membebaskan. Tanpa pemilik, alokasi akan bocor.
- Fungsi yang mengembalikan pointer harus mendokumentasikan kepemilikan: apakah pemanggil harus membebaskan hasil, atau ada fungsi cleanup tersendiri.
- Bebaskan dalam urutan terbalik dari alokasi: alokasi terakhir dibebaskan pertama. Pola LIFO ini menghindari use-after-free pada resource yang saling bergantung.
- Pertimbangkan RAII pattern atau
cleanupattribute (GCC extension):__attribute__((cleanup(free_func)))otomatis memanggil fungsi cleanup saat variabel keluar dari scope.