Skip to content

Commit e41ef6f

Browse files
committed
journal: adapt for new improved LZ4_decompress_safe_partial()
With lz4 1.8.3, this function can now decompress partial results into a smaller buffer. The release news don't say anything interesting, but the test case that was previously failing now works OK. Fixes systemd#10259. A test is added. It shows that with *older* lz4, a partial decompression can occur with the returned size smaller then the requested number of bytes _and_ smaller then the size of the compressed data: (lz4-libs-1.8.2-1.fc29.x86_64) Compressed 4194304 → 16464 Decompressed → 4194304 Decompressed partial 12/4194304 → 4194304 Decompressed partial 1/1 → -2 (bad) Decompressed partial 2/2 → -2 (bad) Decompressed partial 3/3 → -2 (bad) Decompressed partial 4/4 → -2 (bad) Decompressed partial 5/5 → -2 (bad) Decompressed partial 6/6 → 6 (good) Decompressed partial 7/7 → 6 (good) Decompressed partial 8/8 → 6 (good) Decompressed partial 9/9 → 6 (good) Decompressed partial 10/10 → 6 (good) Decompressed partial 11/11 → 6 (good) Decompressed partial 12/12 → 6 (good) Decompressed partial 13/13 → 6 (good) Decompressed partial 14/14 → 6 (good) Decompressed partial 15/15 → 6 (good) Decompressed partial 16/16 → 6 (good) Decompressed partial 17/17 → 6 (good) Decompressed partial 18/18 → -16459 (bad) (lz4-libs-1.8.3-1.fc29.x86_64) Compressed 4194304 → 16464 Decompressed → 4194304 Decompressed partial 12/4194304 → 12 Decompressed partial 1/1 → 1 (good) Decompressed partial 2/2 → 2 (good) Decompressed partial 3/3 → 3 (good) Decompressed partial 4/4 → 4 (good) ... If we got such a short "successful" decompression in decompress_startswith() as implemented before this patch, we could be confused and return a false negative result. But it turns out that this only occurs with small output buffer sizes. We use greedy_realloc() to manager the buffer, so it is always at least 64 bytes. I couldn't hit a case where decompress_startswith() would actually return a bogus result. But since the lack of proof is not conclusive, the code for *older* lz4 is changed too, just to be safe. We cannot rule out that on a different architecture or with some unlucky compressed string we could hit this corner case. The fallback code is guarded by a version check. The check uses a function not the compile-time define, because there was no soversion bump in lz4 or new symbols, and we could be compiled against a newer lz4 and linked at runtime with an older one. (This happens routinely e.g. when somebody upgrades a subset of distro packages.)
1 parent ba17efc commit e41ef6f

File tree

2 files changed

+37
-23
lines changed

2 files changed

+37
-23
lines changed

src/journal/compress.c

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ int decompress_startswith_lz4(const void *src, uint64_t src_size,
290290
* prefix */
291291

292292
int r;
293-
size_t size;
294293

295294
assert(src);
296295
assert(src_size > 0);
@@ -307,23 +306,37 @@ int decompress_startswith_lz4(const void *src, uint64_t src_size,
307306

308307
r = LZ4_decompress_safe_partial((char*)src + 8, *buffer, src_size - 8,
309308
prefix_len + 1, *buffer_size);
310-
if (r >= 0)
311-
size = (unsigned) r;
312-
else {
313-
/* lz4 always tries to decode full "sequence", so in
314-
* pathological cases might need to decompress the
315-
* full field. */
309+
/* One lz4 < 1.8.3, we might get "failure" (r < 0), or "success" where
310+
* just a part of the buffer is decompressed. But if we get a smaller
311+
* amount of bytes than requested, we don't know whether there isn't enough
312+
* data to fill the requested size or whether we just got a partial answer.
313+
*/
314+
if (r < 0 || (size_t) r < prefix_len + 1) {
315+
size_t size;
316+
317+
if (LZ4_versionNumber() >= 10803)
318+
/* We trust that the newer lz4 decompresses the number of bytes we
319+
* requested if available in the compressed string. */
320+
return 0;
321+
322+
if (r > 0)
323+
/* Compare what we have first, in case of mismatch we can
324+
* shortcut the full comparison. */
325+
if (memcmp(*buffer, prefix, r) != 0)
326+
return 0;
327+
328+
/* Before version 1.8.3, lz4 always tries to decode full a "sequence",
329+
* so in pathological cases might need to decompress the full field. */
316330
r = decompress_blob_lz4(src, src_size, buffer, buffer_size, &size, 0);
317331
if (r < 0)
318332
return r;
319-
}
320333

321-
if (size >= prefix_len + 1)
322-
return memcmp(*buffer, prefix, prefix_len) == 0 &&
323-
((const uint8_t*) *buffer)[prefix_len] == extra;
324-
else
325-
return 0;
334+
if (size < prefix_len + 1)
335+
return 0;
336+
}
326337

338+
return memcmp(*buffer, prefix, prefix_len) == 0 &&
339+
((const uint8_t*) *buffer)[prefix_len] == extra;
327340
#else
328341
return -EPROTONOSUPPORT;
329342
#endif

src/journal/test-compress.c

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,13 @@ static void test_compress_stream(int compression,
224224

225225
#if HAVE_LZ4
226226
static void test_lz4_decompress_partial(void) {
227-
char buf[20000];
227+
char buf[20000], buf2[100];
228228
size_t buf_size = sizeof(buf), compressed;
229229
int r;
230230
_cleanup_free_ char *huge = NULL;
231231

232232
#define HUGE_SIZE (4096*1024)
233-
huge = malloc(HUGE_SIZE);
233+
assert_se(huge = malloc(HUGE_SIZE));
234234
memset(huge, 'x', HUGE_SIZE);
235235
memcpy(huge, "HUGE=", 5);
236236

@@ -249,14 +249,15 @@ static void test_lz4_decompress_partial(void) {
249249
assert_se(r >= 0);
250250
log_info("Decompressed partial %i/%i → %i", 12, HUGE_SIZE, r);
251251

252-
/* We expect this to fail, because that's how current lz4 works. If this
253-
* call succeeds, then lz4 has been fixed, and we need to change our code.
254-
*/
255-
r = LZ4_decompress_safe_partial(buf, huge,
256-
compressed,
257-
12, HUGE_SIZE-1);
258-
assert_se(r < 0);
259-
log_info("Decompressed partial %i/%i → %i", 12, HUGE_SIZE-1, r);
252+
for (size_t size = 1; size < sizeof(buf2); size++) {
253+
/* This failed in older lz4s but works in newer ones. */
254+
r = LZ4_decompress_safe_partial(buf, buf2, compressed, size, size);
255+
log_info("Decompressed partial %zu/%zu → %i (%s)", size, size, r,
256+
r < 0 ? "bad" : "good");
257+
if (r >= 0 && LZ4_versionNumber() >= 10803)
258+
/* lz4 <= 1.8.2 should fail that test, let's only check for newer ones */
259+
assert_se(memcmp(buf2, huge, r) == 0);
260+
}
260261
}
261262
#endif
262263

0 commit comments

Comments
 (0)