Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
Relevant replacement documents include:
BetterAuthorizationSampleLib.c
/* |
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 = 65 // any given request must be completely in 65 seconds |
}; |
// 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" |
// There are no program arguments, other that the path to the helper tool itself. |
// |
// IMPORTANT |
// kBASToolPathFormat embeds a %s |
" <key>ProgramArguments</key>\n" |
" <array>\n" |
" <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) { |
toolURL = CFBundleCopyAuxiliaryExecutableURL(bundle, toolName); |
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, |
CFStringRef installToolName, |
CFStringRef helperToolName, |
BASFailCode failCode |
) |
// See comment in header. |
{ |
OSStatus retval; |
Boolean success; |
char bundleIDC[PATH_MAX]; |
char installToolPath[PATH_MAX]; |
char helperToolPath[PATH_MAX]; |
// Pre-conditions |
assert(auth != NULL); |
assert(bundleID != NULL); |
assert(installToolName != NULL); |
assert(helperToolName != 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) { |
retval = GetToolPath(bundleID, installToolName, installToolPath, sizeof(installToolPath)); |
} |
if (retval == noErr) { |
retval = GetToolPath(bundleID, helperToolName, 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, installToolPath, kBASInstallToolEnableCommand, bundleIDC, NULL); |
} else { |
retval = BASInstall(auth, bundleIDC, installToolPath, helperToolPath); |
} |
} |
return retval; |
} |
Copyright © 2007 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2007-11-27