mirror of
https://github.com/macports/pallet.git
synced 2026-07-12 18:18:47 -07:00
9f1a8b1afc
git-svn-id: https://svn.macports.org/repository/macports/branches/gsoc09-gui@60025 d073be05-634f-4543-b044-5fe20cf6d1d6
2490 lines
81 KiB
C
Executable File
2490 lines
81 KiB
C
Executable File
/*
|
|
File: BetterAuthorizationSampleLib.c
|
|
|
|
Contains: Implementation of reusable code for privileged helper tools.
|
|
|
|
Written by: DTS
|
|
|
|
Copyright: Copyright (c) 2007 Apple Inc. All Rights Reserved.
|
|
|
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple, Inc.
|
|
("Apple") in consideration of your agreement to the following terms, and your
|
|
use, installation, modification or redistribution of this Apple software
|
|
constitutes acceptance of these terms. If you do not agree with these terms,
|
|
please do not use, install, modify or redistribute this Apple software.
|
|
|
|
In consideration of your agreement to abide by the following terms, and subject
|
|
to these terms, Apple grants you a personal, non-exclusive license, under Apple's
|
|
copyrights in this original Apple software (the "Apple Software"), to use,
|
|
reproduce, modify and redistribute the Apple Software, with or without
|
|
modifications, in source and/or binary forms; provided that if you redistribute
|
|
the Apple Software in its entirety and without modifications, you must retain
|
|
this notice and the following text and disclaimers in all such redistributions of
|
|
the Apple Software. Neither the name, trademarks, service marks or logos of
|
|
Apple, Inc. may be used to endorse or promote products derived from the
|
|
Apple Software without specific prior written permission from Apple. Except as
|
|
expressly stated in this notice, no other rights or licenses, express or implied,
|
|
are granted by Apple herein, including but not limited to any patent rights that
|
|
may be infringed by your derivative works or by other works in which the Apple
|
|
Software may be incorporated.
|
|
|
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO
|
|
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
|
|
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
|
|
COMBINATION WITH YOUR PRODUCTS.
|
|
|
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
|
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
|
|
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
|
|
(INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
|
|
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
// Define BAS_PRIVATE so that we pick up our private definitions from
|
|
// "BetterAuthorizationSampleLib.h".
|
|
|
|
|
|
#define BAS_PRIVATE 1
|
|
|
|
#include "BetterAuthorizationSampleLib.h"
|
|
|
|
#include <launch.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/event.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/un.h>
|
|
#include <sys/socket.h>
|
|
|
|
// At runtime BAS only requires CoreFoundation. However, at build time we need
|
|
// CoreServices for the various OSStatus error codes in "MacErrors.h". Thus, by default,
|
|
// we include CoreServices at build time. However, you can flip this switch to check
|
|
// that you're not accidentally using any other CoreServices things.
|
|
|
|
#if 1
|
|
#include <CoreServices/CoreServices.h>
|
|
#else
|
|
#warning Do not ship this way!
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include "/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Headers/MacErrors.h"
|
|
#endif
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark ***** Constants
|
|
|
|
enum {
|
|
kIdleTimeoutInSeconds = 120, // if we get no requests in 2 minutes, we quit
|
|
kWatchdogTimeoutInSeconds = 500000 // any given request must be completed in ~5 days (is this ok?)
|
|
};
|
|
|
|
// IMPORTANT:
|
|
// These values must be greater than 60 seconds. If a job runs for less than 60
|
|
// seconds, launchd will consider it to have failed.
|
|
|
|
// kBASMaxNumberOfKBytes has two uses:
|
|
//
|
|
// 1. When receiving a dictionary, it is used to limit the size of the incoming
|
|
// data. This ensures that a non-privileged client can't exhaust the
|
|
// address space of a privileged helper tool.
|
|
//
|
|
// 2. Because it's less than 4 GB, this limit ensures that the dictionary size
|
|
// can be sent as an architecture-neutral uint32_t.
|
|
|
|
#define kBASMaxNumberOfKBytes (1024 * 1024)
|
|
|
|
// A hard-wired file system path for the UNIX domain socket; %s is the placeholder
|
|
// for the bundle ID (in file system representation).
|
|
|
|
#define kBASSocketPathFormat "/var/run/%s.socket"
|
|
|
|
// The key used to get our describe our socket in the launchd property list file.
|
|
|
|
#define kLaunchDSocketDictKey "MasterSocket"
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
#pragma mark ***** Common Code
|
|
|
|
extern int BASOSStatusToErrno(OSStatus errNum)
|
|
// See comment in header.
|
|
{
|
|
int retval;
|
|
|
|
#define CASE(ident) \
|
|
case k ## ident ## Err: \
|
|
retval = ident; \
|
|
break
|
|
switch (errNum) {
|
|
case noErr:
|
|
retval = 0;
|
|
break;
|
|
case kENORSRCErr:
|
|
retval = ESRCH; // no ENORSRC on Mac OS X, so use ESRCH
|
|
break;
|
|
case memFullErr:
|
|
retval = ENOMEM;
|
|
break;
|
|
CASE(EDEADLK);
|
|
CASE(EAGAIN);
|
|
case kEOPNOTSUPPErr:
|
|
retval = ENOTSUP;
|
|
break;
|
|
CASE(EPROTO);
|
|
CASE(ETIME);
|
|
CASE(ENOSR);
|
|
CASE(EBADMSG);
|
|
case kECANCELErr:
|
|
retval = ECANCELED; // note spelling difference
|
|
break;
|
|
CASE(ENOSTR);
|
|
CASE(ENODATA);
|
|
CASE(EINPROGRESS);
|
|
CASE(ESRCH);
|
|
CASE(ENOMSG);
|
|
default:
|
|
if ( (errNum <= kEPERMErr) && (errNum >= kENOMSGErr) ) {
|
|
retval = (-3200 - errNum) + 1; // OT based error
|
|
} else if ( (errNum >= errSecErrnoBase) && (errNum <= (errSecErrnoBase + ELAST)) ) {
|
|
retval = (int) errNum - errSecErrnoBase; // POSIX based error
|
|
} else {
|
|
retval = (int) errNum; // just return the value unmodified
|
|
}
|
|
}
|
|
#undef CASE
|
|
return retval;
|
|
}
|
|
|
|
extern OSStatus BASErrnoToOSStatus(int errNum)
|
|
// See comment in header.
|
|
{
|
|
OSStatus retval;
|
|
|
|
if ( errNum == 0 ) {
|
|
retval = noErr;
|
|
} else if ( (errNum >= EPERM) && (errNum <= ELAST) ) {
|
|
retval = (OSStatus) errNum + errSecErrnoBase;
|
|
} else {
|
|
retval = (int) errNum; // just return the value unmodified
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
static Boolean BASIsBinaryPropertyListData(const void * plistBuffer, size_t plistSize)
|
|
// Make sure that whatever is passed into the buffer that will
|
|
// eventually become a plist (and then sequentially a dictionary)
|
|
// is NOT in binary format.
|
|
{
|
|
static const char kBASBinaryPlistWatermark[6] = "bplist";
|
|
|
|
assert(plistBuffer != NULL);
|
|
|
|
return (plistSize >= sizeof(kBASBinaryPlistWatermark))
|
|
&& (memcmp(plistBuffer, kBASBinaryPlistWatermark, sizeof(kBASBinaryPlistWatermark)) == 0);
|
|
}
|
|
|
|
static void NormaliseOSStatusErrorCode(OSStatus *errPtr)
|
|
// Normalise the cancelled error code to reduce the number of checks that our clients
|
|
// have to do. I made this a function in case I ever want to expand this to handle
|
|
// more than just this one case.
|
|
{
|
|
assert(errPtr != NULL);
|
|
|
|
if ( (*errPtr == errAuthorizationCanceled) || (*errPtr == (errSecErrnoBase + ECANCELED)) ) {
|
|
*errPtr = userCanceledErr;
|
|
}
|
|
}
|
|
|
|
static int BASRead(int fd, void *buf, size_t bufSize, size_t *bytesRead)
|
|
// A wrapper around <x-man-page://2/read> that keeps reading until either
|
|
// bufSize bytes are read or until EOF is encountered, in which case you get
|
|
// EPIPE.
|
|
//
|
|
// If bytesRead is not NULL, *bytesRead will be set to the number
|
|
// of bytes successfully read. On success, this will always be equal to
|
|
// bufSize. On error, it indicates how much was read before the error
|
|
// occurred (which could be zero).
|
|
{
|
|
int err;
|
|
char * cursor;
|
|
size_t bytesLeft;
|
|
ssize_t bytesThisTime;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
assert(buf != NULL);
|
|
// bufSize may be 0
|
|
assert(bufSize <= kBASMaxNumberOfKBytes);
|
|
// bytesRead may be NULL
|
|
|
|
err = 0;
|
|
bytesLeft = bufSize;
|
|
cursor = (char *) buf;
|
|
while ( (err == 0) && (bytesLeft != 0) ) {
|
|
bytesThisTime = read(fd, cursor, bytesLeft);
|
|
if (bytesThisTime > 0) {
|
|
cursor += bytesThisTime;
|
|
bytesLeft -= bytesThisTime;
|
|
} else if (bytesThisTime == 0) {
|
|
err = EPIPE;
|
|
} else {
|
|
assert(bytesThisTime == -1);
|
|
|
|
err = errno;
|
|
assert(err != 0);
|
|
if (err == EINTR) {
|
|
err = 0; // let's loop again
|
|
}
|
|
}
|
|
}
|
|
if (bytesRead != NULL) {
|
|
*bytesRead = bufSize - bytesLeft;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int BASWrite(int fd, const void *buf, size_t bufSize, size_t *bytesWritten)
|
|
// A wrapper around <x-man-page://2/write> that keeps writing until either
|
|
// all the data is written or an error occurs, in which case
|
|
// you get EPIPE.
|
|
//
|
|
// If bytesWritten is not NULL, *bytesWritten will be set to the number
|
|
// of bytes successfully written. On success, this will always be equal to
|
|
// bufSize. On error, it indicates how much was written before the error
|
|
// occurred (which could be zero).
|
|
{
|
|
int err;
|
|
char * cursor;
|
|
size_t bytesLeft;
|
|
ssize_t bytesThisTime;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
assert(buf != NULL);
|
|
// bufSize may be 0
|
|
assert(bufSize <= kBASMaxNumberOfKBytes);
|
|
// bytesWritten may be NULL
|
|
|
|
// SIGPIPE occurs when you write to pipe or socket
|
|
// whose other end has been closed. The default action
|
|
// for SIGPIPE is to terminate the process. That's
|
|
// probably not what you wanted. So, in the debug build,
|
|
// we check that you've set the signal action to SIG_IGN
|
|
// (ignore). Of course, you could be building a program
|
|
// that needs SIGPIPE to work in some special way, in
|
|
// which case you should define BAS_WRITE_CHECK_SIGPIPE
|
|
// to 0 to bypass this check.
|
|
|
|
#if !defined(BAS_WRITE_CHECK_SIGPIPE)
|
|
#define BAS_WRITE_CHECK_SIGPIPE 1
|
|
#endif
|
|
#if !defined(NDEBUG) && BAS_WRITE_CHECK_SIGPIPE
|
|
{
|
|
int junk;
|
|
struct stat sb;
|
|
struct sigaction currentSignalState;
|
|
int val;
|
|
socklen_t valLen;
|
|
|
|
junk = fstat(fd, &sb);
|
|
assert(junk == 0);
|
|
|
|
if ( S_ISFIFO(sb.st_mode) || S_ISSOCK(sb.st_mode) ) {
|
|
junk = sigaction(SIGPIPE, NULL, ¤tSignalState);
|
|
assert(junk == 0);
|
|
|
|
valLen = sizeof(val);
|
|
junk = getsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &val, &valLen);
|
|
assert(junk == 0);
|
|
assert(valLen == sizeof(val));
|
|
|
|
// If you hit this assertion, you need to either disable SIGPIPE in
|
|
// your process or on the specific socket you're writing to. The
|
|
// standard code for the former is:
|
|
//
|
|
// (void) signal(SIGPIPE, SIG_IGN);
|
|
//
|
|
// You typically add this code to your main function.
|
|
//
|
|
// The standard code for the latter is:
|
|
//
|
|
// static const int kOne = 1;
|
|
// err = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &kOne, sizeof(kOne));
|
|
//
|
|
// You typically do this just after creating the socket.
|
|
|
|
assert( (currentSignalState.sa_handler == SIG_IGN) || (val == 1) );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
err = 0;
|
|
bytesLeft = bufSize;
|
|
cursor = (char *) buf;
|
|
while ( (err == 0) && (bytesLeft != 0) ) {
|
|
bytesThisTime = write(fd, cursor, bytesLeft);
|
|
if (bytesThisTime > 0) {
|
|
cursor += bytesThisTime;
|
|
bytesLeft -= bytesThisTime;
|
|
} else if (bytesThisTime == 0) {
|
|
assert(false);
|
|
err = EPIPE;
|
|
} else {
|
|
assert(bytesThisTime == -1);
|
|
|
|
err = errno;
|
|
assert(err != 0);
|
|
if (err == EINTR) {
|
|
err = 0; // let's loop again
|
|
}
|
|
}
|
|
}
|
|
if (bytesWritten != NULL) {
|
|
*bytesWritten = bufSize - bytesLeft;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int BASReadDictionary(int fdIn, CFDictionaryRef *dictPtr)
|
|
// Create a CFDictionary by reading the XML data from fdIn.
|
|
// It first reads the size of the XML data, then allocates a
|
|
// buffer for that data, then reads the data in, and finally
|
|
// unflattens the data into a CFDictionary.
|
|
//
|
|
// On success, the caller is responsible for releasing *dictPtr.
|
|
//
|
|
// See also the companion routine, BASWriteDictionary, below.
|
|
{
|
|
int err = 0;
|
|
uint32_t dictSize;
|
|
void * dictBuffer;
|
|
CFDataRef dictData;
|
|
CFPropertyListRef dict;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fdIn >= 0);
|
|
assert( dictPtr != NULL);
|
|
assert(*dictPtr == NULL);
|
|
|
|
dictBuffer = NULL;
|
|
dictData = NULL;
|
|
dict = NULL;
|
|
|
|
// Read the data size and allocate a buffer. Always read the length as a big-endian
|
|
// uint32_t, so that the app and the helper tool can be different architectures.
|
|
|
|
err = BASRead(fdIn, &dictSize, sizeof(dictSize), NULL);
|
|
if (err == 0) {
|
|
dictSize = OSSwapBigToHostInt32(dictSize);
|
|
if (dictSize == 0) {
|
|
// According to the C language spec malloc(0) may return NULL (although the Mac OS X
|
|
// malloc doesn't ever do this), so we specifically check for and error out in
|
|
// that case.
|
|
err = EINVAL;
|
|
} else if (dictSize > kBASMaxNumberOfKBytes) {
|
|
// Abitrary limit to prevent potentially hostile client overwhelming us with data.
|
|
err = EINVAL;
|
|
}
|
|
}
|
|
if (err == 0) {
|
|
dictBuffer = malloc( (size_t) dictSize);
|
|
if (dictBuffer == NULL) {
|
|
err = ENOMEM;
|
|
}
|
|
}
|
|
|
|
// Read the data and unflatten.
|
|
|
|
if (err == 0) {
|
|
err = BASRead(fdIn, dictBuffer, dictSize, NULL);
|
|
}
|
|
if ( (err == 0) && BASIsBinaryPropertyListData(dictBuffer, dictSize) ) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
if (err == 0) {
|
|
dictData = CFDataCreateWithBytesNoCopy(NULL, dictBuffer, dictSize, kCFAllocatorNull);
|
|
if (dictData == NULL) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
}
|
|
if (err == 0) {
|
|
dict = CFPropertyListCreateFromXMLData(NULL, dictData, kCFPropertyListImmutable, NULL);
|
|
if (dict == NULL) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
}
|
|
if ( (err == 0) && (CFGetTypeID(dict) != CFDictionaryGetTypeID()) ) {
|
|
err = EINVAL; // only CFDictionaries need apply
|
|
}
|
|
// CFShow(dict);
|
|
|
|
// Clean up.
|
|
|
|
if (err != 0) {
|
|
if (dict != NULL) {
|
|
CFRelease(dict);
|
|
}
|
|
dict = NULL;
|
|
}
|
|
*dictPtr = (CFDictionaryRef) dict;
|
|
free(dictBuffer);
|
|
if (dictData != NULL) {
|
|
CFRelease(dictData);
|
|
}
|
|
|
|
assert( (err == 0) == (*dictPtr != NULL) );
|
|
|
|
return err;
|
|
}
|
|
|
|
static int BASWriteDictionary(CFDictionaryRef dict, int fdOut)
|
|
// Write a dictionary to a file descriptor by flattening
|
|
// it into XML. Send the size of the XML before sending
|
|
// the data so that BASReadDictionary knows how much to
|
|
// read.
|
|
//
|
|
// See also the companion routine, BASReadDictionary, above.
|
|
{
|
|
int err = 0;
|
|
CFDataRef dictData;
|
|
uint32_t dictSize;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(dict != NULL);
|
|
assert(fdOut >= 0);
|
|
|
|
dictData = NULL;
|
|
|
|
// Get the dictionary as XML data.
|
|
|
|
dictData = CFPropertyListCreateXMLData(NULL, dict);
|
|
if (dictData == NULL) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
|
|
// Send the length, then send the data. Always send the length as a big-endian
|
|
// uint32_t, so that the app and the helper tool can be different architectures.
|
|
//
|
|
// The MoreAuthSample version of this code erroneously assumed that CFDataGetBytePtr
|
|
// can fail and thus allocated an extra buffer to copy the data into. In reality,
|
|
// CFDataGetBytePtr can't fail, so this version of the code doesn't do the unnecessary
|
|
// allocation.
|
|
|
|
if ( (err == 0) && (CFDataGetLength(dictData) > kBASMaxNumberOfKBytes) ) {
|
|
err = EINVAL;
|
|
}
|
|
if (err == 0) {
|
|
dictSize = OSSwapHostToBigInt32( CFDataGetLength(dictData) );
|
|
err = BASWrite(fdOut, &dictSize, sizeof(dictSize), NULL);
|
|
}
|
|
if (err == 0) {
|
|
err = BASWrite(fdOut, CFDataGetBytePtr(dictData), CFDataGetLength(dictData), NULL);
|
|
}
|
|
|
|
if (dictData != NULL) {
|
|
CFRelease(dictData);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
// When we pass a descriptor, we have to pass at least one byte
|
|
// of data along with it, otherwise the recvmsg call will not
|
|
// block if the descriptor hasn't been written to the other end
|
|
// of the socket yet.
|
|
|
|
static const char kDummyData = 'D';
|
|
|
|
// Due to a kernel bug in Mac OS X 10.4.x and earlier <rdar://problem/4650646>,
|
|
// you will run into problems if you write data to a socket while a process is
|
|
// trying to receive a descriptor from that socket. A common symptom of this
|
|
// problem is that, if you write two descriptors back-to-back, the second one
|
|
// just disappears.
|
|
//
|
|
// To avoid this problem, we explicitly ACK all descriptor transfers.
|
|
// After writing a descriptor, the sender reads an ACK byte from the socket.
|
|
// After reading a descriptor, the receiver sends an ACK byte (kACKData)
|
|
// to unblock the sender.
|
|
|
|
static const char kACKData = 'A';
|
|
|
|
static int BASReadDescriptor(int fd, int *fdRead)
|
|
// Read a descriptor from fd and place it in *fdRead.
|
|
//
|
|
// On success, the caller is responsible for closing *fdRead.
|
|
//
|
|
// See the associated BASWriteDescriptor, below.
|
|
{
|
|
int err;
|
|
int junk;
|
|
struct msghdr msg;
|
|
struct iovec iov;
|
|
struct {
|
|
struct cmsghdr hdr;
|
|
int fd;
|
|
} control;
|
|
char dummyData;
|
|
ssize_t bytesReceived;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
assert( fdRead != NULL);
|
|
assert(*fdRead == -1);
|
|
|
|
iov.iov_base = (char *) &dummyData;
|
|
iov.iov_len = sizeof(dummyData);
|
|
|
|
msg.msg_name = NULL;
|
|
msg.msg_namelen = 0;
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = (caddr_t) &control;
|
|
msg.msg_controllen = sizeof(control);
|
|
msg.msg_flags = MSG_WAITALL;
|
|
|
|
do {
|
|
bytesReceived = recvmsg(fd, &msg, 0);
|
|
if (bytesReceived == sizeof(dummyData)) {
|
|
if ( (dummyData != kDummyData)
|
|
|| (msg.msg_flags != 0)
|
|
|| (msg.msg_control == NULL)
|
|
|| (msg.msg_controllen != sizeof(control))
|
|
|| (control.hdr.cmsg_len != sizeof(control))
|
|
|| (control.hdr.cmsg_level != SOL_SOCKET)
|
|
|| (control.hdr.cmsg_type != SCM_RIGHTS)
|
|
|| (control.fd < 0) ) {
|
|
err = EINVAL;
|
|
} else {
|
|
*fdRead = control.fd;
|
|
err = 0;
|
|
}
|
|
} else if (bytesReceived == 0) {
|
|
err = EPIPE;
|
|
} else {
|
|
assert(bytesReceived == -1);
|
|
|
|
err = errno;
|
|
assert(err != 0);
|
|
}
|
|
} while (err == EINTR);
|
|
|
|
// Send the ACK. If that fails, we have to act like we never got the
|
|
// descriptor in our to maintain our post condition.
|
|
|
|
if (err == 0) {
|
|
err = BASWrite(fd, &kACKData, sizeof(kACKData), NULL);
|
|
if (err != 0) {
|
|
junk = close(*fdRead);
|
|
assert(junk == 0);
|
|
*fdRead = -1;
|
|
}
|
|
}
|
|
|
|
assert( (err == 0) == (*fdRead >= 0) );
|
|
|
|
return err;
|
|
}
|
|
|
|
static int BASWriteDescriptor(int fd, int fdToWrite)
|
|
// Write the descriptor fdToWrite to fd.
|
|
//
|
|
// See the associated BASReadDescriptor, above.
|
|
{
|
|
int err;
|
|
struct msghdr msg;
|
|
struct iovec iov;
|
|
struct {
|
|
struct cmsghdr hdr;
|
|
int fd;
|
|
} control;
|
|
ssize_t bytesSent;
|
|
char ack;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
assert(fdToWrite >= 0);
|
|
|
|
control.hdr.cmsg_len = sizeof(control);
|
|
control.hdr.cmsg_level = SOL_SOCKET;
|
|
control.hdr.cmsg_type = SCM_RIGHTS;
|
|
control.fd = fdToWrite;
|
|
|
|
iov.iov_base = (char *) &kDummyData;
|
|
iov.iov_len = sizeof(kDummyData);
|
|
|
|
msg.msg_name = NULL;
|
|
msg.msg_namelen = 0;
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = (caddr_t) &control;
|
|
msg.msg_controllen = control.hdr.cmsg_len;
|
|
msg.msg_flags = 0;
|
|
do {
|
|
bytesSent = sendmsg(fd, &msg, 0);
|
|
if (bytesSent == sizeof(kDummyData)) {
|
|
err = 0;
|
|
} else {
|
|
assert(bytesSent == -1);
|
|
|
|
err = errno;
|
|
assert(err != 0);
|
|
}
|
|
} while (err == EINTR);
|
|
|
|
// After writing the descriptor, try to read an ACK back from the
|
|
// recipient. If that fails, or we get the wrong ACK, we've failed.
|
|
|
|
if (err == 0) {
|
|
err = BASRead(fd, &ack, sizeof(ack), NULL);
|
|
if ( (err == 0) && (ack != kACKData) ) {
|
|
err = EINVAL;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
extern void BASCloseDescriptorArray(
|
|
CFArrayRef descArray
|
|
)
|
|
// See comment in header.
|
|
{
|
|
int junk;
|
|
CFIndex descCount;
|
|
CFIndex descIndex;
|
|
|
|
// I decided to allow descArray to be NULL because it makes it
|
|
// easier to call this routine using the code.
|
|
//
|
|
// BASCloseDescriptorArray((CFArrayRef) CFDictionaryGetValue(response, CFSTR(kBASDescriptorArrayKey)));
|
|
|
|
if (descArray != NULL) {
|
|
if (CFGetTypeID(descArray) == CFArrayGetTypeID()) {
|
|
descCount = CFArrayGetCount(descArray);
|
|
|
|
for (descIndex = 0; descIndex < descCount; descIndex++) {
|
|
CFNumberRef thisDescNum;
|
|
int thisDesc;
|
|
|
|
thisDescNum = (CFNumberRef) CFArrayGetValueAtIndex(descArray, descIndex);
|
|
if ( (thisDescNum == NULL)
|
|
|| (CFGetTypeID(thisDescNum) != CFNumberGetTypeID())
|
|
|| ! CFNumberGetValue(thisDescNum, kCFNumberIntType, &thisDesc) ) {
|
|
assert(false);
|
|
} else {
|
|
assert(thisDesc >= 0);
|
|
junk = close(thisDesc);
|
|
assert(junk == 0);
|
|
}
|
|
}
|
|
} else {
|
|
assert(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int BASReadDictioanaryTranslatingDescriptors(int fd, CFDictionaryRef *dictPtr)
|
|
// Reads a dictionary and its associated descriptors (if any) from fd,
|
|
// putting the dictionary (modified to include the translated descriptor
|
|
// numbers) in *dictPtr.
|
|
//
|
|
// On success, the caller is responsible for releasing *dictPtr and for
|
|
// closing any descriptors it references (BASCloseDescriptorArray makes
|
|
// the second part easy).
|
|
{
|
|
int err;
|
|
int junk;
|
|
CFDictionaryRef dict;
|
|
CFArrayRef incomingDescs;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
assert( dictPtr != NULL);
|
|
assert(*dictPtr == NULL);
|
|
|
|
dict = NULL;
|
|
|
|
// Read the dictionary.
|
|
|
|
err = BASReadDictionary(fd, &dict);
|
|
|
|
// Now read the descriptors, if any.
|
|
|
|
if (err == 0) {
|
|
incomingDescs = (CFArrayRef) CFDictionaryGetValue(dict, CFSTR(kBASDescriptorArrayKey));
|
|
if (incomingDescs == NULL) {
|
|
// No descriptors. Not much to do. Just use dict as the response,
|
|
// NULLing it out so that we don't release it at the end.
|
|
|
|
*dictPtr = dict;
|
|
dict = NULL;
|
|
} else {
|
|
CFMutableArrayRef translatedDescs;
|
|
CFMutableDictionaryRef mutableDict;
|
|
CFIndex descCount;
|
|
CFIndex descIndex;
|
|
|
|
// We have descriptors, so there's lots of stuff to do. Have to
|
|
// receive each of the descriptors assemble them into the
|
|
// translatedDesc array, then create a mutable dictionary based
|
|
// on response (mutableDict) and replace the
|
|
// kBASDescriptorArrayKey with translatedDesc.
|
|
|
|
translatedDescs = NULL;
|
|
mutableDict = NULL;
|
|
|
|
// Start by checking incomingDescs.
|
|
|
|
if ( CFGetTypeID(incomingDescs) != CFArrayGetTypeID() ) {
|
|
err = EINVAL;
|
|
}
|
|
|
|
// Create our output data.
|
|
|
|
if (err == 0) {
|
|
translatedDescs = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
if (translatedDescs == NULL) {
|
|
err = coreFoundationUnknownErr;
|
|
}
|
|
}
|
|
if (err == 0) {
|
|
mutableDict = CFDictionaryCreateMutableCopy(NULL, 0, dict);
|
|
if (mutableDict == NULL) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
}
|
|
|
|
// Now read each incoming descriptor, appending the results
|
|
// to translatedDescs as we go. By keeping our working results
|
|
// in translatedDescs, we make sure that we can clean up if
|
|
// we fail.
|
|
|
|
if (err == 0) {
|
|
descCount = CFArrayGetCount(incomingDescs);
|
|
|
|
// We don't actually depend on the descriptor values in the
|
|
// response (that is, the elements of incomingDescs), because
|
|
// they only make sense it the context of the sending process.
|
|
// All we really care about is the number of elements, which
|
|
// tells us how many times to go through this loop. However,
|
|
// just to be paranoid, in the debug build I check that the
|
|
// incoming array is well formed.
|
|
|
|
#if !defined(NDEBUG)
|
|
for (descIndex = 0; descIndex < descCount; descIndex++) {
|
|
int thisDesc;
|
|
CFNumberRef thisDescNum;
|
|
|
|
thisDescNum = (CFNumberRef) CFArrayGetValueAtIndex(incomingDescs, descIndex);
|
|
assert(thisDescNum != NULL);
|
|
assert(CFGetTypeID(thisDescNum) == CFNumberGetTypeID());
|
|
assert(CFNumberGetValue(thisDescNum, kCFNumberIntType, &thisDesc));
|
|
assert(thisDesc >= 0);
|
|
}
|
|
#endif
|
|
|
|
// Here's the real work. For descCount times, read a descriptor
|
|
// from fd, wrap it in a CFNumber, and append it to translatedDescs.
|
|
// Note that we have to be very careful not to leak a descriptor
|
|
// if we get an error here.
|
|
|
|
for (descIndex = 0; descIndex < descCount; descIndex++) {
|
|
int thisDesc;
|
|
CFNumberRef thisDescNum;
|
|
|
|
thisDesc = -1;
|
|
thisDescNum = NULL;
|
|
|
|
err = BASReadDescriptor(fd, &thisDesc);
|
|
if (err == 0) {
|
|
thisDescNum = CFNumberCreate(NULL, kCFNumberIntType, &thisDesc);
|
|
if (thisDescNum == NULL) {
|
|
err = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
}
|
|
if (err == 0) {
|
|
CFArrayAppendValue(translatedDescs, thisDescNum);
|
|
// The descriptor is now stashed in translatedDescs,
|
|
// so this iteration of the loop is no longer responsible
|
|
// for closing it.
|
|
thisDesc = -1;
|
|
}
|
|
|
|
if (thisDescNum != NULL) {
|
|
CFRelease(thisDescNum);
|
|
}
|
|
if (thisDesc != -1) {
|
|
junk = close(thisDesc);
|
|
assert(junk == 0);
|
|
}
|
|
|
|
if (err != 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up and establish output parameters.
|
|
|
|
if (err == 0) {
|
|
CFDictionarySetValue(mutableDict, CFSTR(kBASDescriptorArrayKey), translatedDescs);
|
|
*dictPtr = mutableDict;
|
|
} else {
|
|
BASCloseDescriptorArray(translatedDescs);
|
|
if (mutableDict != NULL) {
|
|
CFRelease(mutableDict);
|
|
}
|
|
}
|
|
if (translatedDescs != NULL) {
|
|
CFRelease(translatedDescs);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dict != NULL) {
|
|
CFRelease(dict);
|
|
}
|
|
|
|
assert( (err == 0) == (*dictPtr != NULL) );
|
|
|
|
return err;
|
|
}
|
|
|
|
static int BASWriteDictionaryAndDescriptors(CFDictionaryRef dict, int fd)
|
|
// Writes a dictionary and its associated descriptors to fd.
|
|
{
|
|
int err;
|
|
CFArrayRef descArray;
|
|
CFIndex descCount;
|
|
CFIndex descIndex;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(dict != NULL);
|
|
assert(fd >= 0);
|
|
|
|
// Write the dictionary.
|
|
|
|
err = BASWriteDictionary(dict, fd);
|
|
|
|
// Process any descriptors. The descriptors are indicated by
|
|
// a special key in the dictionary. If that key is present,
|
|
// it's a CFArray of CFNumbers that present the descriptors to be
|
|
// passed.
|
|
|
|
if (err == 0) {
|
|
descArray = (CFArrayRef) CFDictionaryGetValue(dict, CFSTR(kBASDescriptorArrayKey));
|
|
|
|
// We only do the following if the special key is present.
|
|
|
|
if (descArray != NULL) {
|
|
|
|
// If it's not an array, that's bad.
|
|
|
|
if ( CFGetTypeID(descArray) != CFArrayGetTypeID() ) {
|
|
err = EINVAL;
|
|
}
|
|
|
|
// Loop over the array, getting each descriptor and writing it.
|
|
|
|
if (err == 0) {
|
|
descCount = CFArrayGetCount(descArray);
|
|
|
|
for (descIndex = 0; descIndex < descCount; descIndex++) {
|
|
CFNumberRef thisDescNum;
|
|
int thisDesc;
|
|
|
|
thisDescNum = (CFNumberRef) CFArrayGetValueAtIndex(descArray, descIndex);
|
|
if ( (thisDescNum == NULL)
|
|
|| (CFGetTypeID(thisDescNum) != CFNumberGetTypeID())
|
|
|| ! CFNumberGetValue(thisDescNum, kCFNumberIntType, &thisDesc) ) {
|
|
err = EINVAL;
|
|
}
|
|
if (err == 0) {
|
|
err = BASWriteDescriptor(fd, thisDesc);
|
|
}
|
|
|
|
if (err != 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static OSStatus FindCommand(
|
|
CFDictionaryRef request,
|
|
const BASCommandSpec commands[],
|
|
size_t * commandIndexPtr
|
|
)
|
|
// FindCommand is a simple utility routine for checking that the
|
|
// command name within a request is valid (that is, matches one of the command
|
|
// names in the BASCommandSpec array).
|
|
//
|
|
// On success, *commandIndexPtr will be the index of the requested command
|
|
// in the commands array. On error, the value in *commandIndexPtr is undefined.
|
|
{
|
|
OSStatus retval = noErr;
|
|
CFStringRef commandStr;
|
|
char * command;
|
|
UInt32 commandSize = 0;
|
|
size_t index = 0;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(request != NULL);
|
|
assert(commands != NULL);
|
|
assert(commands[0].commandName != NULL); // there must be at least one command
|
|
assert(commandIndexPtr != NULL);
|
|
|
|
command = NULL;
|
|
|
|
// Get the command as a C string. To prevent untrusted command string from
|
|
// trying to run us out of memory, we limit its length to 1024 UTF-16 values.
|
|
|
|
commandStr = CFDictionaryGetValue(request, CFSTR(kBASCommandKey));
|
|
if ( (commandStr == NULL) || (CFGetTypeID(commandStr) != CFStringGetTypeID()) ) {
|
|
retval = paramErr;
|
|
}
|
|
commandSize = CFStringGetLength(commandStr);
|
|
if ( (retval == noErr) && (commandSize > 1024) ) {
|
|
retval = paramErr;
|
|
}
|
|
if (retval == noErr) {
|
|
size_t bufSize;
|
|
|
|
bufSize = CFStringGetMaximumSizeForEncoding(CFStringGetLength(commandStr), kCFStringEncodingUTF8) + 1;
|
|
command = malloc(bufSize);
|
|
|
|
if (command == NULL) {
|
|
retval = memFullErr;
|
|
} else if ( ! CFStringGetCString(commandStr, command, bufSize, kCFStringEncodingUTF8) ) {
|
|
retval = coreFoundationUnknownErr;
|
|
}
|
|
}
|
|
|
|
// Search the commands array for that command.
|
|
|
|
if (retval == noErr) {
|
|
do {
|
|
if ( strcmp(commands[index].commandName, command) == 0 ) {
|
|
*commandIndexPtr = index;
|
|
break;
|
|
}
|
|
index += 1;
|
|
if (commands[index].commandName == NULL) {
|
|
retval = BASErrnoToOSStatus(ENOENT);
|
|
break;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
free(command);
|
|
|
|
return retval;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
#pragma mark ***** Tool Code
|
|
|
|
/*
|
|
Watchdog Timer
|
|
--------------
|
|
BetterAuthorizationSampleLib's privileged helper tool server is single threaded. Thus,
|
|
it's possible for a broken or malicious client to stop progress within the helper
|
|
tool simply by sending the tool half a request. The single thread of execution
|
|
within the tool will wait forever for the rest of the request and, while it's
|
|
waiting, it won't be able to service other requests. Clearly this is not good.
|
|
|
|
I contemplated a number of solutions to this problem, but eventually settled
|
|
on a very simple solution. When it starts processing a request, the tool
|
|
starts a watchdog timer. If the timer expires, the tool dies. The single
|
|
request that the tool is blocked on will fail (because our end of the per-connection
|
|
socket for that request closed when we died) and subsequent requests will
|
|
relaunch the tool on demand, courtesy of launchd.
|
|
|
|
I use SIGALRM to implement this functionality. As stated in our header, the
|
|
BetterAuthorizationSampleLib code claims this signal and our clients are required not
|
|
to use it. Also, the default disposition for SIGALRM is to quit the process,
|
|
which is exactly what I want.
|
|
*/
|
|
|
|
static void EnableWatchdog(void)
|
|
// Start the watchdog timer. If you don't call DisableWatchdog before the
|
|
// timer expires, the process will die with a SIGALRM.
|
|
{
|
|
(void) alarm(kWatchdogTimeoutInSeconds);
|
|
}
|
|
|
|
static void DisableWatchdog(void)
|
|
// Disable the watchdog timer.
|
|
{
|
|
(void) alarm(0);
|
|
}
|
|
|
|
#if ! defined(NDEBUG)
|
|
|
|
static bool CommandArraySizeMatchesCommandProcArraySize(
|
|
const BASCommandSpec commands[],
|
|
const BASCommandProc commandProcs[]
|
|
)
|
|
{
|
|
size_t commandCount;
|
|
size_t procCount;
|
|
|
|
commandCount = 0;
|
|
while ( commands[commandCount].commandName != NULL ) {
|
|
commandCount += 1;
|
|
}
|
|
|
|
procCount = 0;
|
|
while ( commandProcs[procCount] != NULL ) {
|
|
procCount += 1;
|
|
}
|
|
|
|
return (commandCount == procCount);
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
On-The-'Wire' Protocol
|
|
----------------------
|
|
The on-the-'wire' protocol for a BetterAuthorizationSampleLib connection (from the
|
|
perspective of the client) is:
|
|
|
|
connect
|
|
|
|
send AuthorizationExternalForm (32 byte blob)
|
|
send request dictionary length (4 bytes, uint32_t, big endian)
|
|
send request dictionary (N bytes, flattened CFPropertyList)
|
|
|
|
read response dictionary length (4 bytes, uint32_t, big endian)
|
|
read response dictionary (N bytes, flattened CFPropertyList)
|
|
for each descriptor in dictionary
|
|
read 1 byte ('D') with attached descriptor
|
|
write 1 byte ('A')
|
|
|
|
close
|
|
*/
|
|
|
|
static int HandleConnection(
|
|
aslclient asl,
|
|
aslmsg aslMsg,
|
|
const BASCommandSpec commands[],
|
|
const BASCommandProc commandProcs[],
|
|
int fd
|
|
)
|
|
// This routine handles a single connection from a client. This connection, in
|
|
// turn, represents a single command (request/response pair). commands is the
|
|
// list of valid commands. commandProc is a callback to call to actually
|
|
// execute a command. Finally, fd is the file descriptor from which the request
|
|
// should be read, and to which the response should be sent.
|
|
{
|
|
int retval;
|
|
OSStatus junk;
|
|
int junkInt;
|
|
AuthorizationExternalForm extAuth;
|
|
AuthorizationRef auth = NULL;
|
|
CFDictionaryRef request = NULL;
|
|
size_t commandIndex;
|
|
CFMutableDictionaryRef response = NULL;
|
|
OSStatus commandProcStatus;
|
|
|
|
// Pre-conditions
|
|
|
|
// asl may be NULL
|
|
// aslMsg may be NULL
|
|
assert(commands != NULL);
|
|
assert(commands[0].commandName != NULL); // there must be at least one command
|
|
assert(commandProcs != NULL);
|
|
assert( CommandArraySizeMatchesCommandProcArraySize(commands, commandProcs) );
|
|
assert(fd >= 0);
|
|
|
|
// Read in the external authorization reference.
|
|
retval = BASRead(fd, &extAuth, sizeof(extAuth), NULL);
|
|
|
|
// Internalize external authorization reference.
|
|
if (retval == 0) {
|
|
retval = BASOSStatusToErrno( AuthorizationCreateFromExternalForm(&extAuth, &auth) );
|
|
}
|
|
|
|
// Read in CFDictionaryRef request (the command and its arguments).
|
|
if (retval == 0) {
|
|
retval = BASReadDictionary(fd, &request);
|
|
}
|
|
|
|
// Create a mutable response dictionary before calling the client.
|
|
if (retval == 0) {
|
|
response = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
|
if (response == NULL) {
|
|
retval = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
}
|
|
}
|
|
|
|
// Errors that occur within this block are considered command errors, that is, they're
|
|
// reported to the client in the kBASErrorKey value of the response dictionary
|
|
// (that is, BASExecuteRequestInHelperTool returns noErr and valid response dictionary with
|
|
// an error value in the kBASErrorKey entry of the dictionary). In contrast, other errors
|
|
// are considered IPC errors and generally result in a the client getting an error status
|
|
// back from BASExecuteRequestInHelperTool.
|
|
//
|
|
// Notably a request with an unrecognised command string will return an error code
|
|
// in the response, as opposed to an IPC error. This means that a client can check
|
|
// whether a tool supports a particular command without triggering an IPC teardown.
|
|
|
|
if (retval == 0) {
|
|
// Get the command name from the request dictionary and check to see whether or
|
|
// not the command is valid by comparing with the BASCommandSpec array. Also,
|
|
// if the command is valid, return the associated right (if any).
|
|
|
|
commandProcStatus = FindCommand(request, commands, &commandIndex);
|
|
|
|
// Acquire the associated right for the command. If rightName is NULL, the
|
|
// commandProc is required to do its own authorization.
|
|
|
|
if ( (commandProcStatus == noErr) && (commands[commandIndex].rightName != NULL) ) {
|
|
AuthorizationItem item = { commands[commandIndex].rightName, 0, NULL, 0 };
|
|
AuthorizationRights rights = { 1, &item };
|
|
|
|
commandProcStatus = AuthorizationCopyRights(
|
|
auth,
|
|
&rights,
|
|
kAuthorizationEmptyEnvironment,
|
|
kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed,
|
|
NULL
|
|
);
|
|
}
|
|
|
|
// Call callback to execute command based on the request.
|
|
|
|
if (commandProcStatus == noErr) {
|
|
commandProcStatus = commandProcs[commandIndex](auth, commands[commandIndex].userData, request, response, asl, aslMsg);
|
|
|
|
if (commandProcStatus == noErr) {
|
|
junkInt = asl_log(asl, aslMsg, ASL_LEVEL_DEBUG, "Command callback succeeded");
|
|
assert(junkInt == 0);
|
|
} else {
|
|
junkInt = asl_log(asl, aslMsg, ASL_LEVEL_DEBUG, "Command callback failed: %ld", (long) commandProcStatus);
|
|
assert(junkInt == 0);
|
|
}
|
|
}
|
|
|
|
// If the command didn't insert its own error value, we use its function
|
|
// result as the error value.
|
|
|
|
if ( ! CFDictionaryContainsKey(response, CFSTR(kBASErrorKey)) ) {
|
|
CFNumberRef numRef;
|
|
|
|
numRef = CFNumberCreate(NULL, kCFNumberSInt32Type, &commandProcStatus);
|
|
if (numRef == NULL) {
|
|
retval = BASOSStatusToErrno( coreFoundationUnknownErr );
|
|
} else {
|
|
CFDictionaryAddValue(response, CFSTR(kBASErrorKey), numRef);
|
|
CFRelease(numRef);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write response back to the client.
|
|
if (retval == 0) {
|
|
retval = BASWriteDictionaryAndDescriptors(response, fd);
|
|
}
|
|
|
|
// Clean up.
|
|
|
|
if (response != NULL) {
|
|
// If there are any descriptors in response, we've now passed them off to the client,
|
|
// so we can (and must) close our references to them.
|
|
BASCloseDescriptorArray( CFDictionaryGetValue(response, CFSTR(kBASDescriptorArrayKey)) );
|
|
CFRelease(response);
|
|
}
|
|
if (request != NULL) {
|
|
CFRelease(request);
|
|
}
|
|
if (auth != NULL) {
|
|
junk = AuthorizationFree(auth, kAuthorizationFlagDefaults);
|
|
assert(junk == noErr);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
#if !defined(NDEBUG)
|
|
|
|
static void WaitForDebugger(aslclient asl, aslmsg aslMsg)
|
|
// You can force a debug version of the tool to stop and wait on
|
|
// launch using the following Terminal command:
|
|
//
|
|
// $ sudo launchctl stop com.example.BetterAuthorizationSample
|
|
// $ sudo launchctl setenv BASWaitForDebugger 1
|
|
{
|
|
int err;
|
|
const char *value;
|
|
|
|
// asl may be NULL
|
|
// aslMsg may be NULL
|
|
|
|
value = getenv("BASWaitForDebugger");
|
|
if ( ((value != NULL) && (atoi(value) != 0)) ) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_DEBUG, "Waiting for debugger");
|
|
assert(err == 0);
|
|
(void) pause();
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
static int CheckInWithLaunchd(aslclient asl, aslmsg aslMsg, const char **errStrPtr)
|
|
// Checks in with launchd and gets back our listening socket.
|
|
// Returns the socket as the function result (or -1 on error).
|
|
// Also, on error, set *errStrPtr to a error string suitable
|
|
// for logging with ASL. If the message contains a %m, which
|
|
// causes ASL to log errno, errno will be set appropriately.
|
|
{
|
|
int err;
|
|
launch_data_t checkinRequest = NULL;
|
|
launch_data_t checkinResponse = NULL;
|
|
launch_data_t socketsDict;
|
|
launch_data_t fdArray;
|
|
launch_data_t fdData;
|
|
int fd = -1;
|
|
|
|
// Pre-conditions
|
|
|
|
// asl may be NULL
|
|
// aslMsg may be NULL
|
|
assert( errStrPtr != NULL);
|
|
assert(*errStrPtr == NULL);
|
|
|
|
// Check in with launchd. Create a checkin request, then run it, then
|
|
// check if we got an error.
|
|
|
|
checkinRequest = launch_data_new_string(LAUNCH_KEY_CHECKIN);
|
|
if (checkinRequest == NULL) {
|
|
*errStrPtr = "Could not create checkin request: %m";
|
|
goto done;
|
|
}
|
|
checkinResponse = launch_msg(checkinRequest);
|
|
if (checkinResponse == NULL) {
|
|
*errStrPtr = "Error checking in: %m";
|
|
goto done;
|
|
}
|
|
if (launch_data_get_type(checkinResponse) == LAUNCH_DATA_ERRNO) {
|
|
errno = launch_data_get_errno(checkinResponse); // set errno so %m picks it up
|
|
*errStrPtr = "Checkin failed: %m";
|
|
goto done;
|
|
}
|
|
|
|
// Retrieve the dictionary of sockets entries from the job. This corresponds to the
|
|
// value of the "Sockets" key in our plist file.
|
|
|
|
socketsDict = launch_data_dict_lookup(checkinResponse, LAUNCH_JOBKEY_SOCKETS);
|
|
if (socketsDict == NULL) {
|
|
*errStrPtr = "Could not get socket dictionary from checkin response: %m";
|
|
goto done;
|
|
}
|
|
if (launch_data_get_type(socketsDict) != LAUNCH_DATA_DICTIONARY) {
|
|
*errStrPtr = "Could not get socket dictionary from checkin response: Type mismatch";
|
|
goto done;
|
|
}
|
|
if (launch_data_dict_get_count(socketsDict) > 1) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_WARNING, "Some sockets in dictionary will be ignored");
|
|
assert(err == 0);
|
|
}
|
|
|
|
// Get the dictionary value from the key "MasterSocket", as defined in the launchd
|
|
// property list file.
|
|
|
|
fdArray = launch_data_dict_lookup(socketsDict, kLaunchDSocketDictKey);
|
|
if (fdArray == NULL) {
|
|
*errStrPtr = "Could not get file descriptor array: %m";
|
|
goto done;
|
|
}
|
|
if (launch_data_get_type(fdArray) != LAUNCH_DATA_ARRAY) {
|
|
*errStrPtr = "Could not get file descriptor array: Type mismatch";
|
|
goto done;
|
|
}
|
|
if (launch_data_array_get_count(fdArray) > 1) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_WARNING, "Some sockets in array will be ignored");
|
|
assert(err == 0);
|
|
}
|
|
|
|
// Get the socket file descriptor from the array.
|
|
|
|
fdData = launch_data_array_get_index(fdArray, 0);
|
|
if (fdData == NULL) {
|
|
*errStrPtr = "Could not get file descriptor array entry: %m";
|
|
goto done;
|
|
}
|
|
if (launch_data_get_type(fdData) != LAUNCH_DATA_FD) {
|
|
*errStrPtr = "Could not get file descriptor array entry: Type mismatch";
|
|
goto done;
|
|
}
|
|
fd = launch_data_get_fd(fdData);
|
|
assert(fd >= 0);
|
|
|
|
// The following was used to debug a problem with launchd <rdar://problem/5410487>.
|
|
// I'm going to leave it in, disabled, until that problem is resolved.
|
|
|
|
if (false) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_INFO, "Listening descriptor is %d", fd);
|
|
assert(err == 0);
|
|
}
|
|
|
|
done:
|
|
if (checkinResponse != NULL) {
|
|
launch_data_free(checkinResponse);
|
|
}
|
|
if (checkinRequest != NULL) {
|
|
launch_data_free(checkinRequest);
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
static int SetNonBlocking(int fd, Boolean nonBlocking)
|
|
// Sets the non-blocking state of fd.
|
|
{
|
|
int err;
|
|
int flags;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(fd >= 0);
|
|
|
|
// Get the flags.
|
|
|
|
err = 0;
|
|
flags = fcntl(fd, F_GETFL);
|
|
if (flags < 0) {
|
|
err = errno;
|
|
}
|
|
|
|
// If the current state of O_NONBLOCK doesn't match the required
|
|
// state, toggle that flag and set it back.
|
|
|
|
if ( (err == 0) && (((flags & O_NONBLOCK) != 0) != nonBlocking) ) {
|
|
flags ^= O_NONBLOCK;
|
|
err = fcntl(fd, F_SETFL, flags);
|
|
if (err < 0) {
|
|
err = errno;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
extern int BASHelperToolMain(
|
|
const BASCommandSpec commands[],
|
|
const BASCommandProc commandProcs[]
|
|
)
|
|
// See comment in header.
|
|
{
|
|
const char * errStr = NULL;
|
|
int err;
|
|
aslclient asl = NULL;
|
|
aslmsg aslMsg = NULL;
|
|
sig_t pipeSet;
|
|
int listener;
|
|
int kq;
|
|
struct kevent initEvent;
|
|
|
|
|
|
// Pre-conditions
|
|
|
|
assert(commands != NULL);
|
|
assert(commands[0].commandName != NULL); // there must be at least one command
|
|
assert(commandProcs != NULL);
|
|
assert( CommandArraySizeMatchesCommandProcArraySize(commands, commandProcs) );
|
|
|
|
// Create a new ASL client object, and a template message for any messages that
|
|
// we log. We don't care if these fail because ASL will do the right thing
|
|
// if you pass it NULL (that is, nothing).
|
|
|
|
asl = asl_open(NULL, "HelperTools", ASL_OPT_STDERR);
|
|
assert(asl != NULL);
|
|
|
|
aslMsg = asl_new(ASL_TYPE_MSG);
|
|
assert(aslMsg != NULL);
|
|
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_INFO, "Starting up");
|
|
assert(err == 0);
|
|
|
|
#if !defined(NDEBUG)
|
|
WaitForDebugger(asl, aslMsg);
|
|
#endif
|
|
|
|
// Set up the signal handlers we are interested in.
|
|
//
|
|
// o SIGTERM -- launchd sends us this when it wants us to quit. We don't
|
|
// actually need to set up a handler because the default behaviour (process
|
|
// termination) is fine.
|
|
//
|
|
// o SIGALRM -- No need to set it up because the default behaviour (process
|
|
// termination) is fine. See the "Watchdog Timer" comment (above) for details.
|
|
//
|
|
// o SIGPIPE -- We don't want to quit when write to a dead socket, so we
|
|
// ignore this signal.
|
|
|
|
pipeSet = signal(SIGPIPE, SIG_IGN);
|
|
if (pipeSet == SIG_ERR) {
|
|
errStr = "Could not ignore SIGPIPE: %m";
|
|
goto done;
|
|
}
|
|
|
|
// Check in with launchd and get our listening socket.
|
|
|
|
listener = CheckInWithLaunchd(asl, aslMsg, &errStr);
|
|
if (listener < 0) {
|
|
assert(errStr != NULL);
|
|
goto done;
|
|
}
|
|
|
|
// Create a kqueue and wrap the listening socket in it.
|
|
|
|
kq = kqueue();
|
|
if (kq < 0) {
|
|
errStr = "Could not create kqueue: %m";
|
|
goto done;
|
|
}
|
|
|
|
EV_SET(&initEvent, listener, EVFILT_READ, EV_ADD, 0, 0, NULL);
|
|
err = kevent(kq, &initEvent, 1, NULL, 0, NULL);
|
|
if (err < 0) {
|
|
errStr = "Could not add listening socket to kqueue: %m";
|
|
goto done;
|
|
}
|
|
|
|
// Force the listening socket to non-blocking mode. Without this, our timeout
|
|
// handling won't work properly. Specifically, we could get stuck in an accept
|
|
// if a connection request appears and then disappears. Eventually the watchdog
|
|
// would clean up, but that's not a great solution.
|
|
|
|
err = SetNonBlocking(listener, true);
|
|
if (err != 0) {
|
|
errno = err; // for %m
|
|
errStr = "Could not check/set socket flags: %m";
|
|
goto done;
|
|
}
|
|
|
|
// Loop servicing connection requests one at a time.
|
|
|
|
while (true) {
|
|
int eventCount;
|
|
struct kevent thisEvent;
|
|
int thisConnection;
|
|
int thisConnectionError;
|
|
struct sockaddr_storage clientAddr; // we don't need this info, but accept won't let us ignore it
|
|
socklen_t clientAddrLen = sizeof(clientAddr);
|
|
static const struct timespec kIdleTimeout = { kIdleTimeoutInSeconds , 0 };
|
|
|
|
// Wait on the kqueue for a connection request.
|
|
|
|
eventCount = kevent(kq, NULL, 0, &thisEvent, 1, &kIdleTimeout);
|
|
if (eventCount == 0) {
|
|
// We've hit our idle timer. Just break out of the connection loop.
|
|
break;
|
|
} else if (eventCount == -1) {
|
|
// We got some sort of error from kevent; quit with an error.
|
|
errStr = "Unexpected error while listening for connections: %m";
|
|
goto done;
|
|
}
|
|
|
|
// From this point on, we're running on the watchdog timer. If we get
|
|
// stuck anywhere, the watchdog will fire eventually and we'll quit.
|
|
|
|
EnableWatchdog();
|
|
|
|
// The accept should never get stuck because this is a non-blocking
|
|
// socket.
|
|
|
|
thisConnection = accept(thisEvent.ident, (struct sockaddr *) &clientAddr, &clientAddrLen);
|
|
if (thisConnection == -1) {
|
|
if (errno == EWOULDBLOCK) {
|
|
// If the incoming connection just disappeared (perhaps the client
|
|
// died before we accepted the connection), don't log that as an error
|
|
// and don't quit.
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_INFO, "Connection disappeared before we could accept it: %m");
|
|
assert(err == 0);
|
|
} else {
|
|
// Other errors mean that we're in a very weird state; we respond by
|
|
// failing out with an error.
|
|
errStr = "Unexpected error while accepting a connection: %m";
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
// Because the accept can fail in a non-fatal fashion, thisConnection can be
|
|
// -1 here. In that case, we just skip the next step.
|
|
|
|
if (thisConnection != -1) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_DEBUG, "Request started");
|
|
assert(err == 0);
|
|
|
|
// thisConnection inherits its non-blocking setting from listener, but
|
|
// we want it to be blocking from here on in, so we switch the status.
|
|
// We're now relying on the watchdog to kill us if we get stuck.
|
|
|
|
thisConnectionError = BASErrnoToOSStatus( SetNonBlocking(thisConnection, false) );
|
|
|
|
// Entering heavy liftiing. We have a separate routine to actually
|
|
// read the request from the connection, call the client, and send
|
|
// the reply.
|
|
|
|
if (thisConnectionError == noErr) {
|
|
thisConnectionError = HandleConnection(asl, aslMsg, commands, commandProcs, thisConnection);
|
|
}
|
|
|
|
err = close(thisConnection);
|
|
assert(err == 0);
|
|
|
|
if (thisConnectionError == 0) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_DEBUG, "Request finished");
|
|
} else {
|
|
errno = thisConnectionError; // so it can be picked up by %m
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_ERR, "Request failed: %m");
|
|
}
|
|
assert(err == 0);
|
|
}
|
|
|
|
DisableWatchdog();
|
|
}
|
|
|
|
done:
|
|
// At this point, errStr is either NULL, in which case we're quitting because
|
|
// of our idle timer, or non-NULL, in which case we're dying with an error.
|
|
|
|
// We expect the caller to immediately quit once we return. Thus, we
|
|
// don't bother cleaning up any resources we have allocated here, including
|
|
// asl, aslMsg, and kq.
|
|
|
|
if (errStr != NULL) {
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_ERR, errStr);
|
|
assert(err == 0);
|
|
}
|
|
err = asl_log(asl, aslMsg, ASL_LEVEL_INFO, "Shutting down");
|
|
assert(err == 0);
|
|
return (errStr == NULL) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
#pragma mark ***** App Code
|
|
|
|
extern void BASSetDefaultRules(
|
|
AuthorizationRef auth,
|
|
const BASCommandSpec commands[],
|
|
CFStringRef bundleID,
|
|
CFStringRef descriptionStringTableName
|
|
)
|
|
// See comment in header.
|
|
{
|
|
OSStatus err;
|
|
CFBundleRef bundle;
|
|
size_t commandIndex;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(commands != NULL);
|
|
assert(commands[0].commandName != NULL); // there must be at least one command
|
|
assert(bundleID != NULL);
|
|
// descriptionStringTableName may be NULL
|
|
|
|
bundle = CFBundleGetBundleWithIdentifier(bundleID);
|
|
assert(bundle != NULL);
|
|
|
|
// For each command, set up the default authorization right specification, as
|
|
// indicated by the command specification.
|
|
|
|
commandIndex = 0;
|
|
while (commands[commandIndex].commandName != NULL) {
|
|
// Some no-obvious assertions:
|
|
|
|
// If you have a right name, you must supply a default rule.
|
|
// If you have no right name, you can't supply a default rule.
|
|
|
|
assert( (commands[commandIndex].rightName == NULL) == (commands[commandIndex].rightDefaultRule == NULL) );
|
|
|
|
// If you have no right name, you can't supply a right description.
|
|
// OTOH, if you have a right name, you may supply a NULL right description
|
|
// (in which case you get no custom prompt).
|
|
|
|
assert( (commands[commandIndex].rightName != NULL) || (commands[commandIndex].rightDescriptionKey == NULL) );
|
|
|
|
// If there's a right name but no current right specification, set up the
|
|
// right specification.
|
|
|
|
if (commands[commandIndex].rightName != NULL) {
|
|
err = AuthorizationRightGet(commands[commandIndex].rightName, (CFDictionaryRef*) NULL);
|
|
if (err == errAuthorizationDenied) {
|
|
CFStringRef thisDescription;
|
|
CFStringRef thisRule;
|
|
|
|
// The right is not already defined. Set up a definition based on
|
|
// the fields in the command specification.
|
|
|
|
thisRule = CFStringCreateWithCString(
|
|
kCFAllocatorDefault,
|
|
commands[commandIndex].rightDefaultRule,
|
|
kCFStringEncodingUTF8
|
|
);
|
|
assert(thisRule != NULL);
|
|
|
|
thisDescription = NULL;
|
|
if (commands[commandIndex].rightDescriptionKey != NULL) {
|
|
thisDescription = CFStringCreateWithCString (
|
|
kCFAllocatorDefault,
|
|
commands[commandIndex].rightDescriptionKey,
|
|
kCFStringEncodingUTF8
|
|
);
|
|
assert(thisDescription != NULL);
|
|
}
|
|
|
|
err = AuthorizationRightSet(
|
|
auth, // authRef
|
|
commands[commandIndex].rightName, // rightName
|
|
thisRule, // rightDefinition
|
|
thisDescription, // descriptionKey
|
|
bundle, // bundle
|
|
descriptionStringTableName // localeTableName
|
|
); // NULL indicates "Localizable.strings"
|
|
assert(err == noErr);
|
|
|
|
if (thisDescription != NULL) {
|
|
CFRelease(thisDescription);
|
|
}
|
|
if (thisRule != NULL) {
|
|
CFRelease(thisRule);
|
|
}
|
|
} else {
|
|
// A right already exists (err == noErr) or any other error occurs, we
|
|
// assume that it has been set up in advance by the system administrator or
|
|
// this is the second time we've run. Either way, there's nothing more for
|
|
// us to do.
|
|
}
|
|
}
|
|
commandIndex += 1;
|
|
}
|
|
}
|
|
|
|
extern OSStatus BASExecuteRequestInHelperTool(
|
|
AuthorizationRef auth,
|
|
const BASCommandSpec commands[],
|
|
CFStringRef bundleID,
|
|
CFDictionaryRef request,
|
|
CFDictionaryRef * response
|
|
)
|
|
// See comment in header.
|
|
{
|
|
|
|
OSStatus retval = noErr;
|
|
int junk;
|
|
size_t commandIndex;
|
|
char bundleIDC[PATH_MAX];
|
|
int fd = -1;
|
|
struct sockaddr_un addr;
|
|
AuthorizationExternalForm extAuth;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(commands != NULL);
|
|
assert(commands[0].commandName != NULL); // there must be at least one command
|
|
assert(bundleID != NULL);
|
|
assert(request != NULL);
|
|
assert( response != NULL);
|
|
assert(*response == NULL);
|
|
|
|
// For debugging.
|
|
|
|
assert(CFDictionaryContainsKey(request, CFSTR(kBASCommandKey)));
|
|
assert(CFGetTypeID(CFDictionaryGetValue(request, CFSTR(kBASCommandKey))) == CFStringGetTypeID());
|
|
|
|
// Look up the command and preauthorize. This has the nice side effect that
|
|
// the authentication dialog comes up, in the typical case, here, rather than
|
|
// in the helper tool. This is good because the helper tool is global /and/
|
|
// single threaded, so if it's waiting for an authentication dialog for user A
|
|
// it can't handle requests from user B.
|
|
|
|
retval = FindCommand(request, commands, &commandIndex);
|
|
|
|
#if !defined(BAS_PREAUTHORIZE)
|
|
#define BAS_PREAUTHORIZE 1
|
|
#endif
|
|
#if BAS_PREAUTHORIZE
|
|
if ( (retval == noErr) && (commands[commandIndex].rightName != NULL) ) {
|
|
AuthorizationItem item = { commands[commandIndex].rightName, 0, NULL, 0 };
|
|
AuthorizationRights rights = { 1, &item };
|
|
|
|
retval = AuthorizationCopyRights(auth, &rights, kAuthorizationEmptyEnvironment, kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize, NULL);
|
|
}
|
|
#endif
|
|
|
|
// Create the socket and tell it to not generate SIGPIPE.
|
|
|
|
if (retval == noErr) {
|
|
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd == -1) {
|
|
retval = BASErrnoToOSStatus(errno);
|
|
}
|
|
}
|
|
if (retval == noErr) {
|
|
static const int kOne = 1;
|
|
|
|
if ( setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &kOne, sizeof(kOne)) < 0 ) {
|
|
retval = BASErrnoToOSStatus(errno);
|
|
}
|
|
}
|
|
|
|
// Form the socket address, including a path based on the bundle ID.
|
|
|
|
if (retval == noErr) {
|
|
if ( ! CFStringGetFileSystemRepresentation(bundleID, bundleIDC, sizeof(bundleIDC)) ) {
|
|
retval = coreFoundationUnknownErr;
|
|
}
|
|
}
|
|
if (retval == noErr) {
|
|
int pathLen;
|
|
|
|
memset(&addr, 0, sizeof(addr));
|
|
|
|
addr.sun_family = AF_UNIX;
|
|
pathLen = snprintf(addr.sun_path, sizeof(addr.sun_path), kBASSocketPathFormat, bundleIDC);
|
|
if (pathLen >= sizeof(addr.sun_path)) {
|
|
retval = paramErr; // length of bundle pushed us over the UNIX domain socket path length limit
|
|
} else {
|
|
addr.sun_len = SUN_LEN(&addr);
|
|
}
|
|
}
|
|
|
|
// Attempt to connect.
|
|
|
|
if (retval == noErr) {
|
|
if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
|
|
retval = BASErrnoToOSStatus(errno);
|
|
}
|
|
}
|
|
|
|
// Send the flattened AuthorizationRef to the tool.
|
|
|
|
if (retval == noErr) {
|
|
retval = AuthorizationMakeExternalForm(auth, &extAuth);
|
|
}
|
|
if (retval == noErr) {
|
|
retval = BASErrnoToOSStatus( BASWrite(fd, &extAuth, sizeof(extAuth), NULL) );
|
|
}
|
|
|
|
// Write the request.
|
|
|
|
if (retval == noErr) {
|
|
retval = BASErrnoToOSStatus( BASWriteDictionary(request, fd) );
|
|
}
|
|
|
|
// Read response, including any descriptors.
|
|
|
|
if (retval == noErr) {
|
|
retval = BASErrnoToOSStatus( BASReadDictioanaryTranslatingDescriptors(fd, response) );
|
|
}
|
|
|
|
// Clean up.
|
|
|
|
if (fd != -1) {
|
|
junk = close(fd);
|
|
assert(junk == 0);
|
|
}
|
|
NormaliseOSStatusErrorCode(&retval);
|
|
|
|
assert( (retval == noErr) == (*response != NULL) );
|
|
|
|
return retval;
|
|
}
|
|
|
|
extern OSStatus BASGetErrorFromResponse(CFDictionaryRef response)
|
|
// See comment in header.
|
|
{
|
|
OSStatus err;
|
|
CFNumberRef num;
|
|
|
|
assert(response != NULL);
|
|
|
|
num = (CFNumberRef) CFDictionaryGetValue(response, CFSTR(kBASErrorKey));
|
|
err = noErr;
|
|
if ( (num == NULL) || (CFGetTypeID(num) != CFNumberGetTypeID()) ) {
|
|
err = coreFoundationUnknownErr;
|
|
}
|
|
if (err == noErr) {
|
|
if ( ! CFNumberGetValue(num, kCFNumberSInt32Type, &err) ) {
|
|
err = coreFoundationUnknownErr;
|
|
}
|
|
}
|
|
|
|
NormaliseOSStatusErrorCode(&err);
|
|
return err;
|
|
}
|
|
|
|
extern BASFailCode BASDiagnoseFailure(
|
|
AuthorizationRef auth,
|
|
CFStringRef bundleID
|
|
)
|
|
// See comment in header.
|
|
{
|
|
BASFailCode retval = kBASFailUnknown;
|
|
int err;
|
|
int pathLen;
|
|
char bundleIDC [ PATH_MAX ];
|
|
char toolPath [ PATH_MAX ];
|
|
char plistPath [ PATH_MAX ];
|
|
|
|
struct stat fileStatus;
|
|
int toolErr;
|
|
int plistErr;
|
|
int fd;
|
|
struct sockaddr_un addr;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(bundleID != NULL);
|
|
|
|
// Construct paths to the tool and plist.
|
|
|
|
if ( CFStringGetFileSystemRepresentation(bundleID, bundleIDC, sizeof(bundleIDC)) ) {
|
|
|
|
pathLen = snprintf(toolPath, sizeof(toolPath), kBASToolPathFormat, bundleIDC);
|
|
assert(pathLen < PATH_MAX); // snprintf truncated the string; won't crash us, but we want to know
|
|
|
|
pathLen = snprintf(plistPath, sizeof(plistPath), kBASPlistPathFormat, bundleIDC);
|
|
assert(pathLen < PATH_MAX); // snprintf truncated the string; won't crash us, but we want to know
|
|
|
|
// Check if files exist at those paths.
|
|
|
|
toolErr = stat(toolPath, &fileStatus);
|
|
plistErr = stat(plistPath, &fileStatus);
|
|
|
|
if ( (toolErr == 0) && (plistErr == 0) ) {
|
|
// If both items are present, try to connect and see what we get.
|
|
|
|
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd != -1) {
|
|
memset(&addr, 0, sizeof(addr));
|
|
|
|
addr.sun_family = AF_UNIX;
|
|
(void) snprintf(addr.sun_path, sizeof(addr.sun_path), kBASSocketPathFormat, bundleIDC);
|
|
addr.sun_len = SUN_LEN(&addr);
|
|
|
|
// Attempt to connect to the socket. If we get ECONNREFUSED, it means no one is
|
|
// listening.
|
|
|
|
if ( (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1) && (errno == ECONNREFUSED) ) {
|
|
retval = kBASFailDisabled;
|
|
}
|
|
err = close(fd);
|
|
assert(err == 0);
|
|
}
|
|
} else {
|
|
if ( (toolErr == 0) || (plistErr == 0) ) {
|
|
retval = kBASFailPartiallyInstalled;
|
|
} else {
|
|
retval = kBASFailNotInstalled;
|
|
}
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
// kPlistTemplate is a template for our launchd.plist file.
|
|
|
|
static const char * kPlistTemplate =
|
|
// The standard plist header.
|
|
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
|
|
"<plist version=\"1.0\">\n"
|
|
"<dict>\n"
|
|
|
|
// We install the job disabled, then enable it as the last step.
|
|
|
|
" <key>Disabled</key>\n"
|
|
" <true/>\n"
|
|
|
|
// Use the bundle identifier as the job label.
|
|
|
|
" <key>Label</key>\n"
|
|
" <string>%s</string>\n"
|
|
|
|
// Use launch on demaind.
|
|
|
|
" <key>OnDemand</key>\n"
|
|
" <true/>\n"
|
|
|
|
// We don't want our helper tool to be respawned every 10 seconds
|
|
// after a faliure ... hopefully this won't ALSO prevent us from
|
|
// rerunning the helper tool without rebooting the machine
|
|
" <key>LaunchOnlyOnce</key>\n"
|
|
" <true/>\n"
|
|
|
|
// There are no program arguments, other that the path to the helper tool itself
|
|
// but we must force it to run using the i386 binary if the Framework is
|
|
// being used as a i386 binary.
|
|
//
|
|
// IMPORTANT
|
|
// kBASToolPathFormat embeds a %s
|
|
|
|
" <key>ProgramArguments</key>\n"
|
|
" <array>\n"
|
|
#ifdef __i386__
|
|
" <string>/usr/bin/arch</string>\n"
|
|
" <string>-i386</string>\n"
|
|
#endif
|
|
" <string>" kBASToolPathFormat "</string>\n"
|
|
" </array>\n"
|
|
|
|
// The tool is required to check in with launchd.
|
|
|
|
" <key>ServiceIPC</key>\n"
|
|
" <true/>\n"
|
|
|
|
// This specifies the UNIX domain socket used to launch the tool, including
|
|
// the permissions on the socket (438 is 0666).
|
|
//
|
|
// IMPORTANT
|
|
// kBASSocketPathFormat embeds a %s
|
|
|
|
" <key>Sockets</key>\n"
|
|
" <dict>\n"
|
|
" <key>" kLaunchDSocketDictKey "</key>\n"
|
|
" <dict>\n"
|
|
" <key>SockFamily</key>\n"
|
|
" <string>Unix</string>\n"
|
|
" <key>SockPathMode</key>\n"
|
|
" <integer>438</integer>\n"
|
|
" <key>SockPathName</key>\n"
|
|
" <string>" kBASSocketPathFormat "</string>\n"
|
|
" <key>SockType</key>\n"
|
|
" <string>Stream</string>\n"
|
|
" </dict>\n"
|
|
" </dict>\n"
|
|
"</dict>\n"
|
|
"</plist>\n"
|
|
;
|
|
|
|
|
|
// Installation
|
|
// ------------
|
|
// We install by running our "InstallTool" using AuthorizationExecuteWithPrivileges
|
|
// (AEWP) and passing the relevant parameters to it through AEWP.
|
|
//
|
|
// There is an obvious issue with the way we are handling installation as the user
|
|
// is executing some non-privileged code by way of AEWP. The scenario could exist
|
|
// that the code is malicious (or they have other malicious code running at the
|
|
// same time) and it could swap in any other tool that it would want executed as
|
|
// EUID == 0.
|
|
//
|
|
// We decided on this design primarily because the only other option was to run a
|
|
// shell via AEWP and pipe a script to it. That would have given us the nice
|
|
// properties of not having to have a separate installer on disk and the script
|
|
// could be embedded within the executable making it a little more difficult for
|
|
// casual hacking.
|
|
//
|
|
// However, running a shell as root is /not/ a very good paradigm to follow, thus,
|
|
// weighing the cost-benefits from a security perspective impelled us to just use
|
|
// a separate installer tool. The assumption being that, no matter what, if a user
|
|
// has malicious code running on their system the added security of having an
|
|
// embedded script is negligible and not worth pulling in an entire shell
|
|
// environment as root.
|
|
//
|
|
// The obvious disadvantages stem from the first advantage of the former, namely,
|
|
// it's a little more coding and accounting effort (-:
|
|
//
|
|
//
|
|
// What's This About Zombies?
|
|
// --------------------------
|
|
// AuthorizationExecuteWithPrivileges creates a process that runs with privileges.
|
|
// This process is a child of our process. Thus, we need to reap the process
|
|
// (by calling <x-man-page://2/waitpid>). If we don't do this, we create a 'zombie'
|
|
// process (<x-man-page://1/ps> displays its status as "Z") that persists until
|
|
// our process quits (at which point the zombie gets reparented to launchd, and
|
|
// launchd automatically reaps it). Zombies are generally considered poor form.
|
|
// Thus, we want to avoid creating them.
|
|
//
|
|
// Unfortunately, AEWP doesn't return the process ID of the child process
|
|
// <rdar://problem/3090277>, which makes it challenging for us to reap it. We could
|
|
// reap all children (by passing -1 to waitpid) but that's not cool for library code
|
|
// (we could end up reaping a child process that's completely unrelated to this
|
|
// code, perhaps created by some other part of the host application). Thus, we need
|
|
// to find the child process's PID. And the only way to do that is for the child
|
|
// process to tell us.
|
|
//
|
|
// So, in the child process (the install tool) we echo the process ID and in the
|
|
// parent we look for that in the returned text. *sigh* It's pretty ugly, but
|
|
// that's the best I can come up with. We delimit the process ID with some
|
|
// pretty distinctive text to make it clear that we've got the right thing.
|
|
|
|
#if !defined(NDEBUG)
|
|
|
|
static Boolean gBASLogInteractions = false;
|
|
// Set gBASLogInteractions to have BASFixFailure log its interactions with
|
|
// the installation tool to stderr.
|
|
|
|
static Boolean gBASLogInteractionsInitialised = false;
|
|
// This indicates whether we've initialised gBASLogInteractions from the
|
|
// environment variable.
|
|
|
|
#endif
|
|
|
|
static OSStatus RunInstallToolAsRoot(
|
|
AuthorizationRef auth,
|
|
const char * installToolPath,
|
|
const char * command,
|
|
...
|
|
)
|
|
// Run the specified install tool as root. The arguments to the tool are
|
|
// given as a sequence of (char *)s, terminated be a NULL. The tool is
|
|
// expected to output special tokens to indicate success or failure.
|
|
{
|
|
OSStatus retval;
|
|
size_t argCount;
|
|
size_t argIndex;
|
|
va_list ap;
|
|
char ** args;
|
|
Boolean success;
|
|
FILE * channel;
|
|
int junk;
|
|
pid_t childPID;
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(installToolPath != NULL);
|
|
assert(command != NULL);
|
|
|
|
channel = NULL;
|
|
args = NULL;
|
|
childPID = -1;
|
|
|
|
// Count the number of arguments.
|
|
|
|
argCount = 0;
|
|
va_start(ap, command);
|
|
while ( va_arg(ap, char *) != NULL ) {
|
|
argCount += 1;
|
|
}
|
|
va_end(ap);
|
|
|
|
// Allocate an argument array and populate it, checking each argument along the way.
|
|
|
|
retval = noErr;
|
|
args = calloc(argCount + 3, sizeof(char *)); // +3 for installToolPath, command and trailing NULL
|
|
if (args == NULL) {
|
|
retval = memFullErr;
|
|
}
|
|
if (retval == noErr) {
|
|
argIndex = 0;
|
|
|
|
args[argIndex] = (char *) installToolPath; // Annoyingly, AEWP (and exec) takes a (char * const *)
|
|
argIndex += 1; // argument, implying that it might modify the individual
|
|
args[argIndex] = (char *) command; // strings. That means you can't pass a (const char *) to
|
|
argIndex += 1; // the routine. However, AEWP never modifies its input
|
|
// arguments, so we just cast away the const.
|
|
// *sigh* <rdar://problem/3090294>
|
|
va_start(ap, command);
|
|
do {
|
|
args[argIndex] = va_arg(ap, char *);
|
|
if (args[argIndex] == NULL) {
|
|
break;
|
|
}
|
|
argIndex += 1;
|
|
} while (true);
|
|
va_end(ap);
|
|
}
|
|
|
|
// Go go gadget AEWP!
|
|
|
|
if (retval == noErr) {
|
|
#if !defined(NDEBUG)
|
|
if ( ! gBASLogInteractionsInitialised ) {
|
|
const char * value;
|
|
|
|
value = getenv("BASLogInteractions");
|
|
gBASLogInteractions = ( ((value != NULL) && (atoi(value) != 0)) );
|
|
|
|
gBASLogInteractionsInitialised = true;
|
|
}
|
|
|
|
if (gBASLogInteractions) {
|
|
argIndex = 0;
|
|
while (args[argIndex] != NULL) {
|
|
fprintf(stderr, "args[%zd] = %s\n", argIndex, args[argIndex]);
|
|
argIndex += 1;
|
|
}
|
|
}
|
|
#endif
|
|
retval = AuthorizationExecuteWithPrivileges(auth, args[0], kAuthorizationFlagDefaults, &args[1], &channel);
|
|
}
|
|
|
|
// Process the tool's output. We read every line of output from the tool until
|
|
// we receive either an EOF or the success or failure tokens.
|
|
//
|
|
// AEWP provides us with no way to get to the tool's stderr or exit status,
|
|
// so we rely on the tool to send us this "oK" to indicate successful completion.
|
|
|
|
if (retval == noErr) {
|
|
char thisLine[1024];
|
|
long tmpLong;
|
|
int tmpInt;
|
|
|
|
// This loops is a little more complex than you might expect. There are
|
|
// a number of reasons for this:
|
|
//
|
|
// o AEWP does not return us the child PID, so we have to scan the tool's
|
|
// output look for a line that contains that information (surrounded
|
|
// by special tokens).
|
|
//
|
|
// o Because we can't be guaranteed to get the child PID, we can't be
|
|
// guaranteed to get the child's exit status. Thus, rather than relying
|
|
// on the exit status, we have the child explicitly print special tokens
|
|
// on success and failure.
|
|
//
|
|
// o Because we're parsing special tokens anyway, we might as well extract
|
|
// the real error code from the failure token.
|
|
//
|
|
// o A change made to launchctl in Mac OS X 10.4.7 <rdar://problem/4389914>
|
|
// causes it to fork a copy of itself. The forked copy then delays
|
|
// for 30 seconds before doing some stuff, eventually printing a message
|
|
// like "Workaround Bonjour: 0". This causes us two problems.
|
|
//
|
|
// 1. The second copy of launchd still has our communications channel
|
|
// (that is, the other end of "channel") as its stdin/stdout.
|
|
// Thus, we don't get an EOF on channel until that copy quits.
|
|
// This causes a 30 second delay in installation.
|
|
//
|
|
// 2. The second copy of launchd prints its status line (that is,
|
|
// "Workaround Bonjour: 0") well after the tool prints the success
|
|
// token.
|
|
//
|
|
// I solved these problems by parsing each line for the success or failure
|
|
// token and ignoring any output after that.
|
|
//
|
|
// To minimise the danger of interpreting one of the tool's commands
|
|
// output as one of our tokens, I've given them a wacky case (for example,
|
|
// "oK", not "ok" or "OK" or "Ok").
|
|
|
|
do {
|
|
success = (fgets(thisLine, sizeof(thisLine), channel) != NULL);
|
|
if ( ! success ) {
|
|
// We hit the end of the output without seeing a success or failure
|
|
// token. Note good. errState is an ADSP error code, but it says
|
|
// exactly what I want to say and it's not likely to crop up any
|
|
// other way.
|
|
retval = errState;
|
|
break;
|
|
}
|
|
|
|
// This echo doesn't work properly if the line coming back from the tool
|
|
// is longer than the line buffer. However, as the echo is only relevant for
|
|
// debugging, and the detection of the "oK" isn't affected by this problem,
|
|
// I'm going to leave it as it is.
|
|
|
|
#if !defined(NDEBUG)
|
|
if (gBASLogInteractions) {
|
|
fprintf(stderr, ">%s", thisLine);
|
|
}
|
|
#endif
|
|
|
|
// Look for the success token and terminate with no error in that case.
|
|
|
|
if (strcmp(thisLine, kBASInstallToolSuccess "\n") == 0) {
|
|
assert(retval == noErr);
|
|
break;
|
|
}
|
|
|
|
// Look for the failure token and extract the error result from that.
|
|
|
|
if ( sscanf(thisLine, kBASInstallToolFailure "\n", &tmpInt) == 1 ) {
|
|
retval = BASErrnoToOSStatus( tmpInt );
|
|
if (retval == noErr) {
|
|
assert(false);
|
|
retval = errState;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// If we haven't already found a child process ID, look for a line
|
|
// that contains it (surrounded by special tokens). For details, see
|
|
// the discussion of zombies above.
|
|
|
|
if ( (childPID == -1) && (sscanf(thisLine, kBASAntiZombiePIDToken1 "%ld" kBASAntiZombiePIDToken2 "\n", &tmpLong) == 1) ) {
|
|
childPID = (pid_t) tmpLong;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
// If we successfully managed to determine the PID of our child process, reap
|
|
// that child. Note that we ignore any errors from this step. If an error
|
|
// occurs, we end up creating a zombie, which isn't too big a deal. We also
|
|
// junk the status result from the tool, relying exclusively on the presence
|
|
// of the "oK" in the output.
|
|
|
|
#if !defined(NDEBUG)
|
|
if (gBASLogInteractions) {
|
|
fprintf(stderr, "childPID=%ld\n", (long) childPID);
|
|
}
|
|
#endif
|
|
if (childPID != -1) {
|
|
pid_t waitResult;
|
|
int junkStatus;
|
|
|
|
do {
|
|
waitResult = waitpid(childPID, &junkStatus, 0);
|
|
} while ( (waitResult < 0) && (errno == EINTR) );
|
|
}
|
|
|
|
// Clean up.
|
|
|
|
if (channel != NULL) {
|
|
junk = fclose(channel);
|
|
assert(junk == 0);
|
|
}
|
|
free(args);
|
|
|
|
NormaliseOSStatusErrorCode(&retval);
|
|
return retval;
|
|
}
|
|
|
|
static OSStatus BASInstall(
|
|
AuthorizationRef auth,
|
|
const char * bundleID,
|
|
const char * installToolPath,
|
|
const char * helperToolPath
|
|
)
|
|
// Do an install from scratch. Get the specified tool from the bundle
|
|
// and install it in the "/Library/PrivilegedHelperTools" directory,
|
|
// along with a plist in "/Library/LaunchDaemons".
|
|
{
|
|
OSStatus retval;
|
|
int junk;
|
|
char * plistText;
|
|
int fd;
|
|
char plistPath[PATH_MAX];
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(bundleID != NULL);
|
|
assert(installToolPath != NULL);
|
|
assert(helperToolPath != NULL);
|
|
|
|
// Prepare for failure
|
|
|
|
plistText = NULL;
|
|
fd = -1;
|
|
plistPath[0] = 0;
|
|
|
|
// Create the property list from the template, substituting the bundle identifier in
|
|
// three different places. I realise that this isn't very robust (if you change
|
|
// the template you have to change this code), but it is /very/ easy.
|
|
|
|
retval = asprintf(&plistText, kPlistTemplate, bundleID, bundleID, bundleID);
|
|
if (retval < 0) {
|
|
retval = memFullErr;
|
|
} else {
|
|
retval = noErr;
|
|
}
|
|
|
|
// Write the plist to a temporary file.
|
|
|
|
if (retval == noErr) {
|
|
strlcpy(plistPath, "/tmp/BASTemp-XXXXXXXX.plist", sizeof(plistPath));
|
|
|
|
fd = mkstemps(plistPath, strlen( strrchr(plistPath, '.') ) );
|
|
if (fd < 0) {
|
|
retval = BASErrnoToOSStatus( errno );
|
|
}
|
|
}
|
|
if (retval == noErr) {
|
|
retval = BASErrnoToOSStatus( BASWrite(fd, plistText, strlen(plistText), NULL) );
|
|
}
|
|
|
|
// Run the tool as root using AuthorizationExecuteWithPrivileges.
|
|
|
|
if (retval == noErr) {
|
|
retval = RunInstallToolAsRoot(auth, installToolPath, kBASInstallToolInstallCommand, bundleID, helperToolPath, plistPath, NULL);
|
|
}
|
|
|
|
// Clean up.
|
|
|
|
free(plistText);
|
|
if (fd != -1) {
|
|
junk = close(fd);
|
|
assert(junk == 0);
|
|
|
|
junk = unlink(plistPath);
|
|
assert(junk == 0);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
//static OSStatus GetToolPath(CFStringRef bundleID, CFStringRef toolName, char *toolPath, size_t toolPathSize)
|
|
// // Given a bundle identifier and the name of a tool embedded within that bundle,
|
|
// // get a file system path to the tool.
|
|
//{
|
|
// OSStatus err;
|
|
// CFBundleRef bundle;
|
|
// Boolean success;
|
|
// CFURLRef toolURL;
|
|
//
|
|
// assert(bundleID != NULL);
|
|
// assert(toolName != NULL);
|
|
// assert(toolPath != NULL);
|
|
// assert(toolPathSize > 0);
|
|
//
|
|
// toolURL = NULL;
|
|
//
|
|
//
|
|
// err = noErr;
|
|
// bundle = CFBundleGetBundleWithIdentifier(bundleID);
|
|
// if (bundle == NULL) {
|
|
// err = coreFoundationUnknownErr;
|
|
// }
|
|
//
|
|
// if (err == noErr) {
|
|
// //Modify this code to load the bundle if its not already loaded
|
|
//
|
|
// if (CFBundleIsExecutableLoaded(bundle)) {
|
|
// toolURL = CFBundleCopyResourceURL(bundle, toolName, NULL, NULL);
|
|
// }
|
|
// else{ //We need to load the bundle first
|
|
// Boolean loaded = CFBundleLoadExecutable(bundle);
|
|
//
|
|
// if (loaded && bundle != NULL) {
|
|
// toolURL = CFBundleCopyResourceURL(bundle, toolName, NULL, NULL);
|
|
// }
|
|
// else { // we couldn't load the bundle .. this IS bad
|
|
// err = coreFoundationUnknownErr;
|
|
// }
|
|
// }
|
|
//
|
|
//
|
|
// assert(toolURL != NULL);
|
|
//
|
|
// if (toolURL == NULL) {
|
|
// err = coreFoundationUnknownErr;
|
|
// }
|
|
// }
|
|
// if (err == noErr) {
|
|
// success = CFURLGetFileSystemRepresentation(toolURL, true, (UInt8 *) toolPath, toolPathSize);
|
|
// if ( ! success ) {
|
|
// err = coreFoundationUnknownErr;
|
|
// }
|
|
// }
|
|
//
|
|
// if (toolURL != NULL) {
|
|
// CFRelease(toolURL);
|
|
// }
|
|
//
|
|
// return err;
|
|
//}
|
|
|
|
|
|
|
|
extern OSStatus BASFixFailure(
|
|
AuthorizationRef auth,
|
|
CFStringRef bundleID,
|
|
CFURLRef installToolURL,
|
|
CFURLRef helperToolURL,
|
|
BASFailCode failCode
|
|
)
|
|
// See comment in header.
|
|
{
|
|
OSStatus retval;
|
|
Boolean success;
|
|
char bundleIDC[PATH_MAX];
|
|
char installToolPathC[PATH_MAX];
|
|
char helperToolPathC[PATH_MAX];
|
|
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(bundleID != NULL);
|
|
assert(installToolURL != NULL);
|
|
assert(helperToolURL != NULL);
|
|
|
|
// Get the bundle identifier as a UTF-8 C string. Also, get paths for both of
|
|
// the tools.
|
|
|
|
retval = noErr;
|
|
success = CFStringGetFileSystemRepresentation(bundleID, bundleIDC, sizeof(bundleIDC));
|
|
if ( ! success ) {
|
|
retval = coreFoundationUnknownErr;
|
|
}
|
|
if (retval == noErr) {
|
|
success = CFURLGetFileSystemRepresentation(installToolURL, true, (UInt8 *) installToolPathC, sizeof(installToolPathC));
|
|
if ( ! success )
|
|
retval = coreFoundationUnknownErr;
|
|
}
|
|
if (retval == noErr) {
|
|
success = CFURLGetFileSystemRepresentation(helperToolURL, true, (UInt8 *) helperToolPathC, sizeof(helperToolPathC));
|
|
if ( ! success )
|
|
retval = coreFoundationUnknownErr;
|
|
}
|
|
|
|
// if (retval == noErr) {
|
|
// retval = GetToolPath(bundleID, installToolURL, installToolPath, sizeof(installToolPath));
|
|
// }
|
|
// if (retval == noErr) {
|
|
// retval = GetToolPath(bundleID, helperToolURL, helperToolPath, sizeof(helperToolPath));
|
|
// }
|
|
|
|
|
|
// Depending on the failure code, either run the enable command or the install
|
|
// from scratch command.
|
|
|
|
if (retval == noErr) {
|
|
if (failCode == kBASFailDisabled) {
|
|
retval = RunInstallToolAsRoot(auth, installToolPathC, kBASInstallToolEnableCommand, bundleIDC, NULL);
|
|
} else {
|
|
retval = BASInstall(auth, bundleIDC, installToolPathC, helperToolPathC);
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
extern void BASTerminateCommand (
|
|
AuthorizationRef auth,
|
|
const char * bundleID,
|
|
const char * installToolPath
|
|
)
|
|
// Terminate the HelperTool.
|
|
{
|
|
// Pre-conditions
|
|
|
|
assert(auth != NULL);
|
|
assert(bundleID != NULL);
|
|
assert(installToolPath != NULL);
|
|
|
|
// Run the install tool as root using AuthorizationExecuteWithPrivileges.
|
|
RunInstallToolAsRoot(auth, installToolPath, kBASInstallToolTerminateCommand, bundleID, NULL);
|
|
|
|
return;
|
|
}
|
|
|