X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/bac41a7b9a0a9254fa30f8bb6e6038ab71a483e2..ce0ac947b4708d0bc1c7e6789b3e1f3bfc80d6e9:/cdsa/cdsa_utilities/AtomicFile.cpp?ds=inline diff --git a/cdsa/cdsa_utilities/AtomicFile.cpp b/cdsa/cdsa_utilities/AtomicFile.cpp index 710c70e8..bbde5634 100644 --- a/cdsa/cdsa_utilities/AtomicFile.cpp +++ b/cdsa/cdsa_utilities/AtomicFile.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved. + * Copyright (c) 2000-2001, 2003 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'). @@ -16,573 +16,433 @@ */ -// -// AtomicFile.cpp - Description t.b.d. -// -#ifdef __MWERKS__ -#define _CPP_ATOMICFILE -#endif - #include -#include -#include -#include -#include -#include -#include +#include -#if _USE_IO == _USE_IO_POSIX -#include +#include +#include +#include +#include +#include #include -#include +#include -#include -//#include -#include -#include -#include -#elif _USE_IO == _USE_IO_MACOS -typedef SInt32 ssize_t; -#endif +#define kAtomicFileMaxBlockSize INT_MAX -using namespace std; -AtomicFile::AtomicFile(const DbName &inDbName) : - mReadFile(nil), - mReadFilename(inDbName.dbName()), - mWriteFile(nil), - mWriteFilename(mReadFilename + ",") // XXX Do some more work here like resolving symlinks/aliases etc. +// +// AtomicFile.cpp - Description t.b.d. +// +AtomicFile::AtomicFile(const std::string &inPath) : + mPath(inPath) { - // We only support databases with string names of non-zero length. - if (inDbName.dbLocation() != nil || inDbName.dbName().length() == 0) - CssmError::throwMe(CSSMERR_DL_INVALID_DB_LOCATION); + pathSplit(inPath, mDir, mFile); } AtomicFile::~AtomicFile() { - // Assume there are no more running theads in this object. - - // Try hard to clean up as much as possible. - try - { - // Rollback any pending write. - if (mWriteFile) - rollback(); - } - catch(...) {} - - // Close and delete all files in mOpenFileMap - for (OpenFileMap::iterator it = mOpenFileMap.begin(); it != mOpenFileMap.end(); it++) - { - try - { - it->second->close(); - } - catch(...) {} - try - { - delete it->second; - } - catch(...) {} - } } +// Aquire the write lock and remove the file. void -AtomicFile::close() +AtomicFile::performDelete() { - StLock _(mReadLock); - - // If we have no read file we have nothing to close. - if (mReadFile == nil) - return; + 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); + } +} - // Remember mReadFile and set it to nil, so that it will be closed after any pending write completes - OpenFile *aOpenFile = mReadFile; - mReadFile = nil; +// 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(); - // If aOpenFile has a zero use count no other thread is currently using it, - // so we can safely remove it from the map. - if (aOpenFile->mUseCount == 0) - { - // Do not close any files (nor remove them from the map) while some thread is writing - // since doing so might release the lock we are holding. - if (mWriteLock.tryLock()) - { - // Release the write lock immediately since tryLock just aquired it and we don't want to write. - mWriteLock.unlock(); - - // Remove aOpenFile from the map of open files. - mOpenFileMap.erase(aOpenFile->versionId()); - try - { - aOpenFile->close(); - } - catch(...) - { - delete aOpenFile; - throw; - } - delete aOpenFile; - } - } + // @@@ 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); + } } -AtomicFile::VersionId -AtomicFile::enterRead(const uint8 *&outFileAddress, size_t &outLength) +// Lock the file for writing and return a newly created AtomicTempFile. +RefPointer +AtomicFile::create(mode_t mode) { - StLock _(mReadLock); + const char *path = mPath.c_str(); - // If we already have a read file check if it is still current. - if (mReadFile != nil) - { - if (mReadFile->isDirty()) - { - // Remember mReadFile and set it to nil in case an exception is thrown - OpenFile *aOpenFile = mReadFile; - mReadFile = nil; - - // If aOpenFile has a zero use count no other thread is currently using it, - // so we can safely remove it from the map. - if (aOpenFile->mUseCount == 0) - { - // Do not close any files (nor remove them from the map) while some thread is writing - // since doing so might release the lock we are holding. - if (mWriteLock.tryLock()) - { - // Release the write lock immediately since tryLock just aquired it and we don't want to write. - mWriteLock.unlock(); - - // Remove aOpenFile from the map of open files. - mOpenFileMap.erase(aOpenFile->versionId()); - try - { - aOpenFile->close(); - } - catch(...) - { - delete aOpenFile; - throw; - } - delete aOpenFile; - } - } - } - } + // First make sure the directory to this file exists and is writable + mkpath(mDir); - // If we never had or no longer have an open read file. Open it now. - if (mReadFile == nil) + RefPointer lock(new AtomicLockedFile(*this)); + int fileRef = ropen(path, O_WRONLY|O_CREAT|O_EXCL, mode); + if (fileRef == -1) { - mReadFile = new OpenFile(mReadFilename, false, false, 0); - mOpenFileMap.insert(OpenFileMap::value_type(mReadFile->versionId(), mReadFile)); - } - // Note that mReadFile->isDirty() might actually return true here, but all that mean is - // that we are looking at data that was commited after we opened the file which might - // happen in a few miliseconds anyway. + int error = errno; + secdebug("atomicfile", "open %s: %s", path, strerror(error)); - // Bump up the use count of our OpenFile. - mReadFile->mUseCount++; + // 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); - // Return the length of the file and the mapped address. - outLength = mReadFile->length(); - outFileAddress = mReadFile->address(); - return mReadFile->versionId(); + try + { + // Now that we have created the lock and the new db file create a tempfile + // object. + RefPointer 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; + } } -void -AtomicFile::exitRead(VersionId inVersionId) +// Lock the database file for writing and return a newly created AtomicTempFile. +RefPointer +AtomicFile::write() { - StLock _(mReadLock); - OpenFileMap::iterator it = mOpenFileMap.find(inVersionId); - // If the inVersionId is not in the map anymore something really bad happned. - if (it == mOpenFileMap.end()) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); - - OpenFile *aOpenFile = it->second; - aOpenFile->mUseCount--; + RefPointer lock(new AtomicLockedFile(*this)); + return new AtomicTempFile(*this, lock); +} - // Don't close the current active file even if its mUseCount hits 0 since someone - // else will probably request it soon. - if (aOpenFile->mUseCount == 0 && aOpenFile != mReadFile) - { - // Do not close any files (nor remove them from the map) while some thread is writing - // since doing so might release the lock we are holding. - if (mWriteLock.tryLock()) - { - // Release the write lock immidiatly since tryLock just aquired it and we don't want to write. - mWriteLock.unlock(); - - // Remove from the map, close and delete aOpenFile. - mOpenFileMap.erase(it); - try - { - aOpenFile->close(); - } - catch(...) - { - delete aOpenFile; - throw; - } - delete aOpenFile; - } - } +// Return a bufferedFile containing current version of the file for reading. +RefPointer +AtomicFile::read() +{ + return new AtomicBufferedFile(mPath); } -bool AtomicFile::isDirty(VersionId inVersionId) +mode_t +AtomicFile::mode() const { - StLock _(mReadLock); - OpenFileMap::iterator it = mOpenFileMap.find(inVersionId); - // If the inVersionId is not in the map anymore something really bad happned. - if (it == mOpenFileMap.end()) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + 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; +} - return it->second->isDirty(); +// 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); + } } +// +// Make sure the directory up to inDir exists inDir *must* end in a slash. +// void -AtomicFile::performDelete() +AtomicFile::mkpath(const std::string &inDir, mode_t mode) { - // Prevent any other threads in this process from writing. - mWriteLock.lock(); + 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 (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 + } +} - OpenFile *aReadFile = nil; - try - { - // Keep reopening mReadFilename until the lock has been aquired on a non-dirty file. - // XXX This is a potential infinite loop. - for (;;) - { - aReadFile = new OpenFile(mReadFilename, true, true, 0); - if (!aReadFile->isDirty()) - break; +int +AtomicFile::ropen(const char *const name, int flags, mode_t mode) +{ + int fd, tries_left = 4 /* kNoResRetry */; + do + { + fd = ::open(name, flags, mode); + } while (fd < 0 && (errno == EINTR || errno == ENFILE && --tries_left >= 0)); - aReadFile->close(); - delete aReadFile; - aReadFile = nil; - } + return fd; +} - // Aquire the read lock so no other thread will open the file - StLock _(mReadLock); +int +AtomicFile::rclose(int fd) +{ + int result; + do + { + result = ::close(fd); + } while(result && errno == EINTR); - // Delete the file. - unlink(mReadFilename); + return result; +} - // Clear our current mReadFile since it refers to the deleted file. - mReadFile = nil; +// +// 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) : + mPath(inPath), + mFileRef(-1), + mBuffer(NULL), + mLength(0) +{ +} - // Mark the old file as modified - aReadFile->setDirty(); +AtomicBufferedFile::~AtomicBufferedFile() +{ + if (mFileRef >= 0) + { + AtomicFile::rclose(mFileRef); + secdebug("atomicfile", "%p closed %s", this, mPath.c_str()); + } - // Close any open files. - endWrite(); - } - catch(...) - { - if (aReadFile) - { - try - { - VersionId aVersionId = aReadFile->versionId(); - aReadFile->close(); - mOpenFileMap.erase(aVersionId); - } catch(...) {} - delete aReadFile; - } - endWrite(); - throw; - } - endWrite(); + if (mBuffer) + { + secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer); + free(mBuffer); + } } -AtomicFile::VersionId -AtomicFile::enterCreate(FileRef &outWriteRef) +// +// Open the file and return the length in bytes. +// +off_t +AtomicBufferedFile::open() { - // Prevent any other threads in this process from writing. - mWriteLock.lock(); - OpenFile *aReadFile = nil; - try + 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) { - // No threads can read during creation - StLock _(mReadLock); + int error = errno; + secdebug("atomicfile", "open %s: %s", path, strerror(error)); - // Create mReadFilename until the lock has been aquired on a non-dirty file. - aReadFile = new OpenFile(mReadFilename, false, true, 1); + // 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); + } - // Open mWriteFile for writing. - mWriteFile = new OpenFile(mWriteFilename, true, false, aReadFile->versionId() + 1); + mLength = ::lseek(mFileRef, 0, SEEK_END); + if (mLength == -1) + { + int error = errno; + secdebug("atomicfile", "lseek(%s, END): %s", path, strerror(error)); + AtomicFile::rclose(mFileRef); + UnixError::throwMe(error); + } - // Insert aReadFile into the map (do this after opening mWriteFile just in case that throws). - mOpenFileMap.insert(OpenFileMap::value_type(-1, aReadFile)); + secdebug("atomicfile", "%p opened %s: %qd bytes", this, path, mLength); - outWriteRef = mWriteFile->fileRef(); - mCreating = true; // So rollback() will delete mReadFileName. - return aReadFile->versionId(); - } - catch(...) - { - // Make sure we don't thow during cleanup since that would clobber the original - // error and prevent us from releasing mWriteLock - try - { - if (aReadFile) - { - try - { - aReadFile->close(); - // XXX We should only unlink if we know that no one else is currently creating the file. - //unlink(mReadFilename); - mOpenFileMap.erase(-1); - } catch(...) {} - delete aReadFile; - } - - if (mWriteFile) - { - try - { - mWriteFile->close(); - unlink(mWriteFilename); - } catch(...) {} - delete mWriteFile; - mWriteFile = nil; - } - } - catch(...) {} // Do not throw since we already have an error. - - // Release the write lock and remove any unused files from the map - endWrite(); - throw; - } + return mLength; } -AtomicFile::VersionId -AtomicFile::enterWrite(const uint8 *&outFileAddress, size_t &outLength, FileRef &outWriteRef) +// +// 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) { - // Wait for all other threads in this process to finish writing. - mWriteLock.lock(); - mCreating = false; // So rollback() will not delete mReadFileName. - OpenFile *aReadFile = nil; - try - { - // Keep reopening mReadFilename until the lock has been aquired on a non-dirty file. - // XXX This is a potential infinite loop. - for (;;) - { - aReadFile = new OpenFile(mReadFilename, true, true, 0); - if (!aReadFile->isDirty()) - break; - - aReadFile->close(); - delete aReadFile; - aReadFile = nil; - } - - // We have the write lock on the file now we start modifying our shared data - // stuctures so aquire the read lock. - StLock _(mReadLock); - - // Open mWriteFile for writing. - mWriteFile = new OpenFile(mWriteFilename, true, false, aReadFile->versionId() + 1); - - // Insert aReadFile into the map (do this after opening mWriteFile just in case that throws). - mOpenFileMap.insert(OpenFileMap::value_type(-1, aReadFile)); - - outWriteRef = mWriteFile->fileRef(); - outLength = aReadFile->length(); - outFileAddress = aReadFile->address(); - return aReadFile->versionId(); - } - catch(...) - { - // Make sure we don't thow during cleanup since that would clobber the original - // error and prevent us from releasing mWriteLock - try - { - if (aReadFile) - { - try - { - aReadFile->close(); - mOpenFileMap.erase(-1); - } catch(...) {} - delete aReadFile; - } - - if (mWriteFile) - { - try - { - mWriteFile->close(); - unlink(mWriteFilename); - } catch(...) {} - delete mWriteFile; - mWriteFile = nil; - } - } - catch(...) {} // Do not throw since we already have an error. - - // Release the write lock and remove any unused files from the map - endWrite(); - throw; - } -} + if (mFileRef < 0) + { + secdebug("atomicfile", "read %s: file yet not opened, opening", mPath.c_str()); + open(); + } -AtomicFile::VersionId -AtomicFile::commit() -{ - StLock _(mReadLock); - if (mWriteFile == nil) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + off_t bytesLeft = inLength; + uint8 *ptr; + if (mBuffer) + { + secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer); + free(mBuffer); + } - try - { - VersionId aVersionId = mWriteFile->versionId(); - mWriteFile->close(); - delete mWriteFile; - mWriteFile = nil; + mBuffer = ptr = reinterpret_cast(malloc(bytesLeft)); + secdebug("atomicfile", "%p allocated %s buffer %p size %qd", this, mPath.c_str(), mBuffer, bytesLeft); + off_t pos = inOffset; + while (bytesLeft) + { + size_t toRead = bytesLeft > kAtomicFileMaxBlockSize ? kAtomicFileMaxBlockSize : size_t(bytesLeft); + ssize_t bytesRead = ::pread(mFileRef, ptr, toRead, pos); + if (bytesRead == -1) + { + int error = errno; + if (error == EINTR) + { + // We got interrupted by a signal, so try again. + secdebug("atomicfile", "pread %s: interrupted, retrying", mPath.c_str()); + continue; + } - OpenFileMap::iterator it = mOpenFileMap.find(-1); - if (it == mOpenFileMap.end()) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + secdebug("atomicfile", "pread %s: %s", mPath.c_str(), strerror(error)); + free(mBuffer); + mBuffer = NULL; + UnixError::throwMe(error); + } - // First rename the file and them mark the old one as modified - rename(mWriteFilename, mReadFilename); - OpenFile *aOpenFile = it->second; + // Read returning 0 means EOF was reached so we're done. + if (bytesRead == 0) + break; - // Clear our current mReadFile since it refers to the old file. - mReadFile = nil; + secdebug("atomicfile", "%p read %s: %d bytes to %p", this, mPath.c_str(), bytesRead, ptr); - // Mark the old file as modified - aOpenFile->setDirty(); + bytesLeft -= bytesRead; + ptr += bytesRead; + pos += bytesRead; + } - // Close all unused files (in particular aOpenFile) and remove them from mOpenFileMap - endWrite(); - return aVersionId; - } - catch (...) - { - // Unlink the new file to rollback the transaction and close any open files. - try - { - unlink(mWriteFilename); - }catch(...) {} - endWrite(); - throw; - } + // Compute length + outLength = ptr - mBuffer; + + return mBuffer; } void -AtomicFile::rollback() +AtomicBufferedFile::close() { - StLock _(mReadLock); - if (mWriteFile == nil) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + 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); + } - try - { - mWriteFile->close(); - delete mWriteFile; - mWriteFile = nil; - - // First rename the file and them mark the old one as modified - unlink(mWriteFilename); - if (mCreating) - unlink(mReadFilename); - endWrite(); - } - catch(...) - { - // Unlink the new file to rollback the transaction and close any open files. - try - { - unlink(mWriteFilename); - }catch(...) {} - endWrite(); - throw; - } + secdebug("atomicfile", "%p closed %s", this, mPath.c_str()); + } } -// This private function is called by a successfull commit(), rollback() or performDelete() as well -// as by a failed enterWrite() or enterCreate(). -void -AtomicFile::endWrite() + +// +// AtomicTempFile - A temporary file to write changes to. +// +AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer &inLockedFile, mode_t mode) : + mFile(inFile), + mLockedFile(inLockedFile), + mCreating(true) { - try - { - // We need to go in and close and delete all unused files from the queue - stack aDeleteList; - OpenFileMap::iterator it; - for (it = mOpenFileMap.begin(); - it != mOpenFileMap.end(); - it++) - { - OpenFile *aOpenFile = it->second; - // If aOpenFile is unused and it is not the mReadFile schedule it for close and removal. - // Note that if this is being called after a commit mReadFile will have been set to nil. - if (aOpenFile != mReadFile && aOpenFile->mUseCount == 0) - aDeleteList.push(it->first); - } - - // Remove everything that was scheduled for removal - while (!aDeleteList.empty()) - { - it = mOpenFileMap.find(aDeleteList.top()); - aDeleteList.pop(); - try - { - it->second->close(); - } - catch(...) {} - delete it->second; - mOpenFileMap.erase(it); - } - - if (mWriteFile) - { - mWriteFile->close(); - } - } - catch(...) - { - delete mWriteFile; - mWriteFile = nil; - mWriteLock.unlock(); - throw; - } + create(mode); +} - delete mWriteFile; - mWriteFile = nil; - mWriteLock.unlock(); +AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer &inLockedFile) : + mFile(inFile), + mLockedFile(inLockedFile), + mCreating(false) +{ + create(mFile.mode()); } -void -AtomicFile::rename(const string &inSrcFilename, const string &inDestFilename) +AtomicTempFile::~AtomicTempFile() { - if (::rename(inSrcFilename.c_str(), inDestFilename.c_str())) - UnixError::throwMe(errno); + // rollback if we didn't commit yet. + if (mFileRef >= 0) + rollback(); } +// +// Open the file and return the length in bytes. +// void -AtomicFile::unlink(const string &inFilename) +AtomicTempFile::create(mode_t mode) { - if (::unlink(inFilename.c_str())) - UnixError::throwMe(errno); + mPath = mFile.dir() + "," + mFile.file(); + 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); + } + + secdebug("atomicfile", "%p created %s", this, path); } void -AtomicFile::write(OffsetType inOffsetType, uint32 inOffset, const uint32 inData) +AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 inData) { uint32 aData = htonl(inData); write(inOffsetType, inOffset, reinterpret_cast(&aData), sizeof(aData)); } void -AtomicFile::write(OffsetType inOffsetType, uint32 inOffset, +AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 *inData, uint32 inCount) { #ifdef HOST_LONG_IS_NETWORK_LONG - // XXX Optimize this for the case where hl == nl + // Optimize this for the case where hl == nl const uint32 *aBuffer = inData; #else auto_array aBuffer(inCount); @@ -595,423 +455,430 @@ AtomicFile::write(OffsetType inOffsetType, uint32 inOffset, } void -AtomicFile::write(OffsetType inOffsetType, uint32 inOffset, const uint8 *inData, uint32 inLength) +AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint8 *inData, size_t inLength) { - // Seriously paranoid check. - if (mWriteFile == nil) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); - - if (inOffsetType != None) - { - if (::lseek(mWriteFile->mFileRef, inOffset, inOffsetType == FromStart ? SEEK_SET : SEEK_CUR) == -1) - UnixError::throwMe(errno); - } - - if (::write(mWriteFile->mFileRef, reinterpret_cast(inData), - inLength) != static_cast(inLength)) - UnixError::throwMe(errno); -} - -// AtomicFile::OpenFile implementation - -AtomicFile::OpenFile::OpenFile(const string &inFilename, bool write, bool lock, VersionId inVersionId) : - mUseCount(0), - mVersionId(inVersionId), - mAddress(NULL), - mLength(0) -{ - int flags, mode = 0; - if (write && lock) - { - flags = O_RDWR; - mState = ReadWrite; - } - else if (write && !lock) - { - flags = O_WRONLY|O_CREAT|O_TRUNC; - mode = 0666; - mState = Write; - } - else if (!write && lock) - { - flags = O_WRONLY|O_CREAT|O_TRUNC|O_EXCL; - mode = 0666; - mState = Create; - } - else - { - flags = O_RDONLY; - mState = Read; - } - - mFileRef = ::open(inFilename.c_str(), flags, mode); - if (mFileRef == -1) - { - int error = errno; - -#if _USE_IO == _USE_IO_POSIX - // Do the obvious error code translations here. - if (error == ENOENT) + 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) { - // Throw CSSMERR_DL_DATASTORE_DOESNOT_EXIST even in Write state since it means someone threw away our parent directory. - if (mState == ReadWrite || mState == Read || mState == Write) - CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST); - if (mState == Create) + int error = errno; + if (error == EINTR) { - // Attempt to create the path to inFilename since one or more of the directories - // in the path do not yet exist. - mkpath(inFilename); - - // Now try the open again. - mFileRef = ::open(inFilename.c_str(), flags, mode); - error = mFileRef == -1 ? errno : 0; - if (error == ENOENT) - CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); + // We got interrupted by a signal, so try again. + secdebug("atomicfile", "write %s: interrupted, retrying", mPath.c_str()); + continue; } - } - - if (error == EACCES) - CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); - if (error == EEXIST) - CssmError::throwMe(CSSMERR_DL_DATASTORE_ALREADY_EXISTS); -#endif - - // Check if we are still in an error state. - if (error) - UnixError::throwMe(errno); - } - - // If this is a new file write out the versionId - if (mState == Create) - writeVersionId(mVersionId); + secdebug("atomicfile", "write %s: %s", mPath.c_str(), strerror(error)); + UnixError::throwMe(error); + } - // If this is a temp output file we are done. - if (mState == Write) - return; + // 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); + } - try - { - mLength = ::lseek(mFileRef, 0, SEEK_END); - if (mLength == static_cast(-1)) - UnixError::throwMe(errno); - if (mLength == 0) - { - // XXX What to set versionId to? - mVersionId = 0; - return; // No point in mapping a zero length file. - } - -#if _USE_IO == _USE_IO_POSIX - // Lock the file if required. - if (lock) - { - struct flock mLock; - mLock.l_start = 0; - mLock.l_len = 1; - mLock.l_pid = getpid(); - mLock.l_type = F_WRLCK; - mLock.l_whence = SEEK_SET; - - // Keep trying to obtain the lock if we get interupted. - for (;;) - { - if (::fcntl(mFileRef, F_SETLKW, reinterpret_cast(&mLock)) == -1) - { - int error = errno; - if (error == EINTR) - continue; - - if (error != ENOTSUP) - UnixError::throwMe(error); - - // XXX Filesystem does not support locking with fcntl use an alternative. - mFcntlLock = false; - } - else - mFcntlLock = true; - - break; - } - } - - if (mState != Create) - { - mAddress = reinterpret_cast - (::mmap(0, mLength, PROT_READ, MAP_FILE|MAP_SHARED, - mFileRef, 0)); - if (mAddress == reinterpret_cast(-1)) - { - mAddress = NULL; - UnixError::throwMe(errno); - } - - mVersionId = readVersionId(); - } -#else - if (mState != Create) - { - mAddress = reinterpret_cast(-1); - auto_array aBuffer(mLength); - if (::read(mFileRef, aBuffer.get(), mLength) != mLength) - UnixError::throwMe(errno); - - mAddress = reinterpret_cast(aBuffer.release()); - mVersionId = readVersionId(); - } -#endif - } - catch(...) - { - if (mState != Closed) - ::close(mFileRef); - throw; - } -} + secdebug("atomicfile", "%p wrote %s %d bytes from %p", this, mPath.c_str(), bytesWritten, ptr); -AtomicFile::OpenFile::~OpenFile() -{ - close(); + bytesLeft -= bytesWritten; + ptr += bytesWritten; + pos += bytesWritten; + } } void -AtomicFile::OpenFile::close() +AtomicTempFile::fsync() { - int error = 0; - if (mAddress != NULL) - { -#if _USE_IO == _USE_IO_POSIX - if (::munmap(const_cast(mAddress), mLength) == -1) - error = errno; -#else - delete[] mAddress; -#endif - - mAddress = NULL; - } - - if (mState == Write) - writeVersionId(mVersionId); - - if (mState != Closed) - { - mState = Closed; - if (::close(mFileRef) == -1) - error = errno; - } - - if (error != 0) - UnixError::throwMe(error); -} + if (mFileRef < 0) + { + secdebug("atomicfile", "fsync %s: already closed", mPath.c_str()); + } + else + { + int result; + do + { + result = ::fsync(mFileRef); + } while (result && errno == EINTR); -bool -AtomicFile::OpenFile::isDirty() -{ - if (mAddress == NULL) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + if (result == -1) + { + int error = errno; + secdebug("atomicfile", "fsync %s: %s", mPath.c_str(), strerror(errno)); + UnixError::throwMe(error); + } - return (mVersionId != readVersionId()) || mVersionId == 0; + secdebug("atomicfile", "%p fsynced %s", this, mPath.c_str()); + } } -// Set the files dirty bit (requires the file to be writeable and locked). void -AtomicFile::OpenFile::setDirty() +AtomicTempFile::close() { - if (mState != ReadWrite && mState != Create) - CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); + 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); + } - writeVersionId(0); + 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 -AtomicFile::OpenFile::unlock() +AtomicTempFile::commit() { -// XXX This should be called. -#if 0 - if (mFcntlLock) + try { - struct flock mLock; - mLock.l_start = 0; - mLock.l_len = 1; - mLock.l_pid = getpid(); - mLock.l_type = F_UNLCK; - mLock.l_whence = SEEK_SET; - if (::fcntl(mFileRef, F_SETLK, reinterpret_cast(&mLock)) == -1) - UnixError::throwMe(errno); - } -#endif -} - -AtomicFile::VersionId -AtomicFile::OpenFile::readVersionId() -{ - const uint8 *ptr; - char buf[4]; - - // Read the VersionId - if (mAddress == NULL) - { - // Seek to the end of the file minus 4 - if (mLength < 4) - CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); - - if (::lseek(mFileRef, mLength - 4, SEEK_SET) == -1) - UnixError::throwMe(errno); - - ptr = reinterpret_cast(buf); - if (::read(mFileRef, buf, 4) != 4) - UnixError::throwMe(errno); - } - else - { - ptr = mAddress + mLength - 4; - if (mLength < 4) - CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); - } + fsync(); + close(); + const char *oldPath = mPath.c_str(); + const char *newPath = mFile.path().c_str(); + if (::rename(oldPath, newPath) == -1) + { + int error = errno; + secdebug("atomicfile", "rename (%s, %s): %s", oldPath, newPath, strerror(errno)); + UnixError::throwMe(error); + } - VersionId aVersionId = 0; - for (int i = 0; i < 4; i++) - { - aVersionId = (aVersionId << 8) + ptr[i]; - } + // Unlock the lockfile + mLockedFile = NULL; - return aVersionId; + 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 -AtomicFile::OpenFile::writeVersionId(VersionId inVersionId) +AtomicTempFile::rollback() throw() { - if (mState == ReadWrite) + if (mFileRef >= 0) { - // Seek to the end of the file minus 4 - if (mLength < 4) - CssmError::throwMe(CSSMERR_DL_DATABASE_CORRUPT); + AtomicFile::rclose(mFileRef); + mFileRef = -1; + } - if (::lseek(mFileRef, mLength - 4, SEEK_SET) == -1) - UnixError::throwMe(errno); + // @@@ 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 } - else /* if (mState == Create || mState == Write) */ + + // @@@ Think about this. Depending on how we do locking we might not need this. + if (mCreating) { - // Seek to the end of the file. - if (::lseek(mFileRef, 0, SEEK_END) == -1) - UnixError::throwMe(errno); + const char *path = mFile.path().c_str(); + if (::unlink(path) == -1) + { + secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); + // rollback can't throw + } } +} - uint8 buf[4]; - // Serialize the VersionId - for (int i = 3; i >= 0; i--) - { - buf[i] = inVersionId & 0xff; - inVersionId = inVersionId >> 8; - } - // Write the VersionId - if (::write(mFileRef, reinterpret_cast(buf), 4) != 4) - UnixError::throwMe(errno); +// +// An advisory write lock for inFile. +// +AtomicLockedFile::AtomicLockedFile(AtomicFile &inFile) : + mDir(inFile.dir()), + mPath(inFile.dir() + "lck~" + inFile.file()) +{ + lock(); } -void -AtomicFile::OpenFile::mkpath(const std::string &inFilename) +AtomicLockedFile::~AtomicLockedFile() { - char *path = const_cast(inFilename.c_str()); // @@@ Const_cast is a lie!!! - struct stat sb; - char *slash; - mode_t dir_mode = (0777 & ~umask(0)) | S_IWUSR | S_IXUSR; + unlock(); +} - slash = path; +std::string +AtomicLockedFile::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 (;;) + for (int retries = 0; retries < 10; ++retries) { - slash += strspn(slash, "/"); - slash += strcspn(slash, "/"); - - if (*slash == '\0') - break; + /* 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); + } - *slash = '\0'; + 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 (stat(path, &sb)) + if (result && errno == ENOENT) { - if (errno != ENOENT || mkdir(path, dir_mode)) - UnixError::throwMe(errno); - /* The mkdir() and umask() calls both honor only the low - nine bits, so if you try to set a mode including the - sticky, setuid, setgid bits you lose them. So chmod(). */ - if (chmod(path, dir_mode) == -1) - UnixError::throwMe(errno); + fd = AtomicFile::ropen(fullname.c_str(), O_WRONLY|O_CREAT|O_EXCL, mode); + if (fd >= 0 || errno != EEXIST) + break; } - else if (!S_ISDIR(sb.st_mode)) - CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); // @@@ Should be is a directory - - *slash = '/'; } -} + 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 */ -// Constructor uglyness to work around C++ language limitations. -struct AtomicFileRef::InitArg -{ - AtomicFile::VersionId versionId; - const uint8 *address; - size_t length; -}; + AtomicFile::rclose(fd); -AtomicFileRef::~AtomicFileRef() -{ + return fullname; } -AtomicFileRef::AtomicFileRef(AtomicFile &inAtomicFile, const InitArg &inInitArg) : - mVersionId(inInitArg.versionId), - mAtomicFile(inAtomicFile), - mAddress(inInitArg.address), - mLength(inInitArg.length) +/* 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 +AtomicLockedFile::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; } -AtomicFileReadRef::~AtomicFileReadRef() +/* 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 +AtomicLockedFile::myrename(const char *const old, const char *const newn) { - try { - mAtomicFile.exitRead(mVersionId); - } - catch(...) { + 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; + } } -} -AtomicFileRef::InitArg -AtomicFileReadRef::enterRead(AtomicFile &inAtomicFile) -{ - InitArg anInitArg; - anInitArg.versionId = inAtomicFile.enterRead(anInitArg.address, anInitArg.length); - return anInitArg; + /* 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; } -AtomicFileReadRef::AtomicFileReadRef(AtomicFile &inAtomicFile) : - AtomicFileRef(inAtomicFile, enterRead(inAtomicFile)) +int +AtomicLockedFile::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); } -AtomicFileWriteRef::~AtomicFileWriteRef() +void +AtomicLockedFile::lock(mode_t mode) { - if (mOpen) { - try { - mAtomicFile.rollback(); + 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; } - catch (...) + 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; + sleep(8 /* DEFlocksleep */); + 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 < (7 + 1)) /* nfsTRY number of times+1 to ignore spurious NFS errors */ + sleep(8 /* DEFlocksleep */); + 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; } } -} -AtomicFileRef::InitArg -AtomicFileWriteRef::enterWrite(AtomicFile &inAtomicFile, AtomicFile::FileRef &outWriteFileRef) -{ - InitArg anInitArg; - anInitArg.versionId = inAtomicFile.enterWrite(anInitArg.address, anInitArg.length, outWriteFileRef); - return anInitArg; + 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); + } } -AtomicFileWriteRef::AtomicFileWriteRef(AtomicFile &inAtomicFile) : - AtomicFileRef(inAtomicFile, enterWrite(inAtomicFile, mFileRef)) +void +AtomicLockedFile::unlock() throw() { + const char *path = mPath.c_str(); + if (::unlink(path) == -1) + { + secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); + // unlock can't throw + } } + + +#undef kAtomicFileMaxBlockSize