Sanitizer という英単語はあまり聞き慣れないですよね。辞書的には以下のような意味です。
清浄剤、清浄薬、消毒剤、消毒薬、殺菌剤、殺菌薬、サニタイザー
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
タブで設定できますが、両方オンにはできなかったり他の設定と両立できないものがあります。
それでは、それぞれ見ていきましょう。
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機能をオンにしてみます。すると、このようにブレークし、
次のログが出力されます。
===================================================================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 _TFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyPSb AppDelegate.swift:26
#2 0x102783daf in _TToFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyPSb 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 _TFC9Sanitizer11AppDelegate11applicationfTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyPSb 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 TF9Sanitizer3addFTGSPSiGSPSiSi
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
クラスの単一インスタンスm
のvalue
書き換えを、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
は最終的には1
か2
になります(大抵2
になりそうですが不定)。
一方、Thread Sanitizer機能をオンにすると次のようにブレークし(Pause on issuesもオンにした時のみ)、
次の警告ログが出力されます。
WARNING: ThreadSanitizer: data race (pid=29835)
==================
Write of size 8 at 0x7d080000d1b0 by thread T4:
#0 TFC9Sanitizer4Monos5valueSi AppDelegate.swift (Sanitizer+0x000100002927)
#1 _TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyPSbU0_FT_T AppDelegate.swift:28 (Sanitizer+0x0001000035b8)
#2 _TPATFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP_SbU0_FT_T AppDelegate.swift (Sanitizer+0x00010000368e)
#3 _TTRXFoXFdCb__ 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 _TFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyPSbU_FT_T_ AppDelegate.swift:25 (Sanitizer+0x000100003218)
#2 _TPATFFC9Sanitizer11AppDelegate11applicationFTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVs10DictionaryVSC29UIApplicationLaunchOptionsKeyP_SbU_FT_T AppDelegate.swift (Sanitizer+0x00010000332e)
#3 _TTRXFoXFdCb__ 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 (libclangrt.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の左ペインにも表示されます。
また、次のようにロック処理を挟むと、同じキューつまり同じスレッドで処理されるので、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
スレッドセーフになってないオブジェクトに、意図せず複数スレッドでアクセスしてしまった時の検知に良い感じに使えそうですね👀
その他の参考リンク
- Advanced Debugging and the Address Sanitizer - WWDC 2015 - Videos - Apple Developer
- Thread Sanitizer and Static Analysis - WWDC 2016 - Videos - Apple Developer
-
Practical Swift | Eric Downey | Apress
- 大いに参考にさせていただきました
- 🌨Swiftレター #9🌨 – Swift・iOSコラム – Medium
-
mookmook radio » Blog Archive » 熊谷と繪面がプログラミングコードの内から聴こえてくる声に耳を傾けて楽しむラジオ #9
-
unsafe
系 APIの話してます( ´・‿・`)
-