Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:dislocker
User: [email protected]
Usertags: pu

[ Reason ]
Upstream fixed CVE-2026-107634 and CVE-2026-107635 with dislocker
0.7.4, each by a dedicated commit.  With this upload I aim at fixing
the CVEs also for trixie applying those commits, slightly backported.

[ Impact ]
Without this update the listed CVEs would stay unfixed in trixie.

[ Tests ]
1) dislocker was successfully built and tested on debusine, see
https://debusine.debian.net/debian/developers/work-request/1517686/
2) The local-only autopkgtest from dislocker/0.7.4-1 was run
successfully on dislocker/0.7.3+git20240607-3+deb13u1

[ Risks ]
I consider the risk low as dislocker is a leaf package and
libdislocker0.7t64 is only used by dislocker itself.

[ Checklist ]
  [*] *all* changes are documented in the d/changelog
  [*] I reviewed all changes and I approve them
  [*] attach debdiff against the package in (old)stable
  [*] the issue is verified as fixed in unstable

-- 
GPG Fingerprint
3DF5 E8AA 43FC 9FDF D086 F195 ADF5 0EDA F8AD D585
diff -Nru dislocker-0.7.3+git20240607/debian/changelog dislocker-0.7.3+git20240607/debian/changelog
--- dislocker-0.7.3+git20240607/debian/changelog	2025-02-11 16:13:38.000000000 +0100
+++ dislocker-0.7.3+git20240607/debian/changelog	2026-10-23 12:19:37.000000000 +0200
@@ -1,3 +1,11 @@
+dislocker (0.7.3+git20240607-3+deb13u1) trixie; urgency=medium
+
+  * Team upload.
+  * d/p/*: Add patches fixing CVE-2026-107634 and CVE-2026-107635.
+   (Closes: #1150456)
+
+ -- Sven Geuer <[email protected]>  Fri, 23 Oct 2026 12:19:37 +0200
+
 dislocker (0.7.3+git20240607-3) unstable; urgency=medium
 
   * Team upload.
diff -Nru dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107634.patch dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107634.patch
--- dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107634.patch	1970-01-01 01:00:00.000000000 +0100
+++ dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107634.patch	2026-10-23 12:19:37.000000000 +0200
@@ -0,0 +1,202 @@
+Description: fix: bound the metadata parser to the allocated metadata buffer
+ get_metadata() allocates the metadata buffer from the volume header's
+ information.size field, but the dataset embedded in that buffer carries
+ its own size field which was only ever checked against its sibling
+ copy_size, never against the actual allocation.
+ . 
+ A crafted BitLocker image announcing dataset->size = 0xFFFFFFFF therefore
+ made get_next_datum() walk far past the end of the heap allocation,
+ reading adjacent heap memory as datum headers and copying it into datum
+ payloads, or segfaulting the (root-privileged, under FUSE) process.
+ .
+ CWE-125. Many thanks to Tristan <[email protected]> for reporting
+ this and for the detailed, reproducible write-up.
+ .
+ - get_dataset() now bounds the dataset against the metadata buffer it
+   lives in, computing that buffer's size exactly as get_metadata() did.
+   It also requires header_size to cover the dataset structure and size
+   to be at least header_size, both of which the datum walk relies on.
+ - get_next_datum() recomputes the buffer's end itself and clamps its
+   walk to it, so the walker stays in bounds whatever dataset->size says.
+ - A datum whose announced size overruns the dataset is now rejected,
+   since every consumer of a datum reads datum_size bytes from it.
+ - get_nested_datum() and get_nested_datumvaluetype() now require nested
+   datums to be entirely contained in their enclosing datum.
+ .
+ While here, fix the dis_printf() in get_dataset() which had format
+ specifiers but no matching arguments.
+ .
+ Reported-by: Tristan <[email protected]>
+Origin: backport, commit:9158e9e41dd669b360cbc53cc18ccae729b67b48
+Author: Sven Geuer <[email protected]>
+Forwarded: not-needed
+Bug-Debian: https://bugs.debian.org/1150456
+Last-Update: 2026-10-26
+
+--- a/src/metadata/datums.c
++++ b/src/metadata/datums.c
+@@ -589,9 +589,27 @@
+ 
+ 	bitlocker_dataset_t* dataset = dis_meta->dataset;
+ 	void* datum = NULL;
+-	void* limit = (char*)dataset + dataset->size;
++	char* limit = (char*)dataset + dataset->size;
+ 	datum_header_safe_t header;
+ 
++	/*
++	 * Defense in depth: get_dataset() already bounds dataset->size against the
++	 * metadata buffer holding it, but don't take that field's word for it here.
++	 * Recompute the end of that buffer -- the very same way get_metadata()
++	 * sized it -- and never walk past it, whatever dataset->size claims.
++	 */
++	if(dis_meta->information)
++	{
++		bitlocker_information_t* information = dis_meta->information;
++		size_t metadata_size = (size_t)(information->version == V_SEVEN ?
++		                  (size_t)information->size << 4 : information->size);
++		char* metadata_end = (char*)information + metadata_size;
++
++		/* The `< dataset' case catches a pointer wrap-around */
++		if(limit > metadata_end || limit < (char*)dataset)
++			limit = metadata_end;
++	}
++
+ 	*datum_result = NULL;
+ 	memset(&header, 0, sizeof(datum_header_safe_t));
+ 	if(datum_begin)
+@@ -601,7 +619,7 @@
+ 
+ 	while(1)
+ 	{
+-		if(datum + 8 >= limit)
++		if((char*)datum + sizeof(datum_header_safe_t) >= limit)
+ 		{
+ 			dis_printf(L_DEBUG, "Hit limit, search failed.\n");
+ 			break;
+@@ -610,6 +628,17 @@
+ 		if(!get_header_safe(datum, &header))
+ 			break;
+ 
++		/*
++		 * A datum announcing a size which makes it overrun the dataset cannot
++		 * be trusted: every consumer of this datum (get_payload_safe(),
++		 * get_nested_datum(), get_vmk(), ...) reads datum_size bytes from it
++		 */
++		if((char*)datum + header.datum_size > limit)
++		{
++			dis_printf(L_DEBUG, "Datum overruns the dataset, search failed.\n");
++			break;
++		}
++
+ 		if(value_type == UINT16_MAX && entry_type == UINT16_MAX)
+ 		{
+ 			/*
+@@ -669,6 +698,14 @@
+ 		return FALSE;
+ 
+ 	uint16_t size = datum_value_types_prop[header.value_type].size_header;
++
++	/*
++	 * There's only a nested datum if the enclosing datum is big enough to hold
++	 * both its own header and, at the very least, the nested datum's header
++	 */
++	if(header.datum_size < size + sizeof(datum_header_safe_t))
++		return FALSE;
++
+ 	*datum_nested = (char*)datum + size;
+ 
+ 	return TRUE;
+@@ -702,6 +739,9 @@
+ 	if(!get_header_safe(*datum_nested, &nested_header))
+ 		return FALSE;
+ 
++	/* The enclosing datum's end, no nested datum may go past it */
++	char* nested_limit = (char*)datum + header.datum_size;
++
+ 	/* While we don't have the type we're looking for */
+ 	while(nested_header.value_type != value_type)
+ 	{
+@@ -709,7 +749,7 @@
+ 		*datum_nested += nested_header.datum_size;
+ 
+ 		/* If we're not into the datum anymore */
+-		if((char*)datum + header.datum_size <= (char*)*datum_nested)
++		if(nested_limit < (char*)*datum_nested + sizeof(datum_header_safe_t))
+ 			return FALSE;
+ 
+ 		if(!get_header_safe(*datum_nested, &nested_header))
+@@ -717,6 +757,10 @@
+ 
+ 	}
+ 
++	/* The nested datum found has to be entirely contained into its parent */
++	if(nested_limit < (char*)*datum_nested + nested_header.datum_size)
++		return FALSE;
++
+ 	return TRUE;
+ }
+ 
+--- a/src/metadata/metadata.c
++++ b/src/metadata/metadata.c
+@@ -28,6 +28,8 @@
+ #include "dislocker/metadata/print_metadata.h"
+ #include "dislocker/dislocker.priv.h"
+ 
++#include <stddef.h>
++
+ #include <sys/ioctl.h>
+ 
+ #define BLKSSZGET  _IO(0x12,104)/* get block device sector size */
+@@ -771,14 +773,50 @@
+ 	bitlocker_information_t* information = metadata;
+ 	*dataset = &information->dataset;
+ 
++	/*
++	 * The dataset is embedded into the metadata buffer, which get_metadata()
++	 * allocated using the very same computation as below. Therefore the dataset
++	 * cannot span further than what's left of that buffer once the information
++	 * header preceding it is taken into account.
++	 *
++	 * Without this bound, a crafted volume could announce a dataset size way
++	 * bigger than the buffer actually holding it, leading the datum-walking
++	 * code to read heap memory past the allocation.
++	 */
++	size_t metadata_size = (size_t)(information->version == V_SEVEN ?
++	                      (size_t)information->size << 4 : information->size);
++	size_t dataset_offset = offsetof(bitlocker_information_t, dataset);
++
++	if(metadata_size < dataset_offset + sizeof(bitlocker_dataset_t))
++	{
++		dis_printf(
++			L_DEBUG,
++			"Metadata size (%#zx) too small to hold a dataset\n",
++			metadata_size
++		);
++		return FALSE;
++	}
++
++	size_t max_dataset_size = metadata_size - dataset_offset;
++
+ 	/* Check this dataset validity */
+ 	if(
+-		(*dataset)->copy_size < (*dataset)->header_size
++		(*dataset)->header_size < sizeof(bitlocker_dataset_t)
++		|| (*dataset)->copy_size < (*dataset)->header_size
+ 		|| (*dataset)->size   > (*dataset)->copy_size
++		|| (*dataset)->size   < (*dataset)->header_size
++		|| (*dataset)->size   > max_dataset_size
+ 		|| (*dataset)->copy_size - (*dataset)->header_size < 8
+ 	)
+ 	{
+-		dis_printf(L_DEBUG, "size=%#x, copy_size=%#x, header_size=%#x\n");
++		dis_printf(
++			L_DEBUG,
++			"size=%#x, copy_size=%#x, header_size=%#x, max_size=%#zx\n",
++			(*dataset)->size,
++			(*dataset)->copy_size,
++			(*dataset)->header_size,
++			max_dataset_size
++		);
+ 		return FALSE;
+ 	}
+ 
diff -Nru dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107635.patch dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107635.patch
--- dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107635.patch	1970-01-01 01:00:00.000000000 +0100
+++ dislocker-0.7.3+git20240607/debian/patches/CVE-2026-107635.patch	2026-10-23 12:19:37.000000000 +0200
@@ -0,0 +1,286 @@
+Description: fix: reject datums smaller than their own type header
+ get_header_safe() only guarantees datum_size to be 8 or more, but most
+ datum types have a bigger type-specific header -- AES_CCM, the type used
+ for the VMK and the FVEK, has a 36-byte one. get_vmk() and get_fvek()
+ computed their encrypted payload's size by subtracting the latter from
+ the former without checking, so a crafted image announcing datum_size = 9
+ underflowed to 0xffffffe5 (~4GB).
+ .
+ That size was then handed to decrypt_key(), whose dis_malloc() fails and
+ calls exit(2), killing the root-privileged FUSE process.
+ .
+ CWE-191. Many thanks to Tristan <[email protected]> for reporting
+ this and for the detailed, reproducible write-up.
+ .
+ get_payload_safe() already guarded against this, but the same subtraction
+ was open-coded in every print_datum_*() routine and its Ruby to_s
+ counterpart, where the underflowed size is passed to hexdump(). That path
+ is worse than a failed allocation: hexdump() reads every byte it is given
+ before dis_printf() gets to filter on the log level, so it reads ~4GB
+ past the datum whatever the verbosity, and it is reached from get_vmk()
+ before any decryption happens.
+ .
+ Introduce datum_payload_size(), which returns 0 rather than wrapping, and
+ use it everywhere the payload size is derived from datum_size. get_vmk()
+ and get_fvek() reject the datum outright, and get_vmk() now does so
+ before printing it, not after.
+ .
+ Both also bound value_type before indexing datum_value_types_prop[],
+ which nothing did for them since f9674c4 removed that check from
+ get_header_safe().
+ .
+ Reported-by: Tristan <[email protected]>
+Origin: backport, commit:0706462db88efe8df88150e4c3e4332b808f4581
+Author: Sven Geuer <[email protected]>
+Forwarded: not-needed
+Bug-Debian: https://bugs.debian.org/1150456
+Last-Update: 2026-10-22
+
+--- a/src/metadata/datums.c
++++ b/src/metadata/datums.c
+@@ -210,6 +210,28 @@
+ 
+ 
+ /**
++ * Compute the size of a datum's payload, that is what remains of the datum once
++ * its type-specific header is taken away.
++ *
++ * datum_size comes straight from the volume and get_header_safe() only
++ * guarantees it to be 8 or more, while most type-specific headers are bigger
++ * than that. Subtracting one from the other without checking would wrap around
++ * and yield a ~4GB size, hence this helper.
++ *
++ * @param datum_size The datum's announced size
++ * @param header_size The size of this datum type's header
++ * @return The payload's size, or 0 when the datum is too small to hold one
++ */
++static size_t datum_payload_size(uint16_t datum_size, size_t header_size)
++{
++	if((size_t)datum_size <= header_size)
++		return 0;
++
++	return (size_t)datum_size - header_size;
++}
++
++
++/**
+  * Get the payload based on the datum's size and value type
+  *
+  * @param data The data to take the payload from
+@@ -234,10 +256,10 @@
+ 
+ 	size_header = datum_value_types_prop[header.value_type].size_header;
+ 
+-	if(header.datum_size <= size_header)
+-		return FALSE;
++	*size_payload = datum_payload_size(header.datum_size, size_header);
+ 
+-	*size_payload = (size_t)(header.datum_size - size_header);
++	if(*size_payload == 0)
++		return FALSE;
+ 
+ 	*payload = dis_malloc(*size_payload);
+ 
+@@ -308,7 +330,8 @@
+ 
+ 	dis_printf(level, "Generic datum:\n");
+ 	hexdump(level, (void*)((char*)datum + sizeof(datum_generic_type_t)),
+-			datum->header.datum_size - sizeof(datum_generic_type_t));
++			datum_payload_size(datum->header.datum_size,
++			                   sizeof(datum_generic_type_t)));
+ }
+ 
+ 
+@@ -336,7 +359,7 @@
+ 	hexdump(
+ 		level,
+ 		(void*) ((char*) datum + sizeof(datum_key_t)),
+-		datum->header.datum_size - sizeof(datum_key_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_key_t))
+ 	);
+ 
+ 	dis_free(cipher_str);
+@@ -346,11 +369,14 @@
+ {
+ 	datum_unicode_t* datum = (datum_unicode_t*) vdatum;
+ 
+-	size_t utf16_length = (datum->header.datum_size - sizeof(datum_unicode_t));
+-	wchar_t* wchar_s = dis_malloc(
+-		((datum->header.datum_size - sizeof(datum_unicode_t)) / 2)
+-		* sizeof(wchar_t)
+-	);
++	size_t utf16_length = datum_payload_size(datum->header.datum_size,
++	                                         sizeof(datum_unicode_t));
++
++	/* Nothing to print if the datum can't hold a string */
++	if(utf16_length < 2)
++		return;
++
++	wchar_t* wchar_s = dis_malloc((utf16_length / 2) * sizeof(wchar_t));
+ 
+ 	/*
+ 	 * This datum's payload is an UTF-16 string finished by \0
+@@ -408,7 +434,7 @@
+ 	hexdump(
+ 		level,
+ 		(void*) ((char*) datum + sizeof(datum_aes_ccm_t)),
+-		datum->header.datum_size - sizeof(datum_aes_ccm_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_aes_ccm_t))
+ 	);
+ }
+ 
+@@ -421,7 +447,7 @@
+ 	hexdump(
+ 		level,
+ 		(void*) ((char*) datum + sizeof(datum_tpm_enc_t)),
+-		datum->header.datum_size - sizeof(datum_tpm_enc_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_tpm_enc_t))
+ 	);
+ }
+ 
+@@ -971,7 +997,8 @@
+ 
+ 	rb_str_concat(rb_str, rb_hexdump(
+ 		(uint8_t*) datum + sizeof(datum_generic_type_t),
+-		datum->header.datum_size - sizeof(datum_generic_type_t)
++		datum_payload_size(datum->header.datum_size,
++		                   sizeof(datum_generic_type_t))
+ 	));
+ 
+ 	return rb_str;
+@@ -1009,7 +1036,7 @@
+ 	rb_str_cat2(rb_str, "Key:\n");
+ 	rb_str_concat(rb_str, rb_hexdump(
+ 		(uint8_t*) datum + sizeof(datum_key_t),
+-		datum->header.datum_size - sizeof(datum_key_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_key_t))
+ 	));
+ 
+ 	dis_free(cipher_str);
+@@ -1027,11 +1054,14 @@
+ 	if(datum == NULL)
+ 		return rb_str;
+ 
+-	size_t utf16_length = (datum->header.datum_size - sizeof(datum_unicode_t));
+-	wchar_t* wchar_s = dis_malloc(
+-		((datum->header.datum_size - sizeof(datum_unicode_t)) / 2)
+-		* sizeof(wchar_t)
+-	);
++	size_t utf16_length = datum_payload_size(datum->header.datum_size,
++	                                         sizeof(datum_unicode_t));
++
++	/* Nothing to print if the datum can't hold a string */
++	if(utf16_length < 2)
++		return rb_str;
++
++	wchar_t* wchar_s = dis_malloc((utf16_length / 2) * sizeof(wchar_t));
+ 
+ 	/*
+ 	 * This datum's payload is an UTF-16 string finished by \0
+@@ -1119,7 +1149,7 @@
+ 	rb_str_cat2(rb_str, "Payload:\n");
+ 	rb_str_concat(rb_str, rb_hexdump(
+ 		(uint8_t*) ((char*) datum + sizeof(datum_aes_ccm_t)),
+-		datum->header.datum_size - sizeof(datum_aes_ccm_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_aes_ccm_t))
+ 	));
+ 
+ 	return rb_str;
+@@ -1139,7 +1169,7 @@
+ 	rb_str_cat2(rb_str, "Payload:\n");
+ 	rb_str_concat(rb_str, rb_hexdump(
+ 		(uint8_t*) ((char*) datum + sizeof(datum_tpm_enc_t)),
+-		datum->header.datum_size - sizeof(datum_tpm_enc_t)
++		datum_payload_size(datum->header.datum_size, sizeof(datum_tpm_enc_t))
+ 	));
+ 
+ 	return rb_str;
+--- a/src/metadata/fvek.c
++++ b/src/metadata/fvek.c
+@@ -94,7 +94,41 @@
+ 	}
+ 
+ 	fvek = (datum_aes_ccm_t*)*fvek_datum;
++	/*
++	 * NOTE: get_header_safe() does not check value_type, so the bound below is
++	 * what keeps the datum_value_types_prop[] lookup in range.
++	 */
++	if(fvek->header.value_type >= NB_DATUMS_VALUE_TYPES)
++	{
++		dis_printf(
++			L_CRITICAL,
++			"The FVEK datum's value type is out of range: %hu. Abort.\n",
++			fvek->header.value_type
++		);
++		dis_free(vmk_key);
++		return FALSE;
++	}
++
+ 	header_size = datum_value_types_prop[fvek->header.value_type].size_header;
++
++	/*
++	 * get_header_safe() only enforces a datum_size of at least 8 bytes, which
++	 * is less than this datum type's header. Subtracting without checking would
++	 * wrap around and hand a huge size down to decrypt_key()
++	 */
++	if(fvek->header.datum_size <= header_size)
++	{
++		dis_printf(
++			L_CRITICAL,
++			"The FVEK datum's size (%hu) is not bigger than its header's size "
++			"(%u). Abort.\n",
++			fvek->header.datum_size,
++			header_size
++		);
++		dis_free(vmk_key);
++		return FALSE;
++	}
++
+ 	fvek_size = fvek->header.datum_size - header_size;
+ 
+ 	if(vmk_key_size > (size_t) (UINT_MAX / 8))
+--- a/src/metadata/vmk.c
++++ b/src/metadata/vmk.c
+@@ -159,7 +159,42 @@
+ 	hexdump(L_DEBUG, recovery_key, key_size);
+ 	dis_printf(L_DEBUG, "==========================================================\n");
+ 
++	/*
++	 * Validate the datum before anything else, printing included: the printing
++	 * routines derive a payload size from datum_size too.
++	 *
++	 * NOTE: get_header_safe() does not check value_type, so the bound below is
++	 * what keeps the datum_value_types_prop[] lookup in range.
++	 */
++	if(vmk_datum->header.value_type >= NB_DATUMS_VALUE_TYPES)
++	{
++		dis_printf(
++			L_ERROR,
++			"The VMK datum's value type is out of range: %hu. Abort.\n",
++			vmk_datum->header.value_type
++		);
++		return FALSE;
++	}
++
+ 	header_size = datum_value_types_prop[vmk_datum->header.value_type].size_header;
++
++	/*
++	 * get_header_safe() only enforces a datum_size of at least 8 bytes, which
++	 * is less than this datum type's header. Subtracting without checking would
++	 * wrap around and hand a huge size down to decrypt_key()
++	 */
++	if(vmk_datum->header.datum_size <= header_size)
++	{
++		dis_printf(
++			L_ERROR,
++			"The VMK datum's size (%hu) is not bigger than its header's size "
++			"(%u). Abort.\n",
++			vmk_datum->header.datum_size,
++			header_size
++		);
++		return FALSE;
++	}
++
+ 	vmk_size = vmk_datum->header.datum_size - header_size;
+ 
+ 	if(key_size > (size_t) (UINT_MAX / 8))
diff -Nru dislocker-0.7.3+git20240607/debian/patches/series dislocker-0.7.3+git20240607/debian/patches/series
--- dislocker-0.7.3+git20240607/debian/patches/series	2025-02-11 16:13:38.000000000 +0100
+++ dislocker-0.7.3+git20240607/debian/patches/series	2026-10-23 12:19:37.000000000 +0200
@@ -1,3 +1,5 @@
+CVE-2026-107634.patch
+CVE-2026-107635.patch
 fix-spelling.patch
 always-pick-linux-manpages.patch
 wrong-libdir.patch

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to