/* * agentclient.cpp * SecurityAgent * * Copyright (c) 2002 Apple Computer, Inc.. All rights reserved. * */ #include "agentclient.h" #include /* For now all the calls into agentclient will be synchronous, with timeouts On a timeout, we will return control to the client, but we really need to send the appropriate abort right there and then, otherwise they'll need to call the same method again to check that the reply still isn't there. If we receive a reply that is not confirming attempts to abort, we'll process these and return them to the caller. Alternatively, there can be an answer that isn't the answer we expected: setError, where the server aborts the transaction. We can't support interrupt() with a synchronous interface unless we add some notification that let's the client know that the "server" is dead */ #include #include #include #include // for size of replies #include #include #include #include #include #include using Security::MachPlusPlus::VMGuard; using LowLevelMemoryUtilities::increment; using LowLevelMemoryUtilities::difference; using Security::DataWalkers::walk; using Authorization::AuthItemSet; using Authorization::AuthItemRef; using Authorization::AuthValueOverlay; namespace SecurityAgent { class CheckingReconstituteWalker { public: CheckingReconstituteWalker(void *ptr, void *base, size_t size) : mBase(base), mLimit(increment(base, size)), mOffset(difference(ptr, base)) { } template void operator () (T &obj, size_t size = sizeof(T)) { } template void operator () (T * &addr, size_t size = sizeof(T)) { blob(addr, size); } template void blob(T * &addr, size_t size) { DEBUGWALK("checkreconst:*"); if (addr) { if (addr < mBase || increment(addr, size) > mLimit) MacOSError::throwMe(errAuthorizationInternal); addr = increment(addr, mOffset); } } static const bool needsRelinking = true; static const bool needsSize = false; private: void *mBase; // old base address void *mLimit; // old last byte address + 1 off_t mOffset; // relocation offset }; template void relocate(T *obj, T *base, size_t size) { if (obj) { CheckingReconstituteWalker w(obj, base, size); walk(w, base); } } void Client::check(mach_msg_return_t returnCode) { // first check the Mach IPC return code switch (returnCode) { case MACH_MSG_SUCCESS: // peachy break; case MIG_SERVER_DIED: // explicit can't-send-it's-dead CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION); default: // some random Mach error MachPlusPlus::Error::throwMe(returnCode); } } void Client::checkResult() { // now check the OSStatus return from the server side switch (result()) { case kAuthorizationResultAllow: return; case kAuthorizationResultDeny: case kAuthorizationResultUserCanceled: CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED); default: MacOSError::throwMe(errAuthorizationInternal); } } extern "C" boolean_t secagentreply_server(mach_msg_header_t *, mach_msg_header_t *); //extern "C" struct sa_reply_server_secagentreply_subsystem sa_reply_server_secagentreply_subsystem; #pragma mark administrative operations Client::Client() : mActive(false), mState(init), mDesktopUid(0), mKeepAlive(false), mAgentName("com.apple.SecurityAgent") { } Client::Client(uid_t clientUID, Bootstrap clientBootstrap, const char *name) : mActive(false), mState(init), mDesktopUid(clientUID), mKeepAlive(false), mAgentName(name), mClientBootstrap(clientBootstrap) { } // // Get the port for the SecurityAgent. // Start it if necessary (and possible). Throw an exception if we can't get to it. // Sets mServerPort on success. // void Client::establishServer() { // if the server is already running, we're done if (mServerPort = mClientBootstrap.lookupOptional(mAgentName.c_str())) return; #if defined(AGENTNAME) && defined(AGENTPATH) // switch the bootstrap port to that of the logged-in user Security::MachPlusPlus::StBootstrap bootSaver(mClientBootstrap); // try to start the agent switch (pid_t pid = fork()) { case 0: // child { // Setup the environment for the SecurityAgent unsetenv("USER"); unsetenv("LOGNAME"); unsetenv("HOME"); // tell agent which name to register setenv("AGENTNAME", mAgentName.c_str(), 1); if (mDesktopUid) // if the user is running as root, or we're not told what uid to use, we stick with what we are { struct group *grent = getgrnam("nobody"); gid_t desktopGid = grent ? grent->gr_gid : unsigned(-2); //@@@ questionable endgrent(); secdebug("SAclnt", "setgid(%d)", desktopGid); setgid(desktopGid); // switch to login-user gid secdebug("SAclnt", "setuid(%d)", mDesktopUid); // Must be setuid and not seteuid since we do not want the agent to be able // to call seteuid(0) successfully. setuid(mDesktopUid); // switch to login-user uid } // close down any files that might have been open at this point int maxDescriptors = getdtablesize (); int i; for (i = 3; i < maxDescriptors; ++i) { // XXX/cs should dup(2) to /dev/null close (i); } // construct path to SecurityAgent char agentExecutable[PATH_MAX + 1]; const char *path = getenv("SECURITYAGENT"); if (!path) path = AGENTPATH; snprintf(agentExecutable, sizeof(agentExecutable), "%s/Contents/MacOS/" AGENTNAME, path); secdebug("SAclnt", "execl(%s)", agentExecutable); execl(agentExecutable, agentExecutable, NULL); secdebug("SAclnt", "execl of SecurityAgent failed, errno=%d", errno); // Unconditional suicide follows. // See comments below on why we can't use abort() #if 1 _exit(1); #else // NOTE: OS X abort() is implemented as kill(getuid()), which fails // for a setuid-root process that has setuid'd. Go back to root to die... setuid(0); abort(); #endif } case -1: // error (in parent) UnixError::throwMe(); default: // parent { static const int timeout = 300; secdebug("SAclnt", "Starting security agent (%d seconds timeout)", timeout); struct timespec rqtp; memset(&rqtp, 0, sizeof(rqtp)); rqtp.tv_nsec = 100000000; /* 10^8 nanaseconds = 1/10th of a second */ for (int n = timeout; n > 0; nanosleep(&rqtp, NULL), n--) { if (mServerPort = mClientBootstrap.lookupOptional(mAgentName.c_str())) break; int status; switch (IFDEBUG(pid_t rc =) waitpid(pid, &status, WNOHANG)) { case 0: // child still running continue; case -1: // error switch (errno) { case EINTR: case EAGAIN: // transient continue; case ECHILD: // no such child (dead; already reaped elsewhere) secdebug("SAclnt", "child is dead (reaped elsewhere)"); CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION); default: secdebug("SAclnt", "waitpid failed: errno=%d", errno); UnixError::throwMe(); } default: assert(rc == pid); secdebug("SAclnt", "child died without claiming the SecurityAgent port"); CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION); } } if (mServerPort == 0) { // couldn't contact Security Agent secdebug("SAclnt", "Autolaunch failed"); CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION); } secdebug("SAclnt", "SecurityAgent located"); return; } } #endif // well, this didn't work. Too bad secdebug("SAclnt", "Cannot contact SecurityAgent"); CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION); //@@@ or INTERNAL_ERROR? } OSStatus Client::activate() { if (!mActive) { // set mServerPort establishServer(); if (!mServerPort) MacOSError::throwMe(errAuthorizationInternal); // create reply port mClientPort.allocate(); //implicit MACH_PORT_RIGHT_RECEIVE // we need the send right for passing it to the agent to talk back mClientPort.insertRight(MACH_MSG_TYPE_MAKE_SEND); // get notified if the server dies (shouldn't happen, but...) mServerPort.requestNotify(mClientPort, MACH_NOTIFY_DEAD_NAME, true); // register with agentclients Clients::gClients().insert(this); // ready mActive = true; } return noErr; } Client::~Client() { teardown(); } void Client::setState(PluginState inState) { // validate state transition: might be more useful where change is requested if that implies anything to interpreting what to do. { // Mutex mState = inState; } } int Client::state() { return mState; } void Client::teardown() throw() { Clients::gClients().remove(this); if (mActive) { mServerPort.deallocate(); mStagePort.deallocate(); mClientPort.destroy(); mActive = false; } } AuthItemSet Client::clientHints(CodeSigning::OSXCode *clientCode, pid_t clientPid, uid_t clientUid) { AuthItemSet clientHints; string encodedBundle = clientCode->encode(); char bundleType = (encodedBundle.c_str())[0]; // yay, no accessor SecurityAgent::RequestorType requestorType; string bundlePath = clientCode->canonicalPath(); switch(bundleType) { case 'b': requestorType = SecurityAgent::bundle; break; case 't': requestorType = SecurityAgent::tool; break; default: requestorType = SecurityAgent::unknown; break; } clientHints.insert(AuthItemRef(AGENT_HINT_CLIENT_TYPE, AuthValueOverlay(sizeof(requestorType), &requestorType))); clientHints.insert(AuthItemRef(AGENT_HINT_CLIENT_PATH, AuthValueOverlay(bundlePath))); clientHints.insert(AuthItemRef(AGENT_HINT_CLIENT_PID, AuthValueOverlay(sizeof(clientPid), &clientPid))); clientHints.insert(AuthItemRef(AGENT_HINT_CLIENT_UID, AuthValueOverlay(sizeof(clientUid), &clientUid))); return clientHints; } #pragma mark request operations OSStatus Client::create(const char *inPluginId, const char *inMechanismId, const SessionId inSessionId ) { activate(); secdebug("agentclient", "asking server at port %d to create %s:%s; replies to %d", mServerPort.port(), inPluginId, inMechanismId, mClientPort.port()); // XXX/cs kern_return_t ret = sa_request_client_create(mServerPort, mClientPort, inSessionId, inPluginId, inMechanismId); if (ret) { // XXX/cs any conditions we could handle from here? return ret; } // wait for message (either didCreate, reportError) try { receive(); } catch (...) { return errAuthorizationInternal; } // verify that we got didCreate if (state() == created) return noErr; // and not reportError if (state() == dead) return Client::getError(); // something we don't deal with secdebug("agentclient", "we got an error on create"); // XXX/cs return errAuthorizationInternal; } // client maintains their own copy of the current data OSStatus Client::invoke( const Authorization::AuthValueVector& inArguments, const Authorization::AuthItemSet& inHints, const Authorization::AuthItemSet& inContext) { if ((state() != created) && (state() != active)) return errAuthorizationInternal; AuthorizationValueVector *arguments; AuthorizationItemSet *hints, *context; size_t argumentSize, hintSize, contextSize; inHints.copy(hints, hintSize); inContext.copy(context, contextSize); inArguments.copy(&arguments, &argumentSize); check(sa_request_client_invoke(mStagePort.port(), arguments, argumentSize, arguments, // data, size, offset hints, hintSize, hints, context, contextSize, context)); receive(); switch(state()) { case active: switch(result()) { case kAuthorizationResultUndefined: MacOSError::throwMe(errAuthorizationInternal); default: return noErr; } case dead: return mErrorState; } return errAuthorizationInternal; } OSStatus Client::deactivate() { // check state is current if (state() != current) return errAuthorizationInternal; secdebug("agentclient", "deactivating mechanism at request port %d", mStagePort.port()); // tell mechanism to deactivate check(sa_request_client_deactivate(mStagePort.port())); setState(deactivating); receive(); // didDeactivate? // setResult? // server died? // reportError? // if failed destroy it return noErr; } OSStatus Client::destroy() { // check state is active or created if ((state() == current) && deactivate()) return errAuthorizationInternal; if (state() == active || state() == created) { secdebug("agentclient", "destroying mechanism at request port %d", mStagePort.port()); // tell mechanism to destroy check(sa_request_client_destroy(mStagePort.port())); setState(dead); return noErr; } return errAuthorizationInternal; } // destroy plugin and don't care for how it feels about that. // return an error if that didn't work so the entire host can get shot down OSStatus Client::terminate() { check(sa_request_client_terminate(mServerPort.port())); return noErr; } void Client::receive() { try { // maximum known message size (variable sized elements are already forced OOL) Message in(sizeof(union __ReplyUnion__sa_reply_client_secagentreply_subsystem)); Message out(sizeof(union __ReplyUnion__sa_reply_client_secagentreply_subsystem)); in.receive(mClientPort, 0, 0); // got the message, now demux it; call secagentreply_server to handle any call // this is asynchronous, so no reply message, although not apparent if (!secagentreply_server(in, out)) { // @@@ port death notification? -> (can't talk to user error) // message not recognized, chances of the right message coming back are zero MacOSError::throwMe(errAuthorizationInternal); } } catch (Security::MachPlusPlus::Error &e) { secdebug("agentclient", "interpret error %ul", e.error); check(e.error); } catch (...) { MacOSError::throwMe(errAuthorizationInternal); } } #pragma mark result operations void Client::setResult(const AuthorizationResult inResult, const AuthorizationItemSet *inHints, const AuthorizationItemSet *inContext) { // construct AuthItemSet for hints and context (deep copy - previous contents are released) mHints = (*inHints); mContext = (*inContext); mResult = inResult; setState(active); } void Client::setError(const OSStatus inMechanismError) { setState(dead); mErrorState = inMechanismError; // setMessageType(reportError); setError(mechanismError); } OSStatus Client::getError() { return mErrorState; } void Client::requestInterrupt() { // check state for active // change state setState(interrupting); // hint at engine that it's time to stop // XXX add a callback to report this // if successfully stopped setState(created); // if not able to restart, abort the evaluation - we'll be expecting the destroy } void Client::didDeactivate() { // check state for deactivating // change state setState(active); } void Client::didCreate(const mach_port_t inStagePort) { setStagePort(inStagePort); setState(created); } #pragma mark client instances ModuleNexus Clients::gClients; bool Clients::compare(const Client * client, mach_port_t instance) { if (client->instance() == instance) return true; return false; } // throw so the agent client operation is aborted Client& Clients::find(mach_port_t instanceReplyPort) const { StLock _(mLock); for (set::const_iterator foundClient = mClients.begin(); foundClient != mClients.end(); foundClient++) { Client *client = *foundClient; if (client->instance() == instanceReplyPort) return *client; } // can't be receiving for a client we didn't create MacOSError::throwMe(errAuthorizationInternal); } #pragma mark demux requests replies // external C symbols for the mig message handling code to call into extern "C" { #define COPY_IN(type,name) type *name, mach_msg_type_number_t name##Length, type *name##Base //#define COPY_OUT(type,name) type **name, mach_msg_type_number_t *name##Length, type **name##Base // callbacks that key off instanceReplyPort to find the right agentclient instance // to deliver the message to. // they make the data readable to the receiver (relocate internal references) kern_return_t sa_reply_server_didCreate(mach_port_t instanceReplyPort, mach_port_t instanceRequestPort) { secdebug("agentclient", "got didCreate at port %u; requests go to port %u", instanceReplyPort, instanceRequestPort); Clients::gClients().find(instanceReplyPort).didCreate(instanceRequestPort); return KERN_SUCCESS; } kern_return_t sa_reply_server_setResult(mach_port_t instanceReplyPort, AuthorizationResult result, COPY_IN(AuthorizationItemSet,inHints) , COPY_IN(AuthorizationItemSet,inContext) ) { VMGuard _(inHints, inHintsLength); VMGuard _2(inContext, inContextLength); secdebug("agentclient", "got setResult at port %u; result %ld", instanceReplyPort, result); // relink internal references according to current place in memory try { relocate(inHints, inHintsBase, inHintsLength); } catch (MacOSError &e) { return e.osStatus(); } catch (...) { return errAuthorizationInternal; } try { relocate(inContext, inContextBase, inContextLength); } catch (MacOSError &e) { return e.osStatus(); } catch (...) { return errAuthorizationInternal; } Clients::gClients().find(instanceReplyPort).setResult(result, inHints, inContext); return KERN_SUCCESS; } kern_return_t sa_reply_server_requestInterrupt(mach_port_t instanceReplyPort) { secdebug("agentclient", "got requestInterrupt at port %u", instanceReplyPort); Clients::gClients().find(instanceReplyPort).requestInterrupt(); return KERN_SUCCESS; } kern_return_t sa_reply_server_didDeactivate(mach_port_t instanceReplyPort) { secdebug("agentclient", "got didDeactivate at port %u", instanceReplyPort); Clients::gClients().find(instanceReplyPort).didDeactivate(); return KERN_SUCCESS; } kern_return_t sa_reply_server_reportError(mach_port_t instanceReplyPort, OSStatus status) { secdebug("agentclient", "got reportError at port %u; error is %ld", instanceReplyPort, status); Clients::gClients().find(instanceReplyPort).setError(status); return KERN_SUCCESS; } } /* end extern "C" */ } /* end namesapce Authorization */