
macOS NFSクライアントのアクセスキャッシュ競合(CVE-2026-43687)に関するリバースエンジニアリングノートと自己完結型PoC。kextの逆アセンブリ差分とdtraceによる競合キャプチャを含む。
macOS NFSクライアントのアクセスキャッシュ競合(CVE-2026-43687)の独立したリバースエンジニアリングと、その競合を引き起こしdtraceでライブキャプチャする動作するPoC。
このバグは_nfs_vnop_accessにおけるnfsnodeアクセスキャッシュポインタの非同期読み取りである。悪意のあるNFSv3サーバーは、別のスレッドがアクセスキャッシュを読み取っている間に再割り当てを引き起こすことができ、敵対的サーバーが影響を与えられるカーネルメモリの漏洩を生じさせる。macOS 26.7はnfsnode+0x158にlck_rw_tを挿入し、キャッシュ読み取りの周囲でそれを共有ロックとして取得することでこれを修正している。
| フィールド | 値 |
|---|---|
| CVE | CVE-2026-43687 |
| コンポーネント | com.apple.filesystems.nfs (_nfs_vnop_access) |
| 影響を受けるバージョン | macOS Tahoe 26.6以前、iOS 26.x以前 |
| 修正済みバージョン | macOS Tahoe 26.7、macOS Golden Gate 27、iOS 26.7、iOS 27 |
| アドバイザリの影響 | 「悪意のあるNFSサーバーに接続すると、カーネルメモリが漏洩する可能性があります。」 |
| CVSS v3.1 | 6.5 (Medium) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| 報告者 | KRsecurityのR4mbbおよびPeter Malone(Appleのアドバイザリによる) |
CVE-2026-43687に関するAppleのアドバイザリは、影響と修正バージョンを文書化している。しかし、技術的なメカニズムについては文書化していない:
nfsnode内のどのフィールドが競合するのかstat(2)ではなくaccess(2)を使用しなければならないのか執筆時点で公開された技術的な記事は見つからなかった。このリポジトリは、26.6と26.7の間のNFS kextの独立したリバースエンジニアリング分析と、ライブターゲット上で競合を再現する動作するPoCによって、そのギャップを埋めるものである。
これは発見の主張ではない。 このCVEはR4mbbとPeter Maloneによって報告され、Appleによって修正された。ここでの貢献は技術分析と再現である。
NFSクライアントの_nfs_vnop_accessは、UIDごとのアクセスキャッシュ配列へのポインタであるnfsnode+0x158を、単一の呼び出し内で2回、いかなるロックも保持せずに読み取る:
; macOS 26.6, com.apple.filesystems.nfs
fffffe000b52def4 ldr w8, [x20, #0x160] ; count
fffffe000b52def8 cmp w23, w8
fffffe000b52defc b.ge ...
fffffe000b52df00 ldr x8, [x20, #0x158] ; cache ptr (read #1)
...
fffffe000b52df98 ldr x8, [x20, #0x158] ; cache ptr (read #2)
fffffe000b52dfb0 ldr w21, [x9] ; dereference
書き込み側の_nfs_ngetは、クライアントがサーバーから新しいUIDを認識するたびに、kalloc_dataで配列を再割り当てし、その結果を+0x158に格納する:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
別のスレッドが、読み取り側の2回のロードの間に同じnfsnode上で_nfs_ngetに到達すると、2回目のロードは新しいポインタを返すが、最初の読み取り(すでにオフセットの計算に使用されている)は古いポインタに基づいていた。その後の逆参照は、解放されたカーネルヒープから読み取る。
26.7の修正:
fffffe000b9e3064 str x0, [x22, #0x168] ; cache ptr moved
fffffe000b9e306c str w28, [x22, #0x170] ; cache count moved
fffffe000b9e307c add x0, x22, #0x158 ; lock slot
fffffe000b9e3084 bl _lck_rw_init ; init RW lock
そして_nfs_vnop_access内:
fffffe000b9f0200 add x0, x20, #0x158
fffffe000b9f0204 bl _lck_rw_lock_shared ; take the lock
fffffe000b9f0208 ldr x9, [x20, #0x168] ; cache pointer
...
fffffe000b9f0230 bl _lck_rw_unlock_shared ; release the lock
構造体レイアウトの変更:
_nfs_ngetと_nfs_vnop_accessの逆アセンブリ差分 — docs/PATCH_DIFF.mdを参照nfsnode+0x158+0x158にlck_rw_tを挿入、キャッシュポインタを+0x168に移動、読み取りの周囲でlck_rw_lock_sharedを取得access(2)は_nfs_vnop_accessに入るが、stat(2)は_nfs_getattrを経由し、脆弱な関数には到達しない完全な記事はdocs/ANALYSIS.mdを、アドレスとログサンプルはdocs/ARTIFACTS.mdを参照。
poc.sh — 単一ファイルの自己完結型PoC:
nfsdを停止してポート2049を解放127.0.0.1上で、すべての応答で報告するUIDをローテーションする悪意のあるNFSv3サーバーを起動noacを付けて新しいマウントポイントにエクスポートをマウントtest -r / test -wを介してaccess(2)を発行するユーザーごとのハンマーを生成nfs_vnop_accessにdtraceプローブをアタッチし、呼び出しの途中でnfsnode+0x158が変化する呼び出しを記録_nfs_vnop_accessにおける非同期読み取りが、単一の呼び出し中に配列が変化するのを観測すること競合は漏洩の前提条件である。競合を実際の漏洩に変えるには、攻撃者は読み取り側が古いポインタを使用しているのを観測し、それらのバイトを自分が読める場所に伝播させる必要がある。arm64eでは、nfsnode+0x158の値はPAC署名されており、ブートごとのキーはユーザーランドからは利用できないため、dtraceだけでは競合を観測できるがポインタをデコードすることはできない。docs/ANALYSIS.mdの「Paths tested and ruled out」セクションを参照。
実証された影響は競合そのものである — 26.7のRWロック修正が閉じるまさにそのウィンドウである。
dtraceが利用可能(一部のインストールではSIPの調整が必要な場合がある)python3、dscl、mountPoCは完全にターゲット上で実行される。悪意のあるNFSサーバーは127.0.0.1にバインドし、マウントはループバック経由である。これにより、ネットワーク設定なしでPoCが自己完結的かつ再現可能になる。
chmod +x poc.sh
sudo ./poc.sh
環境変数によるオプションの調整:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNTはユーザーごとのハンマースレッド数を設定する(存在するnfsuserNNNアカウントを使用し、必要に応じて作成する)。RUN_SECONDSはdtraceのウィンドウを設定する。
[*] ensuring nfsuser accounts exist (UID 201..240)
nfsuser accounts available: 12
[*] starting evil NFS server on 127.0.0.1:2049
[*] mounting /Users/Shared/nfs_test (with noac)
mount check: hello.txt statable
[*] spawning up to 12 per-user threads + 1 root loop
started 12 user threads + 1 root loop
[*] running dtrace for 180s — looking for RACE lines
[*] stopping dtrace
[*] cleaning up
=================== SUMMARY ===================
RACE events caught: 16
Vulnerable interleaving observed — sample:
RACE nd=fffffe5f53cbb940 in=(0,0) out=(b0967e0023297878,0)
RACE nd=fffffe5f534db940 in=(0,0) out=(84dd7e0023297878,0)
RACE nd=fffffe5f53cbb940 in=(0,0) out=(c9a37e0023297878,0)
RACE nd=fffffe5f538ab940 in=(0,0) out=(1867e0023297878,0)
RACE nd=fffffe5f539db940 in=(0,0) out=(149bfe0023297878,0)
CVE-2026-43687 trigger SUCCESSFUL
===============================================
各RACE行は、呼び出しの途中でアクセスキャッシュポインタが変化したnfs_vnop_accessの1回の呼び出しである。
修正済みシステム(26.7 / 27)では、同じワークロードでRACEイベントがゼロになる。逆アセンブリの比較はdocs/PATCH_DIFF.mdを参照。
PoCのサーバーにおける2つのバグは開発中に修正され、同様のツールを構築する他の人が遭遇しないようにここに文書化されている:
ACCESS3resokはfattr3ではなくpost_op_attrを必要とする。 RFC 1813は応答をpost_op_attr obj_attributes; uint32 access;と定義している。post_op_attrはfattr3の前にboolプレフィックスを含む。そのboolを省略すると応答が4バイト短くなり、クライアントはそれを暗黙的に拒否し、マウントは使用可能にならない。
AppleDouble名(._*)のLOOKUPはNFS3ERR_STALE (70)ではなくNFS3ERR_NOENT (2)を返さなければならない。 macOSは通常のパス解決中に._<name>サイドカーをプローブする。STALEを返すとマウントが汚染される。
どちらもdocs/ANALYSIS.mdに文書化されている。
RACE出力のout値は、異なるnfsnode間で一定の下位48ビットパターン(...7e0023297878)を持ち、上位16ビットのみが変化する。これは、このフィールドが生のカーネルポインタではなく、PAC署名または難読化されていることを確認するものである。ユーザーランドから状態遷移(NULL → 値あり)を観測することはできるが、カーネルのブートごとのPACキーなしではポインタをデコードしたり逆参照したりすることはできない。
同じ理由で、KDKに対するシンボリケーションはこれらの値には役に立たない — それらはテキスト相対アドレスではない。
このリポジトリは防御的セキュリティ研究と教育のみを目的として提供されている。
MIT。LICENSEを参照。
| オフセット | macOS 26.6 | macOS 26.7 |
|---|
+0x158 | キャッシュ配列ポインタ | lck_rw_t |
+0x160 | キャッシュカウント | (ロックの一部) |
+0x168 | (その他) | キャッシュ配列ポインタ |
+0x170 | (その他) | キャッシュカウント |