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,