/* File: MoreSecurity.c Contains: Security utilities. Written by: Quinn Copyright: Copyright (c) 2002 by Apple Computer, Inc., All Rights Reserved. Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, 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 Computer, 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. Change History (most recent first): $Log: MoreSecurity.c,v $ Revision 1.5 2002/12/12 23:15:23 eskimo1 Switch EUID back to RUID after we return from the commandProc, just in case the client left it set the wrong way. Revision 1.4 2002/12/12 15:41:53 eskimo1 Eliminate MoreAuthCopyRightCFString because it makes no sense. A helper tool should always have the right name hard-wired into it, and hardwiring a C string is even easier than hardwiring a CFString. Also added some more debugging printfs. Revision 1.3 2002/11/25 16:42:25 eskimo1 Significant changes. Handle more edge cases better (for example, volumes with the "ignore permissions" flag turned on). Also brought MoreSecurity more into the CoreServices world. Revision 1.2 2002/11/14 20:27:59 eskimo1 Compare time stamps in MoreSecCopyHelperToolURLRef to decide whether to throw away the tool and revert to the backup. This greatly improves the debugging experience. Also, in MoreSecExecuteRequestInHelperTool, add code to dispose of a partial response if we get an error (prevents a memory leak in some very specific error conditions). Finally, in MoreSecGetErrorFromResponse, eliminate an unnecessary CFRelease. Revision 1.1 2002/11/09 00:08:36 eskimo1 First checked in. A module containing security helpers. */ ///////////////////////////////////////////////////////////////// // Our prototypes #include "MoreSecurity.h" // System interfaces #include #include #include #include #include #include #include #include // MIB Interfaces #include "MoreUNIX.h" #include "MoreCFQ.h" ///////////////////////////////////////////////////////////////// #pragma mark ***** UID Management extern int MoreSecPermanentlySetNonPrivilegedEUID(void) // See comment in header. { int err; err = setuid(getuid()); if (err != 0) { err = errno; } return err; } extern int MoreSecTemporarilySetNonPrivilegedEUID(void) // See comment in header. { int err; err = seteuid(getuid()); if (err != 0) { err = errno; } return err; } extern int MoreSecSetPrivilegedEUID(void) // See comment in header. { int err; err = seteuid(0); if (err != 0) { err = errno; } return err; } ///////////////////////////////////////////////////////////////// #pragma mark ***** MoreSecDestroyInheritedEnvironment static int GetPathToSelf(char **pathToSelfPtr) // A simple wrapper around MoreGetExecutablePath which returns // the path in a memory block that you must free. { int err; size_t pathSize; char junkChar; assert( pathToSelfPtr != NULL); assert(*pathToSelfPtr == NULL); *pathToSelfPtr = &junkChar; pathSize = 0; err = MoreGetExecutablePath(*pathToSelfPtr, &pathSize); if (pathSize == 0) { assert(err != 0); if (err != 0) { err = -1; } *pathToSelfPtr = NULL; } else { pathSize += 1; err = 0; *pathToSelfPtr = (char *) malloc(pathSize); if ( *pathToSelfPtr == NULL ) { err = ENOMEM; } if (err == 0) { err = MoreGetExecutablePath(*pathToSelfPtr, &pathSize); } if (err != 0) { free(*pathToSelfPtr); *pathToSelfPtr = NULL; } } assert( (err == 0) == (*pathToSelfPtr != NULL) ); return err; } static int ResetArgvZero(const char **argv) // Sets argv[0] to be the true path to the executable // rather than a potentially hostile value supplied by // our parent process. { int err; char * execPath; execPath = NULL; err = GetPathToSelf(&execPath); // Copy the real executable path into argv[0]. if (err == 0) { argv[0] = execPath; } // If we got an error then free any buffer we may have // allocated. On no error we end up leaking execPath, but // that's acceptable because this function is usually only // called once at process startup. if (err != 0) { free(execPath); } return err; } extern char **environ; static int ResetEnvironment(void) // Clears all environment variables. There are circumstances // where your process might depend on certain environment // variables being set correctly, but if that's the case you // shouldn't be relying on inheriting good values from an // untrusted parent. You should, instead, set the environment // variables explicitly after calling // MoreSecDestroyInheritedEnvironment. { while ( environ[0] != NULL ) { unsetenv(environ[0]); } return 0; } static int CloseOpenFileDescriptorsInRange(int start, int limit) // Closes all the file descriptor in the range // start <= fd < limit. { int err; int fd; err = 0; for (fd = start; fd < limit; fd++) { err = close(fd); if (err == -1) { err = errno; } if (err == EBADF) { err = 0; } if (err != 0) { break; } } return err; } static int ResetAllSignalsToDefault(void) // Resets all signals to their default actions and the signal // mask to its default value (empty). If you use signals in // your program you should establish your signal handlers // after calling MoreSecDestroyInheritedEnvironment. { int err; int sig; sigset_t empty; // First set all of the signals to their default actions. err = 0; for (sig = 0; sig < NSIG; sig++) { if ( signal(sig, SIG_DFL) == SIG_ERR ) { if ( sig == 0 || sig == SIGKILL || sig == SIGSTOP) { // ignore the error } else { err = errno; break; } } } // Then set the signal mask to its default value (empty). if (err == 0) { err = sigemptyset(&empty); if (err == -1) { err = errno; } } if (err == 0) { err = sigprocmask(SIG_SETMASK, &empty, NULL); if (err == -1) { err = errno; } } return err; } // The following is a table of resource limits established // by this program. This table is based on the default values // from Mac OS X 10.1.x. Unfortunately there's no way to // determine the system-wide defaults programmatically. typedef struct { int resource; rlim_t rlim_cur; rlim_t rlim_max; } ResourceLimitTemplate; static ResourceLimitTemplate kResourceLimits[9] = { {RLIMIT_CPU, RLIM_INFINITY, RLIM_INFINITY}, {RLIMIT_FSIZE, RLIM_INFINITY, RLIM_INFINITY}, {RLIMIT_DATA, 0x600000, RLIM_INFINITY}, {RLIMIT_STACK, 0x80000, 0x4000000 }, {RLIMIT_CORE, 0, RLIM_INFINITY}, {RLIMIT_RSS, RLIM_INFINITY, RLIM_INFINITY}, {RLIMIT_MEMLOCK, RLIM_INFINITY, RLIM_INFINITY}, {RLIMIT_NPROC, 100, RLIM_INFINITY}, {RLIMIT_NOFILE, 256, RLIM_INFINITY} }; // The upper bound for RLIMIT_NPROC is really 100, // or 532 if you're EUID 0. // // The upper bound for RLIMIT_NOFILE is really 10240, // or 12288 if you're EUID 0. static const int kResourceLimitsCount = sizeof(kResourceLimits) / sizeof(kResourceLimits[0]); static int ResetAllResourceLimitsToDefault(void) // Resets all resource limits to their defaults, // based on the kResourceLimits table. Note that we // only attempt to set the rlim_max if we're EUID 0 // because getrlimit sometimes does not return the // real maximum limit [2941095]. { int err; int i; err = 0; for (i = 0; i < kResourceLimitsCount; i++) { struct rlimit thisLimit; err = getrlimit(kResourceLimits[i].resource, &thisLimit); if (err == -1) { err = errno; } if (err == 0) { thisLimit.rlim_cur = kResourceLimits[i].rlim_cur; if (geteuid() == 0) { thisLimit.rlim_max = kResourceLimits[i].rlim_max; } err = setrlimit(kResourceLimits[i].resource, &thisLimit); if (err == -1 ) { err = errno; } } if (err != 0) { break; } } return err; } static int ResetAllTimers(void) // Disables all interval timers that might have // been inherited from the parent process. { int err; struct itimerval disable; timerclear(&disable.it_interval); timerclear(&disable.it_value); err = setitimer(ITIMER_REAL, &disable, NULL); if (err == -1) { err = errno; } err = setitimer(ITIMER_VIRTUAL, &disable, NULL); if (err == -1) { err = errno; } err = setitimer(ITIMER_PROF, &disable, NULL); if (err == -1) { err = errno; } return err; } extern int MoreSecDestroyInheritedEnvironment(int whatToDubya, const char **argv) // See comment in header. { int err; assert( (argv != NULL) || ((whatToDubya & kMoreSecKeepArg0Mask) != 0) ); err = 0; if ( (err == 0) && !(whatToDubya & kMoreSecKeepArg0Mask) ) { err = ResetArgvZero(argv); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepEnvironmentMask) ) { err = ResetEnvironment(); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepStandardFilesMask) ) { err = CloseOpenFileDescriptorsInRange(0, 3); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepOtherFilesMask) ) { err = CloseOpenFileDescriptorsInRange( 3, getdtablesize()); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepSignalsMask) ) { err = ResetAllSignalsToDefault(); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepUmaskMask) ) { (void) umask(S_IRWXG | S_IRWXO); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepNiceMask) ) { err = nice(0); if (err == -1) { err = errno; } } if ( (err == 0) && !(whatToDubya & kMoreSecKeepResourceLimitsMask) ) { err = ResetAllResourceLimitsToDefault(); } if ( (err == 0) && !(whatToDubya & kMoreSecKeepCurrentDirMask) ) { err = chdir("/"); if (err == -1) { err = errno; } } if ( (err == 0) && !(whatToDubya & kMoreSecKeepTimersMask) ) { err = ResetAllTimers(); } return err; } ///////////////////////////////////////////////////////////////// #pragma mark ***** Helper Tool Common static OSStatus CopyDictionaryFromDescriptor(int fdIn, CFDictionaryRef *dictResult) // 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. // // See also the companion routine, WriteDictionaryToDescriptor, below. { OSStatus err; CFIndex dictSize; UInt8 * dictBuffer; CFDataRef dictData; CFPropertyListRef dict; assert(fdIn >= 0); assert( dictResult != NULL); assert(*dictResult == NULL); dictBuffer = NULL; dictData = NULL; dict = NULL; // Read the data size and allocate a buffer. err = EXXXToOSStatus( MoreUNIXRead(fdIn, &dictSize, sizeof(dictSize), NULL) ); if (err == noErr) { // Abitrary limit to prevent potentially hostile client overwhelming us with data. if (dictSize > (1 * 1024 * 1024)) { err = memFullErr; } } if (err == noErr) { dictBuffer = (UInt8 *) malloc( (size_t) dictSize); if (dictBuffer == NULL) { err = memFullErr; } } // Read the data and unflatten. if (err == noErr) { err = EXXXToOSStatus( MoreUNIXRead(fdIn, dictBuffer, (size_t) dictSize, NULL) ); } if (err == noErr) { dictData = CFDataCreateWithBytesNoCopy(NULL, dictBuffer, dictSize, kCFAllocatorNull); err = CFQError(dictData); } if (err == noErr) { dict = CFPropertyListCreateFromXMLData(NULL, dictData, kCFPropertyListImmutable, NULL); err = CFQError(dict); } if ( (err == noErr) && (CFGetTypeID(dict) != CFDictionaryGetTypeID()) ) { err = paramErr; // only CFDictionaries need apply } // CFShow(dict); // Clean up. if (err != noErr) { CFQRelease(dict); dict = NULL; } *dictResult = (CFDictionaryRef) dict; free(dictBuffer); CFQRelease(dictData); assert( (err == noErr) == (*dictResult != NULL) ); return err; } static OSStatus WriteDictionaryToDescriptor(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 CopyDictionaryFromDescriptor knows // how much to read. { OSStatus err; CFDataRef dictData; CFIndex dictSize; UInt8 * dictBuffer; assert(dict != NULL); assert(fdOut >= 0); dictData = NULL; dictBuffer = NULL; dictData = CFPropertyListCreateXMLData(NULL, dict); err = CFQError(dictData); // Allocate sizeof(size_t) extra bytes in the buffer so that we can // prepend the dictSize. This allows us to write the entire // dict with one MoreUNIXWrite call, which definitely speeds // things up, especially if this is was going over a real wire. // Of course, if I was to send this over a real wire, I would // have to guarantee that dictSize was sent in network byte order (-: if (err == noErr) { dictSize = CFDataGetLength(dictData); dictBuffer = (UInt8 *) malloc( sizeof(size_t) + dictSize ); if (dictBuffer == NULL) { err = memFullErr; } } if (err == noErr) { // Copy dictSize into the first size_t bytes of the buffer. *((size_t *) dictBuffer) = (size_t) dictSize; // Copy the data into the remaining bytes. // // Can't use CFDataGetBytePtr because there's no guarantee that // it will succeed. If it doesn't, we have to copy the bytes anyway, // so the allocation code has to exist. Given that this isn't a // performance critical path, I might as well minimise my code size by // always running the allocation code. CFDataGetBytes(dictData, CFRangeMake(0, dictSize), dictBuffer + sizeof(size_t)); err = EXXXToOSStatus( MoreUNIXWrite(fdOut, dictBuffer, sizeof(size_t) + dictSize, NULL) ); } free(dictBuffer); CFQRelease(dictData); return err; } extern int MoreSecErrorToHelperToolResult(int errNum) // See comment in header. { int result; switch (errNum) { case 0: result = 0; break; case errAuthorizationDenied: result = (kMoreSecResultPrivilegesErr - kMoreSecResultBase); break; case errAuthorizationCanceled: result = (kMoreSecResultCanceledErr - kMoreSecResultBase); break; default: if ( (errNum >= kMoreSecFirstResultErr) && (errNum <= kMoreSecLastResultErr) ) { result = (errNum - kMoreSecResultBase); } else { result = (kMoreSecResultInternalErrorErr - kMoreSecResultBase); } break; } return result; } extern int MoreSecHelperToolResultToError(int toolResult) // See comment in header. { int err; if (toolResult == 0) { err = 0; } else { if ( (toolResult > 0) && (toolResult <= (kMoreSecLastResultErr - kMoreSecResultBase)) ) { err = (toolResult + kMoreSecResultBase); } else { err = kMoreSecResultInternalErrorErr; } } return err; } ///////////////////////////////////////////////////////////////// #pragma mark ***** Implementation Helper Tool // Notes on Code Signing // --------------------- // I've decided *not* to implementing a digital signature verification // as part of this library. There are a number of technical reasons // that would make digitally signing the code tricky (such as prebinding), // but I believe that all of those are surmountable. The reasons I didn't // implementing digital signatures are: // // A) it doesn't improve the security if I implement it here, // B) it's extra work for me, and // C) its presence might lead folks to believe that this is more // secure than it really is. // // The most critical point is point A. I'll spend a little time explaining // that here. // // No matter what you do, the current AuthorizationExecuteWithPrivileges // model allows for security violations [3093666]. Specifically, AEWP lets // you run a non-privileged helper tool as if it was privileged. However, in // the time between the point where you call AEWP (at the point, SigCheck1, // below) and the point where the helper tool runs and changes its own // permissions to prevent tampering (SigCheck2), there's a window of opportunity // where an attacker can modify the tool at will, and the modified tool will be // run as root. They could, for example, open a read/write file descriptor // that allows them to modify the tool. Even if the tool checks its own // integrity with a digital signature (at the point SigCheck2, below), there's // no way it can revoke the read/write file descriptor, so the attacker could // just modify the tool after the digital signature check. // // Moreover, there are even simpler attacks. For example, an attacker // could just delete the application's current helper tool and replace // the application's template copy of the tool with its own. The application // will quite happily launch that tool, at which point the tool can use // AEWP to prompt for the admin password and can launch any program in // privileged mode. The user is not going to be be able to distinguish // between the attacker's tool call AEWP and the real helper tool. // // You could defeat this second attack by digitally signing the helper tool, // but that doesn't really help because if the attacker can change the // application program (which is a precondition of being able to substitute // a helper tool), they can replace your digital signature with theirs. // One way around this would be to use a certificate to verify the // authenticity of the digital signature, but that's beyond what I'm // prepared to do for sample code. // // So, rather than write a lot of code to provide a false sense of security, // I've decided to simply ignore the issue of digital signatures altogether. // File Modes // ---------- // The following is a declaration of constants that I use when setting and checking // the file mode (ie permissions). It's an extreme use of whitespace, but // I found the layout helpful. enum { // The mode_t that we set for the helper tool via fchmod. kSetHelperToolPerms = 0 // r-sr-xr-x | S_ISUID // | S_ISGID // | S_ISVTX | S_IRUSR // | S_IWUSR | S_IXUSR | S_IRGRP // | S_IWGRP | S_IXGRP | S_IROTH // | S_IWOTH | S_IXOTH , // The mode_t that we check, via stat, to see if the helper tool is valid. kRequiredHelperToolPerms = S_IFREG | kSetHelperToolPerms, kRequiredHelperToolMask = 0 | S_IFMT | S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO }; static int RepairOurPrivileges(const char *pathToSelf) // Self-repair code. We ran ourselves using AuthorizationExecuteWithPrivileges // so we need to make ourselves setuid root to avoid the need for this the // next time around. { int err; int junk; int fd; assert(pathToSelf != NULL); // We don't supply O_EXLOCK to open because that's only an advisory // lock so it doesn't buy us anything. [AuthSample makes the claim // that this lock is mandatory, but that's just wrong [3090303].] fd = open(pathToSelf, O_RDONLY, 0); err = MoreUNIXErrno(fd); if (err == 0) { // SigCheck2 // // If I was to implement digital signing of the code, this is the // second place I would check the signature. See note above for // more information on this. } if (err == 0) { // Switch to EUID 0 to do the chown/chmod. err = MoreSecSetPrivilegedEUID(); // Make it owned by root. if (err == 0) { // GID = -1 implies no change err = fchown(fd, 0, -1); err = MoreUNIXErrno(err); } // Force the mode flags. if (err == 0) { err = fchmod(fd, kRequiredHelperToolPerms); err = MoreUNIXErrno(err); } // Switch back to EUID != 0 once we're done. junk = MoreSecTemporarilySetNonPrivilegedEUID(); assert(junk == 0); } // Clean up. if (fd != -1) { junk = close(fd); assert(junk == 0); } return err; } static int ExecuteSelfInPrivilegedSelfRepairMode(int fdIn, int fdOut, AuthorizationRef auth, const char *pathToSelf) // Execute another copy of the tool in privileged mode via // AuthorizationExecuteWithPrivileges. Route the command request // from fdIn to the second instance of the tool, and route the // command response from the second instance of the tool to fdOut. { int err; int err2; int status; int junk; FILE * fileConnToChild; int fdConnToChild; pid_t childPID; static const char * const kSelfRepairArguments[] = { "--self-repair", NULL }; assert(fdIn >= 0); assert(fdOut >= 0); assert(auth != NULL); assert(pathToSelf != NULL); fileConnToChild = NULL; childPID = -1; err = 0; // SigCheck1 // // If I was to implement digital signing of the code, this is the // first place I would check the signature. See note above for // more information on this. if (err == 0) { #if MORE_DEBUG fprintf(stderr, "MoreSecurity: Calling AEWP\n"); #endif err = OSStatusToEXXX( AuthorizationExecuteWithPrivileges(auth, pathToSelf, kAuthorizationFlagDefaults, (char * const *) kSelfRepairArguments, &fileConnToChild) ); #if MORE_DEBUG fprintf(stderr, "MoreSecurity: AEWP returned %d\n", err); #endif // The cast for kSelfRepairArguments is required because of a bug in the prototype // for AuthorizationExecuteWithPrivileges [3090294]. } if (err == 0) { // Extract the descriptor for the returned FILE *. As we // never use the FILE * again and there is no data buffered // in it, it's safe for us to use the descriptor as if it had // never been embedded in a FILE *. fdConnToChild = fileno(fileConnToChild); err = MoreUNIXErrno(fdConnToChild); // Get the PID sent to us by the child. We need to do this because // AuthorizationExecuteWithPrivileges does not return us the child's // PID [3090277], and we need the child PID in order to properly wait // for the child to terminate. if (err == 0) { err = MoreUNIXRead(fdConnToChild, &childPID, sizeof(childPID), NULL); } // At this point we're just acting as a router between the application // and the second instance of the tool we launched using AEWP. All we // do is copy the request data to the tool, and then copy the result // back to the app. This works because we implement a simple // request/response protocol. If the protocol was more complex // (for example, if the tool handled multiple requests per // session), we would have to implement a more complex copying // algorithm using "select". if (err == 0) { err = MoreUNIXCopyDescriptorToDescriptor(fdIn, fdConnToChild); } // Close the write side of our connection to the child. We do this so that // if the child makes a mistake and tries to do a blocking read on its // input for more data that we're sending it, it will see the closed socket // and get EPIPE instead of blocking forever. if (err == 0) { err = shutdown(fdConnToChild, 1); err = MoreUNIXErrno(err); } // Copy the response back to the app. if (err == 0) { err = MoreUNIXCopyDescriptorToDescriptor(fdConnToChild, fdOut); } // Close the connection to the child, which also closes fdConnToChild. if (fileConnToChild != NULL) { junk = fclose(fileConnToChild); assert(junk == 0); } // Wait for the child to terminate. We have to do this, regardless // of whether we get an error, in order to clear the zombie process. // // Note that we don't get the pid of the child back from // AuthorizationExecuteWithPrivileges, so we have to have the child // send us its PID via fdConnToChild (see the MoreUNIXRead above). // Also note that there's no guarantee that the MoreUNIXRead will // execute without error, thus there's no guarantee that childPID // will be valid. We handle that by initialising childPID to -1 // in the error case, which makes "waitpid" work just like "wait", // that is, wait for any child to terminate. Of course, there's // no guarantee that in that case the terminating child will actually // be the child we launched with AEWP. That's sad, but its // the best we can do given the current problems with AEWP [3090277]. err2 = waitpid(childPID, &status, 0); err2 = MoreUNIXErrno(err); if (err == 0) { err = err2; } // If we successfully got a wait status from the client (or // our communications with the client failed because of // a generic communications error), let's go see whether // the child's wait status is a more appropriate source of // error information. if ( (err == 0) || (err == EPIPE) ) { if ( ! WIFEXITED(status) ) { // If we got a wait status but it's not a valid exit status (perhaps // WIFSIGNALED, indication that the child terminated because of a signal), // that's an unexpected error we can't handle. err = kMoreSecResultInternalErrorErr; } else { // If we got a valid exit status from the child, map its exit status // into our range so that we return an equivalent status. The helper // tool's main function can use MoreSecErrorToHelperToolResult to map // this back to a status code. err = MoreSecHelperToolResultToError(WEXITSTATUS(status)); } } } return err; } static int ReadAndDispatchCommand(int fdIn, int fdOut, AuthorizationRef auth, MoreSecCommandProc commandProc) // Read a command from fdIn, execute the command by calling commandProc, // and then return the response via fdOut. { int err; int junk; CFDictionaryRef request; CFDictionaryRef response; assert(fdIn >= 0); assert(fdOut >= 0); assert(auth != NULL); assert(commandProc != NULL); request = NULL; response = NULL; // Read the request and convert it a CFDictionary. err = OSStatusToEXXX( CopyDictionaryFromDescriptor(fdIn, &request) ); // Call the client's commandProc to actually execute the request. // Note that we automatically put the commandProc's function result // into a response dictionary if the commandProc hasn't already done so. // An error from the commandProc does not indicate a failure of the // helper tool itself. if (err == 0) { OSStatus commandErr; CFStringRef errorKey = kMoreSecErrorNumberKey; commandErr = commandProc(auth, (CFDictionaryRef) request, (CFDictionaryRef *) &response); // fprintf(stderr, "commandErr = %ld\n", commandErr); // If the commandProc switched to EUID 0, let's go back to EUID == RUID. junk = MoreSecTemporarilySetNonPrivilegedEUID(); assert(junk == 0); assert( (response == NULL) || (CFGetTypeID(response) == CFDictionaryGetTypeID()) ); if ( (response == NULL) || ! CFDictionaryContainsKey(response, errorKey) ) { CFNumberRef commandErrNum; commandErrNum = CFNumberCreate(NULL, kCFNumberSInt32Type, &commandErr); err = OSStatusToEXXX( CFQError(commandErrNum) ); if (err == 0) { if (response == NULL) { response = CFDictionaryCreate(NULL, (const void **) &errorKey, (const void **) &commandErrNum, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); err = OSStatusToEXXX( CFQError(response) ); } else { CFMutableDictionaryRef temp; temp = CFDictionaryCreateMutableCopy(NULL, 0, response); err = OSStatusToEXXX( CFQError(temp) ); if (err == 0) { CFDictionarySetValue(temp, errorKey, commandErrNum); CFQRelease(response); response = temp; } } } CFQRelease(commandErrNum); } } assert( (err == 0) == (response != NULL) ); // Pass the response back. if (err == 0) { err = OSStatusToEXXX( WriteDictionaryToDescriptor(response, fdOut) ); } CFQRelease(request); CFQRelease(response); return err; } extern AuthorizationRef MoreSecHelperToolCopyAuthRef(void) // See comment in header. { AuthorizationRef result; result = NULL; (void) AuthorizationCopyPrivilegedReference(&result, kAuthorizationFlagDefaults); return result; } extern int MoreSecHelperToolMain(int fdIn, int fdOut, AuthorizationRef auth, MoreSecCommandProc commandProc, int argc, const char *argv[]) // See comment in header. { int err; OSStatus junk; char * pathToSelf; Boolean privileged; assert(fdIn >= 0); assert(fdOut >= 0); assert(commandProc != NULL); assert( (argc == 1) || (argc == 2) ); assert(argv != NULL); assert(argv[0] != NULL); err = 0; pathToSelf = NULL; // Note whether we're privileged, and then switch the EUID to the RUID // so that the rest of the tool runs with a non-zero EUID unless it // specifically requests privileges via MoreSecSetPrivilegedEUID. // This makes things just a little bit safer. privileged = (geteuid() == 0); err = MoreSecTemporarilySetNonPrivilegedEUID(); // We need pathToSelf in both of the following cases, so let's get it here. if (err == 0) { err = GetPathToSelf(&pathToSelf); } // There are three cases: // // 1a. No command line arguments, privileged -- We can just call // ReadAndDispatchCommand to execute the request. // // 1b. No command line arguments, not privileged -- We need to self // repair by calling ExecuteSelfInPrivilegedSelfRepairMode. // This will launch another copy of the tool, and that copy // of the tool will actually execute the request using // ReadAndDispatchCommand. // // 2. --self-repair command line argument -- We got here by // step 1b above. We must be privileged. If not, bail out. // We make ourselve setuid root (RepairOurPrivileges) and then // execute the command via ReadAndDispatchCommand. if (err == 0) { if (argc == 1) { AuthorizationExternalForm extAuth; // The caller gave us a AuthorizationRef, but we're don't use // it in this case. Just throw it away. if (auth != NULL) { junk = AuthorizationFree(auth, kAuthorizationFlagDefaults); assert(junk == noErr); auth = NULL; } // Started directly by the application. Read the authorization // "byte blob" from our input, and use that to create our // AuthorizationRef. err = MoreUNIXRead(fdIn, &extAuth, sizeof(extAuth), NULL); if (err == 0) { err = OSStatusToEXXX( AuthorizationCreateFromExternalForm(&extAuth, &auth) ); } // If we're running as root, we can just read the command // and execute it. Otherwise, we have to self-repair. // Note that this will launch a second instance of this // tool, which is the one that actually reads and executes // the command. if (err == 0) { if (privileged) { err = ReadAndDispatchCommand(fdIn, fdOut, auth, commandProc); } else { err = ExecuteSelfInPrivilegedSelfRepairMode(fdIn, fdOut, auth, pathToSelf); } } } else if ( (argc == 2) && (strcmp(argv[1], "--self-repair") == 0) ) { pid_t myPID; // We get here if we've been launched in self-repair mode by // ourselves (see ExecuteSelfInPrivilegedSelfRepairMode). First we // send our parent our PID. This is needed for reasons that are // explained above (in ExecuteSelfInPrivilegedSelfRepairMode). Then we // verify that we're actually been run with EUID 0. Then we grab our // AuthorizationRef from our parent. Next we self-repair, that is, // make our executable setuid root so that the next time around we won't // need to run this code path. Finally, we actually read and // dispatch the command. // // Note that we don't read the authorization "byte // blob" because it's already been read by our parent. myPID = getpid(); err = MoreUNIXWrite(fdOut, &myPID, sizeof(myPID), NULL); if (err == 0 && ! privileged ) { err = kMoreSecResultParamErr; } if ( (err == 0) && (auth == NULL) ) { err = kMoreSecResultParamErr; } if (err == 0) { err = RepairOurPrivileges(pathToSelf); } if (err == 0) { err = ReadAndDispatchCommand(fdIn, fdOut, auth, commandProc); } } else { err = kMoreSecResultParamErr; } } // Clean up and pass results back to caller. free(pathToSelf); if (auth != NULL) { junk = AuthorizationFree(auth, kAuthorizationFlagDefaults); assert(junk == noErr); } return err; } ///////////////////////////////////////////////////////////////// #pragma mark ***** Calling Helper Tool extern OSStatus MoreSecIsFolderIgnoringPrivileges(const FSRef *folder, Boolean *ignoringPrivs) // See comment in header. { OSStatus err; OSStatus junk; int tries; FSRef fileRef; FSCatalogInfo info; static int kPermissionsGroupIDIndex = 1; static gid_t kPermissionsUnknownGroupID = 99; static gid_t kPermissionsStaffGroupID = 20; assert(folder != NULL); assert(ignoringPrivs != NULL); // Create a temporary file. tries = 1; do { AbsoluteTime now; CFStringRef tmpStr; HFSUniStr255 tmpStrU; now = UpTime(); tmpStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("MoreSecIsFolderIgnoringPrivileges Temp %lx%lx"), now.hi, now.lo); err = CFQError(tmpStr); if (err == noErr) { assert(CFStringGetLength(tmpStr) <= (sizeof(tmpStrU.unicode) / sizeof(UniChar)) ); tmpStrU.length = (UInt16) CFStringGetLength(tmpStr); CFStringGetCharacters(tmpStr, CFRangeMake(0, tmpStrU.length), tmpStrU.unicode); err = FSCreateFileUnicode(folder, tmpStrU.length, tmpStrU.unicode, kFSCatInfoNone, NULL, &fileRef, NULL); } CFQRelease(tmpStr); tries += 1; } while ( (tries < 1000) && (err == dupFNErr) ); // Probe that temporary file to see if permissions are being ignored. if (err == noErr) { err = FSGetCatalogInfo(&fileRef, kFSCatInfoPermissions, &info, NULL, NULL, NULL); if (err == noErr) { // If the FGID is not "unknown", then we already know that the volume // is not ignoring privileges. Otherwise we have to test. if (info.permissions[kPermissionsGroupIDIndex] != kPermissionsUnknownGroupID) { *ignoringPrivs = false; } else { // Change the FGID to "staff". If that change is effective, we're // not ignoring privileges. info.permissions[kPermissionsGroupIDIndex] = kPermissionsStaffGroupID; err = FSSetCatalogInfo(&fileRef, kFSCatInfoPermissions, &info); if (err == noErr) { err = FSGetCatalogInfo(&fileRef, kFSCatInfoPermissions, &info, NULL, NULL, NULL); } if (err == noErr) { *ignoringPrivs = (info.permissions[kPermissionsGroupIDIndex] != kPermissionsStaffGroupID); } } assert( (err != noErr) || (*ignoringPrivs == (info.permissions[kPermissionsGroupIDIndex] == kPermissionsUnknownGroupID)) ); } junk = FSDeleteObject(&fileRef); assert(junk == noErr); } return err; } static OSStatus MoreSecCheckHelperTool(CFURLRef templateTool, CFURLRef tool, Boolean *looksOK) // Checks that the working tool is a reasonably accurate copy of the // templateTool. This checks that the tool exists, is setuid root, // and has the same size and modification date as the template tool. { OSStatus err; char toolPath[MAXPATHLEN]; // 2K on the stack! char templateToolPath[MAXPATHLEN]; // I'm going to burn in hell. struct stat toolStat; struct stat templateToolStat; struct timeval toolStamp; struct timeval templateToolStamp; assert(templateTool != NULL); assert(tool != NULL); assert(looksOK != NULL); // Check that the template tool is present. If the template tool is missing, // we're doooommmmeeeedddd! err = CFQErrorBoolean( CFURLGetFileSystemRepresentation(templateTool, true, (UInt8 *)templateToolPath, sizeof(templateToolPath)) ); if (err == noErr) { err = stat(templateToolPath, &templateToolStat); err = MoreUNIXErrno(err); } // If we successfully found the template tool, go looking for the primary tool. err = CFQErrorBoolean( CFURLGetFileSystemRepresentation(tool, true, (UInt8 *)toolPath, sizeof(toolPath)) ); if (err == 0) { err = stat(toolPath, &toolStat); err = EXXXToOSStatus( MoreUNIXErrno(err) ); // If the primary tool is either missing, has dropped its // owner or setuid or permissions, or is the wrong size, // or the wrong time stamp (the last two checks help debugging), // then try to restore the tool. TIMESPEC_TO_TIMEVAL(&templateToolStamp, &templateToolStat.st_mtimespec); TIMESPEC_TO_TIMEVAL(&toolStamp, &toolStat.st_mtimespec); *looksOK = (err == noErr) && (toolStat.st_uid == 0) && ((toolStat.st_mode & kRequiredHelperToolMask) == kRequiredHelperToolPerms) && (toolStat.st_size == templateToolStat.st_size) && (toolStamp.tv_sec == templateToolStamp.tv_sec) && (toolStamp.tv_usec == toolStamp.tv_usec); err = noErr; } return err; } static OSStatus MoreSecCheckAndFixHelperTool(CFURLRef templateTool, CFURLRef tool) // Checks that the working tool is a reasonably accurate copy of the // templateTool, using MoreSecCheckHelperTool, and if these checks fail, // restores tool from the template tool. // // the tool referenced by templateTool must exist // // tool must not be NULL; if tool does not exist, the directory in // which tool would be contained must exist { int err; char toolPath[MAXPATHLEN]; // 2K on the stack! char templateToolPath[MAXPATHLEN]; // I'm going to burn in hell. struct stat toolStat; Boolean looksOK; assert(templateTool != NULL); assert(tool != NULL); err = MoreSecCheckHelperTool(templateTool, tool, &looksOK); if (err == noErr && !looksOK) { err = CFQErrorBoolean( CFURLGetFileSystemRepresentation(templateTool, true, (UInt8 *)templateToolPath, sizeof(templateToolPath)) ); if (err == noErr) { err = CFQErrorBoolean( CFURLGetFileSystemRepresentation(tool, true, (UInt8 *)toolPath, sizeof(toolPath)) ); } if (err == noErr) { if ( stat(toolPath, &toolStat) == 0 ) { err = unlink(toolPath); err = MoreUNIXErrno(err); if (err == EPERM) { // just in case the file name is being used by a directory err = rmdir(toolPath); err = MoreUNIXErrno(err); } } } if (err == noErr) { err = MoreUNIXCopyFile(templateToolPath, toolPath); } } return err; } static OSStatus MoreSecCopyHelperToolURL(short domain, OSType folder, CFStringRef subFolderName, CFStringRef toolName, Boolean createFolder, CFURLRef *tool) // Create a URL that points to a helper tool named toolName within the // Folder Manager folder specified by domain and folder. If subFolderName is // not NULL, the URL points to the tool within that folder, otherwise it // points to the tool directly within the Folder Manager folder. // The URL might point to a file that does not exist, but if the call // is successful then the parent folder will exist. // // If createFolder is false, this routine will not create any folders; // if it needs to create a folder, it will error instead. { OSStatus err; FSRef folderRef; CFURLRef folderURL; assert(toolName != NULL); assert( tool != NULL); assert(*tool == NULL); folderURL = NULL; err = FSFindFolder(domain, folder, createFolder, &folderRef); if (err == noErr && subFolderName != NULL) { FSRef tmp; HFSUniStr255 subFolderNameU; tmp = folderRef; // Extract the Unicode characters from subFolderName. assert(CFStringGetLength(subFolderName) <= (sizeof(subFolderNameU.unicode) / sizeof(UniChar)) ); subFolderNameU.length = (UInt16) CFStringGetLength(subFolderName); CFStringGetCharacters(subFolderName, CFRangeMake(0, subFolderNameU.length), subFolderNameU.unicode); // If the sub-folder doesn't exist, try to create it. We can't just create it // and ignore the dupFNErr if it already exists because we need to set up // folderRef. err = FSMakeFSRefUnicode(&tmp, subFolderNameU.length, subFolderNameU.unicode, kTextEncodingUnknown, &folderRef); if (err != noErr && createFolder) { err = FSCreateDirectoryUnicode(&tmp, subFolderNameU.length, subFolderNameU.unicode, kFSCatInfoNone, NULL, &folderRef, NULL, NULL); } } // Create a URL to the parent folder, then append the tool name. if (err == noErr) { folderURL = CFURLCreateFromFSRef(NULL, &folderRef); err = CFQError(folderURL); } if (err == noErr) { *tool = CFURLCreateCopyAppendingPathComponent(NULL, folderURL, toolName, false); err = CFQError(*tool); } CFQRelease(folderURL); assert( (err == noErr) == (*tool != NULL) ); return err; } extern OSStatus MoreSecCopyHelperToolURLAndCheck(CFURLRef templateTool, OSType folder, CFStringRef subFolderName, CFStringRef toolName, CFURLRef *tool, Boolean *toolFound) // See comment in header. { OSStatus err; CFURLRef result; UInt32 domainIndex; Boolean found; static const SInt16 kFolderDomains[] = {kUserDomain, kLocalDomain, kNetworkDomain, kSystemDomain, 0}; assert(templateTool != NULL); assert(toolName != NULL); assert( tool != NULL); assert(*tool == NULL); result = NULL; // For each folder domain, check whether there's an appropriate helper tool // present. This allows a sysadmin to put the helper tool in any of the // "Application Support" folders (ie ~/Library, /Library, /Network/Library, // /System/Library) and we'll find it and run without trying to create // another copy of the tool. found = false; domainIndex = 0; do { assert(result == NULL); // Note that we pass false to the createFolder parameter of the // MoreSecCopyHelperToolURL so that it doesn't create any folders // that are missing. That's because at this stage we're just looking // for the file, not trying to create it. err = MoreSecCopyHelperToolURL(kFolderDomains[domainIndex], folder, subFolderName, toolName, false, &result); if (err == noErr) { err = MoreSecCheckHelperTool(templateTool, result, &found); } err = noErr; // we don't care about the specific error, just whether we found the tool or not if (!found) { CFQRelease(result); result = NULL; } domainIndex += 1; } while (!found && kFolderDomains[domainIndex] != 0); // At this point either we found the tool, and result is set to its // URL, or we haven't found the tool and result is NULL. assert( err == noErr ); assert( found == (result != NULL) ); if (found) { // Do nothing; result will be copied out to *tool during clean up (below). } else { FSRef folderRef; Boolean ignoringPrivs; // Let's try to create the tool in the user's "Application Support" folder // (ie ~/Library/Application Support). However, before we do that, make sure // that the user isn't ignoring privileges on the volume containing that folder. // If so, we have no idea where to put the tool, so we fail with a very specific // error code. err = FSFindFolder(kUserDomain, folder, true, &folderRef); if (err == noErr) { err = MoreSecIsFolderIgnoringPrivileges(&folderRef, &ignoringPrivs); } if (err == noErr && ignoringPrivs) { err = kMoreSecIgnoringPrivsInFolderErr; } // Now we pass true to the createFolder parameter of MoreSecCopyHelperToolURL // because we want to create the tool (and any enclosing folders). if (err == noErr) { err = MoreSecCopyHelperToolURL(kUserDomain, folder, subFolderName, toolName, true, &result); } if (err == noErr) { err = MoreSecCheckAndFixHelperTool(templateTool, result); } } // Clean up. if (err == noErr) { *tool = result; } else { CFQRelease(result); } assert( (err == 0) == (*tool != NULL) ); if (toolFound != NULL) *toolFound = found; return err; } extern OSStatus MoreSecCopyHelperToolURLAndCheckBundled(CFBundleRef inBundle, CFStringRef templateToolName, OSType folder, CFStringRef subFolderName, CFStringRef toolName, CFURLRef *tool, Boolean *toolFound) // See comment in header. { OSStatus err; CFURLRef templateTool; assert(inBundle != NULL); assert(templateToolName != NULL); assert(toolName != NULL); assert( tool != NULL); assert(*tool == NULL); // Lots of folks use CFBundleCopyResourceURL, but CFBundleCopyAuxiliaryExecutableURL // is preferred if the resource is an executable because it allows for bundles // to contain multiple different types of executable (Mach-O and CFM, for example). templateTool = CFBundleCopyAuxiliaryExecutableURL(inBundle, templateToolName); err = CFQError(templateTool); if (err == noErr) { err = MoreSecCopyHelperToolURLAndCheck(templateTool, folder, subFolderName, toolName, tool, toolFound); } // Clean up. CFQRelease(templateTool); assert( (err == 0) == (*tool != NULL) ); return err; } extern OSStatus MoreSecExecuteRequestInHelperTool(CFURLRef helperTool, AuthorizationRef auth, CFDictionaryRef request, CFDictionaryRef *response) // See comment in header. { OSStatus err; OSStatus err2; int junk; char toolPath[MAXPATHLEN]; AuthorizationExternalForm extAuth; // spot the Mac OS type! int fdChild; int fdParent; int childPID; int status; assert(helperTool != NULL); assert(auth != NULL); assert(request != NULL); assert( response != NULL); assert(*response == NULL); childPID = -1; fdChild = -1; fdParent = -1; // Preparatory work. Stuff we want to do before forking, like getting the // tool's path and creating auth's external form. If either of these fail, // we want to bail out before the fork. err = CFQErrorBoolean( CFURLGetFileSystemRepresentation(helperTool, true, (UInt8 *)toolPath, sizeof(toolPath)) ); if (err == noErr) { err = AuthorizationMakeExternalForm(auth, &extAuth); } // Create a pair of anonymous UNIX domain sockets for communication between // the us and the tool. Name them fdChild and fdParent, just to make things // clear. It does't make any difference which is which because UNIX domain // sockets are bidirectional. if (err == noErr) { int comm[2]; err = socketpair(AF_UNIX, SOCK_STREAM, 0, comm); err = EXXXToOSStatus( MoreUNIXErrno(err) ); if (err == noErr) { fdChild = comm[0]; fdParent = comm[1]; } } // Fork. In the child, replace stdin and stdout with fdChild // (bidirectional, remember). Then close the child's extra // copy of fdChild and fdParent. Finally, exec the helper tool. // Execution continues in the child's "main" function, which // calls MoreSecHelperToolMain (defined above). if (err == noErr) { childPID = fork(); if (childPID == 0) { // Child err = dup2(fdChild, STDIN_FILENO); err = EXXXToOSStatus( MoreUNIXErrno(err) ); if (err == noErr) { err = dup2(fdChild, STDOUT_FILENO); err = EXXXToOSStatus( MoreUNIXErrno(err) ); } if (err == noErr) { junk = close(fdChild); assert(junk == 0); junk = close(fdParent); assert(junk == 0); err = execl(toolPath, toolPath, NULL); err = EXXXToOSStatus( MoreUNIXErrno(err) ); } assert(err != noErr); // otherwise we wouldn't be here // Use "_exit" rather than "exit" because we're still in the // same address space as the parent, so exit's closing // of stdio streams will not be helpful. _exit(MoreSecErrorToHelperToolResult(OSStatusToEXXX(err))); assert(false); // unreached } else if (childPID == -1) { // Error, Parent err = EXXXToOSStatus( MoreUNIXErrno(childPID) ); } else { assert(childPID > 0); // Parent } } // In the parent, things are a little more complex. First we // close our redundant copy of fdChild. Then we sent the // authorization external form to the child via the socket. // Finally, we send the request to the child, also via the // socket. if (fdChild != -1) { junk = close(fdChild); assert(junk == 0); fdChild = -1; } if (err == noErr) { err = EXXXToOSStatus( MoreUNIXWrite(fdParent, &extAuth, sizeof(extAuth), NULL) ); } if (err == noErr) { err = WriteDictionaryToDescriptor(request, fdParent); } // Close the write side of our connection to the child. // We need to this because it self repair mode // (ExecuteSelfInPrivilegedSelfRepairMode) the child simply // copies its input to its output, and doesn't leave its copy // loop until EOF on input. This shutdown triggers that EOF. if (err == noErr) { err = shutdown(fdParent, 1); err = EXXXToOSStatus( MoreUNIXErrno(err) ); } // Read the response back from the child. if (err == noErr) { err = CopyDictionaryFromDescriptor(fdParent, response); } // We're all done with our socket, so close it. It's important // that we do this here, before the waitpid, so that, if the child // is broken and is block waiting on input, it'll get an EPIPE // and quit. if (fdParent != -1) { junk = close(fdParent); junk = MoreUNIXErrno(junk); assert(junk == 0); fdParent = -1; } // If we started a child, we have to reap it, always, regardless of // whether we have encountered an error so far. if (childPID != -1) { err2 = waitpid(childPID, &status, 0); err2 = EXXXToOSStatus( MoreUNIXErrno(err2) ); if (err == noErr) { err = err2; } // If we successfully got a wait status from the client (or // our communications with the client failed because of // a generic communications error), let's go see whether // the child's wait status is a more appropriate source of // error information. if ( (err == noErr) || (err == EPIPE) ) { if ( ! WIFEXITED(status) ) { // If we got a wait status but it's not a valid exit status (perhaps // WIFSIGNALED, indication that the child terminated because of a signal), // that's an unexpected error we can't handle. err = kMoreSecResultInternalErrorErr; } else { // If we got a valid exit status from the child, map its exit status // into our range so that we return an equivalent status. err = EXXXToOSStatus( MoreSecHelperToolResultToError(WEXITSTATUS(status)) ); } } } if (err != noErr) { CFQRelease(*response); *response = NULL; } assert( (err == noErr) == (*response != NULL) ); return err; } extern OSStatus MoreSecGetErrorFromResponse(CFDictionaryRef response) // See comment in header. { OSStatus err; CFNumberRef num; OSStatus tmp; assert(response != NULL); num = (CFNumberRef) CFDictionaryGetValue(response, kMoreSecErrorNumberKey); err = CFQError(num); if (err == noErr) { err = CFQErrorBoolean( CFNumberGetValue(num, kCFNumberSInt32Type, &tmp) ); } if (err == noErr) { err = OSStatusToEXXX(tmp); } return err; }