#include <Security/AtomicFile.h>
#include <Security/devrandom.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <syslog.h>
#include <sys/param.h>
#include <sys/types.h>
#include <unistd.h>
#define kAtomicFileMaxBlockSize INT_MAX
AtomicFile::AtomicFile(const std::string &inPath) :
mPath(inPath)
{
pathSplit(inPath, mDir, mFile);
}
AtomicFile::~AtomicFile()
{
}
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);
}
}
void
AtomicFile::rename(const std::string &inNewPath)
{
const char *path = mPath.c_str();
const char *newPath = inNewPath.c_str();
AtomicLockedFile lock(*this);
if (::rename(path, newPath) != 0)
{
int error = errno;
secdebug("atomicfile", "rename(%s, %s): %s", path, newPath, strerror(error));
UnixError::throwMe(error);
}
}
RefPointer<AtomicTempFile>
AtomicFile::create(mode_t mode)
{
const char *path = mPath.c_str();
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));
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
{
RefPointer<AtomicTempFile> temp(new AtomicTempFile(*this, lock, mode));
secdebug("atomicfile", "%p created %s", this, path);
return temp;
}
catch (...)
{
if (::unlink(path) == -1)
{
secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
}
throw;
}
}
RefPointer<AtomicTempFile>
AtomicFile::write()
{
RefPointer<AtomicLockedFile> lock(new AtomicLockedFile(*this));
return new AtomicTempFile(*this, lock);
}
RefPointer<AtomicBufferedFile>
AtomicFile::read()
{
return new AtomicBufferedFile(mPath);
}
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;
}
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);
}
}
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 (errno != ENOENT || ::mkdir(cpath, mode))
UnixError::throwMe(errno);
}
else if (!S_ISDIR(sb.st_mode))
CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); }
}
int
AtomicFile::ropen(const char *const name, int flags, mode_t mode)
{
int fd, tries_left = 4 ;
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::AtomicBufferedFile(const std::string &inPath) :
mPath(inPath),
mFileRef(-1),
mBuffer(NULL),
mLength(0)
{
}
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);
free(mBuffer);
}
}
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));
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);
}
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);
}
secdebug("atomicfile", "%p opened %s: %qd bytes", this, path, mLength);
return mLength;
}
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;
uint8 *ptr;
if (mBuffer)
{
secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer);
free(mBuffer);
}
mBuffer = ptr = reinterpret_cast<uint8 *>(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)
{
secdebug("atomicfile", "pread %s: interrupted, retrying", mPath.c_str());
continue;
}
secdebug("atomicfile", "pread %s: %s", mPath.c_str(), strerror(error));
free(mBuffer);
mBuffer = NULL;
UnixError::throwMe(error);
}
if (bytesRead == 0)
break;
secdebug("atomicfile", "%p read %s: %d bytes to %p", this, mPath.c_str(), bytesRead, ptr);
bytesLeft -= bytesRead;
ptr += bytesRead;
pos += bytesRead;
}
outLength = ptr - mBuffer;
return mBuffer;
}
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::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()
{
if (mFileRef >= 0)
rollback();
}
void
AtomicTempFile::create(mode_t mode)
{
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));
if (error == EACCES)
CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED);
else
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
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)
{
secdebug("atomicfile", "write %s: interrupted, retrying", mPath.c_str());
continue;
}
secdebug("atomicfile", "write %s: %s", mPath.c_str(), strerror(error));
UnixError::throwMe(error);
}
if (bytesWritten == 0)
{
secdebug("atomicfile", "write %s: 0 bytes written", mPath.c_str());
CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR);
}
secdebug("atomicfile", "%p wrote %s %d 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());
}
}
void
AtomicTempFile::commit()
{
try
{
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);
}
mLockedFile = NULL;
secdebug("atomicfile", "%p commited %s", this, oldPath);
}
catch (...)
{
rollback();
throw;
}
}
void
AtomicTempFile::rollback() throw()
{
if (mFileRef >= 0)
{
AtomicFile::rclose(mFileRef);
mFileRef = -1;
}
const char *path = mPath.c_str();
if (::unlink(path) == -1)
{
secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
}
if (mCreating)
{
const char *path = mFile.path().c_str();
if (::unlink(path) == -1)
{
secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
}
}
}
AtomicLockedFile::AtomicLockedFile(AtomicFile &inFile) :
mDir(inFile.dir()),
mPath(inFile.dir() + "lck~" + inFile.file())
{
lock();
}
AtomicLockedFile::~AtomicLockedFile()
{
unlock();
}
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] = '~';
char buf[randomPart];
struct stat filebuf;
int result, fd = -1;
for (int retries = 0; retries < 10; ++retries)
{
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);
}
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);
}
write(fd, "0", 1);
AtomicFile::rclose(fd);
return fullname;
}
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))
{
return 0;
}
else
result = 1;
}
errno = serrno;
}
return result;
}
int
AtomicLockedFile::myrename(const char *const old, const char *const newn)
{
struct stat stbuf;
int fd = -1;
int ret;
ret = rlink(old, newn, stbuf);
if (ret > 0)
{
if (stbuf.st_nlink < 2 && (errno == EXDEV || errno == ENOTSUP))
{
fd = AtomicFile::ropen(newn, O_WRONLY|O_CREAT|O_EXCL, stbuf.st_mode);
if (fd >= 0)
ret = 0;
}
}
int serrno = errno;
::unlink(old);
if (fd > 0)
AtomicFile::rclose(fd);
errno = serrno;
return ret;
}
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;
stat(uniquePath, &stbuf);
tim = stbuf.st_mtime;
return myrename(uniquePath, name);
}
void
AtomicLockedFile::lock(mode_t mode)
{
const char *path = mPath.c_str();
bool triedforce = false;
struct stat stbuf;
time_t t, locktimeout = 1024;
bool doSyslog = false;
bool failed = false;
int retries = 0;
while (!failed)
{
if (doSyslog)
::syslog(LOG_NOTICE, "Locking %s", path);
else
doSyslog = true;
secdebug("atomicfile", "Locking %s", path);
if (!xcreat(path, mode, t))
{
break;
}
switch(errno)
{
case EEXIST:
if (!lstat(path, &stbuf) && stbuf.st_size <= 16 && locktimeout
&& !lstat(path, &stbuf) && locktimeout < t - stbuf.st_mtime)
{
if (triedforce)
{
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 );
break;
}
}
else
triedforce = false;
retries = 0;
sleep(8 );
break;
case ENOSPC:
#ifdef EDQUOT
case EDQUOT:
#endif
case ENOENT:
case ENOTDIR:
case EIO:
if(++retries < (7 + 1))
sleep(8 );
else
failed = true;
break;
#ifdef ENAMETOOLONG
case ENAMETOOLONG:
if (mPath.size() > mDir.size() + 8)
{
secdebug("atomicfile", "Truncating %s and retrying lock", path);
mPath.erase(mPath.end() - 1);
path = mPath.c_str();
retries = 0;
break;
}
#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
AtomicLockedFile::unlock() throw()
{
const char *path = mPath.c_str();
if (::unlink(path) == -1)
{
secdebug("atomicfile", "unlink %s: %s", path, strerror(errno));
}
}
#undef kAtomicFileMaxBlockSize