free()した後のポインタを使っても動いてしまう(use-after-free)
free()で解放したメモリに対して読み書きを続けても、多くの場合その場ではクラッシュせず、直前まで入っていた値がそのまま読めてしまいます。
なぜエラーが出ないのか
出力: 42とは無関係な値(実行のたびに変わる)
(エラーなし)- Cは何も報告しません。コンパイルも実行も正常に完了するためです
出力: 42とは無関係な値(実行のたびに変わる)- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) { 5 int *p = malloc(sizeof(int)); 6 *p = 42; 7 free(p); ^ 8 printf("%d\n", *p); 9 return 0; 10 }
free()は解放した領域の中身をその場で消しません。ただし多くの実装では、解放した領域の先頭に管理用のポインタ(次に再利用する空き領域への情報)を書き込みます。そのため、free()の直後に読むだけでも書き込んだ値(42)ではなく無関係な値が出ることがあり、しかも実行のたびに変わります。
直し方: free(p); を (空) にします。
パターン2
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) { 5 int *score = malloc(sizeof(int)); 6 *score = 88; 7 free(score); ^ 8 printf("%d\n", *score); 9 return 0; 10 }
クラッシュしないため一見正しく動いているように見えますが、free()した時点でその領域はもう自分のものではありません。読み取った値をそのまま信用してはいけません。
直し方: free(score); を (空) にします。
パターン3
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) { 5 int *counter = malloc(sizeof(int)); 6 *counter = 7; 7 free(counter); ^ 8 printf("%d\n", *counter); 9 return 0; 10 }
エラーにもならず、値が変わっているのに気づきにくいのがuse-after-freeの怖さです。解放したポインタは二度と参照しないのが鉄則です。
直し方: free(counter); を (空) にします。
パターン4
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) { 5 int *val = malloc(sizeof(int)); 6 *val = 55; 7 free(val); ^ 8 printf("%d\n", *val); 9 return 0; 10 }
free()は解放した領域の中身をその場で消しません。読み取った値をそのまま信用してはいけません。
直し方: free(val); を (空) にします。
パターン5
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int main(void) { 5 int *level = malloc(sizeof(int)); 6 *level = 3; 7 free(level); ^ 8 printf("%d\n", *level); 9 return 0; 10 }
クラッシュしないため一見動いているように見えますが、解放済みの領域を読んだ値は信用できません。
直し方: free(level); を (空) にします。
よくある誤解
「クラッシュしないなら安全」ではありません。解放した領域は次のmalloc()で別の目的のために再利用される可能性があり、その瞬間に無関係なデータが化けたように見えるバグになります。クラッシュしないからこそ発見が遅れる、Cで最も厄介な種類の不具合の一つです。
実務での勘所
解放したメモリがすぐに他の用途で上書きされないのは、free()がOSにメモリを即座に返却するわけではなく、多くの場合プロセス自身が管理する「再利用可能な領域のリスト」に追加するだけだからです。次のmalloc()がそのリストから偶然同じ領域を選ぶまで、以前のデータがそのまま残っていることがあります。この種の不具合を実行するたびに再現するとは限らない曖昧なバグにしないためには、AddressSanitizer(コンパイル時に-fsanitize=addressを付けるだけで有効化できる)やValgrindのようなツールで検出するのが実務での標準的な対策です。