AddressSanitizer on Ubuntu — GCC/Clangでメモリバグを即座に検出

開発環境

C/C++ プログラムを書いていると、バッファオーバーフローや解放済みメモリへのアクセスといったバグが、実行してみるまで気づかないことがあります。AddressSanitizer(ASan)は、コンパイル時に -fsanitize=address を付けるだけで、こうしたメモリバグを実行時に即座に検出してくれるツールです。

Ubuntu 24.04 LTS には GCC 13.3.0 と Clang 18.1.3 が収録されており、どちらもすぐに使えます。この記事では、Docker コンテナ上で実際にプログラムを動かして得た ASAN の実出力をもとに、インストールから3種類のメモリバグ検出まで手順を説明します。

この記事のポイント

  • Ubuntu 24.04 では apt install build-essential clang だけで ASan がすぐ使える(GCC 13.3.0 / Clang 18.1.3 / libasan8)
  • コンパイルオプションは -fsanitize=address -g の2つだけ。既存のコードを変更する必要はない
  • heap-buffer-overflow・use-after-free・stack-buffer-overflow の3種類を実測で確認済み
  • GCC・Clang どちらでも同じオプションで動く
  • 本番バイナリには含めない。開発・テスト時専用のツール

目次

  1. AddressSanitizer とは
  2. インストール手順(Ubuntu 24.04)
  3. ヒープバッファオーバーフローを検出する
  4. use-after-free を検出する
  5. スタックバッファオーバーフローを検出する
  6. Clang でも同じように使う
  7. よく使う ASAN_OPTIONS
  8. よくあるエラーと対処
  9. まとめ

AddressSanitizer とは

AddressSanitizer は Google が開発し、現在 GCC と Clang の両方に組み込まれているメモリエラー検出器です。仕組みとしては、コンパイル時にメモリアクセスの前後に検査コードを挿入し、実行時に不正なアドレスへのアクセスが起きた瞬間にプロセスを停止してレポートを出力します。

valgrind と比べると速度が速いのが特徴です(公式では実行速度のオーバーヘッドは約2倍と報告されています)。valgrind はバイナリ変換で動くため大きなオーバーヘッドが生じますが、ASan はコンパイル時の計装なので CIテストに組み込みやすい重さです。

ASan が検出できるバグ種別一覧
ASan が検出できるバグ種別一覧

注意

-fsanitize=address でビルドしたバイナリは、メモリ消費が通常の2〜3倍になります。本番環境へのデプロイには使わず、開発・CI テスト専用にしてください。

インストール手順(Ubuntu 24.04)

Ubuntu 24.04 LTS の公式リポジトリには GCC 13 と Clang 18 が収録されています。Docker コンテナ上で確認した実際のインストールログを見てみます。




ubuntu@linuxlab: ~
$ sudo apt update
$ sudo apt install -y build-essential clang
The following NEW packages will be installed:
build-essential clang clang-18 gcc gcc-13
libasan8 libclang-common-18-dev libclang-rt-18-dev …
Setting up gcc-13 (13.3.0-6ubuntu2~24.04.1) …
Setting up clang-18 (1:18.1.3-1ubuntu1) …
Setting up libasan8:amd64 (14.2.0-4ubuntu2~24.04.1) …
build-essential + clang のインストール実ログ
build-essential + clang のインストール実ログ

インストール後に GCC と Clang のバージョンを確認します。




ubuntu@linuxlab: ~
$ gcc –version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
$ clang –version
Ubuntu clang version 18.1.3 (1ubuntu1)
$ dpkg -l ‘libasan*’ | grep ‘^ii’
ii libasan8:amd64 14.2.0-4ubuntu2~24.04.1 AddressSanitizer — a fast memory error detector

Ubuntu 22.04 との比較も取りました。

Ubuntu 22.04 vs 24.04 バージョン実測比較
Ubuntu 22.04 vs 24.04 バージョン実測比較

ヒープバッファオーバーフローを検出する

一番よく遭遇するのがヒープ上のバッファオーバーフローです。malloc で確保した配列の末尾を1つはみ出して書き込む、いわゆる off-by-one エラーを例にします。

①バグのあるコードを書く




ubuntu@linuxlab: ~/asan_demo
$ cat heap_overflow.c
#include <stdlib.h>
int main(void) {
int *arr = (int *)malloc(5 * sizeof(int)); /* 要素5個分 */
arr[5] = 42; /* index 5 は範囲外(0〜4 が有効) */
free(arr);
return 0;
}

②ASan を有効にしてコンパイル・実行する

通常の gcc コマンドに -fsanitize=address-g(デバッグ情報)を追加するだけです。




ubuntu@linuxlab: ~/asan_demo
$ gcc -fsanitize=address -g -o heap_overflow heap_overflow.c
$ ./heap_overflow
==================================================================
==1079==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000054
WRITE of size 4 at 0x503000000054 thread T0
#0 0x… in main /tmp/heap_overflow.c:4
#1 0x… (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)

0x503000000054 is located 0 bytes after 20-byte region [0x503000000040,0x503000000054)
allocated by thread T0 here:
#0 0x… in malloc asan_malloc_linux.cpp:69
#1 0x… in main /tmp/heap_overflow.c:3

SUMMARY: AddressSanitizer: heap-buffer-overflow heap_overflow.c:4 in main
==1079==ABORTING
heap-buffer-overflow ASAN 検出実出力
heap-buffer-overflow ASAN 検出実出力

実際に確認してみると、出力は想像以上に詳しいものでした。「heap_overflow.c:4」というファイル名と行番号が示されているので、どこで書き込みをしたかがすぐわかります。さらに「heap_overflow.c:3」で確保したメモリが「20バイト」(5要素×4バイト)だったことも教えてくれています。

use-after-free を検出する

free() した後のポインタを読み書きするバグは、通常の実行では再現しないことが多く、本番環境だけでクラッシュする厄介なバグです。ASan はこれも確実に捕捉します。




ubuntu@linuxlab: ~/asan_demo
$ cat uaf.c
#include <stdlib.h>
int main(void) {
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p); /* ここで解放 */
return *p + 1; /* 解放後に読み出し */
}
$ gcc -fsanitize=address -g -o uaf uaf.c && ./uaf
==1079==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010
READ of size 4 at 0x502000000010 thread T0
#0 0x… in main /tmp/uaf.c:6

0x502000000010 is located 0 bytes inside of 4-byte region […]
freed by thread T0 here:
#0 0x… in free asan_malloc_linux.cpp:52
#1 0x… in main /tmp/uaf.c:5
SUMMARY: AddressSanitizer: heap-use-after-free uaf.c:6 in main
heap-use-after-free ASAN 検出実出力
heap-use-after-free ASAN 検出実出力

freed by thread T0 here: uaf.c:5」と「uaf.c:6 で READ」の2か所を同時に教えてくれます。解放した側と使った側を両方示してくれるので、コードが大きくても追いやすいです。

スタックバッファオーバーフローを検出する

スタック上のローカル配列の末尾を踏み越えるバグも検出できます。ループ条件の <=< の書き間違いはよくある例です。




ubuntu@linuxlab: ~/asan_demo
$ cat stack_bof.c
#include <stdio.h>
void fill(int *arr, int n) {
for (int i = 0; i <= n; i++) { /* <= が off-by-one */
arr[i] = i;
}
}
int main(void) {
int buf[8];
fill(buf, 8); /* arr[8] が範囲外 */
printf(“buf[0]=%d\n”, buf[0]);
return 0;
}
$ gcc -fsanitize=address -g -o stack_bof stack_bof.c && ./stack_bof
==1079==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7c605b000040
WRITE of size 4 at 0x7c605b000040 thread T0
#0 0x… in fill /tmp/stack_bof.c:4
#1 0x… in main /tmp/stack_bof.c:9
This frame has 1 object(s):
[32, 64) ‘buf’ (line 8) <== Memory access at offset 64 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow stack_bof.c:4 in fill

'buf' (line 8) <== Memory access at offset 64 overflows this variable」という行が印象的でした。どの変数がオーバーフローしたかまで名前で教えてくれます。

Clang でも同じように使う

GCC と同じオプションで Clang でも ASan が使えます。コンパイラを変えるだけです。今回は 10 バイト確保して 11 バイト目(buf[10])に書き込む例で確認しました。




ubuntu@linuxlab: ~/asan_demo
$ cat heap2.c
#include <stdlib.h>
int main(void) {
char *buf = (char *)malloc(10);
buf[10] = ‘X’; /* 10バイト確保、index 10 は範囲外 */
free(buf);
return 0;
}
$ clang -fsanitize=address -g -o heap2 heap2.c
Clang compile OK
$ ./heap2
==3877==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50200000001a
WRITE of size 1 at 0x50200000001a thread T0
#0 0x… in main /tmp/heap2.c:4:13
0x50200000001a is located 0 bytes after 10-byte region [0x502000000010,0x50200000001a)
allocated by thread T0 here:
#1 0x… in main /tmp/heap2.c:3:25
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/heap2.c:4:13 in main
==3877==ABORTING

GCC と Clang でエラーメッセージのフォーマットはほぼ同じです(Clang は行:カラムまで表示される点が少し詳しい)。既存プロジェクトで使っているコンパイラをそのまま使えます。

よく使う ASAN_OPTIONS

環境変数 ASAN_OPTIONS で挙動を調整できます。実際の開発でよく使う設定をまとめました。

オプション デフォルト 説明
detect_leaks=1 有効 メモリリーク検出(LeakSanitizer)。無効にしたい場合は =0
detect_stack_use_after_return=1 無効 関数リターン後のスタック変数参照を検出。有効にすると精度が上がる
abort_on_error=1 無効 エラー時にシグナルで異常終了(コアダンプが欲しい場合)
log_path=/tmp/asan.log stderr レポートをファイルに書き出す。CI でログを保存したい場合
symbolize=1 有効 シンボル解決(ファイル名と行番号)。=0 にするとアドレスのみ

使い方は次のとおりです。




ubuntu@linuxlab: ~
$ ASAN_OPTIONS=detect_leaks=1:log_path=/tmp/asan.log ./heap_overflow
$ cat /tmp/asan.log.PID # PID ごとにファイルが作られる

よくあるエラーと対処

①「ASan runtime does not come first in initial library list」

共有ライブラリとリンクしている場合に出ます。LD_PRELOAD で ASan ライブラリを先に読み込むか、静的リンク(-static-libasan)で回避できます。




ubuntu@linuxlab: ~
# 方法1: 静的リンク
$ gcc -fsanitize=address -static-libasan -g -o myapp myapp.c
# 方法2: LD_PRELOAD(Ubuntu 24.04 で libasan8 を指定)
$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.8 ./myapp

②false positive が出る

longjmpsetjmp、カスタムスタック切り替えを使っているコードで出ることがあります。出力の最後に HINT: this may be a false positive と表示されます。ASAN_OPTIONS=allow_addr2line=1 で詳細を確認するか、誤検知が続く場合は __attribute__((no_sanitize("address"))) で特定関数を除外できます。

③make でのビルドへの組み込み方




ubuntu@linuxlab: ~/myproject
# Makefile に ASAN_FLAGS を追加する
$ make CFLAGS=”-fsanitize=address -g” LDFLAGS=”-fsanitize=address”

注意

-fsanitize=address-fsanitize=thread(ThreadSanitizer)は同時に使えません。データ競合も疑われる場合は、まず ASan で試して問題を切り分けてから TSan を試してください。

まとめ

Ubuntu 24.04 で AddressSanitizer を使うには、apt install build-essential clang でパッケージを入れて、コンパイルに -fsanitize=address -g を付けるだけです。今回 Docker コンテナで実測した結果をまとめます。

  • GCC 13.3.0 と Clang 18.1.3 のどちらでも -fsanitize=address は同じように使える
  • libasan8(14.2.0)が Ubuntu 24.04 の公式リポジトリに収録済み
  • heap-buffer-overflow・heap-use-after-free・stack-buffer-overflow の3種類を実測確認
  • エラーレポートにはファイル名と行番号が含まれるので原因特定が速い
  • Ubuntu 22.04(GCC 11.4 / Clang 14 / libasan6)でも同じオプションで使える
著者アイコン
著者アイコン

個人的に驚いたのは、stack-buffer-overflow のレポートが「'buf' (line 8) <== Memory access at offset 64 overflows this variable」と変数名まで教えてくれた点です。valgrind だとここまで出ない印象があったので、ASan に乗り換えた理由の一つになりました。

ASan はあくまで実行時に通ったコードパスしか検査できません。CI でテストカバレッジを上げながら ASan を有効にするのが、バグを早期に潰すための実践的な使い方です。

本格的に C/C++ プロジェクトをサーバーで動かしたいなら、VPS での環境構築から始めるのが確実です。

Ubuntu サーバーの初期設定(VPS 向け完全手順)

コメント

タイトルとURLをコピーしました