← 所有文章

macOS 27 不再詢問,直接拒絕跨團隊容器存取

在 macOS 上,即使您對某個 app group 完全沒有 entitlement,containerURL(forSecurityApplicationGroupIdentifier:) 仍會回傳一個看起來完全正常的 URL。Apple 的文件寫得很直白:在 iOS 上,識別碼無效時這個方法會回傳 nil;但在 macOS 上,「即使 app group 無效,也一律回傳格式正確的 URL」。4 到了 macOS 27,這個行為撞上了一項新限制。

macOS 27 在拒絕跨團隊容器存取之前,不再詢問使用者。 過去讀取其他開發團隊的 app 資料容器或 app group 容器,會跳出授權提示;現在則預設直接失敗,唯一的補救途徑,是使用者自己在「隱私權與安全性」裡找到那個項目。1

這兩件事疊在一起,就形成一種在出錯當下毫無訊號的失敗。沒有對話框。方法回傳的是 URL,不是 nil。路徑看起來和您預期的一模一樣。拒絕要到後面執行檔案操作時才浮現,而且看起來就像檔案不存在。

TL;DR

macOS 27 移除了存取其他團隊 app 資料容器與 app group 容器時的授權提示,並將這類存取預設為拒絕,控制權移到「隱私權與安全性」設定中。1 這項變更被歸類在 System Integrity Protection 底下的「新功能」,而非錯誤修正。Apple 自己那份 app group 容器指南,至今仍在描述 macOS 27 已經移除的提示行為。由於 macOS 的 API 即使面對您無權存取的群組,也照樣回傳格式完整的 URL,拒絕會出現在讀取當下,而不是呼叫 API 的當下。同團隊之間的存取不受影響:分界線是 Team ID。

這次改了什麼

macOS 27 的發行說明在 System Integrity Protection 一節裡只有一句話:1

「存取其他開發團隊的 app 資料容器與 app group 容器中的檔案時,不再向使用者要求授權;這類存取預設一律拒絕,使用者可在『隱私權與安全性』設定中管理。」

Radar 161835690。兩個子句,其實是兩項獨立的變更。

前半句拿掉了提示,後半句把拒絕定為預設。談到這項變更的報導,多半從「預設拒絕」講起,但那其實是比較不有趣的一半——預設值收緊本來就是家常便飯。真正關鍵的是消失的那個提示:它改變了使用者遇到的失敗長什麼樣子,也改變了您收到的錯誤回報長什麼樣子。

從前,使用者會看到對話框並做出決定。若他們拒絕,您的 app 至少知道這是一個真人做過的選擇。現在沒有人被問到。存取就是不會發生,而唯一能開啟它的路徑,藏在一個設定面板裡——除非有人特地告訴使用者,否則他們根本沒有理由打開它。

Apple 文件仍在描述的舊行為

這項限制,是疊在兩個版本之前就已經加上的保護之上。Apple 的 app group 容器指南寫道:2

「在 macOS 15 以後,即使 app 沒有啟用 App 沙箱功能,app group 容器仍會為其本機檔案提供 [System Integrity Protection]。這些 app group 容器會限制不屬於該 app group 的 app 存取。不在 app group 內的 app 若試圖存取 app group 或 app 資料容器內的位置,會觸發一個向使用者要求授權的提示。」

那個頁面用現在式描述這個提示。截至撰稿時,Apple 尚未針對 macOS 27 更新它。

實際後果是:開發者踩到這個問題、去翻文件、找到官方頁面,得到的答案是「會跳出授權對話框」。但它不會。接著他們很合理地推論:不是提示壞了,就是自己的 entitlement 設錯了——於是把力氣花在完全錯誤的地方。

建議自行確認那個頁面的現況,不要只信這篇文章的時間戳。文件終究會補上。

為什麼失敗會延後才浮現

把一項政策變更變成除錯難題的,正是 API 的行為。

Apple 在 containerURL(forSecurityApplicationGroupIdentifier:) 的參考文件中,明確點出了平台差異:4

「回傳值為指向該群組共享目錄在檔案系統中位置的 URL。在 iOS 上,當群組識別碼無效時,該值為 nil。在 macOS 上,即使 app group 無效,也一律回傳格式正確的 URL,因此在使用之前,務必先測試您是否真的能存取底層目錄。」

討論一節又重申了一次:用一個您沒有 entitlement 的群組識別碼呼叫這個方法,依然會拿到格式正確的 URL,但該目錄並不存在,而沙箱化的 app 也無法自行建立它。4

因此,最常見的那種防禦式寫法在這裡毫無作用:

// This guard passes on macOS regardless of entitlement.
guard let url = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "ABCDE12345.com.example.shared"
) else {
    return  // never taken on macOS
}

URL 格式完全正確,指向 ~/Library/Group Containers/<team>.<group>——那正是該容器應該存在的位置。在真正對它執行檔案操作之前,一切看起來都像成功。

正確的做法是直接嘗試讀取並處理失敗,而不是先去問「這會不會成功」:

let fm = FileManager.default
guard let url = fm.containerURL(
    forSecurityApplicationGroupIdentifier: groupID
) else { return }  // macOS never takes this branch

do {
    _ = try fm.contentsOfDirectory(atPath: url.path)
    // Reached the container.
} catch {
    // On macOS 27 a cross-team denial lands here.
    // No prompt fired. The user was never asked.
    presentContainerUnavailable(error)
}

請忍住用 isReadableFile(atPath:) 做前置檢查的衝動。Apple 明確反對這一整類的測試:3

「不建議依據檔案系統目前的狀態、或系統上某個特定檔案的狀態,來預先決定程式的行為。這麼做可能導致奇怪的行為或競爭條件。與其事先設法判斷某項操作會不會成功,不如直接嘗試該操作(例如載入檔案或建立目錄)、檢查錯誤,並妥善處理這些錯誤——後者遠比前者好得多。」

針對這次的變更,還有第二個理由。同一個頁面提到,isReadableFile(atPath:) 是「使用真實的使用者 ID 與群組 ID」來判斷可讀性。3 那是 POSIX 權限層級的判斷。跨團隊容器的拒絕,則是疊在 POSIX 之上的政策決定,因此檢查權限位元不保證能反映它。一個回傳 true、隨後讀取仍然失敗的前置檢查,比完全不檢查更糟,因為它把意外推得離原因更遠。

讀過〈canOpenURL 在 iOS 27 已棄用〉的人會認得這個模式。Apple 不斷收回「事先詢問」的能力,只留下「直接嘗試」的能力。先做、再處理錯誤,正在變成通則,而不是針對某一個 API 的權宜之計。

值得注意的是兩個平台之間的不對稱。iOS 回傳 nil,在呼叫點誠實地失敗;macOS 回傳 URL,把失敗往後推。而 macOS 27 拿掉的那個提示,正是這個本來就比較安靜的平台上,僅存的一個訊號。

還有一個陷阱:Apple 的建議是一律使用這個方法回傳的 URL,不要自己手動組出 ~/Library/Group Containers/...,因為該位置在未來版本中可能改變。4 把路徑寫死的人,根本沒有任何方法呼叫可以檢查,也沒有地方擺放可讀性測試。

多半與建置用的 SDK 無關

很自然會想問:繼續用舊版 SDK 能不能延後這項變更?發行說明沒有明說。

但它確實透露了一種模式。macOS 27 的發行說明在 11 個獨立條目中,都明確使用了以 SDK 為條件的措辭:「在以 macOS 27.0 SDK 建置的 app 中」、「在以 27.0 SDK 建置的 app 中」、「當專案的最低部署目標低於 27.0 時」。1 只要這種但書成立,Apple 就會加上去。

容器那一條沒有任何這類但書。若把這個缺席視為刻意安排——這是合理的解讀——那麼這項限制就屬於作業系統層級的政策,適用於所有在 macOS 27 上執行的二進位檔,無論它是用哪個 SDK 建置的。

請把它當成一個有力的推論,而非既定事實,因為 Apple 並沒有直接這樣說。但不論如何,實務上的結論都一樣:別把「繼續用舊版 SDK」當成緩解方案,就當這個存取一定會失敗來規劃。

真正受影響的是誰

同團隊之間的存取完全不受影響。Apple 的規則讓 Team ID 這條界線成為結構性的、而非偶然的限制:「不同的開發團隊不能使用同一個 app group」,但同一個團隊可以在自己的 app 與支援行程之間共用一個群組。2

會壞掉的,是那些跨過這條線的 app:

移轉與匯入工具。 任何為了匯入使用者資料而去讀取競品或前代產品容器的程式。這是最明顯的一類;過去它前面本來就擋著一道提示,只是使用者有時還是會按下同意。

備份與同步工具。 那些會列舉 app 容器來進行備份的工具,如今會靜靜跳過所有不屬於自己團隊的內容。

易主過的搭配 app。 這是最尖銳的一類,因為程式碼完全沒有改動。兩個 app 原本在同一個 Team ID 底下發行,共用一個群組,相安無事。一次併購、一次團隊拆分,或是搬到另一個開發者帳號,就會讓它們落在不同的 Team ID 之下。app group 識別碼看起來依然正確,URL 依然解析得出來,資料卻再也送不到了。

最後這一類值得多談幾句,因為時間點對您極為不利。Team ID 的變更通常由負責簽署與發布的人處理,而共用容器這層依賴,從那個位置往往完全看不見。程式碼照樣編譯通過,兩個 app 照樣發行。兩邊的測試套件都抓不到,因為單元測試不會真的去碰一個跨團隊的容器,而 CI 是用建置機器手上那組身分同時建置兩個 target。失敗只會出現在正式環境、出現在使用者的機器上——表現為兩個在使用者眼中理應屬於同一產品的 app,之間的資料不再同步。

如果您正在規劃 Team ID 移轉,稽核方式很機械:用 grep 找出所有 target 宣告過的 app group 識別碼,逐一列出有哪些 bundle 會讀取它。只要某個識別碼會被落在不同 Team ID 底下的 bundle 讀取,那就是一處斷裂。搬遷之前,這只是一次十分鐘的檢查;搬遷之後,它就是一起客服事件。

Apple 列出了可以參與 app group 的項目:bundle 結構中的主執行檔、app extension、App Clips 以及 XPC Services。2 每一項都可能是這個問題浮現的地方。

如何診斷

在斷定這是程式錯誤之前,有兩項檢查值得先跑一遍。

先確認系統究竟有沒有驗證過您的 entitlement。Apple 記載了一種在執行中的行程上檢查 entitlements-validated 旗標的方法:2

sudo launchctl procinfo <pid>

在 app group 授權機制出現之前建立的佈建描述檔,可能不帶這項授權,於是產生的存取失敗看起來與 macOS 27 的拒絕一模一樣,成因卻完全不同。只要開啟「Automatically manage signing」,且 REGISTER_APP_GROUPS 這項建置設定為 Yes,Xcode 就會自動更新描述檔。2

也要清楚自己用的是哪一種識別碼形式。以 group. 為前綴的群組,必須納入 app 的佈建描述檔。採用 <TeamID>.<group name> 形式的群組則不需要描述檔,因為系統會拿團隊識別碼前綴與簽署身分比對;但這種形式僅限 macOS,且 Keychain Access Groups 不支援。2

接著再去看「隱私權與安全性」,因為發行說明說使用者的控制權現在就落在那裡。1

為「無法詢問的拒絕」做設計

那個提示做的不只是安全性的工作,也是產品的工作。它告訴使用者「這裡有一個決定要做」,也告訴您的 app「決定已經做完了」。這兩件事,現在都落到您頭上。

在邊界上探測一次就好。 第一次解析出容器時,就先做一次成本極低的讀取,而不是等到同步迴圈跑到一半才發現被拒絕。失敗若發生在佇列深處,得到的會是一份殘缺的結果,加上一個被歸咎於「剛好排到的那個檔案」的錯誤。在解析當下嘗試一次,您就只有一個地方需要分支;而且不同於權限前置檢查,這次嘗試走的正是真正讀取時會走的那條路徑。

用使用者聽得懂的話說明發生了什麼。「無法讀取資料」只會換來一張抱怨「資料遺失」的客服單。準確的訊息會同時點名界線與解法:這些資料屬於另一位開發者的 app,macOS 預設封鎖這類存取,而開關就在「隱私權與安全性」裡。使用者無法對一個自己定位不到的失敗採取任何行動。

不要重試。 拒絕是一種政策狀態,不是暫時性錯誤。對著被封鎖的容器跑退避重試迴圈,只會耗電、灌爆日誌,什麼也改變不了。失敗一次就好,把狀態呈現出來,並提供一個使用者改完設定後可以自行觸發的重新檢查。

在 app 重新回到前景時重新檢查,而不是輪詢。 使用者離開去改設定、然後回來的那一刻,才是重新測試存取的時機。用計時器輪詢一條被封鎖的路徑,只會照著您挑的間隔,一次又一次拿到同一個拒絕。

問問自己是否真的需要這個跨團隊讀取。 這是最讓人不舒服的一項,卻往往是正確答案。讀取競品容器的移轉工具,做的正是平台連續三個版本一直在限縮的事:macOS 15 加上沙箱、接著加上提示,現在則是直接拒絕。由對方 app 掌控的匯出路徑、由使用者自己操作檔案選取器完成的文件式匯入,或是一份有文件記載的交換格式,這三種做法都不受這次變更影響。跨越 Team ID 界線的容器讀取有一條清楚的軌跡,而這條軌跡只朝一個方向走。

檔案選取器值得特別一提,因為它正是平台預留的出口。使用者親手選取檔案,等於透過 security-scoped 機制明確授予存取權;這在權限的正當性上,比剛剛消失的那個提示更站得住腳,而且今天就能用,不需要動任何設定。

重點整理

給 macOS app 開發者: - 在 macOS 上,絕對不要把 containerURL(forSecurityApplicationGroupIdentifier:) 回傳非 nil 當成有存取權的證明。它一律會回傳 URL。請直接嘗試讀取並處理錯誤。 - 別把 isReadableFile(atPath:) 當前置檢查。Apple 反對預測檔案系統的結果,而且它判斷的是 POSIX 權限——政策層的拒絕未必會動到那裡。 - 稽核所有會讀取其他 Team ID 容器的程式路徑。在 macOS 27 上,它們會在沒有問過任何人的情況下失敗。 - 如果您把 ~/Library/Group Containers/... 寫死了,就沒有任何方法呼叫可以加上防護。請改用 API,至少讓檢查有地方可放。

給推出移轉或備份工具的團隊: - 跨團隊讀取不再只差一個提示。請把「拒絕」當成預設狀態來設計,並告訴使用者設定藏在哪裡。 - 這種失敗表現為「資料不見了」,而不是一則錯誤。請加上明確的說明文案,否則使用者只會把它當成資料遺失來回報。

給任何要變更 Team ID 的人: - 併購或帳號拆分,會悄悄切斷原本共用同一個 Team ID 前綴的 app 之間的 app group 共享。原始碼裡沒有任何跡象會告訴您這件事。

常見問題

同一個開發者帳號底下共用容器的 app 會受影響嗎?

不會。分界線是 Team ID。Apple 的規則本來就禁止不同團隊使用同一個 app group,而同一個團隊可以在自己的 app、extension、App Clips 與 XPC Services 之間共用群組。2

使用者還會看到可以按下同意的提示嗎?

在 macOS 27 上不會。發行說明明白寫著,這類存取不再要求授權,而是預設拒絕,管理權移到「隱私權與安全性」設定。1

改用較舊的 SDK 建置可以避開嗎?

多半不行。發行說明對其他 11 項變更都使用了以 SDK 為條件的措辭,唯獨這一項沒有,這暗示它是適用於所有二進位檔的作業系統層級政策。1 Apple 並未把話說死,因此請把它當成有力推論,並直接假設這個存取會失敗來規劃。

這和佈建描述檔的問題要怎麼分辨?

執行 sudo launchctl procinfo <pid>,檢查系統是否在您的行程上設定了 entitlements-validated 旗標。較舊的佈建描述檔可能早於 app group 授權機制,會產生外觀相似、成因卻無關的失敗。2

為什麼官方文件還在描述那個提示?

Apple 的 app group 容器指南描述的是 macOS 15 的行為,截至撰稿時尚未針對 macOS 27 的變更更新。2 請自行確認該頁面的現況,不要只依賴這篇文章的日期。

資料來源


  1. Apple,〈macOS 27 Golden Gate Beta 4 Release Notes〉。System Integrity Protection,新功能:「存取其他開發團隊的 app 資料容器與 app group 容器中的檔案時,不再向使用者要求授權;這類存取預設一律拒絕,使用者可在『隱私權與安全性』設定中管理。」Radar 161835690。本文提到「其他 11 個條目使用、此條目卻缺席」的 SDK 條件措辭,同樣出自此處。該 HTML 由用戶端渲染,機器可讀的版本位於 developer.apple.com/tutorials/data/documentation/macos-release-notes/macos-27-release-notes.json。 

  2. Apple,〈Accessing app group containers in your existing macOS app〉。此文為以下內容的來源:macOS 15 的提示行為、不同開發團隊不得共用同一個 app group 的規則、group.<TeamID>.<group name> 兩種識別碼形式的差別、sudo launchctl procinfo 的 entitlements-validated 檢查、REGISTER_APP_GROUPS 建置設定,以及可參與容器的項目清單。2026 年 8 月 1 日擷取,當時仍在描述該提示。 

  3. Apple,〈isReadableFile(atPath:)〉。反對預測檔案系統狀態的指引出自此處:「與其事先設法判斷某項操作會不會成功,不如直接嘗試該操作(例如載入檔案或建立目錄)、檢查錯誤,並妥善處理這些錯誤——後者遠比前者好得多。」該方法「使用真實的使用者 ID 與群組 ID,而非有效使用者與群組 ID,來判斷檔案是否可讀」這項說明也出自此處;這正是為什麼 POSIX 層級的檢查無法可靠地代表政策層的拒絕。 

  4. Apple,〈containerURL(forSecurityApplicationGroupIdentifier:)〉。回傳值:「在 iOS 上,當群組識別碼無效時,該值為 nil。在 macOS 上,即使 app group 無效,也一律回傳格式正確的 URL,因此在使用之前,務必先測試您是否真的能存取底層目錄。」不要手動組出容器路徑的指引亦出自此處。 

相關文章

Menu Item Images Disappear in macOS 27 and iPadOS 27

macOS 27 and iPadOS 27 hide menu item images by default, and what disappears depends on the SDK you linked against. Thre…

13 分鐘閱讀

Apple 的字型直譯器現已改用 Swift,速度還快了 13%

Apple 安全團隊將 TrueType hinting 直譯器從 C 改寫為記憶體安全的 Swift,讓它快了 13%,開源釋出,並展示了背後的技術。

2 分鐘閱讀

The Mac App Store Won't Let Window Managers Exist. I Shipped One Anyway.

App Sandbox forbids the API every window tiler needs, and Apple said ship outside the store. 941 Tiles ships inside it: …

8 分鐘閱讀