static-delta: Drop flat per-part decompression-size cap
authorColin Walters <walters@verbum.org>
Tue, 18 Aug 2026 13:14:09 +0000 (09:14 -0400)
committerColin Walters <walters@verbum.org>
Tue, 18 Aug 2026 13:18:14 +0000 (09:18 -0400)
The 512 MiB flat cap on decompressed part size is heuristic.
Since we broke flatpak firefox deltas with incorrect heuristics,
back that out until we come up with a design that is precise and
tighter.

This deliberately reopens <https://github.com/ostreedev/ostree/security/advisories/GHSA-7cgc-gp99-6jmm> for now.

Assisted-by: https://github.com/cgwalters/cgwalters#llms
Signed-off-by: Colin Walters <walters@verbum.org>
src/libostree/ostree-repo-static-delta-compilation.c
src/libostree/ostree-repo-static-delta-core.c
src/libostree/ostree-repo-static-delta-private.h
src/ostree/ot-builtin-static-delta.c

index 7b986a17e79e9df06eb3b068cc1e2f9d7a752f31..c3515cd1a2bfe0d6e1bdfd6dc733f580cbf849a8 100644 (file)
@@ -259,21 +259,6 @@ finish_part (OstreeStaticDeltaBuilder *builder, GError **error)
     g_variant_ref_sink (delta_part_content);
   }
 
-  /* Reject parts whose uncompressed payload exceeds the hard limit that
-   * consumers enforce (OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES).  Without
-   * this check, a large --max-chunk-size would produce deltas that every
-   * client rejects at apply time (CVE / RHEL-189208).
-   */
-  {
-    gsize payload_size = g_variant_get_size (delta_part_content);
-    if (payload_size > OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES)
-      return glnx_throw (error,
-                         "Delta part %u uncompressed payload size %" G_GSIZE_FORMAT
-                         " bytes exceeds maximum %" G_GUINT64_FORMAT "; reduce --max-chunk-size",
-                         builder->parts->len, payload_size,
-                         (guint64)OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES);
-  }
-
   /* Hardcode xz for now */
   compressor = (GConverter *)_ostree_lzma_compressor_new (NULL);
   compression_type_char = 'x';
@@ -1277,7 +1262,7 @@ get_fallback_headers (OstreeRepo *self, OstreeStaticDeltaBuilder *builder, GVari
  * are known:
  *   - min-fallback-size: u: Minimum uncompressed size in megabytes to use fallback, 0 to disable
  * fallbacks
- *   - max-chunk-size: u: Maximum size in megabytes of a delta part (hard cap: 512 MiB)
+ *   - max-chunk-size: u: Maximum size in megabytes of a delta part
  *   - max-bsdiff-size: u: Maximum size in megabytes to consider bsdiff compression
  *   for input files
  *   - compression: y: Compression type: 0=none, x=lzma, g=gzip
index abcca819a31fe5e8d21fdcc736b3af2da5d32bd8..20adb253ec16c56428ae0c8d88c0532f9c1c3b82 100644 (file)
@@ -641,12 +641,6 @@ _ostree_static_delta_part_open (GInputStream *part_in, GBytes *inline_part_bytes
   const gboolean trusted = (flags & OSTREE_STATIC_DELTA_OPEN_FLAGS_VARIANT_TRUSTED) > 0;
   const gboolean skip_checksum = (flags & OSTREE_STATIC_DELTA_OPEN_FLAGS_SKIP_CHECKSUM) > 0;
 
-  /* Decompression-bomb defense: the flat hard cap.  A tighter,
-   * exact-size-derived limit may be layered on top of this by callers
-   * that have that information; see OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES.
-   */
-  guint64 max_part_usize = OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES;
-
   /* We either take a fd or a GBytes reference */
   g_return_val_if_fail (G_IS_FILE_DESCRIPTOR_BASED (part_in) || inline_part_bytes != NULL, FALSE);
   g_return_val_if_fail (skip_checksum || expected_checksum != NULL, FALSE);
@@ -698,15 +692,6 @@ _ostree_static_delta_part_open (GInputStream *part_in, GBytes *inline_part_bytes
           g_variant_ref_sink (ret_part);
         }
 
-      /* Enforce the size limit so that an attacker cannot bypass the
-       * decompression-bomb defence by setting compression type to 0.
-       */
-      if (g_variant_get_size (ret_part) > max_part_usize)
-        return glnx_throw (error,
-                           "Uncompressed delta part size %" G_GSIZE_FORMAT
-                           " exceeds maximum %" G_GUINT64_FORMAT,
-                           g_variant_get_size (ret_part), max_part_usize);
-
       if (!skip_checksum)
         g_checksum_update (checksum, g_variant_get_data (ret_part), g_variant_get_size (ret_part));
 
@@ -715,8 +700,7 @@ _ostree_static_delta_part_open (GInputStream *part_in, GBytes *inline_part_bytes
       {
         g_autoptr (GConverter) decomp = (GConverter *)_ostree_lzma_decompressor_new ();
         g_autoptr (GInputStream) convin = g_converter_input_stream_new (source_in, decomp);
-        g_autoptr (GBytes) buf = ot_map_anonymous_tmpfile_from_content_with_limit (
-            convin, max_part_usize, cancellable, error);
+        g_autoptr (GBytes) buf = ot_map_anonymous_tmpfile_from_content (convin, cancellable, error);
         if (!buf)
           return FALSE;
 
index 2fd5135a7cb8ae75df553267ea9ae7b6615c57d6..a6f36162d42e85763402b535373d6607db530d72 100644 (file)
 
 G_BEGIN_DECLS
 
-/* Maximum uncompressed size for a single static delta part (512 MiB).
- * Enforced on both generation and consumption sides to prevent
- * decompression bombs from exhausting memory/disk (CVE / RHEL-189208).
- *
- * The delta compiler splits parts at max-chunk-size (default 32 MB), so
- * legitimate parts are typically well under 32 MB uncompressed.  512 MiB
- * provides ~16x headroom over the default, which is generous enough to
- * accommodate large custom --max-chunk-size values while still rejecting
- * decompression bombs that would expand to gigabytes.
- *
- * This is also the sole bound applied to deltas that don't carry the
- * exact-payload-size metadata (see
- * OSTREE_STATIC_DELTA_PART_PAYLOAD_SIZES_KEY below): the declared "usize"
- * in a delta part header only accounts for the final on-disk size of the
- * objects the part will produce, not the mode/xattr tables, opcode
- * bytecode, or raw payload data (including, for bsdiff'd objects, the
- * entire patch stream) that also make up the part's decompressed
- * payload.  There is no way to derive a tight, correct bound from usize
- * alone -- a part with a single large bsdiff'd object can have a
- * decompressed payload many times its usize -- so deltas lacking the
- * exact size just fall back to this flat cap.
- */
-#define OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES (512ULL * 1024ULL * 1024ULL)
-
 /* 1 byte for object type, 32 bytes for checksum */
 #define OSTREE_STATIC_DELTA_OBJTYPE_CSUM_LEN 33
 
index 66d5faa6f8508ebc3a2f58ebe7e636831039ee5b..ba91175e4553c1ef778051505aed9b302216873b 100644 (file)
@@ -96,7 +96,7 @@ static GOptionEntry generate_options[] = {
   { "max-bsdiff-size", 0, 0, G_OPTION_ARG_STRING, &opt_max_bsdiff_size,
     "Maximum size in megabytes to consider bsdiff compression for input files", NULL },
   { "max-chunk-size", 0, 0, G_OPTION_ARG_STRING, &opt_max_chunk_size,
-    "Maximum size of delta chunks in megabytes (hard cap: 512 MiB)", NULL },
+    "Maximum size of delta chunks in megabytes", NULL },
   { "filename", 0, 0, G_OPTION_ARG_FILENAME, &opt_filename,
     "Write the delta content to PATH (a directory).  If not specified, the OSTree repository is "
     "used",