--- /dev/null
+/*
+ * Copyright (c) 2000-2012 Apple 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 <security_filedb/AtomicFile.h>
+
+#include <security_utilities/devrandom.h>
+#include <CommonCrypto/CommonDigest.h>
+#include <security_cdsa_utilities/cssmerrors.h>
+#include <Security/cssm.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <syslog.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/mount.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/mman.h>
+#include <copyfile.h>
+#include <sandbox.h>
+#include <set>
+
+#define kAtomicFileMaxBlockSize INT_MAX
+
+
+//
+// AtomicFile.cpp
+//
+AtomicFile::AtomicFile(const std::string &inPath) :
+ mPath(inPath)
+{
+ pathSplit(inPath, mDir, mFile);
+
+ if (mDir.length() == 0)
+ {
+ const char* buffer = getwd(NULL);
+ mDir = buffer;
+ free((void*) buffer);
+ }
+
+ mDir += '/';
+
+ // determine if the path is on a local or a networked volume
+ struct statfs info;
+ int result = statfs(mDir.c_str(), &info);
+ if (result == -1) // error on opening?
+ {
+ mIsLocalFileSystem = false; // revert to the old ways if we can't tell what kind of system we have
+ }
+ else
+ {
+ mIsLocalFileSystem = (info.f_flags & MNT_LOCAL) != 0;
+ if (mIsLocalFileSystem)
+ {
+ // compute the name of the lock file for this file
+ CC_SHA1_CTX ctx;
+ CC_SHA1_Init(&ctx);
+ CC_SHA1_Update(&ctx, (const void*) mFile.c_str(), mFile.length());
+ u_int8_t digest[CC_SHA1_DIGEST_LENGTH];
+ CC_SHA1_Final(digest, &ctx);
+
+ u_int32_t hash = (digest[0] << 24) | (digest[1] << 16) | (digest[2] << 8) | digest[3];
+
+ char buffer[256];
+ sprintf(buffer, "%08X", hash);
+ mLockFilePath = mDir + ".fl" + buffer;
+ }
+ }
+}
+
+AtomicFile::~AtomicFile()
+{
+}
+
+// Aquire the write lock and remove the file.
+void
+AtomicFile::performDelete()
+{
+ AtomicLockedFile lock(*this);
+ if (::unlink(mPath.c_str()) != 0)
+ {
+ int error = errno;
+ secdebug("atomicfile", "unlink %s: %s", mPath.c_str(), strerror(error));
+ if (error == ENOENT)
+ CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST);
+ else
+ UnixError::throwMe(error);
+ }
+
+ // unlink our lock file
+ ::unlink(mLockFilePath.c_str());
+}
+
+// Aquire the write lock and rename the file (and bump the version and stuff).
+void
+AtomicFile::rename(const std::string &inNewPath)
+{
+ const char *path = mPath.c_str();
+ const char *newPath = inNewPath.c_str();
+
+ // @@@ lock the destination file too.
+ AtomicLockedFile lock(*this);
+ if (::rename(path, newPath) != 0)
+ {
+ int error = errno;
+ secdebug("atomicfile", "rename(%s, %s): %s", path, newPath, strerror(error));
+ UnixError::throwMe(error);
+ }
+}
+
+// Lock the file for writing and return a newly created AtomicTempFile.
+RefPointer<AtomicTempFile>
+AtomicFile::create(mode_t mode)
+{
+ const char *path = mPath.c_str();
+
+ // First make sure the directory to this file exists and is writable
+ mkpath(mDir);
+
+ RefPointer<AtomicLockedFile> lock(new AtomicLockedFile(*this));
+ int fileRef = ropen(path, O_WRONLY|O_CREAT|O_EXCL, mode);
+ if (fileRef == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "open %s: %s", path, strerror(error));
+
+ // Do the obvious error code translations here.
+ // @@@ Consider moving these up a level.
+ if (error == EACCES)
+ CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
+ else if (error == EEXIST)
+ CssmError::throwMe(CSSMERR_DL_DATASTORE_ALREADY_EXISTS);
+ else
+ UnixError::throwMe(error);
+ }
+ rclose(fileRef);
+
+ try
+ {
+ // Now that we have created the lock and the new db file create a tempfile
+ // object.
+ RefPointer<AtomicTempFile> temp(new AtomicTempFile(*this, lock, mode));
+ secdebug("atomicfile", "%p created %s", this, path);
+ return temp;
+ }
+ catch (...)
+ {
+ // Creating the temp file failed so remove the db file we just created too.
+ if (::unlink(path) == -1)
+ {
+ secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
+ }
+ throw;
+ }
+}
+
+// Lock the database file for writing and return a newly created AtomicTempFile.
+// If the parent directory allows the write we're going to allow this. Previous
+// versions checked for writability of the db file and that caused problems when
+// setuid programs had made entries. As long as the db (keychain) file is readable
+// this function can make the newer keychain file with the correct owner just by virtue
+// of the copy that takes place.
+
+RefPointer<AtomicTempFile>
+AtomicFile::write()
+{
+
+ RefPointer<AtomicLockedFile> lock(new AtomicLockedFile(*this));
+ return new AtomicTempFile(*this, lock);
+}
+
+// Return a bufferedFile containing current version of the file for reading.
+RefPointer<AtomicBufferedFile>
+AtomicFile::read()
+{
+ return new AtomicBufferedFile(mPath, mIsLocalFileSystem);
+}
+
+mode_t
+AtomicFile::mode() const
+{
+ const char *path = mPath.c_str();
+ struct stat st;
+ if (::stat(path, &st) == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "stat %s: %s", path, strerror(error));
+ UnixError::throwMe(error);
+ }
+ return st.st_mode;
+}
+
+// Split full into a dir and file component.
+void
+AtomicFile::pathSplit(const std::string &inFull, std::string &outDir, std::string &outFile)
+{
+ std::string::size_type slash, len = inFull.size();
+ slash = inFull.rfind('/');
+ if (slash == std::string::npos)
+ {
+ outDir = "";
+ outFile = inFull;
+ }
+ else if (slash + 1 == len)
+ {
+ outDir = inFull;
+ outFile = "";
+ }
+ else
+ {
+ outDir = inFull.substr(0, slash + 1);
+ outFile = inFull.substr(slash + 1, len);
+ }
+}
+
+static std::string RemoveDoubleSlashes(const std::string &path)
+{
+ std::string result;
+ unsigned i;
+ for (i = 0; i < path.length(); ++i)
+ {
+ result += path[i];
+ if ((i < path.length() - 2) && path[i] == '/' && path[i + 1] == '/')
+ {
+ i += 1; // skip a second '/'
+ }
+ }
+
+ return result;
+}
+
+
+
+//
+// Make sure the directory up to inDir exists inDir *must* end in a slash.
+//
+void
+AtomicFile::mkpath(const std::string &inDir, mode_t mode)
+{
+ for (std::string::size_type pos = 0; (pos = inDir.find('/', pos + 1)) != std::string::npos;)
+ {
+ std::string path = inDir.substr(0, pos);
+ const char *cpath = path.c_str();
+ struct stat sb;
+ if (::stat(cpath, &sb))
+ {
+ // if we are creating a path in the user's home directory, override the user's mode
+ std::string homedir = getenv("HOME");
+
+ // canonicalize the path (remove double slashes)
+ string canonPath = RemoveDoubleSlashes(cpath);
+
+ if (canonPath.find(homedir, 0) == 0)
+ {
+ mode = 0700;
+ }
+
+ if (errno != ENOENT || ::mkdir(cpath, mode))
+ UnixError::throwMe(errno);
+ }
+ else if (!S_ISDIR(sb.st_mode))
+ CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); // @@@ Should be is a directory
+ }
+}
+
+int
+AtomicFile::ropen(const char *const name, int flags, mode_t mode)
+{
+ bool isCreate = (flags & O_CREAT) != 0;
+
+ /*
+ The purpose of checkForRead and checkForWrite is to mitigate
+ spamming of the log when a user has installed certain third
+ party software packages which create additional keychains.
+ Certain applications use a custom sandbox profile which do not
+ permit this and so the user gets a ton of spam in the log.
+ This turns into a serious performance problem.
+
+ We handle this situation by checking two factors:
+
+ 1: If the user is trying to create a file, we send the
+ request directly to open. This is the right thing
+ to do, as we don't want most applications creating
+ keychains unless they have been expressly authorized
+ to do so.
+
+ The layers above this one only set O_CREAT when a file
+ doesn't exist, so the case where O_CREAT can be called
+ on an existing file is irrelevant.
+
+ 2: If the user is trying to open the file for reading or
+ writing, we check with the sandbox mechanism to see if
+ the operation will be permitted (and tell it not to
+ log if it the operation will fail).
+
+ If the operation is not permitted, we return -1 which
+ emulates the behavior of open. sandbox_check sets
+ errno properly, so the layers which call this function
+ will be able to act as though open had been called.
+ */
+
+ bool checkForRead = false;
+ bool checkForWrite = false;
+
+ int fd, tries_left = 4 /* kNoResRetry */;
+
+ if (!isCreate)
+ {
+ switch (flags & O_ACCMODE)
+ {
+ case O_RDONLY:
+ checkForRead = true;
+ break;
+ case O_WRONLY:
+ checkForWrite = true;
+ break;
+ case O_RDWR:
+ checkForRead = true;
+ checkForWrite = true;
+ break;
+ }
+
+ if (checkForRead)
+ {
+ int result = sandbox_check(getpid(), "file-read-data", (sandbox_filter_type) (SANDBOX_FILTER_PATH | SANDBOX_CHECK_NO_REPORT), name);
+ if (result != 0)
+ {
+ return -1;
+ }
+ }
+
+ if (checkForWrite)
+ {
+ int result = sandbox_check(getpid(), "file-write-data", (sandbox_filter_type) (SANDBOX_FILTER_PATH | SANDBOX_CHECK_NO_REPORT), name);
+ if (result != 0)
+ {
+ return -1;
+ }
+ }
+ }
+
+ do
+ {
+ fd = ::open(name, flags, mode);
+ } while (fd < 0 && (errno == EINTR || (errno == ENFILE && --tries_left >= 0)));
+
+ return fd;
+}
+
+int
+AtomicFile::rclose(int fd)
+{
+ int result;
+ do
+ {
+ result = ::close(fd);
+ } while(result && errno == EINTR);
+
+ return result;
+}
+
+//
+// AtomicBufferedFile - This represents an instance of a file opened for reading.
+// The file is read into memory and closed after this is done.
+// The memory is released when this object is destroyed.
+//
+AtomicBufferedFile::AtomicBufferedFile(const std::string &inPath, bool isLocal) :
+ mPath(inPath),
+ mFileRef(-1),
+ mBuffer(NULL),
+ mLength(0),
+ mIsMapped(isLocal)
+{
+}
+
+AtomicBufferedFile::~AtomicBufferedFile()
+{
+ if (mFileRef >= 0)
+ {
+ AtomicFile::rclose(mFileRef);
+ secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
+ }
+
+ if (mBuffer)
+ {
+ secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer);
+ unloadBuffer();
+ }
+}
+
+//
+// Open the file and return the length in bytes.
+//
+off_t
+AtomicBufferedFile::open()
+{
+ const char *path = mPath.c_str();
+ if (mFileRef >= 0)
+ {
+ secdebug("atomicfile", "open %s: already open, closing and reopening", path);
+ close();
+ }
+
+ mFileRef = AtomicFile::ropen(path, O_RDONLY, 0);
+ if (mFileRef == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "open %s: %s", path, strerror(error));
+
+ // Do the obvious error code translations here.
+ // @@@ Consider moving these up a level.
+ if (error == ENOENT)
+ CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST);
+ else if (error == EACCES)
+ CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
+ else
+ UnixError::throwMe(error);
+ }
+
+ struct stat st;
+ int result = fstat(mFileRef, &st);
+ if (result == 0)
+ {
+ mLength = st.st_size;
+ }
+ else
+ {
+ int error = errno;
+ secdebug("atomicfile", "lseek(%s, END): %s", path, strerror(error));
+ AtomicFile::rclose(mFileRef);
+ UnixError::throwMe(error);
+ }
+
+ secdebug("atomicfile", "%p opened %s: %qd bytes", this, path, mLength);
+
+ return mLength;
+}
+
+//
+// Unload the contents of the file.
+//
+void
+AtomicBufferedFile::unloadBuffer()
+{
+ if (!mIsMapped)
+ {
+ delete [] mBuffer;
+ }
+ else
+ {
+ munmap(mBuffer, mLength);
+ }
+}
+
+//
+// Load the contents of the file into memory.
+// If we are on a local file system, we mmap the file. Otherwise, we
+// read it all into memory
+void
+AtomicBufferedFile::loadBuffer()
+{
+ if (!mIsMapped)
+ {
+ // make a buffer big enough to hold the entire file
+ mBuffer = new uint8[mLength];
+ lseek(mFileRef, 0, SEEK_SET);
+ ssize_t pos = 0;
+
+ ssize_t bytesToRead = mLength;
+ while (bytesToRead > 0)
+ {
+ ssize_t bytesRead = ::read(mFileRef, mBuffer + pos, bytesToRead);
+ if (bytesRead == -1)
+ {
+ if (errno != EINTR)
+ {
+ int error = errno;
+ secdebug("atomicfile", "lseek(%s, END): %s", mPath.c_str(), strerror(error));
+ AtomicFile::rclose(mFileRef);
+ UnixError::throwMe(error);
+ }
+ }
+ else
+ {
+ bytesToRead -= bytesRead;
+ pos += bytesRead;
+ }
+ }
+ }
+ else
+ {
+ // mmap the buffer into place
+ mBuffer = (uint8*) mmap(NULL, mLength, PROT_READ, MAP_PRIVATE, mFileRef, 0);
+ if (mBuffer == (uint8*) -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "lseek(%s, END): %s", mPath.c_str(), strerror(error));
+ AtomicFile::rclose(mFileRef);
+ UnixError::throwMe(error);
+ }
+ }
+}
+
+
+
+//
+// Read the file starting at inOffset for inLength bytes into the buffer and return
+// a pointer to it. On return outLength contain the actual number of bytes read, it
+// will only ever be less than inLength if EOF was reached, and it will never be more
+// than inLength.
+//
+const uint8 *
+AtomicBufferedFile::read(off_t inOffset, off_t inLength, off_t &outLength)
+{
+ if (mFileRef < 0)
+ {
+ secdebug("atomicfile", "read %s: file yet not opened, opening", mPath.c_str());
+ open();
+ }
+
+ off_t bytesLeft = inLength;
+ if (mBuffer)
+ {
+ secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer);
+ unloadBuffer();
+ }
+
+ loadBuffer();
+
+ secdebug("atomicfile", "%p allocated %s buffer %p size %qd", this, mPath.c_str(), mBuffer, bytesLeft);
+
+ ssize_t maxEnd = inOffset + inLength;
+ if (maxEnd > mLength)
+ {
+ maxEnd = mLength;
+ }
+
+ outLength = maxEnd - inOffset;
+
+ return mBuffer + inOffset;
+}
+
+void
+AtomicBufferedFile::close()
+{
+ if (mFileRef < 0)
+ {
+ secdebug("atomicfile", "close %s: already closed", mPath.c_str());
+ }
+ else
+ {
+ int result = AtomicFile::rclose(mFileRef);
+ mFileRef = -1;
+ if (result == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno));
+ UnixError::throwMe(error);
+ }
+
+ secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
+ }
+}
+
+
+//
+// AtomicTempFile - A temporary file to write changes to.
+//
+AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer<AtomicLockedFile> &inLockedFile, mode_t mode) :
+ mFile(inFile),
+ mLockedFile(inLockedFile),
+ mCreating(true)
+{
+ create(mode);
+}
+
+AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer<AtomicLockedFile> &inLockedFile) :
+ mFile(inFile),
+ mLockedFile(inLockedFile),
+ mCreating(false)
+{
+ create(mFile.mode());
+}
+
+AtomicTempFile::~AtomicTempFile()
+{
+ // rollback if we didn't commit yet.
+ if (mFileRef >= 0)
+ rollback();
+}
+
+//
+// Open the file and return the length in bytes.
+//
+void
+AtomicTempFile::create(mode_t mode)
+{
+ // we now generate our temporary file name through sandbox API's.
+
+ // put the dir into a canonical form
+ string dir = mFile.dir();
+ int i = dir.length() - 1;
+
+ // walk backwards until we get to a non / character
+ while (i >= 0 && dir[i] == '/')
+ {
+ i -= 1;
+ }
+
+ // point one beyond the string
+ i += 1;
+
+ const char* temp = _amkrtemp((dir.substr(0, i) + "/" + mFile.file()).c_str());
+ if (temp == NULL)
+ {
+ UnixError::throwMe(errno);
+ }
+
+ mPath = temp;
+ free((void*) temp);
+
+ const char *path = mPath.c_str();
+
+ mFileRef = AtomicFile::ropen(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
+ if (mFileRef == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "open %s: %s", path, strerror(error));
+
+ // Do the obvious error code translations here.
+ // @@@ Consider moving these up a level.
+ if (error == EACCES)
+ CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
+ else
+ UnixError::throwMe(error);
+ }
+
+ // If we aren't creating the inital file, make sure we preserve
+ // the mode of the old file regardless of the current umask.
+ // If we are creating the inital file we respect the users
+ // current umask.
+ if (!mCreating)
+ {
+ if (::fchmod(mFileRef, mode))
+ {
+ int error = errno;
+ secdebug("atomicfile", "fchmod %s: %s", path, strerror(error));
+ UnixError::throwMe(error);
+ }
+ }
+
+ secdebug("atomicfile", "%p created %s", this, path);
+}
+
+void
+AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 inData)
+{
+ uint32 aData = htonl(inData);
+ write(inOffsetType, inOffset, reinterpret_cast<uint8 *>(&aData), sizeof(aData));
+}
+
+void
+AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset,
+ const uint32 *inData, uint32 inCount)
+{
+#ifdef HOST_LONG_IS_NETWORK_LONG
+ // Optimize this for the case where hl == nl
+ const uint32 *aBuffer = inData;
+#else
+ auto_array<uint32> aBuffer(inCount);
+ for (uint32 i = 0; i < inCount; i++)
+ aBuffer.get()[i] = htonl(inData[i]);
+#endif
+
+ write(inOffsetType, inOffset, reinterpret_cast<const uint8 *>(aBuffer.get()),
+ inCount * sizeof(*inData));
+}
+
+void
+AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint8 *inData, size_t inLength)
+{
+ off_t pos;
+ if (inOffsetType == AtomicFile::FromEnd)
+ {
+ pos = ::lseek(mFileRef, 0, SEEK_END);
+ if (pos == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "lseek(%s, %qd): %s", mPath.c_str(), inOffset, strerror(error));
+ UnixError::throwMe(error);
+ }
+ }
+ else if (inOffsetType == AtomicFile::FromStart)
+ pos = inOffset;
+ else
+ CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR);
+
+ off_t bytesLeft = inLength;
+ const uint8 *ptr = inData;
+ while (bytesLeft)
+ {
+ size_t toWrite = bytesLeft > kAtomicFileMaxBlockSize ? kAtomicFileMaxBlockSize : size_t(bytesLeft);
+ ssize_t bytesWritten = ::pwrite(mFileRef, ptr, toWrite, pos);
+ if (bytesWritten == -1)
+ {
+ int error = errno;
+ if (error == EINTR)
+ {
+ // We got interrupted by a signal, so try again.
+ secdebug("atomicfile", "write %s: interrupted, retrying", mPath.c_str());
+ continue;
+ }
+
+ secdebug("atomicfile", "write %s: %s", mPath.c_str(), strerror(error));
+ UnixError::throwMe(error);
+ }
+
+ // Write returning 0 is bad mmkay.
+ if (bytesWritten == 0)
+ {
+ secdebug("atomicfile", "write %s: 0 bytes written", mPath.c_str());
+ CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR);
+ }
+
+ secdebug("atomicfile", "%p wrote %s %ld bytes from %p", this, mPath.c_str(), bytesWritten, ptr);
+
+ bytesLeft -= bytesWritten;
+ ptr += bytesWritten;
+ pos += bytesWritten;
+ }
+}
+
+void
+AtomicTempFile::fsync()
+{
+ if (mFileRef < 0)
+ {
+ secdebug("atomicfile", "fsync %s: already closed", mPath.c_str());
+ }
+ else
+ {
+ int result;
+ do
+ {
+ result = ::fsync(mFileRef);
+ } while (result && errno == EINTR);
+
+ if (result == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "fsync %s: %s", mPath.c_str(), strerror(errno));
+ UnixError::throwMe(error);
+ }
+
+ secdebug("atomicfile", "%p fsynced %s", this, mPath.c_str());
+ }
+}
+
+void
+AtomicTempFile::close()
+{
+ if (mFileRef < 0)
+ {
+ secdebug("atomicfile", "close %s: already closed", mPath.c_str());
+ }
+ else
+ {
+ int result = AtomicFile::rclose(mFileRef);
+ mFileRef = -1;
+ if (result == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno));
+ UnixError::throwMe(error);
+ }
+
+ secdebug("atomicfile", "%p closed %s", this, mPath.c_str());
+ }
+}
+
+// Commit the current create or write and close the write file. Note that a throw during the commit does an automatic rollback.
+void
+AtomicTempFile::commit()
+{
+ try
+ {
+ fsync();
+ close();
+ const char *oldPath = mPath.c_str();
+ const char *newPath = mFile.path().c_str();
+
+ // <rdar://problem/6991037>
+ // Copy the security parameters of one file to another
+ // Adding this to guard against setuid utilities that are re-writing a user's keychain. We don't want to leave them root-owned.
+ // In order to not break backward compatability we'll make a best effort, but continue if these efforts fail.
+ //
+ // To clear something up - newPath is the name the keychain will become - which is the name of the file being replaced
+ // oldPath is the "temp filename".
+
+ copyfile_state_t s;
+ s = copyfile_state_alloc();
+
+ if(copyfile(newPath, oldPath, s, COPYFILE_SECURITY | COPYFILE_NOFOLLOW) == -1) // Not fatal
+ secdebug("atomicfile", "copyfile (%s, %s): %s", oldPath, newPath, strerror(errno));
+
+ copyfile_state_free(s);
+ // END <rdar://problem/6991037>
+
+ ::utimes(oldPath, NULL);
+
+ if (::rename(oldPath, newPath) == -1)
+ {
+ int error = errno;
+ secdebug("atomicfile", "rename (%s, %s): %s", oldPath, newPath, strerror(errno));
+ UnixError::throwMe(error);
+ }
+
+ // Unlock the lockfile
+ mLockedFile = NULL;
+
+ secdebug("atomicfile", "%p commited %s", this, oldPath);
+ }
+ catch (...)
+ {
+ rollback();
+ throw;
+ }
+}
+
+// Rollback the current create or write (happens automatically if commit() isn't called before the destructor is.
+void
+AtomicTempFile::rollback() throw()
+{
+ if (mFileRef >= 0)
+ {
+ AtomicFile::rclose(mFileRef);
+ mFileRef = -1;
+ }
+
+ // @@@ Log errors if this fails.
+ const char *path = mPath.c_str();
+ if (::unlink(path) == -1)
+ {
+ secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
+ // rollback can't throw
+ }
+
+ // @@@ Think about this. Depending on how we do locking we might not need this.
+ if (mCreating)
+ {
+ const char *path = mFile.path().c_str();
+ if (::unlink(path) == -1)
+ {
+ secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
+ // rollback can't throw
+ }
+ }
+}
+
+
+//
+// An advisory write lock for inFile.
+//
+FileLocker::~FileLocker()
+{
+}
+
+
+
+LocalFileLocker::LocalFileLocker(AtomicFile &inFile) :
+ mPath(inFile.lockFileName())
+{
+}
+
+
+LocalFileLocker::~LocalFileLocker()
+{
+}
+
+
+
+#ifndef NDEBUG
+static double GetTime()
+{
+ struct timeval t;
+ gettimeofday(&t, NULL);
+ return ((double) t.tv_sec) + ((double) t.tv_usec) / 1000000.0;
+}
+#endif
+
+
+
+void
+LocalFileLocker::lock(mode_t mode)
+{
+ struct stat st;
+
+ do
+ {
+ // if the lock file doesn't exist, create it
+ mLockFile = open(mPath.c_str(), O_RDONLY | O_CREAT, mode);
+
+ // if we can't open or create the file, something is wrong
+ if (mLockFile == -1)
+ {
+ UnixError::throwMe(errno);
+ }
+
+ // try to get exclusive access to the file
+ IFDEBUG(double startTime = GetTime());
+ int result = flock(mLockFile, LOCK_EX);
+ IFDEBUG(double endTime = GetTime());
+
+ IFDEBUG(secdebug("atomictime", "Waited %.4f milliseconds for file lock", (endTime - startTime) * 1000.0));
+
+ // errors at this point are bad
+ if (result == -1)
+ {
+ UnixError::throwMe(errno);
+ }
+
+ // check and see if the file we have access to still exists. If not, another file shared our file lock
+ // due to a hash collision and has thrown our lock away -- that, or a user blew the lock file away himself.
+
+ result = fstat(mLockFile, &st);
+
+ // errors at this point are bad
+ if (result == -1)
+ {
+ UnixError::throwMe(errno);
+ }
+
+ if (st.st_nlink == 0) // we've been unlinked!
+ {
+ close(mLockFile);
+ }
+ } while (st.st_nlink == 0);
+}
+
+
+void
+LocalFileLocker::unlock()
+{
+ flock(mLockFile, LOCK_UN);
+ close(mLockFile);
+}
+
+
+
+NetworkFileLocker::NetworkFileLocker(AtomicFile &inFile) :
+ mDir(inFile.dir()),
+ mPath(inFile.dir() + "lck~" + inFile.file())
+{
+}
+
+NetworkFileLocker::~NetworkFileLocker()
+{
+}
+
+std::string
+NetworkFileLocker::unique(mode_t mode)
+{
+ static const int randomPart = 16;
+ DevRandomGenerator randomGen;
+ std::string::size_type dirSize = mDir.size();
+ std::string fullname(dirSize + randomPart + 2, '\0');
+ fullname.replace(0, dirSize, mDir);
+ fullname[dirSize] = '~'; /* UNIQ_PREFIX */
+ char buf[randomPart];
+ struct stat filebuf;
+ int result, fd = -1;
+
+ for (int retries = 0; retries < 10; ++retries)
+ {
+ /* Make a random filename. */
+ randomGen.random(buf, randomPart);
+ for (int ix = 0; ix < randomPart; ++ix)
+ {
+ char ch = buf[ix] & 0x3f;
+ fullname[ix + dirSize + 1] = ch +
+ ( ch < 26 ? 'A'
+ : ch < 26 + 26 ? 'a' - 26
+ : ch < 26 + 26 + 10 ? '0' - 26 - 26
+ : ch == 26 + 26 + 10 ? '-' - 26 - 26 - 10
+ : '_' - 26 - 26 - 11);
+ }
+
+ result = lstat(fullname.c_str(), &filebuf);
+ if (result && errno == ENAMETOOLONG)
+ {
+ do
+ fullname.erase(fullname.end() - 1);
+ while((result = lstat(fullname.c_str(), &filebuf)) && errno == ENAMETOOLONG && fullname.size() > dirSize + 8);
+ } /* either it stopped being a problem or we ran out of filename */
+
+ if (result && errno == ENOENT)
+ {
+ fd = AtomicFile::ropen(fullname.c_str(), O_WRONLY|O_CREAT|O_EXCL, mode);
+ if (fd >= 0 || errno != EEXIST)
+ break;
+ }
+ }
+
+ if (fd < 0)
+ {
+ int error = errno;
+ ::syslog(LOG_ERR, "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error));
+ secdebug("atomicfile", "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error));
+ UnixError::throwMe(error);
+ }
+
+ /* @@@ Check for EINTR. */
+ write(fd, "0", 1); /* pid 0, `works' across networks */
+
+ AtomicFile::rclose(fd);
+
+ return fullname;
+}
+
+/* Return 0 on success and 1 on failure if st is set to the result of stat(old) and -1 on failure if the stat(old) failed. */
+int
+NetworkFileLocker::rlink(const char *const old, const char *const newn, struct stat &sto)
+{
+ int result = ::link(old,newn);
+ if (result)
+ {
+ int serrno = errno;
+ if (::lstat(old, &sto) == 0)
+ {
+ struct stat stn;
+ if (::lstat(newn, &stn) == 0
+ && sto.st_dev == stn.st_dev
+ && sto.st_ino == stn.st_ino
+ && sto.st_uid == stn.st_uid
+ && sto.st_gid == stn.st_gid
+ && !S_ISLNK(sto.st_mode))
+ {
+ /* Link failed but files are the same so the link really went ok. */
+ return 0;
+ }
+ else
+ result = 1;
+ }
+ errno = serrno; /* Restore errno from link() */
+ }
+
+ return result;
+}
+
+/* NFS-resistant rename()
+ * rename with fallback for systems that don't support it
+ * Note that this does not preserve the contents of the file. */
+int
+NetworkFileLocker::myrename(const char *const old, const char *const newn)
+{
+ struct stat stbuf;
+ int fd = -1;
+ int ret;
+
+ /* Try a real hardlink */
+ ret = rlink(old, newn, stbuf);
+ if (ret > 0)
+ {
+ if (stbuf.st_nlink < 2 && (errno == EXDEV || errno == ENOTSUP))
+ {
+ /* Hard link failed so just create a new file with O_EXCL instead. */
+ fd = AtomicFile::ropen(newn, O_WRONLY|O_CREAT|O_EXCL, stbuf.st_mode);
+ if (fd >= 0)
+ ret = 0;
+ }
+ }
+
+ /* We want the errno from the link or the ropen, not that of the unlink. */
+ int serrno = errno;
+
+ /* Unlink the temp file. */
+ ::unlink(old);
+ if (fd > 0)
+ AtomicFile::rclose(fd);
+
+ errno = serrno;
+ return ret;
+}
+
+int
+NetworkFileLocker::xcreat(const char *const name, mode_t mode, time_t &tim)
+{
+ std::string uniqueName = unique(mode);
+ const char *uniquePath = uniqueName.c_str();
+ struct stat stbuf; /* return the filesystem time to the caller */
+ stat(uniquePath, &stbuf);
+ tim = stbuf.st_mtime;
+ return myrename(uniquePath, name);
+}
+
+void
+NetworkFileLocker::lock(mode_t mode)
+{
+ const char *path = mPath.c_str();
+ bool triedforce = false;
+ struct stat stbuf;
+ time_t t, locktimeout = 1024; /* DEFlocktimeout, 17 minutes. */
+ bool doSyslog = false;
+ bool failed = false;
+ int retries = 0;
+
+ while (!failed)
+ {
+ /* Don't syslog first time through. */
+ if (doSyslog)
+ ::syslog(LOG_NOTICE, "Locking %s", path);
+ else
+ doSyslog = true;
+
+ secdebug("atomicfile", "Locking %s", path); /* in order to cater for clock skew: get */
+ if (!xcreat(path, mode, t)) /* time t from the filesystem */
+ {
+ /* lock acquired, hurray! */
+ break;
+ }
+ switch(errno)
+ {
+ case EEXIST: /* check if it's time for a lock override */
+ if (!lstat(path, &stbuf) && stbuf.st_size <= 16 /* MAX_locksize */ && locktimeout
+ && !lstat(path, &stbuf) && locktimeout < t - stbuf.st_mtime)
+ /* stat() till unlink() should be atomic, but can't guarantee that. */
+ {
+ if (triedforce)
+ {
+ /* Already tried, force lock override, not trying again */
+ failed = true;
+ break;
+ }
+ else if (S_ISDIR(stbuf.st_mode) || ::unlink(path))
+ {
+ triedforce=true;
+ ::syslog(LOG_ERR, "Forced unlock denied on %s", path);
+ secdebug("atomicfile", "Forced unlock denied on %s", path);
+ }
+ else
+ {
+ ::syslog(LOG_ERR, "Forcing lock on %s", path);
+ secdebug("atomicfile", "Forcing lock on %s", path);
+ sleep(16 /* DEFsuspend */);
+ break;
+ }
+ }
+ else
+ triedforce = false; /* legitimate iteration, clear flag */
+
+ /* Reset retry counter. */
+ retries = 0;
+ usleep(250000);
+ break;
+
+ case ENOSPC: /* no space left, treat it as a transient */
+#ifdef EDQUOT /* NFS failure */
+ case EDQUOT: /* maybe it was a short term shortage? */
+#endif
+ case ENOENT:
+ case ENOTDIR:
+ case EIO:
+ /*case EACCES:*/
+ if(++retries < (256 + 1)) /* nfsTRY number of times+1 to ignore spurious NFS errors */
+ usleep(250000);
+ else
+ failed = true;
+ break;
+
+#ifdef ENAMETOOLONG
+ case ENAMETOOLONG: /* Filename is too long, shorten and retry */
+ if (mPath.size() > mDir.size() + 8)
+ {
+ secdebug("atomicfile", "Truncating %s and retrying lock", path);
+ mPath.erase(mPath.end() - 1);
+ path = mPath.c_str();
+ /* Reset retry counter. */
+ retries = 0;
+ break;
+ }
+ /* DROPTHROUGH */
+#endif
+ default:
+ failed = true;
+ break;
+ }
+ }
+
+ if (failed)
+ {
+ int error = errno;
+ ::syslog(LOG_ERR, "Lock failure on %s: %s", path, strerror(error));
+ secdebug("atomicfile", "Lock failure on %s: %s", path, strerror(error));
+ UnixError::throwMe(error);
+ }
+}
+
+void
+NetworkFileLocker::unlock()
+{
+ const char *path = mPath.c_str();
+ if (::unlink(path) == -1)
+ {
+ secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
+ // unlock can't throw
+ }
+}
+
+
+
+AtomicLockedFile::AtomicLockedFile(AtomicFile &inFile)
+{
+ if (inFile.isOnLocalFileSystem())
+ {
+ mFileLocker = new LocalFileLocker(inFile);
+ }
+ else
+ {
+ mFileLocker = new NetworkFileLocker(inFile);
+ }
+
+ lock();
+}
+
+
+
+AtomicLockedFile::~AtomicLockedFile()
+{
+ unlock();
+ delete mFileLocker;
+}
+
+
+
+void
+AtomicLockedFile::lock(mode_t mode)
+{
+ mFileLocker->lock(mode);
+}
+
+
+
+void AtomicLockedFile::unlock() throw()
+{
+ mFileLocker->unlock();
+}
+
+
+
+#undef kAtomicFileMaxBlockSize