Using xattr to set the Mac OSX quarantine property - macos

There is a lot of information on StackOverflow and elsewhere about how to clear the Mac quarantine property.
In my case I would like to set it.
This is in order to test that my app is properly signed so that the user will hot get the "Untrusted Developer" warning after downloading it.
My app is particularly large (we distribute from a large file download site, not the store) and it is not convenient to have to upload and download to test this.
I have had some battles with code signing the past week so this testing is important to me.
Once a file has the quarantine property I see how I can alter it to have the values:
0002 = downloaded but never opened (this is the one that causes the warning)
0022 = app aborted by user from the warning dialogue (you hit 'cancel' in the dialogue)
0062 = app opened (at least) once (you hit 'open' in the dialogue)
But I don't know how to give it the property in the first place.

The code for this isn't hard, but you need FSRef's to do it, which are deprecated. That said, it still works on 10.9. You have to link with CoreServices.
int main(int argc, const char * argv[]) {
#autoreleasepool {
if (argc != 2) {
printf("quarantine <path>\n");
exit(1);
}
NSString *path = #(argv[1]);
OSStatus result;
FSRef pathRef;
result = FSPathMakeRef((UInt8*)[path UTF8String], &pathRef, 0);
if (result != noErr) {
NSLog(#"Error making ref (%d): %s", result, GetMacOSStatusCommentString(result));
exit(result);
}
NSDictionary *quarantineProperties = #{(__bridge id)kLSQuarantineTypeKey: (__bridge id)kLSQuarantineTypeOtherDownload};
result = LSSetItemAttribute(&pathRef,
kLSRolesAll,
kLSItemQuarantineProperties,
(__bridge CFTypeRef)quarantineProperties);
if (result != noErr) {
NSLog(#"Error setting attribute (%d): %s", result, GetMacOSStatusCommentString(result));
}
exit(result);
}
return 0;
}
Another approach is to just copy the quarantine information from one file to another. You can serialize xattr information like this:
xattr -p com.apple.quarantine file > file.xattr
You can then apply those attributes to another file like this:
xattr -w com.apple.quarantine "`cat file.xattr`" file
(That should work, but I haven't tested it with quarantining in particular. I use a similar technique to save code signatures and reapply them.)

Related

Determine if a file is inside any macOS Trash folder

There's a similar question for iOS, but I found that the proprosed solutions do not work on macOS in all cases.
On a Mac, there are many possible Trash folders:
/.Trashes
~/.Trash
~/Library/Mobile Documents/com~apple~CloudDocs/.Trash – this one is from iCloud
/Users/xxx/.Trash – any other user's trash
/Volumes/xxx/.Trashes
This code should work but doesn't for the case of the iCloud trash:
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains);
If the URL points to any iCloud folder (including the Trash folder shown above), I get this error:
Error Domain=NSCocoaErrorDomain Code=3328
"The requested operation couldn’t be completed because the feature is not supported."
Curiously, even the header file of "NSFileManager" in the 10.15 SDK suggests to use this same code:
/* trashItemAtURL:resultingItemURL:error: [...]
To easily discover if an item is in the Trash, you may use
[fileManager getRelationship:&result ofDirectory:NSTrashDirectory
inDomain:0 toItemAtURL:url error:&error]
&& result == NSURLRelationshipContains.
*/
There also seems to be an issue with trashItemAtURL: on iCloud-synched folders.
So, how do I solve this? If the Finder can detect the iCloud trash, I should be, too.
(Note: The app I use for testing this is not even sandboxed)
More findings: Fails with dead symlinks, too
The officially suggested method of using getRelationship: also fails with an error if the url points to a symlink whose target doesn't exist.
So, basically, this function is quite broken (verified in 10.13.6, 10.15.7 and 11.0.1).
Here's code to demonstrate the bug, which I've filed with Apple under FB8890518:
#import <Foundation/Foundation.h>
static void testSymlink (NSString* symlinkName, NSString* symlinkTarget)
{
NSString *path = [[NSString stringWithFormat:#"~/.Trash/%#", symlinkName] stringByExpandingTildeInPath];
NSURL *url = [NSURL fileURLWithPath:path];
symlink (symlinkTarget.UTF8String, path.UTF8String);
NSLog(#"created symlink at <%#> pointing to <%#>", url.path, symlinkTarget);
NSURLRelationship relationship = -1;
NSError *error = nil;
[NSFileManager.defaultManager getRelationship:&relationship ofDirectory:NSTrashDirectory inDomain:0 toItemAtURL:url error:&error];
NSString *rel = #"undetermined";
if (relationship == 0) rel = #"NSURLRelationshipContains";
if (relationship == 1) rel = #"NSURLRelationshipSame";
if (relationship == 2) rel = #"NSURLRelationshipOther";
NSLog(#"result:\n relationship: %#\n error: %#", rel, error);
}
int main(int argc, const char * argv[])
{
#autoreleasepool {
testSymlink (#"validSymlink", #"/System");
testSymlink (#"brokenSymlink", #"/nonexisting_file");
}
return 0;
}
With the realization that [NSFileManager getRelationship:] even fails for broken symlinks, I conclude that this is a bug in macOS that's been existing undetected for years.
I came up with the following work-around:
Use the getRelationship: operation, then check the returned error first:
If there's no error, then check if relationship == NSURLRelationshipContains, and use that as my result.
Else, in case of any error, check whether the path contains "/.Trash/" or "/.Trashes/" - if so, assume the item is inside the Trash folder.
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains)
|| error && (
[theURL.path containsString:#"/.Trash/"]
|| [theURL.path containsString:#"/.Trashes/"]
)
);

Permission to access /Library/Application Support from Mac App bundle

I have an app bundle (built with Unity 3d if that is relevant) that I am able to create a .pkg installer using productbuild and distribute on the App Store without issue. However, the app downloads and caches a fair amount of media and has some optional configuration files that need to be shared between all users on the machine. According to Apple's documentation the configuration files should probably go in the /Library/Application Support directory and the media in /Library/Caches. I've made a modified version of that app that uses those directories instead of the ones that the sandboxed app can access, but it doesn't have permission to the /Library directory unless I run the app as root, which isn't a realistic option.
I've searched google for several hours, but I can't seem to find anything about creating such an installer. I did read this answer which has a screenshot of an installer that has the option to install for all users, but either I'm missing some option to enable that or that screenshot is just outdated because I can't seem to create a .pkg that gives me that option.
So I guess my question boils down to this: how do I package my app so it can be installed for all users, and have permission to read and write to /Library/Application Support/{app name}, or is there another preferred way to share configuration files and/or media between multiple users on the same machine?
For anyone else who has a similar problem, the correct answer is that you can't do this using productbuild, but you can using pkgbuild.
My productbuild build step for the app store looks like this:
productbuild --component "{mystoreapp.app/}" /Applications --sign "{signing identity}" "{mystorepkg.pkg}"
The corresponding packing command for pkgbuild looks like this:
pkgbuild --component "{mymodifiedapp.app/}" --sign "{signing identity}" --ownership preserve --scripts "{path/to/my/scripts}" --identifier {com.yourcompany.yourapp} --version "{versionNumber}" --install-location /Applications "{mymodifiedpkg.pkg}"
Note that signing is optional here as it will be distributed outside the store. where {path/to/my/scripts} has a file called postinstall in it that looks like this:
#this function creates a directory if it doesn't exist
create_directory() {
if [[ ! -d "$1" ]]
then
if [[ ! -L "$1" ]]
then
echo "directory $1 doesn't exist. Creating now"
mkdir "$1"
echo "directory $1 created"
else
echo "directory $1 exists"
fi
fi
}
#this function gives all users read and write (and execute) access to the directory
fix_dir_permissions() {
chmod 777 "$1"
}
baseDirName="/Library/Application Support/{your app name bere}"
subDirs[0]="$baseDirName/{sub dir here}"
#create base directory
create_directory "$baseDirName"
#create all subdirectories and give permissions
for subDir in "${subDirs[#]}"
do
create_directory "$subDir"
fix_dir_permissions "$subDir"
done
exit 0
This script will run after the install is over, and will create your application support directory as well as any subdirectories you need and change the permissions on them so all users have access to them.
I solved the same problem by creating a little help app that is called from the main program at any time access to "Application Support" is needed. The helper program is spawned from the main program using AuthorizationExecuteWithPrivileges. It only calls functions to create a directory and set permissions. So all is done from within the main application, not from a installer package. AuthorizationExecuteWithPrivileges (yes its offically deprecated) then shows a macos system window that requires to enter a password etc.
For this part quite a nice implementation exists here:
https://github.com/sveinbjornt/STPrivilegedTask/
It is important to:
wait until the called program is finished
then wait again until the folder exists (which may need some msecs)
It took me quite a while to get this done, so i'll share it here. Although the application is C++ the code here must use obective-c.
See my post here for signing and notarizing this:
How to notarize an app bundle containing helpers embedded in it?
The helper program:
#import <Foundation/Foundation.h>
#include <iostream>
#include <fstream>
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSString *supportDirectory = [NSString stringWithFormat:#"%s", argv[1] ];
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:supportDirectory withIntermediateDirectories:YES attributes:nil error:&error];
if (!success)
return -1;
BOOL success2 = [[NSFileManager defaultManager] setAttributes:#{ NSFilePosixPermissions : #0777 } ofItemAtPath:supportDirectory error:&error];
}
return 0;
}
Caller Process:
void macCheckCreateFolderAdmin(std::string path)
{
NSString *supportDirectory = [NSString stringWithFormat:#"%s", path.c_str() ];
NSError *error = nil;
BOOL success;
if ([[NSFileManager defaultManager] fileExistsAtPath: supportDirectory])
return;
char *args[] =
{
const_cast<char *>(path.c_str()),
NULL
};
AuthorizationRef authorizationRef;
OSStatus status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authorizationRef);
NSString *nsExecPath = NSProcessInfo.processInfo.arguments[0];
const char *applFolder = [nsExecPath UTF8String];
std::string s = std::string(applFolder);
std::size_t found = s.rfind("/");
if( found != std::string::npos)
s = s.substr(0, found + 1);
s += "myHelperProgram";
FILE *outputFile = nullptr;
OSStatus status2 = AuthorizationExecuteWithPrivileges(authorizationRef, s.c_str(), kAuthorizationFlagDefaults, args, &outputFile);
if(status2 != 0)
return;
int processIdentifier = fcntl(fileno(outputFile), F_GETOWN, 0);
pid_t pid = 0, count = 0;
while(count < 30 && (pid = waitpid(processIdentifier, &status, WNOHANG)) == 0)
{
count ++;
usleep(200 * 1000);
}
}
}

How does OS X's defaults command get access to prefs of sandboxed apps?

I am writing a preferences editor tool (see http://www.tempel.org/PrefsEditor). It is effectively a GUI version of the defaults command.
I have trouble reading (let alone writing) preferences of random sandboxed applications, though.
For instance, when I try to get the keys of the Maps app, I get NULL returned:
CFArrayRef prefs = CFPreferencesCopyKeyList (CFSTR("com.apple.Maps"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
However, the defaults command is able to read those prefs:
defaults read com.apple.Maps
I like to know how the defaults command accomplishes this, trying to do the same in my tool.
try that:
CFPropertyListRef prop = CFPreferencesCopyValue(CFSTR("ElementsVersion"),
CFSTR("/Users/karsten/Library/Containers/com.apple.Maps/Data/Library/Preferences/com.apple.Maps"),
CFSTR("kCFPreferencesCurrentUser"),
CFSTR("kCFPreferencesAnyHost"));
seems you need the path to the file, not just the bundle-id
Karsten’s answer is correct but for the sake of completeness, the defaults command uses the undocumented _CFPreferencesCopyApplicationMap() function to retrieve the full URL of the preferences.
#import <CoreFoundation/CoreFoundation.h>
extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);
int main(int argc, char *argv[])
{
#autoreleasepool
{
CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFArrayRef urls = CFDictionaryGetValue(applicationMap, CFSTR("com.apple.mail"));
CFShow(urls);
CFRelease(applicationMap);
}
}
I've added to 0xced's excellent answer so that the code can be packaged into a command-line tool that accepts the bundle ID as an argument. Forgive me if this is obvious to experienced Mac programmers, but as someone who has never used CoreFoundation I found this to be non-trivial.
#import <CoreFoundation/CoreFoundation.h>
extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);
int main(int argc, char *argv[]) {
#autoreleasepool {
if (argc < 2) {
// Print usage string & exit.
fprintf(stderr, "usage: GetPrefDomains bundle_id\n");
exit(1);
}
// Get the bundle ID from the first command-line argument.
CFStringRef bundleID = CFStringCreateWithCString(NULL, argv[1], kCFStringEncodingUTF8);
// Get the list of preference domain urls.
CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFArrayRef urls = CFDictionaryGetValue(applicationMap, bundleID);
// If no urls exist (invalid bundle ID), exit.
if (!urls) {
fprintf(stderr, "No preference domains found.\n");
exit(0);
}
// Print the paths to the preference domains.
CFIndex urlsCount = CFArrayGetCount(urls);
for (int i = 0; i < urlsCount; i++) {
CFURLRef url = CFArrayGetValueAtIndex(urls, i);
CFStringRef path = CFURLCopyPath(url);
printf("%s\n", CFStringGetCStringPtr(path, kCFStringEncodingUTF8));
}
// Clean up.
CFRelease(bundleID);
CFRelease(applicationMap);
}
}
Save the code as GetPrefDomains.m, compile, and invoke as:
GetPrefDomains com.apple.mail
This was useful to me because surprisingly the defaults command is case-sensitive and misbehaves silently with certain Apple applications that are under the filesystem protections in SIP on Mojave 10.14 or later (Safari & Mail, most notably). Add in the fact that Apple's capitalization rules are not consistent (com.apple.mail vs. com.apple.Notes), sandboxed preference paths, and the fact that that the filesystem is not case-sensitive and you quickly run into some very frustrating edge cases.

bundlePath returns empty when running as a Launch Deamon on MacOSX 10.5

I've got a Cocoa command-line app which is built to target 10.5 SDK. In the app, I have
NSString *appPath = [[NSBundle mainBundle] bundlePath];
NSLog(#"%#", appPath);
On Mac OSX 10.5, when I run the app from the command line, I get the expected output of the path. However, if I set the app up to run as a LaunchDeamon, it only outputs a '/'
It works as expected on 10.6 and on 10.7 both as a Deamon and as an app. Anyone know why the difference would be? Is there a better way to get the application path that would work on 10.5+?
UPDATE
For me, the solution in the accepted answer did not work. However, the comment about adding the "WorkingDirectory" key to the LaunchDeamon's plist file worked out. Apparently this is needed for Mac OS X 10.5, but not 10.6+.
Thanks for answering my clarifying question.
NSBundle depends on a existing bundle, with it's associated Info.plists and bundle ID's (e.g. com.apple.textedit.app), etc.
While a single binary is not a bundle, I'm guessing that Apple engineering fixed up [[NSBundle mainBundle] bundlePath] to do "the right thing" in 10.6 & 10.7. But you still need a solution for 10.5.
Maybe the UNIX library function char * getcwd(char *buf, size_t size) would get you to where you need to be.
For a proper solution, I'd recommend doing a run-time conditional check with code that looks something like this:
+ (NSString *) getAppPath
{
NSString * appPath = NULL;
SInt32 minorVersionNum;
OSErr err;
err = Gestalt(gestaltSystemVersionMinor,&minorVersionNum);
// do this only if we're running on anything *older* than 10.6
if((noErr == err) && (minorVersionNumber < 6))
{
// hopefully the below define is enough space for any returned path
#define kMaxPathLength 512
size_t bufferLength = kMaxPathLength;
char bufferToHoldPath[kMaxPathLength];
// initialize the buffer & guarantee null-terminated strings
bzero(&bufferToHoldPath,bufferLength);
if( getcwd(&bufferToHoldPath, bufferLength) != NULL)
{
appPath = [NSString stringWithUTF8String: bufferToHoldPath];
}
}
// this code runs for 10.6 and *newer*, and attempts it on
// 10.5 only if the above getcwd call failed
if(NULL == appPath)
{
appPath = [[NSBundle mainBundle] bundlePath];
}
return(appPath);
}
I did not test this code out so YMMV.

Setting process name on Mac OS X at runtime

I'm trying to change my process' name as it appears in ps and Activity Monitor at runtime. I found several notes that there is no portable way to do this (which I don't care about).
Here's what I tried. None of these approaches worked for me.
Changing argv[0] (seems to be the way to go on some Unix systems)
Calling [[NSProcessInfo processInfo] setProcessName:#"someName"]
Calling setprogname (calling getprogname returns the name I set, but that is irrelevant)
I also read about a function called setproctitle which should be defined in stdlib.h if it is available, but it's not there.
There must be a way to accomplish this because QTKitServer - the faceless decoder for QuickTime Player X - has its corresponding QuickTime Player's PID in its process name.
Does anybody have a clue about how to accomplish this? I'd very much prefer a Core Foundation or POSIXy way over an Objective-C method to do this.
Thanks,
Marco
Edit: If it is in any way relevant, I'm using Mac OS X 10.6.5 and Xcode 3.2.5
There are good reasons to change the process name. Java software should change process names because when running different java tools I want to see which java process is for which tool.
Chromium does it: http://src.chromium.org/viewvc/chrome/trunk/src/base/mac/mac_util.mm.
Node.js uses same code to implement Process.title = 'newtitle': https://github.com/joyent/node/blob/master/src/platform_darwin_proctitle.cc
Note: This fails if someone does su to a different not logged-in user: https://github.com/joyent/node/issues/1727
Here the source code in its full complex glory. By the way, someone told me it also works for Mac OS X Lion and also fails with su.
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
void SetProcessName(CFStringRef process_name) {
if (!process_name || CFStringGetLength(process_name) == 0) {
NOTREACHED() << "SetProcessName given bad name.";
return;
}
if (![NSThread isMainThread]) {
NOTREACHED() << "Should only set process name from main thread.";
return;
}
// Warning: here be dragons! This is SPI reverse-engineered from WebKit's
// plugin host, and could break at any time (although realistically it's only
// likely to break in a new major release).
// When 10.7 is available, check that this still works, and update this
// comment for 10.8.
// Private CFType used in these LaunchServices calls.
typedef CFTypeRef PrivateLSASN;
typedef PrivateLSASN (*LSGetCurrentApplicationASNType)();
typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN,
CFStringRef,
CFStringRef,
CFDictionaryRef*);
static LSGetCurrentApplicationASNType ls_get_current_application_asn_func =
NULL;
static LSSetApplicationInformationItemType
ls_set_application_information_item_func = NULL;
static CFStringRef ls_display_name_key = NULL;
static bool did_symbol_lookup = false;
if (!did_symbol_lookup) {
did_symbol_lookup = true;
CFBundleRef launch_services_bundle =
CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices"));
if (!launch_services_bundle) {
LOG(ERROR) << "Failed to look up LaunchServices bundle";
return;
}
ls_get_current_application_asn_func =
reinterpret_cast<LSGetCurrentApplicationASNType>(
CFBundleGetFunctionPointerForName(
launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN")));
if (!ls_get_current_application_asn_func)
LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN";
ls_set_application_information_item_func =
reinterpret_cast<LSSetApplicationInformationItemType>(
CFBundleGetFunctionPointerForName(
launch_services_bundle,
CFSTR("_LSSetApplicationInformationItem")));
if (!ls_set_application_information_item_func)
LOG(ERROR) << "Could not find _LSSetApplicationInformationItem";
CFStringRef* key_pointer = reinterpret_cast<CFStringRef*>(
CFBundleGetDataPointerForName(launch_services_bundle,
CFSTR("_kLSDisplayNameKey")));
ls_display_name_key = key_pointer ? *key_pointer : NULL;
if (!ls_display_name_key)
LOG(ERROR) << "Could not find _kLSDisplayNameKey";
// Internally, this call relies on the Mach ports that are started up by the
// Carbon Process Manager. In debug builds this usually happens due to how
// the logging layers are started up; but in release, it isn't started in as
// much of a defined order. So if the symbols had to be loaded, go ahead
// and force a call to make sure the manager has been initialized and hence
// the ports are opened.
ProcessSerialNumber psn;
GetCurrentProcess(&psn);
}
if (!ls_get_current_application_asn_func ||
!ls_set_application_information_item_func ||
!ls_display_name_key) {
return;
}
PrivateLSASN asn = ls_get_current_application_asn_func();
// Constant used by WebKit; what exactly it means is unknown.
const int magic_session_constant = -2;
OSErr err =
ls_set_application_information_item_func(magic_session_constant, asn,
ls_display_name_key,
process_name,
NULL /* optional out param */);
LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
}
Edit: It's a complex and confusing problem.
On OS X there is no setproctitle(3). One has to write into the argv array (ugly
and a bit dangerous because it is possible to overwrite some environment variables with bogus stuff). Done correctly it works very well.
Additionally Apple has the ActivityMonitor application, something like the Task Manager under Windows. The code above manipulates ActivityMonitor but this manipulation doesn't seem to be encouraged by Apple (hence the use of undocumented functions).
Important: ps and ActivityMonitor don't show the same information.
Also important: ActivityMonitor is not available if you don't have GUI. This can happen if you ssh in to a remote Apple box and there is nobody logged in by GUI. Sadly there is a bug by Apple IMO. Just querying if there is a GUI sends an annoying warning message to stderr.
Summary: If you need to change ActivityMonitor, use the code above. If you have GUI-less situations and and dislike warnings on stderr, redirect stderr temporarily to /dev/null during the call of SetProcessName. If you need to change ps information, write into argv.
You can use the lsappinfo tool which comes with macOS since at least 10.6 and up to present day (10.13.2):
Shell:
lsappinfo setinfo <PID> --name <NAME>
C++:
#include <sstream>
#include <string>
#include <stdlib.h>
void setProcessName (pid_t pid, std::string name)
{
std::ostringstream cmd;
cmd << "/usr/bin/lsappinfo setinfo " << pid;
cmd << " --name \"" << name << "\"";
system (cmd.str().c_str());
}

Resources