X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/80e2389990082500d76eb566d4946be3e786c3ef..d8f41ccd20de16f8ebe2ccc84d47bf1cb2b26bbb:/libsecurity_filedb/lib/AtomicFile.cpp diff --git a/libsecurity_filedb/lib/AtomicFile.cpp b/libsecurity_filedb/lib/AtomicFile.cpp deleted file mode 100644 index 7ff71756..00000000 --- a/libsecurity_filedb/lib/AtomicFile.cpp +++ /dev/null @@ -1,1262 +0,0 @@ -/* - * 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 - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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(), (CC_LONG)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 -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 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 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 -AtomicFile::write() -{ - - RefPointer lock(new AtomicLockedFile(*this)); - return new AtomicTempFile(*this, lock); -} - -// Return a bufferedFile containing current version of the file for reading. -RefPointer -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); - } -} - -// -// Make sure the directory up to inDir exists inDir *must* end in a slash. -// -void -AtomicFile::mkpath(const std::string &inDir, mode_t mode) -{ - // see if the file already exists and is a directory - struct stat st; - int result = stat(inDir.c_str(), &st); - - if (result == 0) // file exists - { - if ((st.st_mode & S_IFDIR) == 0) - { - // whatever was there, it wasn't a directory. That's really bad, so complain - syslog(LOG_ALERT, "Needed a directory at %s, but the file that was there was not one.\n", inDir.c_str()); - UnixError::throwMe(ENOTDIR); - } - } - else - { - // the file did not exist, try to create it - result = mkpath_np(inDir.c_str(), 0777); // make the directory with umask - if (result != 0) - { - // mkpath_np does not set errno, you have to look at the result. - UnixError::throwMe(result); - } - } - - // Double check and see if we got what we hoped for - result = stat(inDir.c_str(), &st); - if (result != 0) - { - UnixError::throwMe(errno); - } - - if ((st.st_mode & S_IFDIR) == 0) - { - // we didn't create a dictionary? That's curious... - syslog(LOG_ALERT, "Failed to create a directory when we asked for one to be created at %s\n", inDir.c_str()); - UnixError::throwMe(ENOTDIR); - } -} - -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, (size_t)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 = (ssize_t)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, (size_t)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); - - off_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 &inLockedFile, mode_t mode) : - mFile(inFile), - mLockedFile(inLockedFile), - mCreating(true) -{ - create(mode); -} - -AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer &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 = (int)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(&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 aBuffer(inCount); - for (uint32 i = 0; i < inCount; i++) - aBuffer.get()[i] = htonl(inData[i]); -#endif - - write(inOffsetType, inOffset, reinterpret_cast(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(); - - // - // 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 - - ::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