Data-Deque-Shared
view release on metacpan or search on metacpan
* mapping, which a hostile peer could rewrite between validation and
* here (double-fetch TOCTOU). */
static inline DeqHandle *deq_setup(void *base, size_t ms, const char *path, int bfd,
uint64_t data_off, uint64_t ctl_off,
uint32_t elem_size) {
DeqHandle *h = (DeqHandle *)calloc(1, sizeof(DeqHandle));
if (!h) { munmap(base, ms); return NULL; }
h->hdr = (DeqHeader *)base;
h->data = (uint8_t *)base + data_off;
h->ctl = (uint64_t *)((uint8_t *)base + ctl_off);
h->mmap_size = ms;
h->elem_size = elem_size;
h->path = path ? strdup(path) : NULL;
h->notify_fd = -1;
h->backing_fd = bfd;
return h;
}
/* Validate a mapped header (shared by deq_create reopen and deq_open_fd). */
static inline int deq_validate_header(const DeqHeader *hdr, uint64_t file_size,
uint32_t expected_variant) {
if (hdr->magic != DEQ_MAGIC) return 0;
if (hdr->version != DEQ_VERSION) return 0;
if (hdr->variant_id != expected_variant) return 0;
if (hdr->elem_size == 0 || hdr->capacity == 0) return 0;
if (hdr->capacity > 0x80000000u) return 0;
/* Variant-specific elem_size sanity: prevents buffer overflows in the
* XS push paths if a corrupted/tampered file claims an impossibly-small
* elem_size (e.g. < 4 for a Str variant where push writes a 4-byte
* length prefix). */
if (expected_variant == DEQ_VAR_INT && hdr->elem_size != sizeof(int64_t))
return 0;
if (expected_variant == DEQ_VAR_STR && hdr->elem_size < sizeof(uint32_t) + 1)
return 0;
if (hdr->total_size != file_size) return 0;
if (hdr->data_off != sizeof(DeqHeader)) return 0;
if (hdr->ctl_off != deq_ctl_offset(hdr->elem_size, hdr->capacity)) return 0;
if (hdr->total_size != deq_total_size(hdr->elem_size, hdr->capacity)) return 0;
return 1;
}
/* Ring slots map via idx % capacity on a free-running 32-bit index; that tiles the
2^32 index space correctly only when capacity divides 2^32, i.e. is a power of two,
so a non-power-of-2 request would collide slots across the 2^32 wrap seam. Round up. */
static inline uint64_t deq_next_pow2(uint64_t n) {
if (n <= 1) return 1;
n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n |= n >> 32;
return n + 1;
}
/* Securely obtain a fd for the path-based backing store. Either create it fresh
* (O_CREAT|O_EXCL|O_NOFOLLOW at `mode`, default 0600 = owner-only), or, if it
* already exists, attach to it (O_RDWR|O_NOFOLLOW, no O_CREAT). O_EXCL blocks a
* pre-seeded or hard-linked file and O_NOFOLLOW a symlink swap, so a local
* attacker can no longer redirect or poison the backing store through the path.
* Cross-user sharing is opt-in via a wider `mode` (e.g. 0660); the caller still
* validates the file's contents. */
static int deq_secure_open(const char *path, mode_t mode, char *errbuf) {
for (int attempt = 0; attempt < 100; attempt++) {
int fd = open(path, O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, mode);
if (fd >= 0) { (void)fchmod(fd, mode); return fd; } /* exact mode: umask narrowed the O_EXCL create */
if (errno != EEXIST) { DEQ_ERR("create %s: %s", path, strerror(errno)); return -1; }
fd = open(path, O_RDWR|O_NOFOLLOW|O_CLOEXEC);
if (fd >= 0) return fd;
if (errno == ENOENT) continue; /* creator unlinked between our two opens; retry */
DEQ_ERR("open %s: %s", path, strerror(errno)); /* ELOOP => symlink rejected */
return -1;
}
DEQ_ERR("open %s: create/attach kept racing", path);
return -1;
}
static DeqHandle *deq_create(const char *path, uint64_t capacity,
uint32_t elem_size, uint32_t variant_id,
mode_t mode, char *errbuf) {
if (errbuf) errbuf[0] = '\0';
if (capacity == 0) { DEQ_ERR("capacity must be > 0"); return NULL; }
if (elem_size == 0) { DEQ_ERR("elem_size must be > 0"); return NULL; }
capacity = deq_next_pow2(capacity);
if (capacity == 0 || capacity > 0x80000000u) {
DEQ_ERR("capacity must be <= 2^31 (32-bit cursor halves)"); return NULL;
}
uint64_t total = deq_total_size(elem_size, capacity);
int anonymous = (path == NULL);
int fd = -1;
size_t map_size;
void *base;
if (anonymous) {
map_size = (size_t)total;
base = mmap(NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); return NULL; }
} else {
fd = deq_secure_open(path, mode, errbuf);
if (fd < 0) return NULL;
if (flock(fd, LOCK_EX) < 0) { DEQ_ERR("flock: %s", strerror(errno)); close(fd); return NULL; }
struct stat st;
if (fstat(fd, &st) < 0) {
DEQ_ERR("fstat: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL;
}
int is_new = (st.st_size == 0);
if (!is_new && (uint64_t)st.st_size < sizeof(DeqHeader)) {
DEQ_ERR("%s: file too small (%lld)", path, (long long)st.st_size);
flock(fd, LOCK_UN); close(fd); return NULL;
}
if (is_new && ftruncate(fd, (off_t)total) < 0) {
DEQ_ERR("ftruncate: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL;
}
map_size = is_new ? (size_t)total : (size_t)st.st_size;
base = mmap(NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL; }
if (!is_new) {
DeqHeader snap; /* single fetch: validate + setup use one copy */
memcpy(&snap, base, sizeof snap);
if (!deq_validate_header(&snap, (uint64_t)st.st_size, variant_id)) {
DEQ_ERR("invalid deque file"); munmap(base, map_size); flock(fd, LOCK_UN); close(fd); return NULL;
}
flock(fd, LOCK_UN); close(fd);
return deq_setup(base, map_size, path, -1,
snap.data_off, snap.ctl_off, snap.elem_size);
( run in 0.521 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )