From 39f16e40cedcfe5de98f28e414dd5ae7fa9136bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Sat, 22 Jun 2024 17:38:08 +0200 Subject: [PATCH] Ensure osxphotos is responsible process by launching disclaimed (#1587) When osxphotos accesses the Photos library the macOS TCC system will check the responsible process for permission to access the library, and if none has been given, the user is asked to give permission via a dialog. Normally, when an executable is launched from Terminal or iTerm, the responsible app is the terminal app itself, not osxphotos, which means the user needs to give the terminal app the permission to access the Photos library, effectively giving any command executed in the terminal at any point access to the library. Worse yet, if the user has on some prior occasion denied the terminal app this access, then osxphotos will not get the needed access either, which has been a source for confusion and failures for users. We now re-launch osxphotos at startup with a special spawn flag that ensures that osxphotos is the responsible process. This will result in TCC dialogs and permissions specific to osxphotos. --- disclaim.cpp | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ disclaim.py | 24 ++++++++++++++++++++ make_cli_exe.sh | 5 ++++- osxphotos.spec | 4 ++-- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 disclaim.cpp create mode 100644 disclaim.py diff --git a/disclaim.cpp b/disclaim.cpp new file mode 100644 index 000000000..3a28f58ee --- /dev/null +++ b/disclaim.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include + +extern "C" { +int responsibility_spawnattrs_setdisclaim(posix_spawnattr_t attrs, int disclaim) +__attribute__((availability(macos,introduced=10.14),weak_import)); +char ***_NSGetArgv(); +} + +#define CHECK_SPAWN(expr) \ + if (int err = (expr)) { \ + posix_spawnattr_destroy(&attr); \ + fprintf(stderr, "[disclaim] %s: %s", #expr, strerror(err)); \ + exit(err); \ + } + +/* + Re-launches the process with disclaimed responsibilities, + effectively making it responsible for its own permissions. + + Based on https://www.qt.io/blog/the-curious-case-of-the-responsible-process +*/ +extern "C" void disclaim() +{ + posix_spawnattr_t attr; + CHECK_SPAWN(posix_spawnattr_init(&attr)); + + // Behave as exec + short flags = POSIX_SPAWN_SETEXEC; + + // Reset signal mask + sigset_t no_signals; + sigemptyset(&no_signals); + CHECK_SPAWN(posix_spawnattr_setsigmask(&attr, &no_signals)); + flags |= POSIX_SPAWN_SETSIGMASK; + + // Reset all signals to their default handlers + sigset_t all_signals; + sigfillset(&all_signals); + CHECK_SPAWN(posix_spawnattr_setsigdefault(&attr, &all_signals)); + flags |= POSIX_SPAWN_SETSIGDEF; + + CHECK_SPAWN(posix_spawnattr_setflags(&attr, flags)); + + if (__builtin_available(macOS 10.14, *)) { + // Disclaim TCC responsibilities for parent, making + // the launched process the responsible process. + if (responsibility_spawnattrs_setdisclaim) + CHECK_SPAWN(responsibility_spawnattrs_setdisclaim(&attr, 1)); + } + + pid_t pid = 0; + char **argv = *_NSGetArgv(); + extern char **environ; + CHECK_SPAWN(posix_spawnp(&pid, argv[0], nullptr, &attr, argv, environ)); +} diff --git a/disclaim.py b/disclaim.py new file mode 100644 index 000000000..fc66b5b42 --- /dev/null +++ b/disclaim.py @@ -0,0 +1,24 @@ +def _pyi_rthook(): + import sys + if sys.platform != 'darwin': + return + + # Avoid redundant disclaims + import os + if os.environ.get('PYI_DISCLAIMED'): + return + os.environ['PYI_DISCLAIMED'] = '1' + + # The bootloader has cleared the _MEIPASS2 environment variable by the + # time we get here, which means re-launching the executable disclaimed + # will unpack the binary again. To avoid this we reset _MEIPASS2 again, + # so that our re-launch will pick up at second stage of the bootstrap. + os.environ['_MEIPASS2'] = sys._MEIPASS + + import ctypes + library_path = os.path.join(sys._MEIPASS, 'libdisclaim.dylib') + libdisclaim = ctypes.cdll.LoadLibrary(library_path) + libdisclaim.disclaim() + +_pyi_rthook() +del _pyi_rthook diff --git a/make_cli_exe.sh b/make_cli_exe.sh index 850e21690..ddaf1c488 100755 --- a/make_cli_exe.sh +++ b/make_cli_exe.sh @@ -5,4 +5,7 @@ # If you need to install pyinstaller: # python3 -m pip install --upgrade pyinstaller -pyinstaller osxphotos.spec \ No newline at end of file +set -e +mkdir -p build +clang -shared -mmacosx-version-min=10.12 disclaim.cpp -o build/libdisclaim.dylib +pyinstaller osxphotos.spec diff --git a/osxphotos.spec b/osxphotos.spec index 09c5b5a71..ad948d897 100644 --- a/osxphotos.spec +++ b/osxphotos.spec @@ -55,11 +55,11 @@ block_cipher = None a = Analysis( ["cli.py"], pathex=[pathex], - binaries=[], + binaries=[("build/libdisclaim.dylib", ".")], datas=datas, hiddenimports=["pkg_resources.py2_warn"], hookspath=[], - runtime_hooks=[], + runtime_hooks=["disclaim.py"], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False,