new file mode 100644
@@ -0,0 +1,150 @@
+From ab6aedbe5a1e9597ba9b9e50462af476a0655f7a Mon Sep 17 00:00:00 2001
+From: Jonathan Dieter <jonathan@dieter.ie>
+Date: Thu, 30 Jul 2026 20:51:11 +0100
+Subject: [PATCH] Fix a number of memory related bugs in zchunk
+
+Credit for this patch and for bringing these bugs to my attention goes to
+glitchfox. Additionally, after glitchfox brought this to my attention, I
+received a PR to fix the optional element bug from lilu5458.
+
+Authored-by: glitchfox@benjaminali.com
+Signed-off-by: Jonathan Dieter <jonathan@dieter.ie>
+
+CVE: CVE-2026-117556
+Upstream-Status: Backport [https://github.com/zchunk/zchunk/commit/ab6aedbe5a1e9597ba9b9e50462af476a0655f7a]
+Signed-off-by: Peter Marko <peter.marko@siemens.com>
+---
+ src/lib/comp/comp.c | 8 +++++---
+ src/lib/dl/range.c | 3 +++
+ src/lib/header.c | 4 ++++
+ src/lib/index/index_read.c | 20 +++++++++++++++-----
+ 4 files changed, 27 insertions(+), 8 deletions(-)
+
+diff --git a/src/lib/comp/comp.c b/src/lib/comp/comp.c
+index 414727d..1c116a6 100644
+--- a/src/lib/comp/comp.c
++++ b/src/lib/comp/comp.c
+@@ -101,7 +101,8 @@ static size_t comp_read_from_dc(zckCtx *zck, zckComp *comp, char *dst,
+ size_t dl_size = dst_size;
+ if(dl_size > comp->dc_data_size - comp->dc_data_loc)
+ dl_size = comp->dc_data_size - comp->dc_data_loc;
+- memcpy(dst, comp->dc_data+comp->dc_data_loc, dl_size);
++ if(dl_size > 0)
++ memcpy(dst, comp->dc_data+comp->dc_data_loc, dl_size);
+ comp->dc_data_loc += dl_size;
+ if(dl_size > 0)
+ zck_log(ZCK_LOG_DEBUG, "Reading %llu bytes from decompressed buffer",
+@@ -414,8 +415,9 @@ bool comp_add_to_dc(zckCtx *zck, zckComp *comp, const char *src,
+ (long long unsigned) comp->dc_data_loc);
+ zck_log(ZCK_LOG_DEBUG, "Adding %llu bytes to decompressed buffer",
+ (long long unsigned) src_size);
+- memcpy(temp, comp->dc_data + comp->dc_data_loc,
+- comp->dc_data_size - comp->dc_data_loc);
++ if(comp->dc_data_size > comp->dc_data_loc)
++ memcpy(temp, comp->dc_data + comp->dc_data_loc,
++ comp->dc_data_size - comp->dc_data_loc);
+ free(comp->dc_data);
+ comp->dc_data_size -= comp->dc_data_loc;
+ comp->dc_data_loc = 0;
+diff --git a/src/lib/dl/range.c b/src/lib/dl/range.c
+index d4f7426..d786551 100644
+--- a/src/lib/dl/range.c
++++ b/src/lib/dl/range.c
+@@ -153,6 +153,9 @@ void ZCK_PUBLIC_API zck_range_free(zckRange **info) {
+ }
+
+ char ZCK_PUBLIC_API *zck_get_range_char(zckCtx *zck, zckRange *range) {
++ if(range == NULL || range->first == NULL)
++ return zmalloc(1);
++
+ int buf_size = BUF_SIZE;
+ char *output = zmalloc(buf_size);
+ if (!output) {
+diff --git a/src/lib/header.c b/src/lib/header.c
+index 57adc31..e28935d 100644
+--- a/src/lib/header.c
++++ b/src/lib/header.c
+@@ -195,6 +195,10 @@ static bool read_preface(zckCtx *zck) {
+ if(!compint_to_size(zck, &data_size, header+length, &length,
+ max_length))
+ return false;
++ if(data_size > max_length - length) {
++ set_fatal_error(zck, "Optional element exceeds header size");
++ return false;
++ }
+ if(!read_optional_element(zck, id, data_size, header+length))
+ return false;
+ length += data_size;
+diff --git a/src/lib/index/index_read.c b/src/lib/index/index_read.c
+index 6ec0a8d..8f57f23 100644
+--- a/src/lib/index/index_read.c
++++ b/src/lib/index/index_read.c
+@@ -47,7 +47,7 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+
+ /* Read and configure hash type */
+ int hash_type = 0;
+- if(!compint_to_int(zck, &hash_type, data + length, &length, max_length)) {
++ if(!compint_to_int(zck, &hash_type, data + length, &length, size)) {
+ set_fatal_error(zck, "Unable to read hash type");
+ return false;
+ }
+@@ -59,7 +59,7 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ /* Read number of index entries */
+ size_t index_count;
+ if(!compint_to_size(zck, &index_count, data + length, &length,
+- max_length)) {
++ size)) {
+ set_fatal_error(zck, "Unable to read index count");
+ return false;
+ }
+@@ -69,7 +69,7 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ size_t idx_loc = 0;
+ int count = 0;
+ while(length < size) {
+- if(length + zck->index.digest_size > max_length) {
++ if(zck->index.digest_size > size - length) {
+ set_fatal_error(zck, "Read past end of header");
+ return false;
+ }
+@@ -99,6 +99,12 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ /* Read uncompressed entry digest, if any */
+ if (zck->has_uncompressed_source) {
+ /* same size for digest as compressed */
++ if(zck->index.digest_size > size - length) {
++ set_fatal_error(zck, "Read past end of index");
++ free(new->digest);
++ free(new);
++ return false;
++ }
+ new->digest_uncompressed = zmalloc(zck->index.digest_size);
+ if (!new->digest_uncompressed) {
+ free(new->digest);
+@@ -116,7 +122,7 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ /* Read and store entry length */
+ size_t chunk_length = 0;
+ if(!compint_to_size(zck, &chunk_length, data+length, &length,
+- max_length)) {
++ size)) {
+ set_fatal_error(zck, "Unable to read chunk %i compressed size",
+ count);
+ return false;
+@@ -127,7 +133,7 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ /* Read and store uncompressed entry length */
+ chunk_length = 0;
+ if(!compint_to_size(zck, &chunk_length, data+length, &length,
+- max_length)) {
++ size)) {
+ set_fatal_error(zck, "Unable to read chunk %i uncompressed size",
+ count);
+ return false;
+@@ -146,6 +152,10 @@ bool index_read(zckCtx *zck, char *data, size_t size, size_t max_length) {
+ zck->index.first = new;
+ prev = new;
+ }
++ if(count == 0 || (size_t)count != index_count) {
++ set_fatal_error(zck, "Index entry count does not match index data");
++ return false;
++ }
+ free(zck->index_string);
+ zck->index_string = NULL;
+ return true;
new file mode 100644
@@ -0,0 +1,329 @@
+From 02b371a64bf98245df97315d07c684a0ca5ea1df Mon Sep 17 00:00:00 2001
+From: Jonathan Dieter <jonathan@dieter.ie>
+Date: Mon, 26 Oct 2026 18:07:06 +0000
+Subject: [PATCH] Fix out-of-bounds read
+
+Also, add a flag for doing memory tests.
+
+Thanks to HaiND (Nguyen Dinh Hai), Post and Telecommunication Institute
+of Technology, github.com/Haind03 for flagging up this issue
+
+Signed-off-by: Jonathan Dieter <jonathan@dieter.ie>
+
+CVE: CVE-2026-117556
+Upstream-Status: Backport [https://github.com/zchunk/zchunk/commit/02b371a64bf98245df97315d07c684a0ca5ea1df]
+Signed-off-by: Peter Marko <peter.marko@siemens.com>
+---
+ src/lib/compint.c | 4 +
+ src/lib/header.c | 16 +--
+ test/meson.build | 10 ++
+ test/read_header_bounds.c | 203 ++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 227 insertions(+), 6 deletions(-)
+ create mode 100644 test/read_header_bounds.c
+
+diff --git a/src/lib/compint.c b/src/lib/compint.c
+index 5178559..7324d12 100644
+--- a/src/lib/compint.c
++++ b/src/lib/compint.c
+@@ -48,6 +48,10 @@ int compint_to_size(zckCtx *zck, size_t *val, const char *compint,
+ VALIDATE_BOOL(zck);
+
+ *val = 0;
++ if(max_length == 0) {
++ set_fatal_error(zck, "Read past end of header");
++ return false;
++ }
+ size_t old_val = 0;
+ const unsigned char *i = (unsigned char *)compint;
+ int count = 0;
+diff --git a/src/lib/header.c b/src/lib/header.c
+index e28935d..05f8895 100644
+--- a/src/lib/header.c
++++ b/src/lib/header.c
+@@ -164,7 +164,8 @@ static bool read_preface(zckCtx *zck) {
+
+ /* Read flags */
+ size_t flags = 0;
+- if(!compint_to_size(zck, &flags, header+length, &length, max_length))
++ if(!compint_to_size(zck, &flags, header+length, &length,
++ max_length - length))
+ return false;
+ if(!check_flags(zck, flags))
+ return false;
+@@ -174,7 +175,8 @@ static bool read_preface(zckCtx *zck) {
+ int tmp = 0;
+
+ /* Read and initialize compression type */
+- if(!compint_to_int(zck, &tmp, header+length, &length, max_length))
++ if(!compint_to_int(zck, &tmp, header+length, &length,
++ max_length - length))
+ return false;
+ if(!comp_ioption(zck, ZCK_COMP_TYPE, tmp))
+ return false;
+@@ -185,15 +187,16 @@ static bool read_preface(zckCtx *zck) {
+ if(zck->has_optional_elems) {
+ size_t opt_count = 0;
+ if(!compint_to_size(zck, &opt_count, header+length, &length,
+- max_length))
++ max_length - length))
+ return false;
+ for(size_t i=0; i<opt_count; i++) {
+ size_t id = 0;
+ size_t data_size = 0;
+- if(!compint_to_size(zck, &id, header+length, &length, max_length))
++ if(!compint_to_size(zck, &id, header+length, &length,
++ max_length - length))
+ return false;
+ if(!compint_to_size(zck, &data_size, header+length, &length,
+- max_length))
++ max_length - length))
+ return false;
+ if(data_size > max_length - length) {
+ set_fatal_error(zck, "Optional element exceeds header size");
+@@ -206,7 +209,8 @@ static bool read_preface(zckCtx *zck) {
+ }
+
+ /* Read and initialize index size */
+- if(!compint_to_int(zck, &tmp, header+length, &length, max_length))
++ if(!compint_to_int(zck, &tmp, header+length, &length,
++ max_length - length))
+ return false;
+ zck->index_size = tmp;
+
+diff --git a/test/meson.build b/test/meson.build
+index 66a1820..fc7a35f 100644
+--- a/test/meson.build
++++ b/test/meson.build
+@@ -52,6 +52,11 @@ exitcodecheck = executable('exitcodecheck',
+ include_directories: incdir,
+ dependencies: [zstd_dep, openssl_dep],
+ c_args: preprocessor_defines)
++read_header_bounds = executable('read_header_bounds',
++ ['read_header_bounds.c'] + util_sources,
++ include_directories: incdir,
++ dependencies: [zstd_dep, openssl_dep],
++ c_args: preprocessor_defines)
+ zck_cmp_uncomp = executable(
+ 'zck_cmp_uncomp',
+ ['zck_cmp_uncomp.c'],
+@@ -73,6 +78,11 @@ test(
+ empty,
+ is_parallel: false
+ )
++test(
++ 'rejects truncated preface without over-reading',
++ read_header_bounds,
++ is_parallel: false
++)
+ test(
+ 'check version info in zck',
+ zck,
+diff --git a/test/read_header_bounds.c b/test/read_header_bounds.c
+new file mode 100644
+index 0000000..2162de6
+--- /dev/null
++++ b/test/read_header_bounds.c
+@@ -0,0 +1,203 @@
++/*
++ * Copyright 2026 Jonathan Dieter <jonathan@dieter.ie>
++ *
++ * Redistribution and use in source and binary forms, with or without
++ * modification, are permitted provided that the following conditions are met:
++ *
++ * 1. Redistributions of source code must retain the above copyright notice,
++ * this list of conditions and the following disclaimer.
++ *
++ * 2. Redistributions in binary form must reproduce the above copyright notice,
++ * this list of conditions and the following disclaimer in the documentation
++ * and/or other materials provided with the distribution.
++ *
++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
++ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
++ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
++ * POSSIBILITY OF SUCH DAMAGE.
++ */
++
++/* Regression test for the read_preface() out-of-bounds read - Only will
++ * report an issue when run under a sanitizer */
++
++#include <stdlib.h>
++#include <stdio.h>
++#include <string.h>
++#include <stdbool.h>
++#include <unistd.h>
++#include <sys/types.h>
++#include <sys/stat.h>
++#include <fcntl.h>
++#include <zck.h>
++#include "zck_private.h"
++#include "util.h"
++
++#ifndef O_BINARY
++#define O_BINARY 0
++#endif
++
++#define TEST_FILE "read_header_bounds.zck"
++
++/* Exit code that tells meson the test was skipped */
++#define SKIP 77
++
++/* The over-read is only caught by a memory checker, so only run under
++ * valgrind (which preloads its own libraries) or AddressSanitizer */
++static bool memory_checked(void) {
++#ifdef __SANITIZE_ADDRESS__
++ return true;
++#else
++ const char *preload = getenv("LD_PRELOAD");
++ return preload != NULL && strstr(preload, "vgpreload") != NULL;
++#endif
++}
++
++/* Compute the SHA-256 header digest over the given bytes, using libzck's own
++ * hashing so it matches what read_header_from_file() will calculate. */
++static char *header_digest(const char *buf, size_t size) {
++ zckCtx *zck = zck_create();
++ if(zck == NULL)
++ return NULL;
++
++ zckHashType ht;
++ zckHash hash;
++ memset(&hash, 0, sizeof(hash)); /* hash_init() closes it before use */
++ if(!hash_setup(zck, &ht, ZCK_HASH_SHA256) ||
++ !hash_init(zck, &hash, &ht) ||
++ !hash_update(zck, &hash, buf, size)) {
++ zck_free(&zck);
++ return NULL;
++ }
++ char *digest = hash_finalize(zck, &hash);
++ zck_free(&zck);
++ return digest;
++}
++
++int main(void) {
++ if(!memory_checked()) {
++ printf("Skipping: needs valgrind or AddressSanitizer\n");
++ return SKIP;
++ }
++
++ char cibuf[MAX_COMP_SIZE];
++ size_t cilen;
++
++ /* SHA-256 digest size, read from libzck rather than hard-coded. */
++ zckHashType ht;
++ if(!hash_setup(NULL, &ht, ZCK_HASH_SHA256)) {
++ printf("ERROR: unable to set up SHA-256\n");
++ exit(1);
++ }
++ size_t digest_size = ht.digest_size;
++
++ /* --- Header body (the preface) ---
++ * full data digest (zero bytes) + flags(0) + a truncated compression-type
++ * compint. The final byte has its high bit clear, so the reader believes
++ * the integer continues past the end of the header. */
++ char body[64];
++ size_t body_len = 0;
++
++ memset(body + body_len, 0, digest_size); /* data digest */
++ body_len += digest_size;
++
++ cilen = 0;
++ compint_from_size(cibuf, 0, &cilen); /* flags = 0 */
++ memcpy(body + body_len, cibuf, cilen);
++ body_len += cilen;
++
++ body[body_len++] = 0x00; /* unterminated compression-type compint */
++
++ /* --- Lead ---
++ * "\0ZCK1" + compint(hash type) + compint(header length). The header
++ * digest that follows is computed over the lead prefix plus the body,
++ * which is exactly the range read_header_from_file() hashes. */
++ char lead[16];
++ size_t lead_len = 0;
++
++ memcpy(lead, "\0ZCK1", 5);
++ lead_len = 5;
++
++ cilen = 0;
++ compint_from_size(cibuf, ZCK_HASH_SHA256, &cilen);
++ memcpy(lead + lead_len, cibuf, cilen);
++ lead_len += cilen;
++
++ cilen = 0;
++ compint_from_size(cibuf, body_len, &cilen);
++ memcpy(lead + lead_len, cibuf, cilen);
++ lead_len += cilen;
++
++ /* digest = SHA256(lead prefix + body) */
++ char *to_hash = zmalloc(lead_len + body_len);
++ if(to_hash == NULL)
++ exit(1);
++ memcpy(to_hash, lead, lead_len);
++ memcpy(to_hash + lead_len, body, body_len);
++ char *digest = header_digest(to_hash, lead_len + body_len);
++ free(to_hash);
++ if(digest == NULL) {
++ printf("ERROR: unable to compute header digest\n");
++ exit(1);
++ }
++
++ /* Write the file: lead, digest, body */
++ int out = open(TEST_FILE, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666);
++ if(out < 0) {
++ perror("Unable to open " TEST_FILE " for writing");
++ free(digest);
++ exit(1);
++ }
++ if(write(out, lead, lead_len) != (ssize_t)lead_len ||
++ write(out, digest, digest_size) != (ssize_t)digest_size ||
++ write(out, body, body_len) != (ssize_t)body_len) {
++ perror("Short write");
++ close(out);
++ free(digest);
++ exit(1);
++ }
++ close(out);
++ free(digest);
++
++ /* Read it back: the header must be rejected, not over-read */
++ int in = open(TEST_FILE, O_RDONLY | O_BINARY);
++ if(in < 0) {
++ perror("Unable to open " TEST_FILE " for reading");
++ exit(1);
++ }
++
++ /* Drive the read stages individually so the test targets the preface */
++ zckCtx *zck = zck_create();
++ if(zck == NULL) {
++ close(in);
++ exit(1);
++ }
++ if(!zck_init_adv_read(zck, in) || !zck_read_lead(zck)) {
++ printf("ERROR: lead parse failed unexpectedly: %s\n",
++ zck_get_error(zck));
++ zck_free(&zck);
++ close(in);
++ unlink(TEST_FILE);
++ exit(1);
++ }
++
++ int rc = 0;
++ if(zck_read_header(zck)) {
++ printf("ERROR: zck_read_header accepted a truncated preface\n");
++ rc = 1;
++ } else {
++ printf("zck_read_header correctly rejected truncated preface: %s\n",
++ zck_get_error(zck));
++ }
++
++ zck_free(&zck);
++ close(in);
++ unlink(TEST_FILE);
++ return rc;
++}
@@ -3,7 +3,11 @@ DESCRIPTION = "A file format designed for highly efficient deltas while maintain
LICENSE = "BSD-2-Clause"
LIC_FILES_CHKSUM = "file://LICENSE;md5=daf6e68539f564601a5a5869c31e5242"
-SRC_URI = "git://github.com/zchunk/zchunk.git;protocol=https;branch=main"
+SRC_URI = " \
+ git://github.com/zchunk/zchunk.git;protocol=https;branch=main \
+ file://CVE-2026-117556-01.patch \
+ file://CVE-2026-117556-02.patch \
+"
SRCREV = "4dd91d31157ede4a1b092721d944ae2fdd161cd9"
S = "${WORKDIR}/git"