-
Notifications
You must be signed in to change notification settings - Fork 908
macOS App Sandbox #9023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
macOS App Sandbox #9023
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
1de9cef
fix(macOS): Enforce valid app group identifier.
i2h3 5d882e2
chore(macOS): Updated Xcode build settings.
i2h3 5f2dd67
fix(macOS): Updated signing settings for all targets.
i2h3 22eec82
feat(macOS): Enabled app sandbox and network client entitlements for …
i2h3 1a5082a
fix(macOS): Relocated socket files into sandboxed containers.
i2h3 202cb4e
fix(macOS): Added container migration manifest.
i2h3 d7a6d7c
chore(macOS): Removed obsolete values from Info.plist
i2h3 645b628
chore(macOS): Outsourced some build settings from Xcode project into …
i2h3 6afb39a
chore(macOS): Updated bundle identifier in Xcode project to Nextcloud.
i2h3 c947db9
chore(macOS): Creating build settings file on demand from template.
i2h3 7bd2315
chore(macOS): Added default values to build settings file.
i2h3 9298122
fix(macOS): NextcloudDesktopClientSocketKit signing.
i2h3 c2e01d4
fix(macOS): Added missing user selected read-write entitlement to mai…
i2h3 b6cfd6d
fix(macOS): Writable check on debug archive destination directory.
i2h3 11be220
fix(macOS): Accessing target of debug archive as a security scoped URL.
i2h3 09005a0
fix(file-provider): App group container lookup API change in Nextclou…
i2h3 52212ab
fix(macOS): File provider path assembly for debug archive.
i2h3 6ebdab4
fix(macOS): Debug archive creation in a sandbox.
i2h3 291156d
fix(macOS): Development team group identifier prefix.
i2h3 eed5950
fix(macOS): Defined "FileProviderUIExt" as target dependency to "desk…
i2h3 c4846ce
fix: Define default DEVELOPMENT_TEAM for main app.
i2h3 1679b82
fix: Restored NCFPKAppGroupIdentifier in main app Info.plist
i2h3 28421ec
fix: Fixed too long socket paths (max 104 characters).
i2h3 f499658
chore(macOS): Made whole "src" instead of just "gui" folder accessibl…
i2h3 ff45457
fix(macOS): Synchronization folder selection.
i2h3 af2e689
fix(file-provider): Consolidated support and log directories.
i2h3 5aca5a6
chore(file-provider): Updated to NextcloudFileProviderKit 4.0.0
i2h3 79aa712
fix(macOS): Signing identity.
i2h3 aeee811
fix(file-provider): File provider domains reset on app sandbox migrat…
i2h3 d5b5a85
chore(macOS): Xcode removed an orphaned reference from the project file.
i2h3 7e68148
fix(file-provider): Synchronous file provider domain removals.
i2h3 9c1e865
chore(macOS): Removed obsolete compatibility switches for no longer s…
i2h3 7897d15
chore(file-provider): Logging improvements.
i2h3 f2ba1b0
chore(file-provider): Renamed fileproviderdomainmanager_mac.mm to fil…
i2h3 87d7694
chore(file-provider): Simplified FileProviderDomainManager.
i2h3 a1462d7
fix(file-provider): Made addFileProviderDomain and removeFileProvider…
i2h3 ed7d303
fix(file-provider): Made findExistingFileProviderDomains synchronous.
i2h3 e5b4d37
chore(file-provider): Consolidated account to domain mapping.
i2h3 52e5d4a
FIXUP: Improved logging.
i2h3 866d709
FIXUP: Removing domain completely.
i2h3 41f37a2
FIXUP: Crash due to dangling pointer.
i2h3 4108791
chore(file-provider): Improved logging.
i2h3 525cb9c
fix(file-provider): Authentication calls via XPC by file provider dom…
i2h3 296567b
fix(file-provider): Actually enable or disable file provider domain i…
i2h3 7751571
chore(ci): macOS build and test action now uses latest macOS and late…
i2h3 1255a0f
fix(file-provider): Resolved circular object initializer dependency.
i2h3 9ae167a
fix(CI): Disabling code signing for macOS by default.
i2h3 6dcc1fd
chore(CI): Removed obsolete environment variables from CI.
i2h3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Move</key> | ||
| <array> | ||
| <string>${Library}/Preferences/@APPLICATION_NAME@</string> | ||
| </array> | ||
| </dict> | ||
| </plist> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
i2h3 marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| <!-- | ||
| - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| - SPDX-License-Identifier: GPL-2.0-or-later | ||
| --> | ||
|
|
||
| # macOS App Sandbox Support for Qt Applications | ||
|
|
||
| ## Overview | ||
|
|
||
| This document explains how to make the Nextcloud Desktop Client work properly with macOS App Sandbox when using Qt. The key issue is that Qt's `QFileDialog` returns security-scoped URLs that require explicit access management in sandboxed applications. | ||
|
|
||
| ## The Problem | ||
|
|
||
| When running a sandboxed macOS application with the `com.apple.security.files.user-selected.read-write` entitlement, file operations on user-selected files (via `QFileDialog`) will fail unless you explicitly: | ||
|
|
||
| 1. Call `startAccessingSecurityScopedResource()` on the URL before accessing the file | ||
| 2. Call `stopAccessingSecurityScopedResource()` when done | ||
|
|
||
| This is **required by macOS sandbox security**, but Qt does not handle this automatically. The underlying issue is that: | ||
|
|
||
| - `QFileDialog::getSaveFileUrl()` returns a `QUrl` that represents a security-scoped bookmark | ||
| - Without calling `startAccessingSecurityScopedResource()`, the sandboxed app has no permission to access the file | ||
| - Even though you have the entitlement, you must explicitly claim access for each user-selected file | ||
|
|
||
| ## The Solution | ||
|
|
||
| ### 1. Security-Scoped Access Wrapper | ||
|
|
||
| We created a RAII wrapper class `MacSandboxSecurityScopedAccess` (in `utility_mac_sandbox.h/mm`) that: | ||
|
|
||
| - Automatically calls `startAccessingSecurityScopedResource()` in the constructor | ||
| - Automatically calls `stopAccessingSecurityScopedResource()` in the destructor | ||
| - Uses unique_ptr for exception safety | ||
| - Provides `isValid()` to check if access was successfully obtained | ||
|
|
||
| ### 2. Usage Pattern | ||
|
|
||
| ```cpp | ||
| #ifdef Q_OS_MACOS | ||
| #include "utility_mac_sandbox.h" | ||
| #endif | ||
|
|
||
| void MyClass::saveFile() | ||
| { | ||
| const auto fileUrl = QFileDialog::getSaveFileUrl( | ||
| this, | ||
| tr("Save File"), | ||
| QUrl::fromLocalFile(QDir::homePath()), | ||
| tr("Text Files (*.txt)") | ||
| ); | ||
|
|
||
| if (fileUrl.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| #ifdef Q_OS_MACOS | ||
| // Acquire security-scoped access for the user-selected file | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
|
|
||
| if (!scopedAccess->isValid()) { | ||
| // Handle error - access could not be obtained | ||
| QMessageBox::critical(this, tr("Error"), tr("Could not access file")); | ||
| return; | ||
| } | ||
| // scopedAccess will automatically release when it goes out of scope | ||
| #endif | ||
|
|
||
| // Now you can safely access the file | ||
| QFile file(fileUrl.toLocalFile()); | ||
| if (file.open(QIODevice::WriteOnly)) { | ||
| // Write to file... | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Required Entitlements | ||
|
|
||
| In `admin/osx/macosx.entitlements.cmake`, ensure you have: | ||
|
|
||
| ```xml | ||
| <key>com.apple.security.app-sandbox</key> | ||
| <true/> | ||
| <key>com.apple.security.files.user-selected.read-write</key> | ||
| <true/> | ||
| ``` | ||
|
|
||
| ## Key Requirements for Qt + macOS Sandbox | ||
|
|
||
| ### 1. Use QFileDialog URL-based Methods | ||
|
|
||
| Always use the URL-based variants of QFileDialog methods: | ||
| - ✅ `QFileDialog::getSaveFileUrl()` | ||
| - ✅ `QFileDialog::getOpenFileUrl()` | ||
| - ✅ `QFileDialog::getOpenFileUrls()` | ||
| - ❌ `QFileDialog::getSaveFileName()` - returns QString, not security-scoped | ||
| - ❌ `QFileDialog::getOpenFileName()` - returns QString, not security-scoped | ||
|
|
||
| ### 2. Wrap File Access with Security Scoping | ||
|
|
||
| ```cpp | ||
| #ifdef Q_OS_MACOS | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
| if (!scopedAccess->isValid()) { | ||
| // Handle error | ||
| return; | ||
| } | ||
| #endif | ||
| // Access file here | ||
| // scopedAccess releases automatically when going out of scope | ||
| ``` | ||
|
|
||
| ### 3. Handle Scope Lifetime Correctly | ||
|
|
||
| The security-scoped access must remain valid for the entire duration of file access: | ||
|
|
||
| ```cpp | ||
| // ✅ CORRECT - scopedAccess lives until after file operations | ||
| #ifdef Q_OS_MACOS | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
| if (!scopedAccess->isValid()) { | ||
| return; | ||
| } | ||
| #endif | ||
|
|
||
| QFile file(fileUrl.toLocalFile()); | ||
| file.open(QIODevice::WriteOnly); | ||
| file.write(data); | ||
| file.close(); | ||
| // scopedAccess destructor called here | ||
|
|
||
| // ❌ WRONG - scopedAccess destroyed before file operations | ||
| #ifdef Q_OS_MACOS | ||
| { | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
| if (!scopedAccess->isValid()) { | ||
| return; | ||
| } | ||
| } // scopedAccess destroyed here! | ||
| #endif | ||
|
|
||
| QFile file(fileUrl.toLocalFile()); // This will fail! | ||
| file.open(QIODevice::WriteOnly); // No longer have access | ||
| ``` | ||
|
|
||
| ### 4. Consider All File Operations | ||
|
|
||
| This applies to ANY file operation on user-selected files: | ||
| - Reading files | ||
| - Writing files | ||
| - Creating archives/zip files | ||
| - Copying files | ||
| - Moving files | ||
| - Checking file existence/permissions | ||
|
|
||
| ## Common Pitfalls | ||
|
|
||
| ### 1. Using QString-based paths instead of QUrl | ||
|
|
||
| ```cpp | ||
| // ❌ WRONG - loses security-scoped bookmark | ||
| QString path = QFileDialog::getSaveFileName(...); | ||
|
|
||
| // ✅ CORRECT - preserves security-scoped bookmark | ||
| QUrl url = QFileDialog::getSaveFileUrl(...); | ||
| ``` | ||
|
|
||
| ### 2. Converting QUrl too early | ||
|
|
||
| ```cpp | ||
| // ❌ WRONG - converts to string before starting access | ||
| QUrl url = QFileDialog::getSaveFileUrl(...); | ||
| QString path = url.toLocalFile(); // Loses security scope! | ||
| #ifdef Q_OS_MACOS | ||
| auto access = Utility::MacSandboxSecurityScopedAccess::create(QUrl::fromLocalFile(path)); // Won't work | ||
| #endif | ||
|
|
||
| // ✅ CORRECT - start access before conversion | ||
| QUrl url = QFileDialog::getSaveFileUrl(...); | ||
| #ifdef Q_OS_MACOS | ||
| auto access = Utility::MacSandboxSecurityScopedAccess::create(url); // Works! | ||
| #endif | ||
| QString path = url.toLocalFile(); | ||
| ``` | ||
|
|
||
| ### 3. Forgetting to check isValid() | ||
|
|
||
| ```cpp | ||
| // ❌ RISKY - doesn't check if access was obtained | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
| QFile file(fileUrl.toLocalFile()); // Might fail silently | ||
|
|
||
| // ✅ CORRECT - always check validity | ||
| auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(fileUrl); | ||
| if (!scopedAccess->isValid()) { | ||
| // Show error to user | ||
| return; | ||
| } | ||
| QFile file(fileUrl.toLocalFile()); // Now safe to use | ||
| ``` | ||
|
|
||
| ## Testing Sandbox Behavior | ||
|
|
||
| To test if your app properly handles sandbox restrictions: | ||
|
|
||
| 1. **Build with proper entitlements**: Ensure the app is codesigned with the entitlements file | ||
| 2. **Test file operations**: Try to save/open files in various locations | ||
| 3. **Check Console.app**: Look for sandbox violation messages like: | ||
| ``` | ||
| Sandbox: MyApp(12345) deny(1) file-write-create /Users/... | ||
| ``` | ||
| 4. **Test without access calls**: Temporarily remove the security-scoped access calls to verify they're needed | ||
|
|
||
| ## References | ||
|
|
||
| - [Apple Documentation: App Sandbox](https://developer.apple.com/documentation/security/app_sandbox) | ||
| - [Apple Documentation: Security-Scoped Bookmarks](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso) | ||
| - [Qt Documentation: QFileDialog](https://doc.qt.io/qt-6/qfiledialog.html) | ||
|
|
||
| ## Files Modified | ||
|
|
||
| - `src/common/utility_mac_sandbox.h` - Header for security-scoped access wrapper | ||
| - `src/common/utility_mac_sandbox.mm` - Implementation using Objective-C++ | ||
| - `src/common/common.cmake` - Added new files to build system | ||
| - `src/gui/generalsettings.cpp` - Fixed debug archive creation to use security-scoped access | ||
|
|
||
| ## Future Work | ||
|
|
||
| Consider auditing all uses of `QFileDialog` in the codebase to ensure they: | ||
| 1. Use URL-based methods (`getSaveFileUrl`, `getOpenFileUrl`, etc.) | ||
| 2. Properly acquire security-scoped access on macOS | ||
| 3. Handle access errors gracefully |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,3 @@ | |
| # SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| DerivedData | ||
|
|
||
| # exception | ||
| !NextcloudDev/Build.xcconfig.template | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it just a random string or something specific ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the unique and stable identifier of the development team registered with Apple which is expected to sign the build later on. Like apps have bundle identifiers (
com.nextcloud.desktopclient), their development teams have it, too (NKUJUXUJ3Bfor Nextcloud GmbH). This is relevant for branded builds in particular.