]> git.saurik.com Git - apple/security.git/blobdiff - libsecurity_filedb/lib/AtomicFile.cpp
Security-55163.44.tar.gz
[apple/security.git] / libsecurity_filedb / lib / AtomicFile.cpp
diff --git a/libsecurity_filedb/lib/AtomicFile.cpp b/libsecurity_filedb/lib/AtomicFile.cpp
new file mode 100644 (file)
index 0000000..1c37c4d
--- /dev/null
@@ -0,0 +1,1267 @@
+/*
+ * 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