post Image
SwiftのAddress/Thread Sanitizer

Sanitizer という英単語はあまり聞き慣れないですよね。辞書的には以下のような意味です。

sanitizerの意味 – 英和辞典 Weblio辞書

清浄剤、清浄薬、消毒剤、消毒薬、殺菌剤、殺菌薬、サニタイザー

Xcode で扱う際の「Address/Thread Sanitizer」の和訳は、「メモリアドレス・スレッドへの不正アクセスの検知機構」といったところでしょうか。

Thread SanitizerはXcode 8で、Address SanitizerはXcode 7で導入された仕組みです。ただ、Address SanitizerはXcode 7時点ではObjective-Cのみの対応で、Swift対応はXcode 8からです。なので、Swiftで扱うという観点だと、どちらもXcode 8からの機能とみなせます。

どちらもプロジェクトのスキーム設定のDiagnosticsタブで設定できますが、両方オンにはできなかったり他の設定と両立できないものがあります。

Screen Shot 2017-01-02 at 20.48.59.png

それでは、それぞれ見ていきましょう。

Address Sanitizer

不正なメモリアクセスを起こすために、UnsafePointerという、いかにも危険そうなstructを使ってみます。
第1・2引数を単純に足し合わせるadd(_:_:)関数を定義してみます。

まず、こちらは以下の例では正しく使っているので、問題無く動きます。Unsafeといえども、必要な箇所で正しい使い方をする分にはまったく問題無いです。

func add(_ x: UnsafePointer<Int>, _ y: UnsafePointer<Int>) -> Int {
    return x.pointee + y.pointee
}

var x = 1
var y = 2
let r = add(&x, &y) // → 3

(もちろん、このadd(_:_:)メソッドは例のためであって、普通UnsafePointerなんて使う必要はまったく無いですが。)

関数定義をこう変えても結果は同じです。

func add(_ x: UnsafePointer<Int>, _ y: UnsafePointer<Int>) -> Int {
    return x[0] + y[0]
}

一方、次のように変えると、rは2に変わります。

func add(_ x: UnsafePointer<Int>, _ y: UnsafePointer<Int>) -> Int {
    return x.pointee + y[1]
}

var x = 1
var y = 2
let r = add(&x, &y) // → 2

何が起こっているかというと、スタックのメモリの不正アクセスをしており、この時の場合y[1]が直前行のxの値の1となってしまっていました。

同様に次のようにすると、rは2018になりました。

var x = 1
var 🐶🎍 = 2017
var y = 2
let r = add(&x, &y) // → 2018

このまましれっと動いてしまうため、ヤバいバグに繋がりそうです( ´・‿・`)

というわけで、Address Sanitizer機能をオンにしてみます。すると、このようにブレークし、

Screen Shot 2017-01-02 at 21.21.33.png

次のログが出力されます。

===================================================================28390==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff5d47cd08 at pc 0x00010278373c bp 0x7fff5d47cbb0 sp 0x7fff5d47cba8

READ of size 8 at 0x7fff5d47cd08 thread T0
#0 0x10278373b in TF9Sanitizer3addFTGSPSi_GSPSiSi AppDelegate.swift:12
#1 0x102783bf0 in _TFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP
Sb AppDelegate.swift:26
#2 0x102783daf in _TToFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP
Sb AppDelegate.swift
#3 0x10406c3c1 in –UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:
#4 0x10406dd46 in –UIApplication _callInitializationDelegatesForMainScene:transitionContext:
#5 0x1040740ec in –UIApplication _runWithMainScene:transitionContext:completion:
#6 0x10407126c in –UIApplication workspaceDidEndTransaction:
#7 0x107e0e6ca in _FBSSERIALQUEUE_IS_CALLINGOUT_TO_A_BLOCK
_ (FrontBoardServices+0x3b6ca)
#8 0x107e0e543 in –FBSSerialQueue _performNext
#9 0x107e0e8cc in –FBSSerialQueue _performNextFromRunLoopSource
#10 0x10674c760 in CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION (CoreFoundation+0x9b760)
#11 0x10673198b in CFRunLoopDoSources0 (CoreFoundation+0x8098b)
#12 0x106730e75 in __CFRunLoopRun (CoreFoundation+0x7fe75)
#13 0x106730883 in CFRunLoopRunSpecific (CoreFoundation+0x7f883)
#14 0x10406fae9 in –UIApplication _run
#15 0x104075c67 in UIApplicationMain (UIKit+0x27c67)
#16 0x102785102 in main AppDelegate.swift:16
#17 0x10767468c in start (libdyld.dylib+0x468c)
Address 0x7fff5d47cd08 is located in stack of thread T0 at offset 104 in frame
#0 0x1027839ef in _TFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP
Sb AppDelegate.swift:21
This frame has 4 object(s):
[32, 40) ”
[64, 72) ”
[96, 104) ” <== Memory access at offset 104 overflows this variable
[128, 144) ”
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions are supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow AppDelegate.swift:12 in TF9Sanitizer3addFTGSPSiGSPSi
Si
Shadow bytes around the buggy address:
0x1fffeba8f950: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f960: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f970: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f990: 00 00 00 00 f1 f1 f1 f1 00 f2 f2 f2 00 f2 f2 f2
=>0x1fffeba8f9a0: 00[f2]f2 f2 00 00 f3 f3 00 00 00 00 00 00 00 00
0x1fffeba8f9b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f9c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f9d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f9e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffeba8f9f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==28390==ABORTING
AddressSanitizer report breakpoint hit. Use ‘thread info -s’ to get extended information about the report.
(lldb)

特に次のログに、Intのサイズを超えたメモリアクセスがなされたことが示されています。

This frame has 4 object(s):
    [32, 40) ''
    [64, 72) ''
    [96, 104) '' <== Memory access at offset 104 overflows this variable
    [128, 144) ''

通常のアプリ開発では、そもそもUnsafePointerなど扱うことは稀ですが、それらをよく扱うライブラリ作っている場合などは、ミス・バグの発見に役立ちそうですね。

Thread Sanitizer

続いて、Thread Sanitizer機能を使ってみます。

次のように、Monoクラスの単一インスタンスmvalue書き換えを、async(group:qos:flags:execute:)メソッドを使って別スレッドからほぼ同時に行います。

class Mono {
    var value: Int = 0
}

let m = Mono()
DispatchQueue.global().async {
    m.value = 1
}
DispatchQueue.global().async {
    m.value = 2
}

Thread Sanitizer機能オフでは何も起こらず、m.valueは最終的には12になります(大抵2になりそうですが不定)。

一方、Thread Sanitizer機能をオンにすると次のようにブレークし(Pause on issuesもオンにした時のみ)、

Screen Shot 2017-01-02 at 21.47.45.png

次の警告ログが出力されます。

WARNING: ThreadSanitizer: data race (pid=29835)

==================
Write of size 8 at 0x7d080000d1b0 by thread T4:
#0 TFC9Sanitizer4Monos5valueSi AppDelegate.swift (Sanitizer+0x000100002927)
#1 _TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP
SbU0_FT_T AppDelegate.swift:28 (Sanitizer+0x0001000035b8)
#2 _TPA
TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP_SbU0_FT_T AppDelegate.swift (Sanitizer+0x00010000368e)
#3 _TTRXFo
XFdCb__ AppDelegate.swift (Sanitizer+0x0001000033d5)
#4 tsan::invoke_and_release_block(void*) :223 (libclang_rt.tsan_iossim_dynamic.dylib+0x00000005c3fb)
#5 _dispatch_client_callout :159 (libdispatch.dylib+0x00000002c0cc)
Previous write of size 8 at 0x7d080000d1b0 by thread T2:
#0 _TFC9Sanitizer4Monos5valueSi AppDelegate.swift (Sanitizer+0x000100002927)
#1 _TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP
SbU_FT_T_ AppDelegate.swift:25 (Sanitizer+0x000100003218)
#2 _TPA
TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP_SbU_FT_T AppDelegate.swift (Sanitizer+0x00010000332e)
#3 _TTRXFo
XFdCb__ AppDelegate.swift (Sanitizer+0x0001000033d5)
#4 _tsan::invoke_and_release_block(void*) :223 (libclang_rt.tsan_iossim_dynamic.dylib+0x00000005c3fb)
#5 dispatch_client_callout :159 (libdispatch.dylib+0x00000002c0cc)
Location is heap block of size 24 at 0x7d080000d1a0 allocated by main thread:
#0 malloc :223 (libclang
rt.tsan_iossim_dynamic.dylib+0x0000000404ba)
#1 swift_slowAlloc :204 (libswiftCore.dylib+0x000000221b28)
#2 TFC9Sanitizer4MonoCfTS0
AppDelegate.swift (Sanitizer+0x0001000029e2)
#3 TFC9Sanitizer11AppDelegatecfT_S0 AppDelegate.swift:22 (Sanitizer+0x000100004415)
#4 TToFC9Sanitizer11AppDelegatecfT_S0 AppDelegate.swift (Sanitizer+0x000100004546)
#5 _UIApplicationMainPreparations :160 (UIKit+0x00000002835d)
#6 start :141 (libdyld.dylib+0x00000000468c)
Thread T4 (tid=2465144, running) created by thread T-1
[failed to restore the stack]
Thread T2 (tid=2465142, running) created by thread T-1
[failed to restore the stack]
SUMMARY: ThreadSanitizer: data race AppDelegate.swift in _TFC9Sanitizer4Monos5valueSi
==================ThreadSanitizer report breakpoint hit. Use ‘thread info -s’ to get extended information about the report.

また、Xcodeの左ペインにも表示されます。

Screen Shot 2017-01-02 at 21.56.50.png

また、次のようにロック処理を挟むと、同じキューつまり同じスレッドで処理されるので、Thread Sanitizerで引っかからなくなることも確認できました👀

class Mono {
    private let lockQueue = DispatchQueue(label: "lock serial queue")
    var _value: Int = 0
    var value: Int {
        get { return lockQueue.sync { _value } }
        set { lockQueue.sync { _value = newValue } }
    }
}

参考: Swift 3での同期処理(排他制御)の基本 – Qiita

スレッドセーフになってないオブジェクトに、意図せず複数スレッドでアクセスしてしまった時の検知に良い感じに使えそうですね👀

その他の参考リンク


『 Swift 』Article List