0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

macOSの権限ダイアログが「どのパス」で出たのかを lldb で特定する

0
Posted at

ファイルマネージャーを作っていて、ホームフォルダを開くたびに写真ライブラリの
権限ダイアログが出るようになりました。アプリには写真関連のコードが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 を付け直してください。
  • 統合ログのバッファはすぐ流れます。 事後に探すと消えていることがあるので、
    再現しながらファイルへストリームしておくのが確実です。

一般化したレシピ

  1. 権限ダイアログ、あるいはフォルダアクセス時の原因不明のハングは、
    「いまこの瞬間、どこかのスレッドが syscall の中で止まっている」という意味
  2. ダイアログが出ている間に lldb をアタッチし、__open / __openat /
    getattrlist / opendir のフレームを持つスレッドを探す
  3. パス引数をレジスタから直接読む(x0、*at 系は x1)
  4. 保護対象についての自分のモデルではなく、レジスタを信じる。
    実際に保護されている集合は、ドキュメント化されている集合より広いです

レジスタは推測をしません。4本のスレッドがひとつの答えを出して、数週間の
当て推量が終わりました。


この話は macOS 用の2画面ファイルマネージャー Unifyl
を作っている中で出てきたものです。英語版のより詳しい記事は
こちら、
1年分の他の「当然」については Zenn の記事
に書きました。

同じダイアログで悩んでいる方の役に立てば幸いです。他に効いた手があれば
コメントで教えてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?