payloads/libpayload: Support legacy LZ4 compression format

Linux kernel images from upstream tree are compressed using legacy LZ4
format and not the modern LZ4 format. Hence support legacy LZ4
compression format to decompress and boot upstream Linux kernel images.
Also add unit test case to verify the currently supported LZ4
compression format as well as legacy LZ4 compression format.

References:
* https://github.com/lz4/lz4

BUG=None
TEST=make tests/liblz4/lz4-test
[==========] tests_liblz4_lz4-test(tests): Running 4 test(s).
[ RUN      ] test_lz4
[       OK ] test_lz4
[ RUN      ] test_lz4_partial_decompression
[       OK ] test_lz4_partial_decompression
[ RUN      ] test_legacy_lz4
[       OK ] test_legacy_lz4
[ RUN      ] test_legacy_lz4_partial_decompression
[       OK ] test_legacy_lz4_partial_decompression
[==========] tests_liblz4_lz4-test(tests): 4 test(s) run.
[  PASSED  ] 4 test(s).

Change-Id: I7e3d407fc313e0937fd8d327840534de60d8c625
Signed-off-by: Karthikeyan Ramasubramanian <kramasub@google.com>
Reviewed-on: https://review.coreboot.org/c/coreboot/+/89854
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Julius Werner <jwerner@chromium.org>
This commit is contained in:
Karthikeyan Ramasubramanian 2025-10-31 14:06:10 -06:00 committed by Karthik Ramasubramanian
commit d62653749c
4 changed files with 230 additions and 6 deletions

View file

@ -47,4 +47,11 @@ size_t ulz4fn(const void *src, size_t srcn, void *dst, size_t dstn);
/* Same as ulz4fn() but does not perform any bounds checks. */
size_t ulz4f(const void *src, void *dst);
/* Decompresses an Legacy LZ4 image from src to dst, ensuring that it doesn't read
* more than srcn bytes and doesn't write more than dstn. Will reliably return an
* error if buffer was too small.
* Returns amount of decompressed bytes, or 0 on error.
*/
size_t ulz4ln(const void *src, size_t srcn, void *dst, size_t dstn);
#endif /* __LZ4_H_ */

View file

@ -74,6 +74,7 @@ typedef uint64_t U64;
#include "lz4.c.inc" /* #include for inlining, do not link! */
#define LZ4F_MAGICNUMBER 0x184D2204
#define LEGACY_MAGICNUMBER 0x184C2102
struct lz4_frame_header {
uint32_t magic;
@ -112,6 +113,14 @@ struct lz4_block_header {
/* + uint32_t block_checksum iff has_block_checksum is set */
} __packed;
/* A wrapper function used by ulz4fn and ulz4ln to inline LZ4_decompress_generic
which is critical for performance. */
static int lz4_wrap(const void *in, void *out, int in_size, int out_size)
{
return LZ4_decompress_generic(in, out, in_size, out_size, endOnInputSize,
full, 0, noDict, out, NULL, 0);
}
size_t ulz4fn(const void *src, size_t srcn, void *dst, size_t dstn)
{
const void *in = src;
@ -163,14 +172,10 @@ size_t ulz4fn(const void *src, size_t srcn, void *dst, size_t dstn)
else
out += size;
} else {
/* constant folding essential, do not touch params! */
int ret = LZ4_decompress_generic(in, out, b.size,
dst + dstn - out, endOnInputSize,
full, 0, noDict, out, NULL, 0);
int ret = lz4_wrap(in, out, b.size, dst + dstn - out);
if (ret < 0)
break; /* decompression error */
else
out += ret;
out += ret;
}
in += b.size;
@ -186,3 +191,40 @@ size_t ulz4f(const void *src, void *dst)
/* LZ4 uses signed size parameters, so can't just use ((u32)-1) here. */
return ulz4fn(src, 1*GiB, dst, 1*GiB);
}
size_t ulz4ln(const void *src, size_t srcn, void *dst, size_t dstn)
{
const void *in = src;
void *out = dst;
size_t out_size = 0;
if (le32toh(*((const uint32_t *)in)) != LEGACY_MAGICNUMBER)
return 0;
in += sizeof(uint32_t);
while (1) {
uint32_t block_size;
if ((size_t)(in - src) == srcn) {
out_size = out - dst; /* decompression is complete */
break;
}
if ((size_t)(in - src) + sizeof(block_size) > srcn)
break; /* input overrun */
block_size = le32toh(*(uint32_t *)in);
in += sizeof(block_size);
if ((size_t)(in - src) + block_size > srcn)
break; /* input overrun */
int ret = lz4_wrap(in, out, block_size, dst + dstn - out);
if (ret < 0)
break; /* decompression error */
out += ret;
in += block_size;
}
return out_size;
}

View file

@ -0,0 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
tests-y += lz4-test
lz4-test-srcs += tests/liblz4/lz4-test.c

View file

@ -0,0 +1,170 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#include <libpayload.h>
#include <tests/test.h>
#include "../liblz4/lz4_wrapper.c"
/* This macro basically does nothing but suppresses linter messages */
#define EMPTY_WRAP(...) __VA_ARGS__
#define TEST_RAW_DATA_SIZE sizeof((u8[]){TEST_RAW_DATA})
/* TEST_RAW_DATA="LLLLLLLLLLLLLLLLZZZZZZZZZZZZZZZZ4444444444444444"
echo ${TEST_RAW_DATA} | hexdump -e '1/1 "0x%02x" ", "' -v */
#define TEST_RAW_DATA EMPTY_WRAP( \
0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, \
0x4c, 0x4c, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, \
0x5a, 0x5a, 0x5a, 0x5a, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, \
0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x0a)
#define TEST_LZ4_DATA_SIZE sizeof((u8[]){TEST_LZ4_DATA})
/* echo ${TEST_RAW_DATA} | lz4 -12 --favor-decSpeed -c | \
hexdump -e '1/1 "0x%02x" ", "' -v */
#define TEST_LZ4_DATA EMPTY_WRAP( \
0x04, 0x22, 0x4d, 0x18, 0x64, 0x40, 0xa7, 0x15, 0x00, 0x00, 0x00, 0x2a, 0x4c, 0x4c, \
0x02, 0x00, 0x2a, 0x5a, 0x5a, 0x02, 0x00, 0x26, 0x34, 0x34, 0x02, 0x00, 0x50, 0x34, \
0x34, 0x34, 0x34, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xe1, 0xea, 0x87, 0xfa)
#define TEST_LEGACY_LZ4_DATA_SIZE sizeof((u8[]){TEST_LEGACY_LZ4_DATA})
/* echo ${TEST_RAW_DATA} | lz4 -l -12 --favor-decSpeed -c | \
hexdump -e '1/1 "0x%02x" ", "' -v */
#define TEST_LEGACY_LZ4_DATA EMPTY_WRAP( \
0x02, 0x21, 0x4c, 0x18, 0x12, 0x00, 0x00, 0x00, 0x1b, 0x4c, 0x01, 0x00, 0x1b, 0x5a, \
0x01, 0x00, 0x17, 0x34, 0x01, 0x00, 0x50, 0x34, 0x34, 0x34, 0x34, 0x0a)
#define TEST_LARGE_RAW_DATA_SIZE sizeof((u8[]){TEST_LARGE_RAW_DATA})
/*
* LINUX_IMG_PATH=<set to available uncompressed ARM Linux Image>;
* hexdump -e '1/1 "0x%02x" ", "' ${LINUX_IMG_PATH} -n 320 -v
* Arm Linux kernel image header is 64 bytes long. Size of scratch buffer in one of the
* payloads is 256 bytes + Arm Linux kernel image header. Hence 320 bytes.
*/
#define TEST_LARGE_RAW_DATA EMPTY_WRAP( \
0x4d, 0x5a, 0x40, 0xfa, 0x19, 0x00, 0x7f, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x41, 0x52, 0x4d, 0x64, 0x40, 0x00, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0xaa, \
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0xa0, 0x00, 0x06, 0x02, 0x0b, 0x02, 0x02, 0x14, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, \
0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x03, 0x02, 0x00, 0x00, 0x01, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x74, 0x65, 0x78, \
0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, \
0x3b, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x60, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x0a, 0x2b, 0x00, \
0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
#define TEST_LARGE_LZ4_DATA_SIZE sizeof((u8[]){TEST_LARGE_LZ4_DATA})
/* head -c 320 ${LINUX_IMG_PATH} | lz4 -12 --favor-decSpeed -c | \
hexdump -e '1/1 "0x%02x" ", "' -v */
#define TEST_LARGE_LZ4_DATA EMPTY_WRAP( \
0x04, 0x22, 0x4d, 0x18, 0x64, 0x40, 0xa7, 0x9a, 0x00, 0x00, 0x00, 0xa4, 0x4d, 0x5a, \
0x40, 0xfa, 0x19, 0x00, 0x7f, 0x14, 0x00, 0x00, 0x02, 0x00, 0x20, 0x72, 0x02, 0x0c, \
0x00, 0x3e, 0x0a, 0x00, 0x00, 0x02, 0x00, 0x07, 0x14, 0x00, 0xf9, 0x00, 0x41, 0x52, \
0x4d, 0x64, 0x40, 0x00, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x64, 0xaa, 0x02, 0x1c, \
0x00, 0xf1, 0x00, 0xa0, 0x00, 0x06, 0x02, 0x0b, 0x02, 0x02, 0x14, 0x00, 0x00, 0x3b, \
0x02, 0x00, 0x00, 0x36, 0x14, 0x00, 0x77, 0x04, 0x04, 0x03, 0x02, 0x00, 0x00, 0x01, \
0x26, 0x00, 0x33, 0x01, 0x00, 0x00, 0x37, 0x00, 0x19, 0x03, 0x3e, 0x00, 0x15, 0x72, \
0x28, 0x00, 0x19, 0x0a, 0x31, 0x00, 0x0e, 0x0a, 0x00, 0x04, 0x1c, 0x00, 0x3f, 0x06, \
0x00, 0x00, 0x02, 0x00, 0x1e, 0x51, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x0a, 0x00, 0x13, \
0x3b, 0x70, 0x00, 0x2c, 0x3b, 0x02, 0x6f, 0x00, 0x90, 0x20, 0x00, 0x00, 0x60, 0x2e, \
0x64, 0x61, 0x74, 0x61, 0x0e, 0x00, 0x01, 0xc8, 0x00, 0x52, 0x3c, 0x02, 0x00, 0x0a, \
0x2b, 0x08, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0xf6, 0xb4, 0xb7, 0x4f)
#define TEST_LARGE_LEGACY_LZ4_DATA_SIZE sizeof((u8[]){TEST_LARGE_LEGACY_LZ4_DATA})
/* head -c 320 ${LINUX_IMG_PATH} | lz4 -l -12 --favor-decSpeed -c | \
hexdump -e '1/1 "0x%02x" ", "' -v */
#define TEST_LARGE_LEGACY_LZ4_DATA EMPTY_WRAP( \
0x02, 0x21, 0x4c, 0x18, 0x93, 0x00, 0x00, 0x00, 0x95, 0x4d, 0x5a, 0x40, 0xfa, 0x19, \
0x00, 0x7f, 0x14, 0x00, 0x01, 0x00, 0x20, 0x72, 0x02, 0x06, 0x00, 0x2f, 0x0a, 0x00, \
0x01, 0x00, 0x0b, 0xf9, 0x00, 0x41, 0x52, 0x4d, 0x64, 0x40, 0x00, 0x00, 0x00, 0x50, \
0x45, 0x00, 0x00, 0x64, 0xaa, 0x02, 0x1c, 0x00, 0xf1, 0x00, 0xa0, 0x00, 0x06, 0x02, \
0x0b, 0x02, 0x02, 0x14, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x36, 0x14, 0x00, 0x76, \
0x04, 0x04, 0x03, 0x02, 0x00, 0x00, 0x01, 0x26, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x37, \
0x00, 0x19, 0x03, 0x3e, 0x00, 0x15, 0x72, 0x28, 0x00, 0x1a, 0x0a, 0x31, 0x00, 0x0f, \
0x01, 0x00, 0x06, 0x2f, 0x06, 0x00, 0x01, 0x00, 0x1f, 0x51, 0x2e, 0x74, 0x65, 0x78, \
0x74, 0x0a, 0x00, 0x13, 0x3b, 0x70, 0x00, 0x2c, 0x3b, 0x02, 0x6f, 0x00, 0x91, 0x20, \
0x00, 0x00, 0x60, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x0e, 0x00, 0x00, 0xc8, 0x00, 0x52, \
0x3c, 0x02, 0x00, 0x0a, 0x2b, 0x08, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00)
static void test_lz4(void **state)
{
const u8 test_src_data[TEST_LZ4_DATA_SIZE] = { TEST_LZ4_DATA };
u8 test_dst_data[TEST_RAW_DATA_SIZE] = {0};
u8 test_uncompressed_data[TEST_RAW_DATA_SIZE] = { TEST_RAW_DATA };
size_t dst_size;
dst_size = ulz4fn(test_src_data, sizeof(test_src_data),
test_dst_data, sizeof(test_dst_data));
assert_int_equal(TEST_RAW_DATA_SIZE, dst_size);
assert_memory_equal(test_dst_data, test_uncompressed_data, TEST_RAW_DATA_SIZE);
}
static void test_lz4_partial_decompression(void **state)
{
const u8 test_src_data[TEST_LARGE_LZ4_DATA_SIZE] = { TEST_LARGE_LZ4_DATA };
u8 test_dst_data[TEST_LARGE_RAW_DATA_SIZE / 2] = {0};
u8 test_uncompressed_data[TEST_LARGE_RAW_DATA_SIZE] = { TEST_LARGE_RAW_DATA };
size_t dst_size;
ulz4fn(test_src_data, sizeof(test_src_data),
test_dst_data, sizeof(test_dst_data));
for (dst_size = 0; dst_size < sizeof(test_dst_data); dst_size++) {
if (test_dst_data[dst_size] != test_uncompressed_data[dst_size])
break;
}
assert_in_range(dst_size, TEST_LARGE_RAW_DATA_SIZE / 4, TEST_LARGE_RAW_DATA_SIZE);
}
static void test_legacy_lz4(void **state)
{
const u8 test_src_data[TEST_LEGACY_LZ4_DATA_SIZE] = { TEST_LEGACY_LZ4_DATA };
u8 test_dst_data[TEST_RAW_DATA_SIZE] = {0};
u8 test_uncompressed_data[TEST_RAW_DATA_SIZE] = { TEST_RAW_DATA };
size_t dst_size;
dst_size = ulz4ln(test_src_data, sizeof(test_src_data),
test_dst_data, sizeof(test_dst_data));
assert_int_equal(TEST_RAW_DATA_SIZE, dst_size);
assert_memory_equal(test_dst_data, test_uncompressed_data, TEST_RAW_DATA_SIZE);
}
static void test_legacy_lz4_partial_decompression(void **state)
{
const u8 test_src_data[TEST_LARGE_LEGACY_LZ4_DATA_SIZE] = { TEST_LARGE_LEGACY_LZ4_DATA };
u8 test_dst_data[TEST_LARGE_RAW_DATA_SIZE / 2] = {0};
u8 test_uncompressed_data[TEST_LARGE_RAW_DATA_SIZE] = { TEST_LARGE_RAW_DATA };
size_t dst_size;
ulz4ln(test_src_data, sizeof(test_src_data),
test_dst_data, sizeof(test_dst_data));
for (dst_size = 0; dst_size < sizeof(test_dst_data); dst_size++) {
if (test_dst_data[dst_size] != test_uncompressed_data[dst_size])
break;
}
assert_in_range(dst_size, TEST_LARGE_RAW_DATA_SIZE / 4, TEST_LARGE_RAW_DATA_SIZE);
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_lz4),
cmocka_unit_test(test_lz4_partial_decompression),
cmocka_unit_test(test_legacy_lz4),
cmocka_unit_test(test_legacy_lz4_partial_decompression),
};
return lp_run_group_tests(tests, NULL, NULL);
}