サニタイザーによるデバッグ

未定義動作サニタイザー

Clang の 未定義動作サニタイザー (UBSan) は Emscripten で使用できます。これにより、コード内のバグを非常に簡単に捕捉できます。

UBSan を使用するには、-fsanitize=undefinedemcc または em++ に渡すだけです。codegen とシステムライブラリの両方に影響するため、コンパイル段階とリンク段階の両方でこれを渡す必要があることに注意してください。

Null 参照の捕捉

デフォルトでは、Emscripten では、従来のプラットフォームとは異なり、null ポインターを逆参照しても、WebAssembly メモリでは 0 が通常のアドレスであるため、すぐにセグメンテーション違反が発生しません。0 は、JavaScript の型付き配列内の通常の位置でもあります。これは、WebAssembly と並行した JavaScript (ランタイムサポートコード、JS ライブラリメソッド、EM_ASM/EM_JS など) で問題となり、-sWASM=0 でビルドする場合のコンパイル済みコードでも問題となります。

ASSERTIONS が有効になっているビルドでは、プログラム実行の最後にアドレス 0 に格納されているマジッククッキーがチェックされます。つまり、プログラムの実行中に何かがその場所に書き込んだ場合、通知されます。これは書き込みのみを検出し、読み取りは検出せず、不正な書き込みが実際に行われた場所を見つけるのには役立ちません。

次のプログラム null-assign.c を考えてみましょう。

int main(void) {
    int *a = 0;
    *a = 0;
}

UBSan を使用しないと、プログラムが終了したときにエラーが発生します。

$ emcc null-assign.c
$ node a.out.js
Runtime error: The application has corrupted its heap memory area (address zero)!

UBSan を使用すると、これが起こった正確な行番号が得られます。

$ emcc -fsanitize=undefined null-assign.c
$ node a.out.js
null-assign.c:3:5: runtime error: store to null pointer of type 'int'
Runtime error: The application has corrupted its heap memory area (address zero)!

次のプログラム null-read.c を考えてみましょう。

int main(void) {
    int *a = 0, b;
    b = *a;
}

UBSan を使用しないと、フィードバックはありません。

$ emcc null-read.c
$ node a.out.js
$

UBSan を使用すると、これが起こった正確な行番号が得られます。

$ emcc -fsanitize=undefined null-assign.c
$ node a.out.js
null-read.c:3:9: runtime error: load of null pointer of type 'int'

最小ランタイム

UBSan のランタイムは自明ではなく、その使用は不要に攻撃対象領域を拡大する可能性があります。このため、実稼働での使用を想定した最小の UBSan ランタイムがあります。

最小ランタイムは Emscripten によってサポートされています。これを使用するには、-fsanitize フラグに加えて、フラグ -fsanitize-minimal-runtime を渡します。

$ emcc -fsanitize=null -fsanitize-minimal-runtime null-read.c
$ node a.out.js
ubsan: type-mismatch
$ emcc -fsanitize=null -fsanitize-minimal-runtime null-assign.c
$ node a.out.js
ubsan: type-mismatch
Runtime error: The application has corrupted its heap memory area (address zero)!

アドレスサニタイザー

Clang の アドレスサニタイザー (ASan) も Emscripten で使用できます。これにより、コード内のバッファオーバーフロー、メモリリーク、およびその他の関連するバグを非常に簡単に捕捉できます。

ASan を使用するには、-fsanitize=addressemcc または em++ に渡すだけです。UBSan と同様に、codegen とシステムライブラリの両方に影響するため、コンパイル段階とリンク段階の両方でこれを渡す必要があります。

ASan が起動するための十分なメモリを確保するために、少なくとも 64 MB に INITIAL_MEMORY を増やすか、ALLOW_MEMORY_GROWTH を設定する必要がある可能性があります。そうしないと、次のようなエラーメッセージが表示されます。

メモリ配列を 55152640 バイト (OOM) のサイズに拡大できません。(1) 現在の値 50331648 よりも大きい X を指定して -sINITIAL_MEMORY=X でコンパイルするか、(2) ランタイムでサイズを増やすことができる -sALLOW_MEMORY_GROWTH でコンパイルするか、(3) このアボートではなく malloc に NULL (0) を返させたい場合は、-sABORTING_MALLOC=0 でコンパイルしてください。

ASan はマルチスレッド環境を完全にサポートしています。ASan は JS サポートコードでも動作します。つまり、JS が有効でないメモリアドレスから読み取ろうとすると、Wasm からアクセスした場合と同様に捕捉されます。

AddressSanitizer を使用してバグを見つけるのに役立つ方法の例を次に示します。

バッファオーバーフロー

buffer_overflow.c を考えてみましょう。

#include <string.h>

int main(void) {
  char x[10];
  memset(x, 0, 11);
}
$ emcc -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH buffer_overflow.c
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x02965e5a at pc 0x000015f0 bp 0x02965a30 sp 0x02965a30
WRITE of size 11 at 0x02965e5a thread T0
    #0 0x15f0 in __asan_memset+0x15f0 (a.out.wasm+0x15f0)
    #1 0xc46 in __original_main stack_buffer_overflow.c:5:3
    #2 0xcbc in main+0xcbc (a.out.wasm+0xcbc)
    #3 0x800019bc in Object.Module._main a.out.js:6588:32
    #4 0x80001aeb in Object.callMain a.out.js:6891:30
    #5 0x80001b25 in doRun a.out.js:6949:60
    #6 0x80001b33 in run a.out.js:6963:5
    #7 0x80001ad6 in runCaller a.out.js:6870:29

Address 0x02965e5a is located in stack of thread T0 at offset 26 in frame
    #0 0x11  (a.out.wasm+0x11)

  This frame has 1 object(s):
    [16, 26) 'x' (line 4) <== Memory access at offset 26 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (a.out.wasm+0x15ef)
...

解放後使用

use_after_free.cpp を考えてみましょう。

int main() {
  int *array = new int[100];
  delete [] array;
  return array[0];
}
$ em++ -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH use_after_free.cpp
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: heap-use-after-free on address 0x03203e40 at pc 0x00000c1b bp 0x02965e70 sp 0x02965e7c
READ of size 4 at 0x03203e40 thread T0
    #0 0xc1b in __original_main use_after_free.cpp:4:10
    #1 0xc48 in main+0xc48 (a.out.wasm+0xc48)

0x03203e40 is located 0 bytes inside of 400-byte region [0x03203e40,0x03203fd0)
freed by thread T0 here:
    #0 0x5fe8 in operator delete[](void*)+0x5fe8 (a.out.wasm+0x5fe8)
    #1 0xb76 in __original_main use_after_free.cpp:3:3
    #2 0xc48 in main+0xc48 (a.out.wasm+0xc48)
    #3 0x800019b5 in Object.Module._main a.out.js:6581:32
    #4 0x80001ade in Object.callMain a.out.js:6878:30
    #5 0x80001b18 in doRun a.out.js:6936:60
    #6 0x80001b26 in run a.out.js:6950:5
    #7 0x80001ac9 in runCaller a.out.js:6857:29

previously allocated by thread T0 here:
    #0 0x5db4 in operator new[](unsigned long)+0x5db4 (a.out.wasm+0x5db4)
    #1 0xb41 in __original_main use_after_free.cpp:2:16
    #2 0xc48 in main+0xc48 (a.out.wasm+0xc48)
    #3 0x800019b5 in Object.Module._main a.out.js:6581:32
    #4 0x80001ade in Object.callMain a.out.js:6878:30
    #5 0x80001b18 in doRun a.out.js:6936:60
    #6 0x80001b26 in run a.out.js:6950:5
    #7 0x80001ac9 in runCaller a.out.js:6857:29

SUMMARY: AddressSanitizer: heap-use-after-free (a.out.wasm+0xc1a)
...

メモリリーク

leak.cpp を考えてみましょう。

int main() {
  new int[10];
}
$ em++ -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH -sEXIT_RUNTIME leak.cpp
$ node a.out.js

=================================================================
==42==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 1 object(s) allocated from:
    #0 0x5ce5 in operator new[](unsigned long)+0x5ce5 (a.out.wasm+0x5ce5)
    #1 0xb24 in __original_main leak.cpp:2:3
    #2 0xb3a in main+0xb3a (a.out.wasm+0xb3a)
    #3 0x800019b8 in Object.Module._main a.out.js:6584:32
    #4 0x80001ae1 in Object.callMain a.out.js:6881:30
    #5 0x80001b1b in doRun a.out.js:6939:60
    #6 0x80001b29 in run a.out.js:6953:5
    #7 0x80001acc in runCaller a.out.js:6860:29

SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).

リークチェックはプログラムの終了時に行われるため、-sEXIT_RUNTIME を使用するか、__lsan_do_leak_check または __lsan_do_recoverable_leak_check を手動で呼び出す必要があることに注意してください。

AddressSanitizer が有効になっていることを検出し、__lsan_do_leak_check を実行するには、次のようにします。

#include <sanitizer/lsan_interface.h>

#if defined(__has_feature)
#if __has_feature(address_sanitizer)
  // code for ASan-enabled builds
  __lsan_do_leak_check();
#endif
#endif

メモリリークがある場合、これは致命的です。メモリリークを確認して、プロセスが引き続き実行できるようにするには、__lsan_do_recoverable_leak_check を使用します。

また、メモリリークのみを確認する場合は、-fsanitize=address の代わりに -fsanitize=leak を使用できます。-fsanitize=leak はすべてのメモリアクセスを計測するわけではないため、結果として -fsanitize=address よりもはるかに高速です。

リターン後の使用

use_after_return.c を考えてみましょう。

#include <stdio.h>

const char *__asan_default_options() {
  return "detect_stack_use_after_return=1";
}

int *f() {
  int buf[10];
  return buf;
}

int main() {
  *f() = 1;
}

このチェックを行うには、ASan オプション detect_stack_use_after_return を使用する必要があることに注意してください。このオプションは、例のように __asan_default_options という名前の関数を宣言するか、生成された JavaScript で Module['ASAN_OPTIONS'] = 'detect_stack_use_after_return=1' を定義することで有効にできます。ここでは --pre-js が役立ちます。

このオプションは、スタック割り当てをヒープ割り当てに変換するため、かなりコストがかかります。また、これらの割り当ては再利用されないため、将来のアクセスがトラップを引き起こす可能性があります。そのため、デフォルトでは有効になっていません。

$ emcc -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH use_after_return.c
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: stack-use-after-return on address 0x02a95010 at pc 0x00000d90 bp 0x02965f70 sp 0x02965f7c
WRITE of size 4 at 0x02a95010 thread T0
    #0 0xd90 in __original_main use_after_return.c:13:10
    #1 0xe0a in main+0xe0a (a.out.wasm+0xe0a)

Address 0x02a95010 is located in stack of thread T0 at offset 16 in frame
    #0 0x11  (a.out.wasm+0x11)

  This frame has 1 object(s):
    [16, 56) 'buf' (line 8) <== Memory access at offset 16 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-return (a.out.wasm+0xd8f)
...

設定

ASan は、--pre-js ファイルを介して設定できます。

Module.ASAN_OPTIONS = 'option1=a:option2=b';

たとえば、上記のスニペットとオプションを asan_options.js に記述し、--pre-js asan_options.js を付けてコンパイルします。

スタンドアロンの LSan の場合は、代わりに Module.LSAN_OPTIONS を使用します。

フラグの詳細については、ASan のドキュメントを参照してください。ほとんどのフラグの組み合わせはテストされておらず、動作する場合としない場合があることに注意してください。

malloc/free のスタックトレースの無効化

malloc/free(または C++ の同等の operator new/operator delete)を非常に頻繁に使用するプログラムでは、malloc/free のすべての呼び出しでスタックトレースを取得すると、非常にコストがかかる可能性があります。その結果、ASan を使用するとプログラムが非常に遅くなる場合は、malloc_context_size=0 オプションを試すことができます。例えば、このようにします。

Module.ASAN_OPTIONS = 'malloc_context_size=0';

これにより、ASan がメモリリークの場所を報告したり、ヒープベースのメモリエラーが発生した場所に関する洞察を提供したりできなくなりますが、大幅な高速化が得られる可能性があります。

SAFE_HEAP との比較

Emscripten は SAFE_HEAP モードを提供しており、emcc-sSAFE_HEAP 付きで実行することで有効にできます。これにより、いくつかのことが実行されます。そのうちのいくつかはサニタイザーと重複しています。

一般に、SAFE_HEAP は Wasm をターゲットにする際に発生する特定の問題点に焦点を当てています。一方、サニタイザーは、C/C++ などの言語の使用に関連する特定の問題点に焦点を当てています。これら 2 つのセットは重複しますが、同一ではありません。どちらを使用するかは、探している問題の種類によって異なります。すべてのサニタイザーと SAFE_HEAP でテストして最大限のカバレッジを確保することを推奨しますが、すべてのサニタイザーが互いに互換性があるわけではなく、すべてが SAFE_HEAP と互換性があるわけではないため、モードごとに個別にビルドする必要があるかもしれません(サニタイザーはかなり根本的な処理を行うため)。渡したフラグに問題がある場合は、コンパイラエラーが発生します。実行する適切な個別のテストビルドのセットは、ASan、UBSan、および SAFE_HEAP である可能性があります。

SAFE_HEAP がエラーを検出する具体的な内容は次のとおりです。

  • NULL ポインター(アドレス 0)の読み取りまたは書き込み。前述のように、WebAssembly および JavaScript では、0 は単なる通常のアドレスであるため、すぐにセグメンテーション違反が発生するわけではなく、混乱を招く可能性があるため、これは厄介です。

  • アラインされていない読み取りまたは書き込み。これらは WebAssembly では機能しますが、一部のプラットフォームでは、正しくアラインされていない読み取りまたは書き込みは非常に遅くなる可能性があり、wasm2js (WASM=0) では、JavaScript の Typed Array はアラインされていない操作を許可しないため、正しく動作しません。

  • sbrk() によって管理される有効なメモリの上限を超える読み取りまたは書き込み。つまり、malloc() によって適切に割り当てられなかったメモリです。これは Wasm に固有のものではありませんが、JavaScript ではアドレスが Typed Array の範囲外になるほど大きい場合、undefined が返され、非常に混乱する可能性があります。そのため、これが追加されました(少なくとも Wasm ではエラーがスローされます。SAFE_HEAP は、sbrk() のメモリの上端と Wasm メモリの終端の間の領域をチェックすることで、Wasm でも役立ちます)。

SAFE_HEAP は、すべてのロードおよびストアを計測することで、これらのチェックを実行します。これにより処理が遅くなるというコストがかかりますが、このような問題をすべて見つけるという単純な保証が得られます。また、コンパイル後、任意の Wasm バイナリに対して実行できます。一方、サニタイザーはソースからコンパイルする際に実行する必要があります。

比較すると、UBSan も null ポインターの読み取りおよび書き込みを検出できます。ただし、ソースコードのコンパイル中に実行されるため、すべてのロードおよびストアを計測するわけではなく、clang が必要と認識している場所にチェックが追加されます。これははるかに効率的ですが、コード生成や最適化によって何かが変更されたり、clang が特定の場所を見逃したりするリスクがあります。

ASan は、sbrk() で管理されたメモリより上のアドレスを含む、割り当てられていないメモリの読み取りまたは書き込みを検出できます。場合によっては、SAFE_HEAP よりも効率的である可能性があります。すべてのロードとストアもチェックしますが、LLVM オプティマイザーはそれらのチェックを追加した後に実行されるため、一部を削除できます。