c2dfb7
From 696d56fc75e72f47e4d3232a2140fac10b6b44de Mon Sep 17 00:00:00 2001
c2dfb7
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
c2dfb7
Date: Mon, 29 Oct 2018 14:55:33 +0100
c2dfb7
Subject: [PATCH] journal: adapt for new improved LZ4_decompress_safe_partial()
c2dfb7
MIME-Version: 1.0
c2dfb7
Content-Type: text/plain; charset=UTF-8
c2dfb7
Content-Transfer-Encoding: 8bit
c2dfb7
c2dfb7
With lz4 1.8.3, this function can now decompress partial results into a smaller
c2dfb7
buffer. The release news don't say anything interesting, but the test case that
c2dfb7
was previously failing now works OK.
c2dfb7
c2dfb7
Fixes #10259.
c2dfb7
c2dfb7
A test is added. It shows that with *older* lz4, a partial decompression can
c2dfb7
occur with the returned size smaller then the requested number of bytes _and_
c2dfb7
smaller then the size of the compressed data:
c2dfb7
c2dfb7
(lz4-libs-1.8.2-1.fc29.x86_64)
c2dfb7
Compressed 4194304 → 16464
c2dfb7
Decompressed → 4194304
c2dfb7
Decompressed partial 12/4194304 → 4194304
c2dfb7
Decompressed partial 1/1 → -2 (bad)
c2dfb7
Decompressed partial 2/2 → -2 (bad)
c2dfb7
Decompressed partial 3/3 → -2 (bad)
c2dfb7
Decompressed partial 4/4 → -2 (bad)
c2dfb7
Decompressed partial 5/5 → -2 (bad)
c2dfb7
Decompressed partial 6/6 → 6 (good)
c2dfb7
Decompressed partial 7/7 → 6 (good)
c2dfb7
Decompressed partial 8/8 → 6 (good)
c2dfb7
Decompressed partial 9/9 → 6 (good)
c2dfb7
Decompressed partial 10/10 → 6 (good)
c2dfb7
Decompressed partial 11/11 → 6 (good)
c2dfb7
Decompressed partial 12/12 → 6 (good)
c2dfb7
Decompressed partial 13/13 → 6 (good)
c2dfb7
Decompressed partial 14/14 → 6 (good)
c2dfb7
Decompressed partial 15/15 → 6 (good)
c2dfb7
Decompressed partial 16/16 → 6 (good)
c2dfb7
Decompressed partial 17/17 → 6 (good)
c2dfb7
Decompressed partial 18/18 → -16459 (bad)
c2dfb7
c2dfb7
(lz4-libs-1.8.3-1.fc29.x86_64)
c2dfb7
Compressed 4194304 → 16464
c2dfb7
Decompressed → 4194304
c2dfb7
Decompressed partial 12/4194304 → 12
c2dfb7
Decompressed partial 1/1 → 1 (good)
c2dfb7
Decompressed partial 2/2 → 2 (good)
c2dfb7
Decompressed partial 3/3 → 3 (good)
c2dfb7
Decompressed partial 4/4 → 4 (good)
c2dfb7
...
c2dfb7
c2dfb7
If we got such a short "successful" decompression in decompress_startswith() as
c2dfb7
implemented before this patch, we could be confused and return a false negative
c2dfb7
result. But it turns out that this only occurs with small output buffer
c2dfb7
sizes. We use greedy_realloc() to manager the buffer, so it is always at least
c2dfb7
64 bytes. I couldn't hit a case where decompress_startswith() would actually
c2dfb7
return a bogus result. But since the lack of proof is not conclusive, the code
c2dfb7
for *older* lz4 is changed too, just to be safe. We cannot rule out that on a
c2dfb7
different architecture or with some unlucky compressed string we could hit this
c2dfb7
corner case.
c2dfb7
c2dfb7
The fallback code is guarded by a version check. The check uses a function not
c2dfb7
the compile-time define, because there was no soversion bump in lz4 or new
c2dfb7
symbols, and we could be compiled against a newer lz4 and linked at runtime
c2dfb7
with an older one. (This happens routinely e.g. when somebody upgrades a subset
c2dfb7
of distro packages.)
c2dfb7
c2dfb7
(cherry picked from commit e41ef6fd0027d3619dc1cf062100b2d224d0ee7e)
c2dfb7
Resolves: #1843871
c2dfb7
---
c2dfb7
 src/journal/compress.c      | 39 ++++++++++++++++++++++++-------------
c2dfb7
 src/journal/test-compress.c | 21 ++++++++++----------
c2dfb7
 2 files changed, 37 insertions(+), 23 deletions(-)
c2dfb7
c2dfb7
diff --git a/src/journal/compress.c b/src/journal/compress.c
c2dfb7
index a4a5e63840..e95ce2bcaa 100644
c2dfb7
--- a/src/journal/compress.c
c2dfb7
+++ b/src/journal/compress.c
c2dfb7
@@ -290,7 +290,6 @@ int decompress_startswith_lz4(const void *src, uint64_t src_size,
c2dfb7
          * prefix */
c2dfb7
 
c2dfb7
         int r;
c2dfb7
-        size_t size;
c2dfb7
 
c2dfb7
         assert(src);
c2dfb7
         assert(src_size > 0);
c2dfb7
@@ -307,23 +306,37 @@ int decompress_startswith_lz4(const void *src, uint64_t src_size,
c2dfb7
 
c2dfb7
         r = LZ4_decompress_safe_partial((char*)src + 8, *buffer, src_size - 8,
c2dfb7
                                         prefix_len + 1, *buffer_size);
c2dfb7
-        if (r >= 0)
c2dfb7
-                size = (unsigned) r;
c2dfb7
-        else {
c2dfb7
-                /* lz4 always tries to decode full "sequence", so in
c2dfb7
-                 * pathological cases might need to decompress the
c2dfb7
-                 * full field. */
c2dfb7
+        /* One lz4 < 1.8.3, we might get "failure" (r < 0), or "success" where
c2dfb7
+         * just a part of the buffer is decompressed. But if we get a smaller
c2dfb7
+         * amount of bytes than requested, we don't know whether there isn't enough
c2dfb7
+         * data to fill the requested size or whether we just got a partial answer.
c2dfb7
+         */
c2dfb7
+        if (r < 0 || (size_t) r < prefix_len + 1) {
c2dfb7
+                size_t size;
c2dfb7
+
c2dfb7
+                if (LZ4_versionNumber() >= 10803)
c2dfb7
+                        /* We trust that the newer lz4 decompresses the number of bytes we
c2dfb7
+                         * requested if available in the compressed string. */
c2dfb7
+                        return 0;
c2dfb7
+
c2dfb7
+                if (r > 0)
c2dfb7
+                        /* Compare what we have first, in case of mismatch we can
c2dfb7
+                         * shortcut the full comparison. */
c2dfb7
+                        if (memcmp(*buffer, prefix, r) != 0)
c2dfb7
+                                return 0;
c2dfb7
+
c2dfb7
+                /* Before version 1.8.3, lz4 always tries to decode full a "sequence",
c2dfb7
+                 * so in pathological cases might need to decompress the full field. */
c2dfb7
                 r = decompress_blob_lz4(src, src_size, buffer, buffer_size, &size, 0);
c2dfb7
                 if (r < 0)
c2dfb7
                         return r;
c2dfb7
-        }
c2dfb7
 
c2dfb7
-        if (size >= prefix_len + 1)
c2dfb7
-                return memcmp(*buffer, prefix, prefix_len) == 0 &&
c2dfb7
-                        ((const uint8_t*) *buffer)[prefix_len] == extra;
c2dfb7
-        else
c2dfb7
-                return 0;
c2dfb7
+                if (size < prefix_len + 1)
c2dfb7
+                        return 0;
c2dfb7
+        }
c2dfb7
 
c2dfb7
+        return memcmp(*buffer, prefix, prefix_len) == 0 &&
c2dfb7
+                ((const uint8_t*) *buffer)[prefix_len] == extra;
c2dfb7
 #else
c2dfb7
         return -EPROTONOSUPPORT;
c2dfb7
 #endif
c2dfb7
diff --git a/src/journal/test-compress.c b/src/journal/test-compress.c
c2dfb7
index 65cd3fbfeb..f60c4ae3d7 100644
c2dfb7
--- a/src/journal/test-compress.c
c2dfb7
+++ b/src/journal/test-compress.c
c2dfb7
@@ -223,13 +223,13 @@ static void test_compress_stream(int compression,
c2dfb7
 
c2dfb7
 #if HAVE_LZ4
c2dfb7
 static void test_lz4_decompress_partial(void) {
c2dfb7
-        char buf[20000];
c2dfb7
+        char buf[20000], buf2[100];
c2dfb7
         size_t buf_size = sizeof(buf), compressed;
c2dfb7
         int r;
c2dfb7
         _cleanup_free_ char *huge = NULL;
c2dfb7
 
c2dfb7
 #define HUGE_SIZE (4096*1024)
c2dfb7
-        huge = malloc(HUGE_SIZE);
c2dfb7
+        assert_se(huge = malloc(HUGE_SIZE));
c2dfb7
         memset(huge, 'x', HUGE_SIZE);
c2dfb7
         memcpy(huge, "HUGE=", 5);
c2dfb7
 
c2dfb7
@@ -248,14 +248,15 @@ static void test_lz4_decompress_partial(void) {
c2dfb7
         assert_se(r >= 0);
c2dfb7
         log_info("Decompressed partial %i/%i → %i", 12, HUGE_SIZE, r);
c2dfb7
 
c2dfb7
-        /* We expect this to fail, because that's how current lz4 works. If this
c2dfb7
-         * call succeeds, then lz4 has been fixed, and we need to change our code.
c2dfb7
-         */
c2dfb7
-        r = LZ4_decompress_safe_partial(buf, huge,
c2dfb7
-                                        compressed,
c2dfb7
-                                        12, HUGE_SIZE-1);
c2dfb7
-        assert_se(r < 0);
c2dfb7
-        log_info("Decompressed partial %i/%i → %i", 12, HUGE_SIZE-1, r);
c2dfb7
+        for (size_t size = 1; size < sizeof(buf2); size++) {
c2dfb7
+                /* This failed in older lz4s but works in newer ones. */
c2dfb7
+                r = LZ4_decompress_safe_partial(buf, buf2, compressed, size, size);
c2dfb7
+                log_info("Decompressed partial %zu/%zu → %i (%s)", size, size, r,
c2dfb7
+                                                                   r < 0 ? "bad" : "good");
c2dfb7
+                if (r >= 0 && LZ4_versionNumber() >= 10803)
c2dfb7
+                        /* lz4 <= 1.8.2 should fail that test, let's only check for newer ones */
c2dfb7
+                        assert_se(memcmp(buf2, huge, r) == 0);
c2dfb7
+        }
c2dfb7
 }
c2dfb7
 #endif
c2dfb7