tests_test_ot_tool_util_LDADD = $(TESTS_LDADD)
tests_test_lzma_SOURCES = src/libostree/ostree-lzma-common.c src/libostree/ostree-lzma-compressor.c \
- src/libostree/ostree-lzma-decompressor.c tests/test-lzma.c
+ src/libostree/ostree-lzma-decompressor.c src/libotutil/ot-fs-utils.c tests/test-lzma.c
tests_test_lzma_CFLAGS = $(TESTS_CFLAGS) $(OT_DEP_LZMA_CFLAGS)
tests_test_lzma_LDADD = $(TESTS_LDADD) $(OT_DEP_LZMA_LIBS)
#include <lzma.h>
#include <string.h>
+/* Cap the LZMA decoder memory limit to 100 MiB to prevent decompression bombs
+ * from exhausting system memory via crafted static delta content (CVE / RHEL-189208).
+ *
+ * ostree's compressor uses preset 8 which implies a 32 MiB dictionary, so
+ * legitimate streams need ~33-35 MiB. 100 MiB gives ~3x headroom and matches
+ * the default used by RPM (rpmio/rpmio.cc, `100<<20`). Even preset 9 (64 MiB
+ * dictionary) fits comfortably within this limit.
+ */
+#define OSTREE_LZMA_DECODER_MEMLIMIT (100ULL * 1024ULL * 1024ULL)
+
enum
{
PROP_0,
if (!self->initialized)
{
- res = lzma_stream_decoder (&self->lstream, G_MAXUINT64, 0);
+ res = lzma_stream_decoder (&self->lstream, OSTREE_LZMA_DECODER_MEMLIMIT, 0);
if (res != LZMA_OK)
goto out;
self->initialized = TRUE;
{
int part_fd = g_file_descriptor_based_get_fd ((GFileDescriptorBased *)part_in);
- /* No compression, no checksums - a fast path */
+ /* No compression - a fast path */
if (!ot_variant_read_fd (part_fd, 1,
G_VARIANT_TYPE (OSTREE_STATIC_DELTA_PART_PAYLOAD_FORMAT_V0),
trusted, &ret_part, error))
g_variant_ref_sink (ret_part);
}
+ /* Enforce the same size limit as for compressed parts so that an
+ * attacker cannot bypass the decompression-bomb defence simply by
+ * setting compression type to 0 (CVE / RHEL-189208).
+ */
+ if (g_variant_get_size (ret_part) > OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES)
+ return glnx_throw (
+ error,
+ "Uncompressed delta part size %" G_GSIZE_FORMAT " exceeds maximum %" G_GUINT64_FORMAT,
+ g_variant_get_size (ret_part), (guint64)OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES);
+
if (!skip_checksum)
g_checksum_update (checksum, g_variant_get_data (ret_part), g_variant_get_size (ret_part));
{
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 (convin, cancellable, error);
+ g_autoptr (GBytes) buf = ot_map_anonymous_tmpfile_from_content_with_limit (
+ convin, OSTREE_STATIC_DELTA_PART_MAX_USIZE_BYTES, cancellable, error);
if (!buf)
return FALSE;
G_BEGIN_DECLS
-/* Arbitrarily chosen */
-#define OSTREE_STATIC_DELTA_PART_MAX_SIZE_BYTES (16 * 1024 * 1024)
+/* 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.
+ */
+#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
return glnx_fd_readall_bytes (fd, NULL, error);
}
-/* Given an input stream, splice it to an anonymous file (O_TMPFILE).
- * Useful for potentially large but transient files.
+/* Given an input stream, splice it to an anonymous file (O_TMPFILE)
+ * with an optional upper bound on the number of bytes written.
+ * If @max_bytes is non-zero and the stream produces more than that
+ * many bytes, an error is returned. This prevents decompression-bomb
+ * style attacks where a small compressed payload expands to exhaust
+ * memory or disk (CVE / RHEL-189208).
*/
GBytes *
-ot_map_anonymous_tmpfile_from_content (GInputStream *instream, GCancellable *cancellable,
- GError **error)
+ot_map_anonymous_tmpfile_from_content_with_limit (GInputStream *instream, guint64 max_bytes,
+ GCancellable *cancellable, GError **error)
{
g_auto (GLnxTmpfile) tmpf = {
0,
return NULL;
g_autoptr (GOutputStream) out = g_unix_output_stream_new (tmpf.fd, FALSE);
- gssize n_bytes_written = g_output_stream_splice (
- out, instream, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
- cancellable, error);
- if (n_bytes_written < 0)
+
+ guint64 total = 0;
+ while (TRUE)
+ {
+ guchar buf[65536];
+ gssize n_read
+ = g_input_stream_read (instream, buf, sizeof (buf), cancellable, error);
+ if (n_read < 0)
+ return NULL;
+ if (n_read == 0)
+ break;
+
+ total += (guint64)n_read;
+ if (max_bytes > 0 && total > max_bytes)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE,
+ "Decompressed delta part exceeds configured limit of %" G_GUINT64_FORMAT
+ " bytes",
+ max_bytes);
+ return NULL;
+ }
+
+ gsize n_written;
+ if (!g_output_stream_write_all (out, buf, (gsize)n_read, &n_written, cancellable, error))
+ return NULL;
+ }
+
+ if (!g_output_stream_close (out, cancellable, error))
return NULL;
g_autoptr (GMappedFile) mfile = g_mapped_file_new_from_fd (tmpf.fd, FALSE, error);
return g_mapped_file_get_bytes (mfile);
}
+/* Given an input stream, splice it to an anonymous file (O_TMPFILE).
+ * Useful for potentially large but transient files.
+ */
+GBytes *
+ot_map_anonymous_tmpfile_from_content (GInputStream *instream, GCancellable *cancellable,
+ GError **error)
+{
+ return ot_map_anonymous_tmpfile_from_content_with_limit (instream, 0, cancellable, error);
+}
+
gboolean
ot_parse_file_by_line (const char *path, gboolean (*cb) (const char *, void *, GError **),
void *cbdata, GCancellable *cancellable, GError **error)
GBytes *ot_map_anonymous_tmpfile_from_content (GInputStream *instream, GCancellable *cancellable,
GError **error);
+GBytes *ot_map_anonymous_tmpfile_from_content_with_limit (GInputStream *instream, guint64 max_bytes,
+ GCancellable *cancellable,
+ GError **error);
+
GBytes *ot_fd_readall_or_mmap (int fd, goffset offset, GError **error);
gboolean ot_parse_file_by_line (const char *path, gboolean (*cb) (const char *, void *, GError **),
#include "libglnx.h"
#include "ostree-lzma-compressor.h"
#include "ostree-lzma-decompressor.h"
+#include "ot-fs-utils.h"
#include <gio/gio.h>
#include <gio/gmemoryoutputstream.h>
#include <gio/gunixoutputstream.h>
#include <glib.h>
+#include <lzma.h>
#include <stdlib.h>
#include <string.h>
helper_test_compress_decompress (buffer, buffer_size);
}
+/* Test that the LZMA decompressor rejects streams whose dictionary size
+ * exceeds the configured memory limit (OSTREE_LZMA_DECODER_MEMLIMIT = 100 MiB).
+ * We craft a minimal valid LZMA stream header that requests a 128 MiB
+ * dictionary, which should trigger LZMA_MEMLIMIT_ERROR -> "Exceeded memory
+ * limit" when the decompressor tries to process it.
+ */
+static void
+test_lzma_memlimit (void)
+{
+ g_autoptr (GError) error = NULL;
+
+ /* Build a minimal XZ stream that requests a 128 MiB dictionary.
+ * lzma_options_lzma with dict_size = 128 MiB, compressed via
+ * lzma_stream_encoder() with LZMA_CHECK_CRC64.
+ */
+ lzma_options_lzma opts;
+ lzma_lzma_preset (&opts, LZMA_PRESET_DEFAULT);
+ opts.dict_size = 128U * 1024U * 1024U; /* 128 MiB - exceeds 100 MiB limit */
+
+ lzma_filter filters[] = { { .id = LZMA_FILTER_LZMA2, .options = &opts },
+ { .id = LZMA_VLI_UNKNOWN, .options = NULL } };
+
+ lzma_stream strm = LZMA_STREAM_INIT;
+ lzma_ret ret = lzma_stream_encoder (&strm, filters, LZMA_CHECK_CRC64);
+ g_assert_cmpint (ret, ==, LZMA_OK);
+
+ /* Compress a tiny payload just to produce a valid stream header */
+ const guint8 input[] = "test";
+ guint8 outbuf[4096];
+ strm.next_in = input;
+ strm.avail_in = sizeof (input);
+ strm.next_out = outbuf;
+ strm.avail_out = sizeof (outbuf);
+ ret = lzma_code (&strm, LZMA_FINISH);
+ g_assert (ret == LZMA_STREAM_END || ret == LZMA_OK);
+ gsize compressed_size = sizeof (outbuf) - strm.avail_out;
+ lzma_end (&strm);
+
+ /* Now try to decompress through our decompressor — it should fail
+ * with "Exceeded memory limit" because the stream header requests
+ * a 128 MiB dictionary but our limit is 100 MiB.
+ */
+ g_autoptr (GConverter) decomp = (GConverter *)_ostree_lzma_decompressor_new ();
+ g_autoptr (GInputStream) raw_in
+ = g_memory_input_stream_new_from_data (outbuf, compressed_size, NULL);
+ g_autoptr (GInputStream) convin = g_converter_input_stream_new (raw_in, decomp);
+ g_autoptr (GOutputStream) out = g_memory_output_stream_new_resizable ();
+
+ gssize n = g_output_stream_splice (
+ out, convin, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL,
+ &error);
+ g_assert_cmpint (n, ==, -1);
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
+ /* The error message from ostree-lzma-common.c for LZMA_MEMLIMIT_ERROR */
+ g_assert (strstr (error->message, "memory limit") != NULL);
+}
+
+/* Test that ot_map_anonymous_tmpfile_from_content_with_limit() correctly
+ * rejects input that exceeds the configured byte limit.
+ */
+static void
+test_lzma_decompressed_size_limit (void)
+{
+ g_autoptr (GError) error = NULL;
+ const gsize data_size = 8192;
+ g_autofree guint8 *data = g_new0 (guint8, data_size);
+ memset (data, 'X', data_size);
+
+ g_autoptr (GInputStream) in = g_memory_input_stream_new_from_data (data, data_size, NULL);
+
+ /* Set a limit smaller than the data — should fail */
+ GBytes *result = ot_map_anonymous_tmpfile_from_content_with_limit (in, 4096, NULL, &error);
+ g_assert_null (result);
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE);
+ g_assert (strstr (error->message, "exceeds configured limit") != NULL);
+}
+
+/* Test that ot_map_anonymous_tmpfile_from_content_with_limit() succeeds
+ * when the input fits within the limit.
+ */
+static void
+test_lzma_decompressed_size_limit_ok (void)
+{
+ g_autoptr (GError) error = NULL;
+ const gsize data_size = 4096;
+ g_autofree guint8 *data = g_new0 (guint8, data_size);
+ memset (data, 'Y', data_size);
+
+ g_autoptr (GInputStream) in = g_memory_input_stream_new_from_data (data, data_size, NULL);
+
+ /* Limit is exactly the data size — should succeed */
+ g_autoptr (GBytes) result
+ = ot_map_anonymous_tmpfile_from_content_with_limit (in, data_size, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (result);
+ g_assert_cmpint (g_bytes_get_size (result), ==, data_size);
+}
+
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/lzma/random-buffer", test_lzma_random);
g_test_add_func ("/lzma/big-buffer", test_lzma_big_buffer);
+ g_test_add_func ("/lzma/memlimit", test_lzma_memlimit);
+ g_test_add_func ("/lzma/decompressed-size-limit", test_lzma_decompressed_size_limit);
+ g_test_add_func ("/lzma/decompressed-size-limit-ok", test_lzma_decompressed_size_limit_ok);
return g_test_run ();
}