ファイルマネージャーを作っていて、ホームフォルダを開くたびに写真ライブラリの
権限ダイアログが出るようになりました。アプリには写真関連のコードが1行もありません。
原因の特定に数週間かかり、最後に効いたのは「ダイアログが出ている間にデバッガを
アタッチする」という一手でした。同じ状況で困っている人向けに、その手順だけを
切り出して書きます。
結論を先に
- 権限ダイアログが表示されている間、それを引き起こした syscall はまだ返っていない
- つまり原因のスレッドは
open()の中で止まっていて、パス引数がレジスタに残っている - ダイアログを閉じずに lldb をアタッチし、
x0を文字列として読めば犯人のパスが出る - 私の場合の犯人は
~/Library/Photosでした(Apple のドキュメントに記載がありません)
症状
バックグラウンドでフォルダサイズを計算する処理があり、ホームフォルダを開くと
このダイアログが出ます。
"アプリ名" が写真ライブラリへのアクセスを求めています。
心当たりがありません。Photos フレームワークを import しておらず、
.photoslibrary バンドルにも触れていません。サイズ計算のスキップリストには
~/Pictures を入れてあります。
TCC ログでわかること・わからないこと
権限ダイアログは tccd が出しています。ライブで観察できます。
log stream --predicate 'subsystem == "com.apple.TCC"' --info
ここから読み取れたことが2つあります。
-
preflight=yesのエントリは状態問い合わせで、ダイアログを出しません。
実際にダイアログになるのはpreflight=noからAUTHREQ_PROMPTINGに
上がっていく要求のほうです。前者を追うと時間を溶かします。 - ログは要求された service(
kTCCServicePhotos)は教えてくれますが、
パスは絶対に教えてくれません。設計思想としてはそのとおりで、
プライバシーログが開発者自身のデバッグセッションからもプライバシーを守ります。
つまり「何が要求されたか」はわかっても「なぜ要求されたか」はわかりません。
数千のパスを歩く処理の、どれが引き金かは別の手段で突き止める必要があります。
スキップリストを監査して「リストは正しい」と確認しましたが、それはリストを
検証しただけでバグを見つけたことにはなりません。実際、保護対象についての
私の理解のほうが間違っていました。
核心:ダイアログが犯人のスレッドを凍らせている
TCC のダイアログには使える性質があります。ダイアログが画面に出ている間、
それをトリガーした syscall はまだリターンしていません。
該当スレッドは open() の中でブロックされたまま座っており、arm64 の呼び出し規約では
syscall の第1引数(パスへのポインタ)は x0 レジスタに置かれたままです。
なので、ダイアログを閉じずにデバッガをアタッチします。
$ lldb -p $(pgrep -x YourApp)
(lldb) thread list
thread #9: tid = 0x..., 0x... libsystem_kernel.dylib`__open + 8
thread #11: tid = 0x..., 0x... libsystem_kernel.dylib`__open + 8
...
(lldb) thread select 9
(lldb) memory read -f s $x0
0x16dae5a30: "/Users/me/Library/Photos"
__open で止まっているワーカースレッドが4本あり、4本とも同じ答えでした。
/Users/me/Library/Photos
*at 系(__openat など)の場合はパスが第2引数なので $x1 を読みます。
犯人:~/Library/Photos
~/Pictures でも .photoslibrary バンドルでもなく、~/Library の下にある
ごく普通に見えるディレクトリでした。
ここには「あなたと共有」(Shared with You)の写真コンテンツが入っており、
macOS は写真ライブラリ本体と同じ kTCCServicePhotos で保護しています。
Apple の写真ライブラリの場所に関するドキュメントで、私はこのパスの記載を
見つけられませんでした。
バックグラウンドのサイズ計算が律儀にここへ降りていき、中での最初の open() で
ダイアログが出ていた、という話でした。修正は1行のポリシー変更です——
~/Library/Photos を ~/Pictures と同じ扱いにして、頼まれていないのに
入らないようにしました。
再現性を確保する(ここでも時間を使いました)
TCC はコード署名アイデンティティ単位で記憶します。
一度「許可しない」を押すと、以降の実行は無言で拒否され、再現が消えます。
-
tccutil reset Photos <bundle-id>でリセットできます - ただし長いセッションでは、コピーをアドホック署名し直すほうが速いです。
新しいアイデンティティになるので、毎回まっさらな TCC 状態から始まります。
codesign -f -s - --deep /path/to/YourApp.app
- アドホック署名は entitlement を落とすので、lldb をアタッチしたいなら
com.apple.security.get-task-allowを付け直してください。 -
統合ログのバッファはすぐ流れます。 事後に探すと消えていることがあるので、
再現しながらファイルへストリームしておくのが確実です。
一般化したレシピ
- 権限ダイアログ、あるいはフォルダアクセス時の原因不明のハングは、
「いまこの瞬間、どこかのスレッドが syscall の中で止まっている」という意味 - ダイアログが出ている間に lldb をアタッチし、
__open/__openat/
getattrlist/opendirのフレームを持つスレッドを探す - パス引数をレジスタから直接読む(
x0、*at系はx1) -
保護対象についての自分のモデルではなく、レジスタを信じる。
実際に保護されている集合は、ドキュメント化されている集合より広いです
レジスタは推測をしません。4本のスレッドがひとつの答えを出して、数週間の
当て推量が終わりました。
この話は macOS 用の2画面ファイルマネージャー Unifyl
を作っている中で出てきたものです。英語版のより詳しい記事は
こちら、
1年分の他の「当然」については Zenn の記事
に書きました。
同じダイアログで悩んでいる方の役に立てば幸いです。他に効いた手があれば
コメントで教えてください。