Data-Log-Shared
view release on metacpan or search on metacpan
#endif
/* log_read / log_read_ex return codes */
#define LOG_READ_EMPTY 0 /* no entry at offset (end / uncommitted in flight) */
#define LOG_READ_OK 1 /* valid entry -- out_data, out_len, next_off set */
#define LOG_READ_ABANDONED 2 /* slot abandoned -- next_off set, no data */
#define LOG_READ_TRUNCATED 3 /* offset below truncation -- next_off = truncation, no data */
/* ================================================================
* Header (128 bytes)
* ================================================================ */
typedef struct {
uint32_t magic;
uint32_t version;
uint64_t data_size; /* 8: usable data region size */
uint64_t total_size; /* 16 */
uint64_t data_off; /* 24 */
uint8_t _pad0[32]; /* 32-63 */
uint64_t tail; /* 64: byte offset past last entry (CAS target) */
uint64_t count; /* 72: number of committed entries */
uint32_t waiters; /* 80: blocked tailers */
uint32_t wake_seq; /* 84: FUTEX_WAIT target (avoids 64-bit count wraparound) */
uint64_t stat_appends; /* 88 */
uint64_t stat_waits; /* 96 */
uint64_t stat_timeouts; /* 104 */
uint64_t truncation; /* 112: entries before this offset are invalid */
uint8_t _pad2[8]; /* 120-127 */
} LogHeader;
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
_Static_assert(sizeof(LogHeader) == 128, "LogHeader must be 128 bytes");
#endif
typedef struct {
LogHeader *hdr;
uint8_t *data;
size_t mmap_size;
char *path;
int notify_fd;
int backing_fd;
} LogHandle;
/* Layer B trusted bound: the true byte size of the mapped data region,
* derived from the process-private mmap_size (NOT the attacker-writable
* hdr->data_size). Every file-stored offset/length that indexes h->data is
* bounded against this before use, so a local peer that corrupts the backing
* file cannot drive an out-of-bounds read or write. For a valid log this
* equals hdr->data_size exactly, so the added checks never fire on good data. */
static inline uint64_t log_region_size(const LogHandle *h) {
return (h->mmap_size > sizeof(LogHeader)) ? (h->mmap_size - sizeof(LogHeader)) : 0;
}
/* ================================================================
* Utility
* ================================================================ */
static inline void log_make_deadline(double t, struct timespec *dl) {
clock_gettime(CLOCK_MONOTONIC, dl);
if (!(t < 1e9)) t = 1e9; /* clamp Inf/NaN/huge: avoid UB (time_t) cast -> instant spurious timeout */
dl->tv_sec += (time_t)t;
dl->tv_nsec += (long)((t - (double)(time_t)t) * 1e9);
if (dl->tv_nsec >= 1000000000L) { dl->tv_sec++; dl->tv_nsec -= 1000000000L; }
}
static inline int log_remaining(const struct timespec *dl, struct timespec *rem) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
rem->tv_sec = dl->tv_sec - now.tv_sec;
rem->tv_nsec = dl->tv_nsec - now.tv_nsec;
if (rem->tv_nsec < 0) { rem->tv_sec--; rem->tv_nsec += 1000000000L; }
return rem->tv_sec >= 0;
}
/* ================================================================
* Append -- CAS reserve space, publish reserve_size, write data,
* commit (len). reserve_size is published BEFORE data so a crashed
* writer leaves a recoverable slot boundary for readers.
* ================================================================ */
static inline int64_t log_append(LogHandle *h, const void *data, uint32_t len, int utf8) {
if (len == 0) return -1; /* 0 is the uncommitted marker */
LogHeader *hdr = h->hdr;
/* Bit 31 of the stored len field carries the UTF8 flag, so cap payloads at
* < 2 GiB -- a single 2 GiB+ log entry is not a real use case. */
if (len >= 0x80000000u) return -1;
/* Pad total slot size up to 4-byte boundary so the next slot's
* header words are naturally aligned for atomic ops on ARM64. */
uint32_t entry_size = (LOG_ENTRY_HDR + len + 3U) & ~3U;
uint64_t region = log_region_size(h);
for (;;) {
uint64_t t = __atomic_load_n(&hdr->tail, __ATOMIC_RELAXED);
/* Layer B: tail is an attacker-writable shared-segment read used just
* below as a pointer offset (h->data + t) and as the memcpy target.
* Bound the slot within BOTH the logical data_size (the "log full"
* limit) and the true mapped region (mmap_size-derived, not the
* attacker-writable data_size), overflow-safe. For valid data
* data_size == region, so this is identical to the original
* full-check and the region test never fires early. */
if (t > hdr->data_size || entry_size > hdr->data_size - t) return -1;
if (t > region || entry_size > region - t) return -1;
if (__atomic_compare_exchange_n(&hdr->tail, &t, t + entry_size,
1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
uint8_t *slot = h->data + t;
/* Explicitly zero len before publishing reserve_size. In the
* common case the slot is already zero (fresh log_init or
* post-reset memset), but this defensive RELAXED store
* tolerates user code that bypasses log_reset's zeroing or
* concurrent edge cases. Release fence comes via the
* subsequent reserve_size store. */
__atomic_store_n((uint32_t *)(slot + sizeof(uint32_t)), 0U, __ATOMIC_RELAXED);
/* Publish slot boundary so readers can skip past us on crash.
* RELEASE so any reader observing reserve_size > 0 also sees
* the len=0 store above and correctly classifies the slot. */
__atomic_store_n((uint32_t *)slot, entry_size, __ATOMIC_RELEASE);
/* Write payload. */
memcpy(slot + LOG_ENTRY_HDR, data, len);
( run in 0.811 second using v1.01-cache-2.11-cpan-84e82930d8c )