#define _GNU_SOURCE #include "snapverify.h" #include #include #include #include #include #include /* Mirrors persist.c's SNAPSHOT_FORMAT_VERSION / header layout exactly — see * persist.c write_snapshot() (~line 1758) and load_snapshot() (~line 2325). * config-upgrade-snapshot task 1 bumped persist.c's SNAPSHOT_FORMAT_VERSION * 1->2 (host/queue admin-state frames migrated to capnp, Req 1.9/5.7) -- * this MUST track that bump, or this tool hard-rejects every real * checkpoint the current codebase writes (task 8.4 finding). */ #define SNAP_FORMAT_VERSION 2u #define SNAP_HEADER_MAX (16 + 1 + 8 + 32) /* magic+lsn + optional cfgref tail */ static void fail(snapverify_report_t* rep, int rc, off_t off, const char* why) { rep->ok = 0; rep->fail_offset = off; snprintf(rep->fail_reason, sizeof(rep->fail_reason), "%s", why); (void)rc; } int snapverify_path(const char* path, FILE* out, snapverify_report_t* rep) { if (!path) return -EINVAL; snapverify_report_t local; memset(&local, 0, sizeof(local)); local.ok = 1; local.fail_offset = -1; int fd = open(path, O_RDONLY); if (fd < 0) { int saved = errno; fail(&local, -saved, -1, strerror(saved)); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s\n", path, strerror(saved)); return -saved; } struct stat st; if (fstat(fd, &st) != 0) { int saved = errno; close(fd); fail(&local, -saved, -1, strerror(saved)); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: fstat: %s\n", path, strerror(saved)); return -saved; } local.file_size = st.st_size; char header[SNAP_HEADER_MAX]; ssize_t n = read(fd, header, sizeof(header)); if (n < 16) { close(fd); fail(&local, -EIO, 0, "short read on header (need >= 16 bytes)"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s\n", path, local.fail_reason); return -EIO; } if (memcmp(header, "XCESNAP\0", 8) != 0) { close(fd); fail(&local, -EINVAL, 0, "bad magic (not XCESNAP)"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s\n", path, local.fail_reason); return -EINVAL; } memcpy(&local.checkpoint_lsn, header + 8, 8); if (n >= SNAP_HEADER_MAX) { /* Decode the tag/hash UNCONDITIONALLY whenever the header extends * this far, regardless of the presence byte -- so a caller can * inspect the raw bytes even in edge cases. has_cfgref keeps its * existing meaning (whether those bytes are semantically * meaningful) untouched. */ memcpy(&local.cfgref_tag, header + 17, 8); memcpy(local.cfgref_sha, header + 25, 32); if (header[16] != 0) local.has_cfgref = 1; } off_t consumed = (off_t)n; if (st.st_size <= consumed) { /* Legacy, position-only checkpoint — not a defect (design.md). */ close(fd); local.consumed = consumed; if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: OK (legacy position-only, lsn=%" PRIu64 ", size=%jd)\n", path, local.checkpoint_lsn, (intmax_t)local.file_size); return 0; } uint32_t fmt = 0; if (read(fd, &fmt, sizeof(fmt)) != (ssize_t)sizeof(fmt)) { close(fd); fail(&local, -EIO, consumed, "short read on format_version"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (offset %jd)\n", path, local.fail_reason, (intmax_t)consumed); return -EIO; } consumed += (off_t)sizeof(fmt); local.has_entity_section = 1; local.format_version = fmt; if (fmt != SNAP_FORMAT_VERSION) { close(fd); fail(&local, -EINVAL, consumed - (off_t)sizeof(fmt), "unrecognized format_version"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (got %u, expected %u, offset %jd)\n", path, local.fail_reason, fmt, SNAP_FORMAT_VERSION, (intmax_t)local.fail_offset); return -EINVAL; } uint32_t job_count = 0; if (read(fd, &job_count, sizeof(job_count)) != (ssize_t)sizeof(job_count)) { close(fd); fail(&local, -EIO, consumed, "short read on job_count"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (offset %jd)\n", path, local.fail_reason, (intmax_t)consumed); return -EIO; } consumed += (off_t)sizeof(job_count); local.job_count = job_count; for (uint32_t i = 0; i < job_count; i++) { off_t len_off = consumed; uint32_t plen = 0; if (read(fd, &plen, sizeof(plen)) != (ssize_t)sizeof(plen)) { close(fd); fail(&local, -EIO, len_off, "short read on job payload_len"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: job %u: %s (offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } consumed += (off_t)sizeof(plen); if (plen == 0 || (off_t)plen > st.st_size - consumed) { close(fd); char why[160]; snprintf(why, sizeof(why), "bad payload_len=%u (remaining=%jd)", plen, (intmax_t)(st.st_size - consumed)); fail(&local, -EIO, len_off, why); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: job %u: %s (length field at " "offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } if (lseek(fd, plen, SEEK_CUR) < 0) { close(fd); fail(&local, -EIO, consumed, "seek past job payload failed"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: job %u: %s\n", path, i, local.fail_reason); return -EIO; } consumed += (off_t)plen; local.jobs_ok++; } uint32_t queue_count = 0; if (read(fd, &queue_count, sizeof(queue_count)) != (ssize_t)sizeof(queue_count)) { close(fd); fail(&local, -EIO, consumed, "short read on queue_count"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (offset %jd)\n", path, local.fail_reason, (intmax_t)consumed); return -EIO; } consumed += (off_t)sizeof(queue_count); local.queue_count = queue_count; for (uint32_t i = 0; i < queue_count; i++) { off_t len_off = consumed; uint32_t plen = 0; if (read(fd, &plen, sizeof(plen)) != (ssize_t)sizeof(plen)) { close(fd); fail(&local, -EIO, len_off, "short read on queue payload_len"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: queue %u: %s (offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } consumed += (off_t)sizeof(plen); if (plen == 0 || (off_t)plen > st.st_size - consumed) { close(fd); char why[160]; snprintf(why, sizeof(why), "bad payload_len=%u (remaining=%jd)", plen, (intmax_t)(st.st_size - consumed)); fail(&local, -EIO, len_off, why); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: queue %u: %s (length field at " "offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } if (lseek(fd, plen, SEEK_CUR) < 0) { close(fd); fail(&local, -EIO, consumed, "seek past queue payload failed"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: queue %u: %s\n", path, i, local.fail_reason); return -EIO; } consumed += (off_t)plen; local.queues_ok++; } /* Host section (config-upgrade-snapshot task 4/8.4, SNAPSHOT_FORMAT_ * VERSION=2): new after the queue section (design.md "Checkpoint File * Layout", persist.c load_snapshot_entities()/write_snapshot()). Same * [payload_len][bytes] per-frame structural shape as the job/queue * loops above -- this tool validates structure only, never decodes the * capnp payload. */ uint32_t host_count = 0; if (read(fd, &host_count, sizeof(host_count)) != (ssize_t)sizeof(host_count)) { close(fd); fail(&local, -EIO, consumed, "short read on host_count"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (offset %jd)\n", path, local.fail_reason, (intmax_t)consumed); return -EIO; } consumed += (off_t)sizeof(host_count); local.host_count = host_count; for (uint32_t i = 0; i < host_count; i++) { off_t len_off = consumed; uint32_t plen = 0; if (read(fd, &plen, sizeof(plen)) != (ssize_t)sizeof(plen)) { close(fd); fail(&local, -EIO, len_off, "short read on host payload_len"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: host %u: %s (offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } consumed += (off_t)sizeof(plen); if (plen == 0 || (off_t)plen > st.st_size - consumed) { close(fd); char why[160]; snprintf(why, sizeof(why), "bad payload_len=%u (remaining=%jd)", plen, (intmax_t)(st.st_size - consumed)); fail(&local, -EIO, len_off, why); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: host %u: %s (length field at " "offset %jd)\n", path, i, local.fail_reason, (intmax_t)len_off); return -EIO; } if (lseek(fd, plen, SEEK_CUR) < 0) { close(fd); fail(&local, -EIO, consumed, "seek past host payload failed"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: host %u: %s\n", path, i, local.fail_reason); return -EIO; } consumed += (off_t)plen; local.hosts_ok++; } /* Config-bytes section (config-upgrade-snapshot task 2.3/8.4): a * single flat length-prefixed blob appended LAST, after the host * section -- NOT a [count][frame...] shape like job/queue/host above, * since there is exactly one active configuration, never a set * (persist.c write_snapshot()). The length field itself may be * legitimately ABSENT (a file that ends right after the host section * is still a valid, if slightly older, fmt=2-consistent shape) -- * distinguish that from the length field being present-but-zero * (persist.c explicitly writes an empty-but-present section when no * local config bytes exist yet) and from a genuinely truncated length * field (a short, non-zero partial read -- real corruption). */ uint32_t cfg_bytes_len = 0; ssize_t cfg_len_n = read(fd, &cfg_bytes_len, sizeof(cfg_bytes_len)); if (cfg_len_n == 0) { /* Clean EOF right after the host section -- no config-bytes * section present at all. Not a defect. */ local.has_config_bytes_section = 0; } else if (cfg_len_n != (ssize_t)sizeof(cfg_bytes_len)) { close(fd); fail(&local, -EIO, consumed, "short read on config_bytes_len"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (offset %jd)\n", path, local.fail_reason, (intmax_t)consumed); return -EIO; } else { off_t len_off = consumed; consumed += (off_t)sizeof(cfg_bytes_len); local.has_config_bytes_section = 1; local.config_bytes_len = cfg_bytes_len; if ((off_t)cfg_bytes_len > st.st_size - consumed) { close(fd); char why[160]; snprintf(why, sizeof(why), "bad config_bytes_len=%u (remaining=%jd)", cfg_bytes_len, (intmax_t)(st.st_size - consumed)); fail(&local, -EIO, len_off, why); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s (length field at offset " "%jd)\n", path, local.fail_reason, (intmax_t)len_off); return -EIO; } if (cfg_bytes_len > 0) { if (lseek(fd, cfg_bytes_len, SEEK_CUR) < 0) { close(fd); fail(&local, -EIO, consumed, "seek past config bytes failed"); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s\n", path, local.fail_reason); return -EIO; } consumed += (off_t)cfg_bytes_len; } } close(fd); local.consumed = consumed; if (consumed != st.st_size) { char why[160]; snprintf(why, sizeof(why), "trailing garbage: consumed=%jd but file_size=%jd", (intmax_t)consumed, (intmax_t)st.st_size); fail(&local, -EIO, consumed, why); if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: %s\n", path, local.fail_reason); return -EIO; } if (rep) *rep = local; if (out) fprintf(out, "snapverify: %s: OK (lsn=%" PRIu64 " fmt=%u jobs=%u queues=%u " "hosts=%u cfgref_tag=%016" PRIx64 " cfg_bytes_len=%u " "has_cfg_bytes=%d size=%jd)\n", path, local.checkpoint_lsn, local.format_version, local.job_count, local.queue_count, local.host_count, local.cfgref_tag, local.config_bytes_len, local.has_config_bytes_section, (intmax_t)local.file_size); return 0; }