--- /dev/null
+/*
+ * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved.
+ *
+ * The contents of this file constitute Original Code as defined in and are
+ * subject to the Apple Public Source License Version 1.2 (the 'License').
+ * You may not use this file except in compliance with the License. Please obtain
+ * a copy of the License at http://www.apple.com/publicsource and read it before
+ * using this file.
+ *
+ * This Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
+ * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
+ * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
+ * specific language governing rights and limitations under the License.
+ */
+
+
+#include "MDSSession.h"
+
+#include <security_cdsa_plugin/DbContext.h>
+#include "MDSModule.h"
+#include "MDSAttrParser.h"
+#include "MDSAttrUtils.h"
+
+#include <memory>
+#include <Security/cssmerr.h>
+#include <security_utilities/logging.h>
+#include <security_utilities/debugging.h>
+#include <security_utilities/cfutilities.h>
+#include <security_cdsa_client/dlquery.h>
+#include <securityd_client/ssclient.h>
+#include <Security/mds_schema.h>
+#include <CoreFoundation/CFBundle.h>
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <assert.h>
+#include <time.h>
+#include <string>
+#include <unistd.h>
+
+using namespace CssmClient;
+
+
+/*
+ * The layout of the various MDS DB files on disk is as follows:
+ *
+ * /var/db/mds -- owner = root, mode = 01777, world writable, sticky
+ * system/ -- owner = root, mode = 0755
+ * mdsObject.db -- owner = root, mode = 0644, object DB
+ * mdsDirectory.db -- owner = root, mode = 0644, MDS directory DB
+ * mds.lock -- temporary, owner = root, protects creation of and
+ * updates to previous two files
+ * mds.install.lock -- owner = root, protects MDS_Install operation
+ * <uid>/ -- owner = <uid>, mode = 0700
+ * mdsObject.db -- owner = <uid>, mode = 000, object DB
+ * mdsDirectory.db -- owner = <uid>, mode = 000, MDS directory DB
+ * mds.lock -- owner = <uid>, protects updates of previous two files
+ *
+ * The /var/db/mds/system directory is created at OS install time. The DB files in
+ * it are created by root at MDS_Install time. The ownership and mode of this directory and
+ * of its parent is also re-checked and forced to the correct state at MDS_Install time.
+ * Each user has their own private directory with two DB files and a lock. The first time
+ * a user accesses MDS, the per-user directory is created and the per-user DB files are
+ * created as copies of the system DB files. Fcntl() with a F_RDLCK is used to lock the system
+ * DB files when they are the source of these copies; this is the same mechanism
+ * used by the underlying AtomicFile.
+ *
+ * The sticky bit in /var/db/mds ensures that users cannot modify other userss private
+ * MDS directories.
+ */
+namespace Security
+{
+
+/*
+ * Nominal location of Security.framework.
+ */
+#define MDS_SYSTEM_PATH "/System/Library/Frameworks"
+#define MDS_SYSTEM_FRAME "Security.framework"
+
+/*
+ * Nominal location of standard plugins.
+ */
+#define MDS_BUNDLE_PATH "/System/Library/Security"
+#define MDS_BUNDLE_EXTEN ".bundle"
+
+
+/*
+ * Location of MDS database and lock files.
+ */
+#define MDS_BASE_DB_DIR "/private/var/db/mds"
+#define MDS_SYSTEM_DB_COMP "system"
+#define MDS_SYSTEM_DB_DIR MDS_BASE_DB_DIR "/" MDS_SYSTEM_DB_COMP
+#define MDS_USER_DB_COMP "mds"
+
+#define MDS_LOCK_FILE_NAME "mds.lock"
+#define MDS_INSTALL_LOCK_NAME "mds.install.lock"
+#define MDS_OBJECT_DB_NAME "mdsObject.db"
+#define MDS_DIRECT_DB_NAME "mdsDirectory.db"
+
+#define MDS_INSTALL_LOCK_PATH MDS_SYSTEM_DB_DIR "/" MDS_INSTALL_LOCK_NAME
+#define MDS_OBJECT_DB_PATH MDS_SYSTEM_DB_DIR "/" MDS_OBJECT_DB_NAME
+#define MDS_DIRECT_DB_PATH MDS_SYSTEM_DB_DIR "/" MDS_DIRECT_DB_NAME
+
+/* hard coded modes and a symbolic UID for root */
+#define MDS_BASE_DB_DIR_MODE (mode_t)0755
+#define MDS_SYSTEM_DB_DIR_MODE (mode_t)0755
+#define MDS_SYSTEM_DB_MODE (mode_t)0644
+#define MDS_USER_DB_DIR_MODE (mode_t)0700
+#define MDS_USER_DB_MODE (mode_t)0600
+#define MDS_SYSTEM_UID (uid_t)0
+
+/*
+ * Location of per-user bundles, relative to home directory.
+ * Per-user DB files are in MDS_BASE_DB_DIR/<uid>/.
+ */
+#define MDS_USER_BUNDLE "Library/Security"
+
+/* time to wait in ms trying to acquire lock */
+#define DB_LOCK_TIMEOUT (2 * 1000)
+
+/* Minimum interval, in seconds, between rescans for plugin changes */
+#define MDS_SCAN_INTERVAL 5
+
+/* trace file I/O */
+#define MSIoDbg(args...) secdebug("MDS_IO", ## args)
+
+/* Trace cleanDir() */
+#define MSCleanDirDbg(args...) secdebug("MDS_CleanDir", ## args)
+
+static std::string GetMDSBaseDBDir(bool isRoot)
+{
+ // what we return depends on whether or not we are root
+ string retValue;
+ if (isRoot)
+ {
+ retValue = MDS_SYSTEM_DB_DIR;
+ }
+ else
+ {
+ char strBuffer[PATH_MAX + 1];
+ confstr(_CS_DARWIN_USER_CACHE_DIR, strBuffer, sizeof(strBuffer));
+ retValue = strBuffer;
+ }
+
+ return retValue;
+}
+
+
+
+static std::string GetMDSDBDir()
+{
+ string retValue;
+ bool isRoot = geteuid() == 0;
+
+ if (isRoot)
+ {
+ retValue = MDS_SYSTEM_DB_DIR;
+ }
+ else
+ {
+ retValue = GetMDSBaseDBDir(isRoot) + "/" + MDS_USER_DB_COMP;
+ }
+
+ return retValue;
+}
+
+
+
+static std::string GetMDSObjectDBPath()
+{
+ return GetMDSDBDir() + "/" + MDS_OBJECT_DB_NAME;
+}
+
+
+
+static std::string GetMDSDirectDBPath()
+{
+ return GetMDSDBDir() + "/" + MDS_DIRECT_DB_PATH;
+}
+
+
+
+static std::string GetMDSDBLockPath()
+{
+ return GetMDSDBDir() + "/" + MDS_LOCK_FILE_NAME;
+}
+
+
+
+/*
+ * Given a path to a directory, remove everything in the directory except for the optional
+ * keepFileNames. Returns 0 on success, else an errno.
+ */
+static int cleanDir(
+ const char *dirPath,
+ const char **keepFileNames, // array of strings, size numKeepFileNames
+ unsigned numKeepFileNames)
+{
+ DIR *dirp;
+ struct dirent *dp;
+ char fullPath[MAXPATHLEN];
+ int rtn = 0;
+
+ MSCleanDirDbg("cleanDir(%s) top", dirPath);
+ if ((dirp = opendir(dirPath)) == NULL) {
+ rtn = errno;
+ MSCleanDirDbg("opendir(%s) returned %d", dirPath, rtn);
+ return rtn;
+ }
+
+ for(;;) {
+ bool skip = false;
+ const char *d_name = NULL;
+
+ /* this block is for breaking on unqualified entries */
+ do {
+ errno = 0;
+ dp = readdir(dirp);
+ if(dp == NULL) {
+ /* end of directory or error */
+ rtn = errno;
+ if(rtn) {
+ MSCleanDirDbg("cleanDir(%s): readdir err %d", dirPath, rtn);
+ }
+ break;
+ }
+ d_name = dp->d_name;
+
+ /* skip "." and ".." */
+ if( (d_name[0] == '.') &&
+ ( (d_name[1] == '\0') ||
+ ( (d_name[1] == '.') && (d_name[2] == '\0') ) ) ) {
+ skip = true;
+ break;
+ }
+
+ /* skip entries in keepFileNames */
+ for(unsigned dex=0; dex<numKeepFileNames; dex++) {
+ if(!strcmp(keepFileNames[dex], d_name)) {
+ skip = true;
+ break;
+ }
+ }
+ } while(0);
+ if(rtn || (dp == NULL)) {
+ /* one way or another, we're done */
+ break;
+ }
+ if(skip) {
+ /* try again */
+ continue;
+ }
+
+ /* We have an entry to delete. Delete it, or recurse. */
+
+ snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, d_name);
+ if(dp->d_type == DT_DIR) {
+ /* directory. Clean it, then delete. */
+ MSCleanDirDbg("cleanDir recursing for dir %s", fullPath);
+ rtn = cleanDir(fullPath, NULL, 0);
+ if(rtn) {
+ break;
+ }
+ MSCleanDirDbg("cleanDir deleting dir %s", fullPath);
+ if(rmdir(fullPath)) {
+ rtn = errno;
+ MSCleanDirDbg("unlink(%s) returned %d", fullPath, rtn);
+ break;
+ }
+ }
+ else {
+ MSCleanDirDbg("cleanDir deleting file %s", fullPath);
+ if(unlink(fullPath)) {
+ rtn = errno;
+ MSCleanDirDbg("unlink(%s) returned %d", fullPath, rtn);
+ break;
+ }
+ }
+
+ /*
+ * Back to beginning of directory for clean scan.
+ * Normally we'd just do a rewinddir() here but the RAMDisk filesystem,
+ * used when booting from DVD, does not implement that properly.
+ */
+ closedir(dirp);
+ if ((dirp = opendir(dirPath)) == NULL) {
+ rtn = errno;
+ MSCleanDirDbg("opendir(%s) returned %d", dirPath, rtn);
+ return rtn;
+ }
+ } /* main loop */
+
+ closedir(dirp);
+ return rtn;
+}
+
+/*
+ * Determine if a file exists as regular file with specified owner. Returns true if so.
+ * If the purge argument is true, and there is something at the specified path that
+ * doesn't meet spec, we do everything we can to delete it. If that fails we throw
+ * CssmError(CSSM_ERRCODE_MDS_ERROR). If the delete succeeds we return false.
+ * Returns the stat info on success for further processing by caller.
+ */
+static bool doesFileExist(
+ const char *filePath,
+ uid_t forUid,
+ bool purge,
+ struct stat &sb) // RETURNED
+{
+ MSIoDbg("stat %s in doesFileExist", filePath);
+ if(lstat(filePath, &sb)) {
+ /* doesn't exist or we can't even get to it. */
+ if(errno == ENOENT) {
+ return false;
+ }
+ if(purge) {
+ /* If we can't stat it we sure can't delete it. */
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ return false;
+ }
+
+ /* it's there...how does it look? */
+ mode_t fileType = sb.st_mode & S_IFMT;
+ if((fileType == S_IFREG) && (sb.st_uid == forUid)) {
+ return true;
+ }
+ if(!purge) {
+ return false;
+ }
+
+ /* not what we want: get rid of it. */
+ if(fileType == S_IFDIR) {
+ /* directory: clean then remove */
+ if(cleanDir(filePath, NULL, 0)) {
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ if(rmdir(filePath)) {
+ MSDebug("rmdir(%s) returned %d", filePath, errno);
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ }
+ else {
+ if(unlink(filePath)) {
+ MSDebug("unlink(%s) returned %d", filePath, errno);
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ }
+
+ /* caller should be somewhat happy */
+ return false;
+}
+
+/*
+ * Determine if both of the specified DB files exist as accessible regular files with specified
+ * owner. Returns true if they do.
+ *
+ * If the purge argument is true, we'll ensure that either both files exist with
+ * the right owner, or neither of the files exist on exit. An error on that operation
+ * throws a CSSM_ERRCODE_MDS_ERROR CssmError exception (i.e., we're hosed).
+ * Returns the stat info for both files on success for further processing by caller.
+ */
+static bool doFilesExist(
+ const char *objDbFile,
+ const char *directDbFile,
+ uid_t forUid,
+ bool purge, // false means "passive" check
+ struct stat &objDbSb, // RETURNED
+ struct stat &directDbSb) // RETURNED
+
+{
+ bool objectExist = doesFileExist(objDbFile, forUid, purge, objDbSb);
+ bool directExist = doesFileExist(directDbFile, forUid, purge, directDbSb);
+ if(objectExist && directExist) {
+ return true;
+ }
+ else if(!purge) {
+ return false;
+ }
+
+ /*
+ * At least one does not exist - ensure neither of them do.
+ * Note that if we got this far, we know the one that exists is a regular file
+ * so it's safe to just unlink it.
+ */
+ if(objectExist) {
+ if(unlink(objDbFile)) {
+ MSDebug("unlink(%s) returned %d", objDbFile, errno);
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ }
+ if(directExist) {
+ if(unlink(directDbFile)) {
+ MSDebug("unlink(%s) returned %d", directDbFile, errno);
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ }
+ return false;
+}
+
+/*
+ * Determine if specified directory exists with specified owner and mode.
+ * Returns true if copacetic, else returns false and also indicates
+ * via the directStatus out param what went wrong.
+ */
+typedef enum {
+ MDS_NotPresent, /* nothing there */
+ MDS_NotDirectory, /* not a directory */
+ MDS_BadOwnerMode, /* wrong owner or mode */
+ MDS_Access /* couldn't search the directories */
+} MdsDirectStatus;
+
+static bool doesDirectExist(
+ const char *dirPath,
+ uid_t forUid,
+ mode_t mode,
+ MdsDirectStatus &directStatus) /* RETURNED */
+{
+ struct stat sb;
+
+ MSIoDbg("stat %s in doesDirectExist", dirPath);
+ if (lstat(dirPath, &sb)) {
+ int err = errno;
+ switch(err) {
+ case EACCES:
+ directStatus = MDS_Access;
+ break;
+ case ENOENT:
+ directStatus = MDS_NotPresent;
+ break;
+ /* Any others? Is this a good SWAG to handle the default? */
+ default:
+ directStatus = MDS_NotDirectory;
+ break;
+ }
+ return false;
+ }
+ mode_t fileType = sb.st_mode & S_IFMT;
+ if(fileType != S_IFDIR) {
+ directStatus = MDS_NotDirectory;
+ return false;
+ }
+ if(sb.st_uid != forUid) {
+ directStatus = MDS_BadOwnerMode;
+ return false;
+ }
+ if((sb.st_mode & 07777) != mode) {
+ directStatus = MDS_BadOwnerMode;
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Create specified directory if it doesn't already exist. If there is something
+ * already there that doesn't meet spec (not a directory, wrong mode, wrong owner)
+ * we'll do everything we can do delete what is there and then try to create it
+ * correctly.
+ *
+ * Returns an errno on any unrecoverable error.
+ */
+static int createDir(
+ const char *dirPath,
+ uid_t forUid, // for checking - we don't try to set this
+ mode_t dirMode)
+{
+ MdsDirectStatus directStatus;
+
+ if(doesDirectExist(dirPath, forUid, dirMode, directStatus)) {
+ /* we're done */
+ return 0;
+ }
+
+ /*
+ * Attempt recovery if there is *something* there.
+ * Anything other than "not present" should be considered to be a possible
+ * attack; syslog it.
+ */
+ int rtn;
+ switch(directStatus) {
+ case MDS_NotPresent:
+ /* normal trivial case: proceed. */
+ break;
+
+ case MDS_NotDirectory:
+ /* there's a file or symlink in the way */
+ if(unlink(dirPath)) {
+ rtn = errno;
+ MSDebug("createDir(%s): unlink() returned %d", dirPath, rtn);
+ return rtn;
+ }
+ break;
+
+ case MDS_BadOwnerMode:
+ /*
+ * It's a directory; try to clean it out (which may well fail if we're
+ * not root).
+ */
+ rtn = cleanDir(dirPath, NULL, 0);
+ if(rtn) {
+ return rtn;
+ }
+ if(rmdir(dirPath)) {
+ rtn = errno;
+ MSDebug("createDir(%s): rmdir() returned %d", dirPath, rtn);
+ return rtn;
+ }
+ /* good to go */
+ break;
+
+ case MDS_Access: /* hopeless */
+ MSDebug("createDir(%s): access failure, bailing", dirPath);
+ return EACCES;
+ }
+ rtn = mkdir(dirPath, dirMode);
+ if(rtn) {
+ rtn = errno;
+ MSDebug("createDir(%s): mkdir() returned %d", dirPath, errno);
+ }
+ else {
+ /* make sure umask does't trick us */
+ rtn = chmod(dirPath, dirMode);
+ if(rtn) {
+ MSDebug("chmod(%s) returned %d", dirPath, errno);
+ }
+ }
+ return rtn;
+}
+
+/*
+ * Create an MDS session.
+ */
+MDSSession::MDSSession (const Guid *inCallerGuid,
+ const CSSM_MEMORY_FUNCS &inMemoryFunctions) :
+ DatabaseSession(MDSModule::get().databaseManager()),
+ mCssmMemoryFunctions (inMemoryFunctions),
+ mModule(MDSModule::get())
+{
+ MSDebug("MDSSession::MDSSession");
+
+ mCallerGuidPresent = inCallerGuid != nil;
+ if (mCallerGuidPresent) {
+ mCallerGuid = *inCallerGuid;
+ }
+}
+
+MDSSession::~MDSSession ()
+{
+ MSDebug("MDSSession::~MDSSession");
+}
+
+void
+MDSSession::terminate ()
+{
+ MSDebug("MDSSession::terminate");
+ closeAll();
+}
+
+const char* kExceptionDeletePath = "messages";
+
+
+/*
+ * Called by security server via MDS_Install().
+ */
+void
+MDSSession::install ()
+{
+ //
+ // Installation requires root
+ //
+ if(geteuid() != (uid_t)0) {
+ CssmError::throwMe(CSSMERR_DL_OS_ACCESS_DENIED);
+ }
+
+ //
+ // install() is only (legitimately) called from securityd.
+ // Mark "server mode" so we don't end up waiting for ourselves when the databases open.
+ //
+ mModule.setServerMode();
+
+ int sysFdLock = -1;
+ try {
+ /* ensure MDS base directory exists with correct permissions */
+ if(createDir(MDS_BASE_DB_DIR, MDS_SYSTEM_UID, MDS_BASE_DB_DIR_MODE)) {
+ MSDebug("Error creating base MDS dir; aborting.");
+ CssmError::throwMe(CSSMERR_DL_OS_ACCESS_DENIED);
+ }
+
+ /* ensure the the system MDS DB directory exists with correct permissions */
+ if(createDir(MDS_SYSTEM_DB_DIR, MDS_SYSTEM_UID, MDS_SYSTEM_DB_DIR_MODE)) {
+ MSDebug("Error creating system MDS dir; aborting.");
+ CssmError::throwMe(CSSMERR_DL_OS_ACCESS_DENIED);
+ }
+
+ if(!obtainLock(MDS_INSTALL_LOCK_PATH, sysFdLock, DB_LOCK_TIMEOUT)) {
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+
+ /*
+ * We own the whole MDS system. Clean everything out except for our lock
+ * (and the directory it's in :-)
+ */
+
+ const char *savedFile = MDS_INSTALL_LOCK_NAME;
+ if(cleanDir(MDS_SYSTEM_DB_DIR, &savedFile, 1)) {
+ /* this should never happen - we're root */
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+
+ const char *savedFiles[] = {MDS_SYSTEM_DB_COMP, kExceptionDeletePath};
+ if(cleanDir(MDS_BASE_DB_DIR, savedFiles, 2)) {
+ /* this should never happen - we're root */
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+
+ /*
+ * Do initial population of system DBs.
+ */
+ createSystemDatabases(CSSM_FALSE, MDS_SYSTEM_DB_MODE);
+ DbFilesInfo dbFiles(*this, MDS_SYSTEM_DB_DIR);
+ dbFiles.updateSystemDbInfo(MDS_SYSTEM_PATH, MDS_BUNDLE_PATH);
+ }
+ catch(...) {
+ if(sysFdLock != -1) {
+ releaseLock(sysFdLock);
+ }
+ throw;
+ }
+ releaseLock(sysFdLock);
+}
+
+//
+// In this implementation, the uninstall() call is not supported since
+// we do not allow programmatic deletion of the MDS databases.
+//
+
+void
+MDSSession::uninstall ()
+{
+ CssmError::throwMe(CSSM_ERRCODE_FUNCTION_NOT_IMPLEMENTED);
+}
+
+/*
+ * Common private open routine given a full specified path.
+ */
+CSSM_DB_HANDLE MDSSession::dbOpen(const char *dbName, bool batched)
+{
+ static CSSM_APPLEDL_OPEN_PARAMETERS batchOpenParams = {
+ sizeof(CSSM_APPLEDL_OPEN_PARAMETERS),
+ CSSM_APPLEDL_OPEN_PARAMETERS_VERSION,
+ CSSM_FALSE, // do not auto-commit
+ 0 // mask - do not use following fields
+ };
+
+ MSDebug("Opening %s%s", dbName, batched ? " in batch mode" : "");
+ MSIoDbg("open %s in dbOpen(name, batched)", dbName);
+ CSSM_DB_HANDLE dbHand;
+ DatabaseSession::DbOpen(dbName,
+ NULL, // DbLocation
+ CSSM_DB_ACCESS_READ,
+ NULL, // AccessCred - hopefully optional
+ batched ? &batchOpenParams : NULL,
+ dbHand);
+ return dbHand;
+}
+
+/* DatabaseSession routines we need to override */
+void MDSSession::DbOpen(const char *DbName,
+ const CSSM_NET_ADDRESS *DbLocation,
+ CSSM_DB_ACCESS_TYPE AccessRequest,
+ const AccessCredentials *AccessCred,
+ const void *OpenParameters,
+ CSSM_DB_HANDLE &DbHandle)
+{
+ if (!mModule.serverMode()) {
+ /*
+ * Make sure securityd has finished initializing (system) MDS data.
+ * Note that activate() only does IPC once and retains global state after that.
+ */
+ SecurityServer::ClientSession client(Allocator::standard(), Allocator::standard());
+ client.activate(); /* contact securityd - won't return until MDS is ready */
+ }
+
+ /* make sure DBs are up-to-date */
+ updateDataBases();
+
+ /*
+ * Only task here is map incoming DbName - specified in the CDSA
+ * spec - to a filename we actually use (which is a path to either
+ * a system MDS DB file or a per-user MDS DB file).
+ */
+ if(DbName == NULL) {
+ CssmError::throwMe(CSSMERR_DL_INVALID_DB_NAME);
+ }
+ const char *dbName;
+ if(!strcmp(DbName, MDS_OBJECT_DIRECTORY_NAME)) {
+ dbName = MDS_OBJECT_DB_NAME;
+ }
+ else if(!strcmp(DbName, MDS_CDSA_DIRECTORY_NAME)) {
+ dbName = MDS_DIRECT_DB_NAME;
+ }
+ else {
+ CssmError::throwMe(CSSMERR_DL_INVALID_DB_NAME);
+ }
+ char fullPath[MAXPATHLEN];
+ dbFullPath(dbName, fullPath);
+ MSIoDbg("open %s in dbOpen(name, loc, accessReq...)", dbName);
+ DatabaseSession::DbOpen(fullPath, DbLocation, AccessRequest, AccessCred,
+ OpenParameters, DbHandle);
+}
+
+CSSM_HANDLE MDSSession::DataGetFirst(CSSM_DB_HANDLE DBHandle,
+ const CssmQuery *Query,
+ CSSM_DB_RECORD_ATTRIBUTE_DATA_PTR Attributes,
+ CssmData *Data,
+ CSSM_DB_UNIQUE_RECORD_PTR &UniqueId)
+{
+ updateDataBases();
+ return DatabaseSession::DataGetFirst(DBHandle, Query, Attributes, Data, UniqueId);
+}
+
+
+void
+MDSSession::GetDbNames(CSSM_NAME_LIST_PTR &outNameList)
+{
+ outNameList = new CSSM_NAME_LIST[1];
+ outNameList->NumStrings = 2;
+ outNameList->String = new char*[2];
+ outNameList->String[0] = MDSCopyCstring(MDS_OBJECT_DIRECTORY_NAME);
+ outNameList->String[1] = MDSCopyCstring(MDS_CDSA_DIRECTORY_NAME);
+}
+
+void
+MDSSession::FreeNameList(CSSM_NAME_LIST &inNameList)
+{
+ delete [] inNameList.String[0];
+ delete [] inNameList.String[1];
+ delete [] inNameList.String;
+}
+
+void MDSSession::GetDbNameFromHandle(CSSM_DB_HANDLE DBHandle,
+ char **DbName)
+{
+ printf("GetDbNameFromHandle: code on demand\n");
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+}
+
+//
+// Attempt to obtain an exclusive lock over the the MDS databases. The
+// parameter is the maximum amount of time, in milliseconds, to spend
+// trying to obtain the lock. A value of zero means to return failure
+// right away if the lock cannot be obtained.
+//
+bool
+MDSSession::obtainLock(
+ const char *lockFile, // e.g. MDS_INSTALL_LOCK_PATH
+ int &fd, // IN/OUT
+ int timeout) // default 0
+{
+ fd = -1;
+ for(;;) {
+ secdebug("mdslock", "obtainLock: calling open(%s)", lockFile);
+ fd = open(lockFile, O_EXLOCK | O_CREAT | O_RDWR, 0644);
+ if(fd == -1) {
+ int err = errno;
+ secdebug("mdslock", "obtainLock: open error %d", errno);
+ if(err == EINTR) {
+ /* got a signal, go again */
+ continue;
+ }
+ else {
+ /* theoretically should never happen */
+ return false;
+ }
+ }
+ else {
+ secdebug("mdslock", "obtainLock: success");
+ return true;
+ }
+ }
+
+ /* not reached */
+ return false;
+}
+
+//
+// Release the exclusive lock over the MDS databases. If this session
+// does not hold the lock, this method does nothing.
+//
+
+void
+MDSSession::releaseLock(int &fd)
+{
+ secdebug("mdslock", "releaseLock");
+ assert(fd != -1);
+ flock(fd, LOCK_UN);
+ close(fd);
+ fd = -1;
+}
+
+/* given DB file name, fill in fully specified path */
+void MDSSession::dbFullPath(
+ const char *dbName,
+ char fullPath[MAXPATHLEN+1])
+{
+ mModule.getDbPath(fullPath);
+ assert(fullPath[0] != '\0');
+ strcat(fullPath, "/");
+ strcat(fullPath, dbName);
+}
+
+/*
+ * See if any per-user bundles exist in specified directory. Returns true if so.
+ * First the check for one entry....
+ */
+static bool isBundle(
+ const struct dirent *dp)
+{
+ if(dp == NULL) {
+ return false;
+ }
+ /* NFS directories show up as DT_UNKNOWN */
+ switch(dp->d_type) {
+ case DT_UNKNOWN:
+ case DT_DIR:
+ break;
+ default:
+ return false;
+ }
+ int suffixLen = strlen(MDS_BUNDLE_EXTEN);
+ int len = strlen(dp->d_name);
+
+ return (len >= suffixLen) &&
+ !strcmp(dp->d_name + len - suffixLen, MDS_BUNDLE_EXTEN);
+}
+
+/* now the full directory search */
+static bool checkUserBundles(
+ const char *bundlePath)
+{
+ MSDebug("searching for user bundles in %s", bundlePath);
+ DIR *dir = opendir(bundlePath);
+ if (dir == NULL) {
+ return false;
+ }
+ struct dirent *dp;
+ bool rtn = false;
+ while ((dp = readdir(dir)) != NULL) {
+ if(isBundle(dp)) {
+ /* any other checking to do? */
+ rtn = true;
+ break;
+ }
+ }
+ closedir(dir);
+ MSDebug("...%s bundle(s) found", rtn ? "" : "No");
+ return rtn;
+}
+
+#define COPY_BUF_SIZE 1024
+
+/*
+ * Single file copy with locking.
+ * Ensures that the source is a regular file with specified owner.
+ * Caller specifies mode of destination file.
+ * Throws a CssmError if the source file doesn't meet spec; throws a
+ * UnixError on any other error (which is generally recoverable by
+ * having the user MDS session use the system DB files).
+ */
+static void safeCopyFile(
+ const char *fromPath,
+ uid_t fromUid,
+ const char *toPath,
+ mode_t toMode)
+{
+ int error = 0;
+ bool haveLock = false;
+ int destFd = 0;
+ int srcFd = 0;
+ struct stat sb;
+ char tmpToPath[MAXPATHLEN+1];
+
+ MSIoDbg("open %s, %s in safeCopyFile", fromPath, toPath);
+
+ if(!doesFileExist(fromPath, fromUid, false, sb)) {
+ MSDebug("safeCopyFile: bad system DB file %s", fromPath);
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+
+ /* create temp destination */
+ snprintf(tmpToPath, sizeof(tmpToPath), "%s_", toPath);
+ destFd = open(tmpToPath, O_WRONLY | O_APPEND | O_CREAT | O_TRUNC | O_EXCL, toMode);
+ if(destFd < 0) {
+ error = errno;
+ MSDebug("Error %d opening user DB file %s\n", error, tmpToPath);
+ UnixError::throwMe(error);
+ }
+
+ struct flock fl;
+ try {
+ /* don't get tripped up by umask */
+ if(fchmod(destFd, toMode)) {
+ error = errno;
+ MSDebug("Error %d chmoding user DB file %s\n", error, tmpToPath);
+ UnixError::throwMe(error);
+ }
+
+ /* open source for reading */
+ srcFd = open(fromPath, O_RDONLY, 0);
+ if(srcFd < 0) {
+ error = errno;
+ MSDebug("Error %d opening system DB file %s\n", error, fromPath);
+ UnixError::throwMe(error);
+ }
+
+ /* acquire the same kind of lock AtomicFile uses */
+ fl.l_start = 0;
+ fl.l_len = 1;
+ fl.l_pid = getpid();
+ fl.l_type = F_RDLCK; // AtomicFile gets F_WRLCK
+ fl.l_whence = SEEK_SET;
+
+ // Keep trying to obtain the lock if we get interupted.
+ for (;;) {
+ if (::fcntl(srcFd, F_SETLKW, &fl) == -1) {
+ error = errno;
+ if (error == EINTR) {
+ error = 0;
+ continue;
+ }
+ MSDebug("Error %d locking system DB file %s\n", error, fromPath);
+ UnixError::throwMe(error);
+ }
+ else {
+ break;
+ haveLock = true;
+ }
+ }
+
+ /* copy */
+ char buf[COPY_BUF_SIZE];
+ while(1) {
+ int bytesRead = read(srcFd, buf, COPY_BUF_SIZE);
+ if(bytesRead == 0) {
+ break;
+ }
+ if(bytesRead < 0) {
+ error = errno;
+ MSDebug("Error %d reading system DB file %s\n", error, fromPath);
+ UnixError::throwMe(error);
+ }
+ int bytesWritten = write(destFd, buf, bytesRead);
+ if(bytesWritten < 0) {
+ error = errno;
+ MSDebug("Error %d writing user DB file %s\n", error, tmpToPath);
+ UnixError::throwMe(error);
+ }
+ }
+ }
+ catch(...) {
+ /* error is nonzero, we'll re-throw below...still have some cleanup */
+ }
+
+ /* unlock source and close both */
+ if(haveLock) {
+ fl.l_type = F_UNLCK;
+ if (::fcntl(srcFd, F_SETLK, &fl) == -1) {
+ MSDebug("Error %d unlocking system DB file %s\n", errno, fromPath);
+ }
+ }
+ MSIoDbg("close %s, %s in safeCopyFile", fromPath, tmpToPath);
+ if(srcFd) {
+ close(srcFd);
+ }
+ if(destFd) {
+ close(destFd);
+ }
+ if(error == 0) {
+ /* commit temp file */
+ if(::rename(tmpToPath, toPath)) {
+ error = errno;
+ MSDebug("Error %d committing %s\n", error, toPath);
+ }
+ }
+ if(error) {
+ UnixError::throwMe(error);
+ }
+}
+
+/*
+ * Copy system DB files to specified user dir. Caller holds user DB lock.
+ * Throws a UnixError on error.
+ */
+static void copySystemDbs(
+ const char *userDbFileDir)
+{
+ char toPath[MAXPATHLEN+1];
+
+ snprintf(toPath, sizeof(toPath), "%s/%s", userDbFileDir, MDS_OBJECT_DB_NAME);
+ safeCopyFile(MDS_OBJECT_DB_PATH, MDS_SYSTEM_UID, toPath, MDS_USER_DB_MODE);
+ snprintf(toPath, sizeof(toPath), "%s/%s", userDbFileDir, MDS_DIRECT_DB_NAME);
+ safeCopyFile(MDS_DIRECT_DB_PATH, MDS_SYSTEM_UID, toPath, MDS_USER_DB_MODE);
+}
+
+/*
+ * Ensure current DB files exist and are up-to-date.
+ * Called from MDSSession constructor and from DataGetFirst, DbOpen, and any
+ * other public functions which access a DB from scratch.
+ */
+void MDSSession::updateDataBases()
+{
+ RecursionBlock::Once once(mUpdating);
+ if (once())
+ return; // already updating; don't recurse
+
+ uid_t ourUid = geteuid();
+ bool isRoot = (ourUid == 0);
+
+ /* if we scanned recently, we're done */
+ double delta = mModule.timeSinceLastScan();
+ if(delta < (double)MDS_SCAN_INTERVAL) {
+ return;
+ }
+
+ /*
+ * If we're root, the first thing we do is to ensure that system DBs are present.
+ * Note that this is a necessary artifact of the problem behind Radar 3800811.
+ * When that is fixed, install() should ONLY be called from the public MDS_Install()
+ * routine.
+ * Anyway, if we *do* have to install here, we're done.
+ */
+ if(isRoot && !systemDatabasesPresent(false)) {
+ install();
+ mModule.setDbPath(MDS_SYSTEM_DB_DIR);
+ mModule.lastScanIsNow();
+ return;
+ }
+
+ /*
+ * Obtain various per-user paths. Root is a special case but follows most
+ * of the same logic from here on.
+ */
+ std::string userDBFileDir = GetMDSDBDir();
+ std::string userObjDBFilePath = GetMDSObjectDBPath();
+ std::string userDirectDBFilePath = GetMDSDirectDBPath();
+ char userBundlePath[MAXPATHLEN+1];
+ std::string userDbLockPath = GetMDSDBLockPath();
+
+ /* this means "no user bundles" */
+ userBundlePath[0] = '\0';
+ if(!isRoot) {
+ char *userHome = getenv("HOME");
+ if((userHome == NULL) ||
+ (strlen(userHome) + strlen(MDS_USER_BUNDLE) + 2) > sizeof(userBundlePath)) {
+ /* Can't check for user bundles */
+ MSDebug("missing or invalid HOME; skipping user bundle check");
+ }
+ /* TBD: any other checking of userHome? */
+ else {
+ snprintf(userBundlePath, sizeof(userBundlePath),
+ "%s/%s", userHome, MDS_USER_BUNDLE);
+ }
+ }
+
+ /*
+ * Create the per-user directory...that's where the lock we'll be using lives.
+ */
+ if(!isRoot) {
+ if(createDir(userDBFileDir.c_str(), ourUid, MDS_USER_DB_DIR_MODE)) {
+ /*
+ * We'll just have to limp along using the read-only system DBs.
+ * Note that this protects (somewhat) against the DoS attack in
+ * Radar 3801292. The only problem is that this user won't be able
+ * to use per-user bundles.
+ */
+ MSDebug("Error creating user DBs; using system DBs");
+ mModule.setDbPath(MDS_SYSTEM_DB_DIR);
+ return;
+ }
+ }
+
+ /* always release userLockFd no matter what happens */
+ int userLockFd = -1;
+ if(!obtainLock(userDbLockPath.c_str(), userLockFd, DB_LOCK_TIMEOUT)) {
+ CssmError::throwMe(CSSM_ERRCODE_MDS_ERROR);
+ }
+ try {
+ if(!isRoot) {
+ try {
+ /*
+ * We copy the system DBs to the per-user DBs in two cases:
+ * -- user DBs don't exist, or
+ * -- system DBs have changed since the the last update to the user DBs.
+ * This happens on smart card insertion and removal.
+ */
+ bool doCopySystem = false;
+ struct stat userObjStat, userDirectStat;
+ if(!doFilesExist(userObjDBFilePath.c_str(), userDirectDBFilePath.c_str(), ourUid, true,
+ userObjStat, userDirectStat)) {
+ doCopySystem = true;
+ }
+ else {
+ /* compare the two mdsDirectory.db files */
+ MSIoDbg("stat %s, %s in updateDataBases",
+ MDS_DIRECT_DB_PATH, userDirectDBFilePath.c_str());
+ struct stat sysStat;
+ if (!stat(MDS_DIRECT_DB_PATH, &sysStat)) {
+ doCopySystem = (sysStat.st_mtimespec.tv_sec > userDirectStat.st_mtimespec.tv_sec) ||
+ ((sysStat.st_mtimespec.tv_sec == userDirectStat.st_mtimespec.tv_sec) &&
+ (sysStat.st_mtimespec.tv_nsec > userDirectStat.st_mtimespec.tv_nsec));
+ if(doCopySystem) {
+ MSDebug("user DB files obsolete at %s", userDBFileDir.c_str());
+ }
+ }
+ }
+ if(doCopySystem) {
+ /* copy system DBs to user DBs */
+ MSDebug("copying system DBs to user at %s", userDBFileDir.c_str());
+ copySystemDbs(userDBFileDir.c_str());
+ }
+ else {
+ MSDebug("Using existing user DBs at %s", userDBFileDir.c_str());
+ }
+ }
+ catch(const CssmError &cerror) {
+ /*
+ * Bad system DB file detected. Fatal.
+ */
+ throw;
+ }
+ catch(...) {
+ /*
+ * Error on delete or create user DBs; fall back on system DBs.
+ */
+ MSDebug("doFilesExist(purge) error; using system DBs");
+ mModule.setDbPath(MDS_SYSTEM_DB_DIR);
+ releaseLock(userLockFd);
+ return;
+ }
+ }
+ else {
+ MSDebug("Using system DBs only");
+ }
+
+ /*
+ * Update per-user DBs from both bundle sources (System bundles, user bundles)
+ * as appropriate.
+ */
+ DbFilesInfo dbFiles(*this, userDBFileDir.c_str());
+ dbFiles.removeOutdatedPlugins();
+ dbFiles.updateSystemDbInfo(NULL, MDS_BUNDLE_PATH);
+ if(userBundlePath[0]) {
+ /* skip for invalid or missing $HOME... */
+ if(checkUserBundles(userBundlePath)) {
+ dbFiles.updateForBundleDir(userBundlePath);
+ }
+ }
+ mModule.setDbPath(userDBFileDir.c_str());
+ } /* main block protected by mLockFd */
+ catch(...) {
+ releaseLock(userLockFd);
+ throw;
+ }
+ mModule.lastScanIsNow();
+ releaseLock(userLockFd);
+}
+
+/*
+ * Remove all records with specified guid (a.k.a. ModuleID) from specified DB.
+ */
+void MDSSession::removeRecordsForGuid(
+ const char *guid,
+ CSSM_DB_HANDLE dbHand)
+{
+ // tell the DB to flush its intermediate data to disk
+ PassThrough(dbHand, CSSM_APPLEFILEDL_COMMIT, NULL, NULL);
+ CssmClient::Query query = Attribute("ModuleID") == guid;
+ clearRecords(dbHand, query.cssmQuery());
+}
+
+
+void MDSSession::clearRecords(CSSM_DB_HANDLE dbHand, const CssmQuery &query)
+{
+ CSSM_DB_UNIQUE_RECORD_PTR record = NULL;
+ CSSM_HANDLE resultHand = DataGetFirst(dbHand,
+ &query,
+ NULL,
+ NULL, // No data
+ record);
+ if (resultHand == CSSM_INVALID_HANDLE)
+ return; // no matches
+ try {
+ do {
+ DataDelete(dbHand, *record);
+ FreeUniqueRecord(dbHand, *record);
+ record = NULL;
+ } while (DataGetNext(dbHand,
+ resultHand,
+ NULL,
+ NULL,
+ record));
+ } catch (...) {
+ if (record)
+ FreeUniqueRecord(dbHand, *record);
+ DataAbortQuery(dbHand, resultHand);
+ }
+}
+
+
+/*
+ * Determine if system databases are present.
+ * If the purge argument is true, we'll ensure that either both or neither
+ * DB files exist on exit; in that case caller must be holding MDS_INSTALL_LOCK_PATH.
+ */
+bool MDSSession::systemDatabasesPresent(bool purge)
+{
+ bool rtn = false;
+
+ try {
+ /*
+ * This can throw on a failed attempt to delete sole existing file....
+ * But if that happens while we're root, our goose is fully cooked.
+ */
+ struct stat objDbSb, directDbSb;
+ if(doFilesExist(MDS_OBJECT_DB_PATH, MDS_DIRECT_DB_PATH,
+ MDS_SYSTEM_UID, purge, objDbSb, directDbSb)) {
+ rtn = true;
+ }
+ }
+ catch(...) {
+
+ }
+ return rtn;
+}
+
+/*
+ * Given a DB name (which is used as an absolute path) and an array of
+ * RelationInfos, create a DB.
+ */
+void
+MDSSession::createSystemDatabase(
+ const char *dbName,
+ const RelationInfo *relationInfo,
+ unsigned numRelations,
+ CSSM_BOOL autoCommit,
+ mode_t mode,
+ CSSM_DB_HANDLE &dbHand) // RETURNED
+{
+ CSSM_DBINFO dbInfo;
+ CSSM_DBINFO_PTR dbInfoP = &dbInfo;
+
+ memset(dbInfoP, 0, sizeof(CSSM_DBINFO));
+ dbInfoP->NumberOfRecordTypes = numRelations;
+ dbInfoP->IsLocal = CSSM_TRUE; // TBD - what does this mean?
+ dbInfoP->AccessPath = NULL; // TBD
+
+ /* alloc numRelations elements for parsingModule, recordAttr, and recordIndex
+ * info arrays */
+ unsigned size = sizeof(CSSM_DB_PARSING_MODULE_INFO) * numRelations;
+ dbInfoP->DefaultParsingModules = (CSSM_DB_PARSING_MODULE_INFO_PTR)malloc(size);
+ memset(dbInfoP->DefaultParsingModules, 0, size);
+ size = sizeof(CSSM_DB_RECORD_ATTRIBUTE_INFO) * numRelations;
+ dbInfoP->RecordAttributeNames = (CSSM_DB_RECORD_ATTRIBUTE_INFO_PTR)malloc(size);
+ memset(dbInfoP->RecordAttributeNames, 0, size);
+ size = sizeof(CSSM_DB_RECORD_INDEX_INFO) * numRelations;
+ dbInfoP->RecordIndexes = (CSSM_DB_RECORD_INDEX_INFO_PTR)malloc(size);
+ memset(dbInfoP->RecordIndexes, 0, size);
+
+ /* cook up attribute and index info for each relation */
+ unsigned relation;
+ for(relation=0; relation<numRelations; relation++) {
+ const struct RelationInfo *relp = &relationInfo[relation]; // source
+ CSSM_DB_RECORD_ATTRIBUTE_INFO_PTR attrInfo =
+ &dbInfoP->RecordAttributeNames[relation]; // dest 1
+ CSSM_DB_RECORD_INDEX_INFO_PTR indexInfo =
+ &dbInfoP->RecordIndexes[relation]; // dest 2
+
+ attrInfo->DataRecordType = relp->DataRecordType;
+ attrInfo->NumberOfAttributes = relp->NumberOfAttributes;
+ attrInfo->AttributeInfo = (CSSM_DB_ATTRIBUTE_INFO_PTR)relp->AttributeInfo;
+
+ indexInfo->DataRecordType = relp->DataRecordType;
+ indexInfo->NumberOfIndexes = relp->NumberOfIndexes;
+ indexInfo->IndexInfo = (CSSM_DB_INDEX_INFO_PTR)relp->IndexInfo;
+ }
+
+ /* set autocommit and mode */
+ CSSM_APPLEDL_OPEN_PARAMETERS openParams;
+ memset(&openParams, 0, sizeof(openParams));
+ openParams.length = sizeof(openParams);
+ openParams.version = CSSM_APPLEDL_OPEN_PARAMETERS_VERSION;
+ openParams.autoCommit = autoCommit;
+ openParams.mask = kCSSM_APPLEDL_MASK_MODE;
+ openParams.mode = mode;
+
+ try {
+ DbCreate(dbName,
+ NULL, // DbLocation
+ *dbInfoP,
+ CSSM_DB_ACCESS_READ | CSSM_DB_ACCESS_WRITE,
+ NULL, // CredAndAclEntry
+ &openParams,
+ dbHand);
+ }
+ catch(...) {
+ MSDebug("Error on DbCreate");
+ free(dbInfoP->DefaultParsingModules);
+ free(dbInfoP->RecordAttributeNames);
+ free(dbInfoP->RecordIndexes);
+ throw;
+ }
+ free(dbInfoP->DefaultParsingModules);
+ free(dbInfoP->RecordAttributeNames);
+ free(dbInfoP->RecordIndexes);
+
+}
+
+/*
+ * Create system databases from scratch if they do not already exist.
+ * MDS_INSTALL_LOCK_PATH held on entry and exit. MDS_SYSTEM_DB_DIR assumed to
+ * exist (that's our caller's job, before acquiring MDS_INSTALL_LOCK_PATH).
+ * Returns true if we actually built the files, false if they already
+ * existed.
+ */
+bool MDSSession::createSystemDatabases(
+ CSSM_BOOL autoCommit,
+ mode_t mode)
+{
+ CSSM_DB_HANDLE objectDbHand = 0;
+ CSSM_DB_HANDLE directoryDbHand = 0;
+
+ assert(geteuid() == (uid_t)0);
+ if(systemDatabasesPresent(true)) {
+ /* both databases exist as regular files with correct owner - we're done */
+ MSDebug("system DBs already exist");
+ return false;
+ }
+
+ /* create two DBs - any exception here results in deleting both of them */
+ MSDebug("Creating MDS DBs");
+ try {
+ createSystemDatabase(MDS_OBJECT_DB_PATH, &kObjectRelation, 1,
+ autoCommit, mode, objectDbHand);
+ MSIoDbg("close objectDbHand in createSystemDatabases");
+ DbClose(objectDbHand);
+ objectDbHand = 0;
+ createSystemDatabase(MDS_DIRECT_DB_PATH, kMDSRelationInfo, kNumMdsRelations,
+ autoCommit, mode, directoryDbHand);
+ MSIoDbg("close directoryDbHand in createSystemDatabases");
+ DbClose(directoryDbHand);
+ directoryDbHand = 0;
+ }
+ catch (...) {
+ MSDebug("Error creating MDS DBs - deleting both DB files");
+ unlink(MDS_OBJECT_DB_PATH);
+ unlink(MDS_DIRECT_DB_PATH);
+ throw;
+ }
+ return true;
+}
+
+/*
+ * DbFilesInfo helper class
+ */
+
+/* Note both DB files MUST exist at construction time */
+MDSSession::DbFilesInfo::DbFilesInfo(
+ MDSSession &session,
+ const char *dbPath) :
+ mSession(session),
+ mObjDbHand(0),
+ mDirectDbHand(0),
+ mLaterTimestamp(0)
+{
+ assert(strlen(dbPath) < MAXPATHLEN);
+ strcpy(mDbPath, dbPath);
+
+ /* stat the two DB files, snag the later timestamp */
+ char path[MAXPATHLEN];
+ sprintf(path, "%s/%s", mDbPath, MDS_OBJECT_DB_NAME);
+ struct stat sb;
+ MSIoDbg("stat %s in DbFilesInfo()", path);
+ int rtn = ::stat(path, &sb);
+ if(rtn) {
+ int error = errno;
+ MSDebug("Error %d statting DB file %s", error, path);
+ UnixError::throwMe(error);
+ }
+ mLaterTimestamp = sb.st_mtimespec.tv_sec;
+ sprintf(path, "%s/%s", mDbPath, MDS_DIRECT_DB_NAME);
+ MSIoDbg("stat %s in DbFilesInfo()", path);
+ rtn = ::stat(path, &sb);
+ if(rtn) {
+ int error = errno;
+ MSDebug("Error %d statting DB file %s", error, path);
+ UnixError::throwMe(error);
+ }
+ if(sb.st_mtimespec.tv_sec > mLaterTimestamp) {
+ mLaterTimestamp = sb.st_mtimespec.tv_sec;
+ }
+}
+
+MDSSession::DbFilesInfo::~DbFilesInfo()
+{
+ if(mObjDbHand != 0) {
+ /* autocommit on, henceforth */
+ mSession.PassThrough(mObjDbHand,
+ CSSM_APPLEFILEDL_COMMIT, NULL, NULL);
+ MSIoDbg("close objectDbHand in ~DbFilesInfo()");
+ mSession.DbClose(mObjDbHand);
+ mObjDbHand = 0;
+ }
+ if(mDirectDbHand != 0) {
+ mSession.PassThrough(mDirectDbHand,
+ CSSM_APPLEFILEDL_COMMIT, NULL, NULL);
+ MSIoDbg("close mDirectDbHand in ~DbFilesInfo()");
+ mSession.DbClose(mDirectDbHand);
+ mDirectDbHand = 0;
+ }
+}
+
+/* lazy evaluation of both DB handlesÊ*/
+CSSM_DB_HANDLE MDSSession::DbFilesInfo::objDbHand()
+{
+ if(mObjDbHand != 0) {
+ return mObjDbHand;
+ }
+ char fullPath[MAXPATHLEN + 1];
+ sprintf(fullPath, "%s/%s", mDbPath, MDS_OBJECT_DB_NAME);
+ MSIoDbg("open %s in objDbHand()", fullPath);
+ mObjDbHand = mSession.dbOpen(fullPath, true); // batch mode
+ return mObjDbHand;
+}
+
+CSSM_DB_HANDLE MDSSession::DbFilesInfo::directDbHand()
+{
+ if(mDirectDbHand != 0) {
+ return mDirectDbHand;
+ }
+ char fullPath[MAXPATHLEN + 1];
+ sprintf(fullPath, "%s/%s", mDbPath, MDS_DIRECT_DB_NAME);
+ MSIoDbg("open %s in directDbHand()", fullPath);
+ mDirectDbHand = mSession.dbOpen(fullPath, true); // batch mode
+ return mDirectDbHand;
+}
+
+/*
+ * Update the info for Security.framework and the system bundles.
+ */
+void MDSSession::DbFilesInfo::updateSystemDbInfo(
+ const char *systemPath, // e.g., /System/Library/Frameworks
+ const char *bundlePath) // e.g., /System/Library/Security
+{
+ /*
+ * Security.framework - CSSM and built-in modules - only for initial population of
+ * system DB files.
+ */
+ if (systemPath) {
+ string path;
+ if (CFRef<CFBundleRef> me = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.security")))
+ if (CFRef<CFURLRef> url = CFBundleCopyBundleURL(me))
+ if (CFRef<CFStringRef> cfpath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle))
+ path = cfString(cfpath); // path to my bundle
+
+ if (path.empty()) // use system default
+ path = string(systemPath) + "/" MDS_SYSTEM_FRAME;
+ updateForBundle(path.c_str());
+ }
+
+ /* Standard loadable bundles */
+ updateForBundleDir(bundlePath);
+}
+
+
+MDSSession::DbFilesInfo::TbdRecord::TbdRecord(
+ const CSSM_DATA &guid)
+{
+ assert(guid.Length <= MAX_GUID_LEN);
+ assert(guid.Length != 0);
+ memmove(mGuid, guid.Data, guid.Length);
+ if(mGuid[guid.Length - 1] != '\0') {
+ mGuid[guid.Length] = '\0';
+ }
+}
+
+/*
+ * Test if plugin specified by pluginPath needs to be deleted from DBs.
+ * If so, add to tbdVector.
+ */
+void MDSSession::DbFilesInfo::checkOutdatedPlugin(
+ const CSSM_DATA &pathValue,
+ const CSSM_DATA &guidValue,
+ TbdVector &tbdVector)
+{
+ /* stat the specified plugin */
+ struct stat sb;
+ bool obsolete = false;
+ string path = CssmData::overlay(pathValue).toString();
+ if (!path.empty() && path[0] == '*') {
+ /* builtin pseudo-path; never obsolete this */
+ return;
+ }
+ MSIoDbg("stat %s in checkOutdatedPlugin()", path.c_str());
+ int rtn = ::stat(path.c_str(), &sb);
+ if(rtn) {
+ /* not there or inaccessible; delete */
+ obsolete = true;
+ }
+ else if(sb.st_mtimespec.tv_sec > mLaterTimestamp) {
+ /* timestamp of plugin's main directory later than that of DBs */
+ obsolete = true;
+ }
+ if(obsolete) {
+ TbdRecord *tbdRecord = new TbdRecord(guidValue);
+ tbdVector.push_back(tbdRecord);
+ MSDebug("checkOutdatedPlugin: flagging %s obsolete", path.c_str());
+ }
+}
+
+/*
+ * Examine dbFiles.objDbHand; remove all fields associated with any bundle
+ * i.e., with any path) which are either not present on disk, or which
+ * have changed since dbFiles.laterTimestamp().
+ */
+void MDSSession::DbFilesInfo::removeOutdatedPlugins()
+{
+ CSSM_QUERY query;
+ CSSM_DB_UNIQUE_RECORD_PTR record = NULL;
+ CSSM_HANDLE resultHand;
+ CSSM_DB_RECORD_ATTRIBUTE_DATA recordAttrs;
+ CSSM_DB_ATTRIBUTE_DATA theAttrs[2];
+ CSSM_DB_ATTRIBUTE_INFO_PTR attrInfo;
+ TbdVector tbdRecords;
+
+ /*
+ * First, scan object directory. All we need are the path and GUID attributes.
+ */
+ recordAttrs.DataRecordType = MDS_OBJECT_RECORDTYPE;
+ recordAttrs.SemanticInformation = 0;
+ recordAttrs.NumberOfAttributes = 2;
+ recordAttrs.AttributeData = theAttrs;
+
+ attrInfo = &theAttrs[0].Info;
+ attrInfo->AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+ attrInfo->Label.AttributeName = (char*) "ModuleID";
+ attrInfo->AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
+ theAttrs[0].NumberOfValues = 0;
+ theAttrs[0].Value = NULL;
+ attrInfo = &theAttrs[1].Info;
+ attrInfo->AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+ attrInfo->Label.AttributeName = (char*) "Path";
+ attrInfo->AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
+ theAttrs[1].NumberOfValues = 0;
+ theAttrs[1].Value = NULL;
+
+ /* just search by recordType, no predicates */
+ query.RecordType = MDS_OBJECT_RECORDTYPE;
+ query.Conjunctive = CSSM_DB_NONE;
+ query.NumSelectionPredicates = 0;
+ query.SelectionPredicate = NULL;
+ query.QueryLimits.TimeLimit = 0; // FIXME - meaningful?
+ query.QueryLimits.SizeLimit = 1; // FIXME - meaningful?
+ query.QueryFlags = 0; // CSSM_QUERY_RETURN_DATA...FIXME - used?
+
+ CssmQuery perryQuery(query);
+ try {
+ resultHand = mSession.DataGetFirst(objDbHand(),
+ &perryQuery,
+ &recordAttrs,
+ NULL, // No data
+ record);
+ }
+ catch(...) {
+ MSDebug("removeOutdatedPlugins: DataGetFirst threw");
+ return; // ???
+ }
+ if(record) {
+ mSession.FreeUniqueRecord(mObjDbHand, *record);
+ }
+ if(resultHand) {
+ if(theAttrs[0].NumberOfValues && theAttrs[1].NumberOfValues) {
+ checkOutdatedPlugin(*theAttrs[1].Value, *theAttrs[0].Value,
+ tbdRecords);
+ }
+ else {
+ MSDebug("removeOutdatedPlugins: incomplete record found (1)!");
+ }
+ for(unsigned dex=0; dex<2; dex++) {
+ CSSM_DB_ATTRIBUTE_DATA *attr = &theAttrs[dex];
+ for (unsigned attrDex=0; attrDex<attr->NumberOfValues; attrDex++) {
+ if(attr->Value[attrDex].Data) {
+ mSession.free(attr->Value[attrDex].Data);
+ }
+ }
+ if(attr->Value) {
+ mSession.free(attr->Value);
+ }
+ }
+ }
+ else {
+ /* empty Object DB - we're done */
+ MSDebug("removeOutdatedPlugins: empty object DB");
+ return;
+ }
+
+ /* now the rest of the object DB records */
+ for(;;) {
+ bool brtn = mSession.DataGetNext(objDbHand(),
+ resultHand,
+ &recordAttrs,
+ NULL,
+ record);
+ if(!brtn) {
+ /* end of data */
+ break;
+ }
+ if(record) {
+ mSession.FreeUniqueRecord(mObjDbHand, *record);
+ }
+ if(theAttrs[0].NumberOfValues && theAttrs[1].NumberOfValues) {
+ checkOutdatedPlugin(*theAttrs[1].Value,
+ *theAttrs[0].Value,
+ tbdRecords);
+ }
+ else {
+ MSDebug("removeOutdatedPlugins: incomplete record found (2)!");
+ }
+ for(unsigned dex=0; dex<2; dex++) {
+ CSSM_DB_ATTRIBUTE_DATA *attr = &theAttrs[dex];
+ for (unsigned attrDex=0; attrDex<attr->NumberOfValues; attrDex++) {
+ if(attr->Value[attrDex].Data) {
+ mSession.free(attr->Value[attrDex].Data);
+ }
+ }
+ if(attr->Value) {
+ mSession.free(attr->Value);
+ }
+ }
+ }
+ /* no DataAbortQuery needed; we scanned until completion */
+
+ /*
+ * We have a vector of plugins to be deleted. Remove all records from both
+ * DBs associated with the plugins, as specified by guid.
+ */
+ unsigned numRecords = tbdRecords.size();
+ for(unsigned i=0; i<numRecords; i++) {
+ TbdRecord *tbdRecord = tbdRecords[i];
+ mSession.removeRecordsForGuid(tbdRecord->guid(), objDbHand());
+ mSession.removeRecordsForGuid(tbdRecord->guid(), directDbHand());
+ }
+ for(unsigned i=0; i<numRecords; i++) {
+ delete tbdRecords[i];
+ }
+}
+
+
+/*
+ * Update DBs for all bundles in specified directory.
+ */
+void MDSSession::DbFilesInfo::updateForBundleDir(
+ const char *bundleDirPath)
+{
+ /* do this with readdir(); CFBundleCreateBundlesFromDirectory is
+ * much too heavyweight */
+ MSDebug("...updating DBs for dir %s", bundleDirPath);
+ DIR *dir = opendir(bundleDirPath);
+ if (dir == NULL) {
+ MSDebug("updateForBundleDir: error %d opening %s", errno, bundleDirPath);
+ return;
+ }
+ struct dirent *dp;
+ char fullPath[MAXPATHLEN];
+ while ((dp = readdir(dir)) != NULL) {
+ if(isBundle(dp)) {
+ sprintf(fullPath, "%s/%s", bundleDirPath, dp->d_name);
+ updateForBundle(fullPath);
+ }
+ }
+ closedir(dir);
+}
+
+/*
+ * lookup by path - just returns true if there is a record assoociated with the path
+ * in mObjDbHand.
+ */
+bool MDSSession::DbFilesInfo::lookupForPath(
+ const char *path)
+{
+ CSSM_QUERY query;
+ CSSM_DB_UNIQUE_RECORD_PTR record = NULL;
+ CSSM_HANDLE resultHand = 0;
+ CSSM_DB_RECORD_ATTRIBUTE_DATA recordAttrs;
+ CSSM_DB_ATTRIBUTE_DATA theAttr;
+ CSSM_DB_ATTRIBUTE_INFO_PTR attrInfo = &theAttr.Info;
+ CSSM_SELECTION_PREDICATE predicate;
+ CSSM_DATA predData;
+
+ recordAttrs.DataRecordType = MDS_OBJECT_RECORDTYPE;
+ recordAttrs.SemanticInformation = 0;
+ recordAttrs.NumberOfAttributes = 1;
+ recordAttrs.AttributeData = &theAttr;
+
+ attrInfo->AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+ attrInfo->Label.AttributeName = (char*) "Path";
+ attrInfo->AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
+
+ theAttr.NumberOfValues = 0;
+ theAttr.Value = NULL;
+
+ predicate.DbOperator = CSSM_DB_EQUAL;
+ predicate.Attribute.Info.AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+ predicate.Attribute.Info.Label.AttributeName = (char*) "Path";
+ predicate.Attribute.Info.AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
+ predData.Data = (uint8 *)path;
+ predData.Length = strlen(path);
+ predicate.Attribute.Value = &predData;
+ predicate.Attribute.NumberOfValues = 1;
+
+ query.RecordType = MDS_OBJECT_RECORDTYPE;
+ query.Conjunctive = CSSM_DB_NONE;
+ query.NumSelectionPredicates = 1;
+ query.SelectionPredicate = &predicate;
+ query.QueryLimits.TimeLimit = 0; // FIXME - meaningful?
+ query.QueryLimits.SizeLimit = 1; // FIXME - meaningful?
+ query.QueryFlags = 0; // CSSM_QUERY_RETURN_DATA...FIXME - used?
+
+ bool ourRtn = true;
+ try {
+ CssmQuery perryQuery(query);
+ resultHand = mSession.DataGetFirst(objDbHand(),
+ &perryQuery,
+ &recordAttrs,
+ NULL, // No data
+ record);
+ }
+ catch (...) {
+ ourRtn = false;
+ }
+ if(record) {
+ mSession.FreeUniqueRecord(mObjDbHand, *record);
+ }
+ else {
+ ourRtn = false;
+ }
+ if(resultHand && ourRtn) {
+ /* more resulting pending; terminate the search */
+ try {
+ mSession.DataAbortQuery(mObjDbHand, resultHand);
+ }
+ catch(...) {
+ MSDebug("exception on DataAbortQuery in lookupForPath");
+ }
+ }
+ for(unsigned dex=0; dex<theAttr.NumberOfValues; dex++) {
+ if(theAttr.Value[dex].Data) {
+ mSession.free(theAttr.Value[dex].Data);
+ }
+ }
+ mSession.free(theAttr.Value);
+ return ourRtn;
+}
+
+/* update entry for one bundle, which is known to exist */
+void MDSSession::DbFilesInfo::updateForBundle(
+ const char *bundlePath)
+{
+ MSDebug("...updating DBs for bundle %s", bundlePath);
+
+ /* Quick lookup - do we have ANY entry for a bundle with this path? */
+ if(lookupForPath(bundlePath)) {
+ /* Yep, we're done */
+ return;
+ }
+ MDSAttrParser parser(bundlePath,
+ mSession,
+ objDbHand(),
+ directDbHand());
+ try {
+ parser.parseAttrs();
+ }
+ catch (const CssmError &err) {
+ // a corrupt MDS info file invalidates the entire plugin
+ const char *guid = parser.guid();
+ if (guid) {
+ mSession.removeRecordsForGuid(guid, objDbHand());
+ mSession.removeRecordsForGuid(guid, directDbHand());
+ }
+ }
+}
+
+
+//
+// Private API: add MDS records from contents of file
+// These files are typically written by securityd and handed to us in this call.
+//
+void MDSSession::installFile(const MDS_InstallDefaults *defaults,
+ const char *inBundlePath, const char *subdir, const char *file)
+{
+ string bundlePath = inBundlePath ? inBundlePath : cfString(CFBundleGetMainBundle());
+ DbFilesInfo dbFiles(*this, MDS_SYSTEM_DB_DIR);
+ MDSAttrParser parser(bundlePath.c_str(),
+ *this,
+ dbFiles.objDbHand(),
+ dbFiles.directDbHand());
+ parser.setDefaults(defaults);
+
+ try {
+ if (file == NULL) // parse a directory
+ if (subdir) // a particular directory
+ parser.parseAttrs(CFTempString(subdir));
+ else // all resources in bundle
+ parser.parseAttrs(NULL);
+ else // parse just one file
+ parser.parseFile(CFRef<CFURLRef>(makeCFURL(file)), CFTempString(subdir));
+ }
+ catch (const CssmError &err) {
+ const char *guid = parser.guid();
+ if (guid) {
+ removeRecordsForGuid(guid, dbFiles.objDbHand());
+ removeRecordsForGuid(guid, dbFiles.directDbHand());
+ }
+ }
+}
+
+
+//
+// Private API: Remove all records for a guid/subservice
+//
+// Note: Multicursors searching for SSID fail because not all records in the
+// database have this attribute. So we have to explicitly run through all tables
+// that do.
+//
+void MDSSession::removeSubservice(const char *guid, uint32 ssid)
+{
+ DbFilesInfo dbFiles(*this, MDS_SYSTEM_DB_DIR);
+
+ CssmClient::Query query =
+ Attribute("ModuleID") == guid &&
+ Attribute("SSID") == ssid;
+
+ // only CSP and DL tables are cleared here
+ // (this function is private to securityd, which only handles those types)
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_CSP_PRIMARY_RECORDTYPE));
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_CSP_CAPABILITY_RECORDTYPE));
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_CSP_ENCAPSULATED_PRODUCT_RECORDTYPE));
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_CSP_SC_INFO_RECORDTYPE));
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_DL_PRIMARY_RECORDTYPE));
+ clearRecords(dbFiles.directDbHand(),
+ CssmQuery(query.cssmQuery(), MDS_CDSADIR_DL_ENCAPSULATED_PRODUCT_RECORDTYPE));
+}
+
+
+} // end namespace Security