From 7546ae79e9525384fc1aa4ee51690ccd1c4a8b79 Mon Sep 17 00:00:00 2001 From: Artem Blagodarenko Date: Wed, 20 Dec 2023 22:53:26 +0000 Subject: [PATCH] EX-8688 csdc: Add header checksum verification and calculation This commit adds functionality to verify and calculate the checksum of the header in the `ll_compr_hdr` struct and some another sanity check. Signed-off-by: Artem Blagodarenko Change-Id: I24a9ab9cb7bea1208ada23aa6550127fe6a55017 Reviewed-on: https://review.whamcloud.com/c/ex/lustre-release/+/53521 Tested-by: jenkins Tested-by: Maloo Reviewed-by: Vitaliy Kuznetsov Reviewed-by: Andreas Dilger --- lustre/include/lustre_compr.h | 2 +- lustre/include/obd_support.h | 1 + lustre/obdclass/lustre_compr.c | 81 +++++++++++++++++++++++++++++++--- lustre/ofd/ofd_compress.c | 49 ++++++++++++--------- lustre/ofd/ofd_compress.h | 7 +-- lustre/ofd/ofd_io.c | 6 ++- lustre/osc/osc_compress.c | 53 ++++++++++++++++------ lustre/tests/Makefile.am | 1 + lustre/tests/compressed.bin | Bin 0 -> 20202 bytes lustre/tests/sanity-compr.sh | 97 ++++++++++++++++++++++++++++++++++++++++- 10 files changed, 248 insertions(+), 49 deletions(-) create mode 100644 lustre/tests/compressed.bin diff --git a/lustre/include/lustre_compr.h b/lustre/include/lustre_compr.h index c877a72..c5b6452 100644 --- a/lustre/include/lustre_compr.h +++ b/lustre/include/lustre_compr.h @@ -47,7 +47,7 @@ static inline struct page *mem_to_page(void *addr) return vmalloc_to_page(addr); } -int is_chunk_start(struct page *page, struct ll_compr_hdr **ret_header); +int is_chunk_start(struct page *page, struct ll_compr_hdr **ret_header, int *rc); int decompress_chunk(const char *obd_name, struct crypto_comp *cc, unsigned char *in, unsigned int in_len, diff --git a/lustre/include/obd_support.h b/lustre/include/obd_support.h index 6f777b3..86ee3b6 100644 --- a/lustre/include/obd_support.h +++ b/lustre/include/obd_support.h @@ -441,6 +441,7 @@ extern char obd_jobid_var[]; #define OBD_FAIL_OSC_SLOW_PAGE_EVICT 0x417 #define OBD_FAIL_OSC_WRONG_COMP_ALG 0x418 #define OBD_FAIL_OSC_MARK_COMPRESSED 0x419 +#define OBD_FAIL_OSC_FORCE_DECOMPR 0x420 #define OBD_FAIL_PTLRPC 0x500 #define OBD_FAIL_PTLRPC_ACK 0x501 diff --git a/lustre/obdclass/lustre_compr.c b/lustre/obdclass/lustre_compr.c index 616d411..dca6e09 100644 --- a/lustre/obdclass/lustre_compr.c +++ b/lustre/obdclass/lustre_compr.c @@ -108,6 +108,7 @@ #include #include #include +#include /* "one-shot" try to load our own compression modules * the first time that a compressed file is accessed @@ -282,6 +283,28 @@ EXPORT_SYMBOL(alloc_decompr); * The minimum delta between compressed and plain data to * use the compressed one. */ + +static __u32 compr_header_csum(struct ll_compr_hdr *header) +{ + int offset = offsetof(struct ll_compr_hdr, llch_hdr_csum); + __u32 csum32; + /* let's skip llch_hdr_csum value and crc 0 instead */ + __u32 dummy_csum = 0; + + csum32 = crc32(~0, (unsigned char *)header, + offset); + csum32 = crc32(csum32, (unsigned char *)&dummy_csum, + sizeof(dummy_csum)); + offset += sizeof(dummy_csum); + + if (offset < header->llch_header_size) { + csum32 = crc32(csum32, (unsigned char *)header + offset, + header->llch_header_size - offset); + } + + return csum32; +} + int compress_chunk(const char *obd_name, struct crypto_comp *cc, const unsigned char *in, unsigned int in_len, unsigned char *out, unsigned int *out_len, @@ -337,7 +360,7 @@ int compress_chunk(const char *obd_name, struct crypto_comp *cc, llch->llch_uncompr_size = in_len; llch->llch_reserved = 0; llch->llch_compr_csum = 0; - llch->llch_hdr_csum = 0; + llch->llch_hdr_csum = compr_header_csum(llch); *out_len = len + sizeof(*llch); @@ -345,23 +368,67 @@ int compress_chunk(const char *obd_name, struct crypto_comp *cc, } EXPORT_SYMBOL(compress_chunk); -int is_chunk_start(struct page *page, struct ll_compr_hdr **ret_header) +static int chunk_header_csum_valid(struct ll_compr_hdr *header) +{ + __u32 csum32; + + csum32 = compr_header_csum(header); + if (csum32 != header->llch_hdr_csum) { + CDEBUG(D_SEC, "Header csum mismatch %x != %x\n", + cpu_to_le32(csum32), header->llch_hdr_csum); + return 0; + } + + return 1; +} + +int is_chunk_start(struct page *page, struct ll_compr_hdr **ret_header, int *rc) { struct ll_compr_hdr *header; - int rc = 1; + int retval = 1; ENTRY; + *rc = 0; if (page == NULL) RETURN(0); - header = (struct ll_compr_hdr *)kmap_atomic(page); - if (header->llch_magic != LLCH_MAGIC) - rc = 0; + + if (header->llch_magic != LLCH_MAGIC) { + retval = 0; + /* If the magic is found but most fields are invalid, it + * implies that the data is likely uncompressed and should + * be passed through unmodified. + */ + } else if (header->llch_hdr_csum != 0 && + !(chunk_header_csum_valid(header))) { + CDEBUG(D_SEC, "Magic found but header csum invalid\n"); + retval = 0; + /* If the magic is valid but a few fields are invalid, it + * suggests that the valid compressed chunk is corrupted and + * -EUCLEAN should be returned when reading it. + */ + } else if (header->llch_compr_size >= + COMPR_GET_CHUNK_SIZE(header->llch_chunk_log_bits) || + /* Asume here that ll_compr_hdr will grow less then + * 4 times from original size + */ + header->llch_header_size > sizeof(header) * 4 || + header->llch_compr_type > LL_COMPR_TYPE_MAX) { + + if (header->llch_hdr_csum == 0) { + CDEBUG(D_SEC, "No csum, sanity failed, skipping\n"); + retval = 0; + } else { + retval = 0; + *rc = -EUCLEAN; + } + } + *ret_header = header; kunmap_atomic(header); - RETURN(rc); + RETURN(retval); } EXPORT_SYMBOL(is_chunk_start); diff --git a/lustre/ofd/ofd_compress.c b/lustre/ofd/ofd_compress.c index 32d16ed..d5200a9 100644 --- a/lustre/ofd/ofd_compress.c +++ b/lustre/ofd/ofd_compress.c @@ -33,7 +33,7 @@ #include #include -static int decompress_chunk_in_lnb(const char *obd_name, +static int decompress_chunk_in_lnb(const char *obd_name, struct lu_fid *fid, struct niobuf_local *lnbs, int lnb_start, void **bounce_src, void **bounce_dst, enum ll_compr_type type, int lvl, @@ -56,8 +56,12 @@ static int decompress_chunk_in_lnb(const char *obd_name, lnbs[lnb_start].lnb_page_offset, lnbs[lnb_start].lnb_len); /* if this chunk isn't compressed, don't uncompress it */ - if (!is_chunk_start(lnbs[lnb_start].lnb_page, &llch)) - GOTO(out, rc = 0); + if (!is_chunk_start(lnbs[lnb_start].lnb_page, &llch, &rc)) { + CERROR("%s: Header sanity failed: "DFID" at %llu: rc = %d\n", + obd_name, PFID(fid), lnbs[lnb_start].lnb_file_offset, + rc); + GOTO(out, rc); + } /* compression type and level in the compressed data can * be different from those set in the layout, because the client @@ -70,8 +74,9 @@ static int decompress_chunk_in_lnb(const char *obd_name, llch->llch_compr_level); if (chunk_size != COMPR_GET_CHUNK_SIZE(llch->llch_chunk_log_bits)) { - CERROR("%s: chunk size disagreement, layout %d, from disk %d\n", - obd_name, chunk_size, + CERROR("%s: chunk size disagreement: "DFID" at %llu: layout %d, from disk %d\n", + obd_name, PFID(fid), lnbs[lnb_start].lnb_file_offset, + chunk_size, COMPR_GET_CHUNK_SIZE(llch->llch_chunk_log_bits)); /* compression type and level can disagree with layout, we just * dump them for debugging @@ -85,8 +90,9 @@ static int decompress_chunk_in_lnb(const char *obd_name, hdr_size = llch->llch_header_size; rc = alloc_decompr(obd_name, &type, &lvl, &cc); if (rc) { - CERROR("%s: Setup for decompression failed, type %i, lvl %d, rc = %d\n", - obd_name, type, lvl, rc); + CERROR("%s: Setup for decompression failed: "DFID" at %llu: type %i, lvl %d: rc = %d\n", + obd_name, PFID(fid), lnbs[lnb_start].lnb_file_offset, + type, lvl, rc); GOTO(out, rc); } @@ -94,8 +100,8 @@ static int decompress_chunk_in_lnb(const char *obd_name, rc = merge_chunk(NULL, lnbs, lnb_start, pages_in_chunk, (char *) bounce_src, &src_size); if (rc < 0) { - CERROR("%s: Data error in lnbs at %llu, rc: %d\n", - obd_name, lnbs[lnb_start].lnb_file_offset, rc); + CERROR("%s: Data error in lnbs: "DFID" at %llu: rc = %d\n", + obd_name, PFID(fid), lnbs[lnb_start].lnb_file_offset, rc); GOTO(out, rc); } LASSERT(src_size <= chunk_size); @@ -112,8 +118,8 @@ static int decompress_chunk_in_lnb(const char *obd_name, (char *) bounce_dst, &dst_size, type, lvl); if (rc != 0) { - CERROR("%s: Failed to decompress %d byte chunk at %llu, rc: %d\n", - obd_name, llch->llch_compr_size, + CERROR("%s: Failed to decompress: "DFID", %d byte chunk at %llu: rc = %d\n", + obd_name, PFID(fid), llch->llch_compr_size, lnbs[lnb_start].lnb_file_offset, rc); GOTO(out, rc); } @@ -121,9 +127,9 @@ static int decompress_chunk_in_lnb(const char *obd_name, * reliably zero unused parts of the header, so we skip if it's zero */ if (llch->llch_uncompr_size != 0 && dst_size != llch->llch_uncompr_size) { - CERROR("%s: Compressed chunk at %llu invalid, uncompressed size from disk %d disagrees with result of decompression %d, rc: %d\n", - obd_name, lnbs[lnb_start].lnb_file_offset, - llch->llch_uncompr_size, dst_size, rc); + CERROR("%s: object invalid, size %d != %d: "DFID" at %llu: rc = %d\n", + obd_name, llch->llch_uncompr_size, dst_size, PFID(fid), + lnbs[lnb_start].lnb_file_offset, rc); GOTO(out, rc = -EUCLEAN); } LASSERT(dst_size <= chunk_size); @@ -143,8 +149,8 @@ out: for(i = lnb_start; i < lnb_start + pages_in_chunk; i++) { /* if there's no data in this page, we must clear it */ if (lnbs[i].lnb_rc == 0) { - CDEBUG(D_SEC, "no data in page %d at %llu, clearing\n", - i, lnbs[i].lnb_file_offset); + CDEBUG(D_SEC, "%s: no data, clearing: page %d, "DFID" at %llu\n", + obd_name, i, PFID(fid), lnbs[i].lnb_file_offset); memset(kmap(lnbs[i].lnb_page), 0, PAGE_SIZE); kunmap(lnbs[i].lnb_page); } @@ -159,9 +165,10 @@ out: * the beginning and end of the read may be unaligned, so we check and if * necessary decompress (in place) the data in those locations */ -int decompress_rnb(const char *obd_name, struct niobuf_local *lnbs, - int lnb_npages, __u64 rnb_start, __u64 rnb_end, - int *lnb_start, void **bounce_src, void **bounce_dst, +int decompress_rnb(const char *obd_name, struct lu_fid *fid, + struct niobuf_local *lnbs, int lnb_npages, + __u64 rnb_start, __u64 rnb_end, int *lnb_start, + void **bounce_src, void **bounce_dst, enum ll_compr_type type, int lvl, int chunk_size, bool write) { @@ -208,7 +215,7 @@ int decompress_rnb(const char *obd_name, struct niobuf_local *lnbs, if (!chunk_found) GOTO(out, rc = -EINVAL); - rc = decompress_chunk_in_lnb(obd_name, lnbs, i, bounce_src, + rc = decompress_chunk_in_lnb(obd_name, fid, lnbs, i, bounce_src, bounce_dst, type, lvl, chunk_size); if (rc) GOTO(out, rc); @@ -247,7 +254,7 @@ int decompress_rnb(const char *obd_name, struct niobuf_local *lnbs, if (!chunk_found) GOTO(out, rc = -EINVAL); - rc = decompress_chunk_in_lnb(obd_name, lnbs, i, bounce_src, + rc = decompress_chunk_in_lnb(obd_name, fid, lnbs, i, bounce_src, bounce_dst, type, lvl, chunk_size); if (rc) GOTO(out, rc); diff --git a/lustre/ofd/ofd_compress.h b/lustre/ofd/ofd_compress.h index 78f3ea1..03aee62 100644 --- a/lustre/ofd/ofd_compress.h +++ b/lustre/ofd/ofd_compress.h @@ -32,9 +32,10 @@ #include #include "ofd_internal.h" -int decompress_rnb(const char *obd_name, struct niobuf_local *lnbs, - int lnb_npages, __u64 rnb_start, __u64 rnb_end, - int *lnb_offset, void **bounce_src, void **bounce_dst, +int decompress_rnb(const char *obd_name, struct lu_fid *fid, + struct niobuf_local *lnbs, int lnb_npages, + __u64 rnb_start, __u64 rnb_end, int *lnb_offset, + void **bounce_src, void **bounce_dst, enum ll_compr_type type, int lvl, int chunk_size, bool write); diff --git a/lustre/ofd/ofd_io.c b/lustre/ofd/ofd_io.c index 0140300..0e426a4 100644 --- a/lustre/ofd/ofd_io.c +++ b/lustre/ofd/ofd_io.c @@ -1332,12 +1332,14 @@ int ofd_decompress_read(const struct lu_env *env, struct obd_export *exp, CDEBUG(D_SEC, "decompressing: rnb %d rnb_start %llu, rnb_end %llu\n", i, rnb_start, rnb_end); - rc = decompress_rnb(exp->exp_obd->obd_name, lnb, npages, + rc = decompress_rnb(exp->exp_obd->obd_name, fid, lnb, npages, rnb_start, rnb_end, &lnb_start, bounce_src, bounce_dst, type, lvl, chunk_size, write); - if (rc) + + if (rc) { GOTO(out, rc); + } } prev_rnb_end = rnb_end; } diff --git a/lustre/osc/osc_compress.c b/lustre/osc/osc_compress.c index b581d51..ece3899 100644 --- a/lustre/osc/osc_compress.c +++ b/lustre/osc/osc_compress.c @@ -342,10 +342,18 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) clpage = oap2cl_page(brw_page2oap(pga[0])); type = clpage->cp_compr_type; + chunk_bits = cl_page_compr_bits(clpage); + + if (OBD_FAIL_CHECK(OBD_FAIL_OSC_FORCE_DECOMPR)) { + /* decompress a sample from the lustre/tests/compressed.bin */ + lvl = 9; + type = LL_COMPR_TYPE_LZ4HC; + chunk_bits = COMPR_CHUNK_MIN_BITS; + } + /* no compression */ if (type == LL_COMPR_TYPE_NONE) RETURN(0); - chunk_bits = cl_page_compr_bits(clpage); chunk_size = 1 << chunk_bits; buf_bits = chunk_bits + 1; pages_per_chunk = chunk_size / PAGE_SIZE; @@ -367,8 +375,16 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) LASSERT(ergo(next_chunk_min, i >= next_chunk_min)); - if (!is_chunk_start(pga[i]->pg, &llch)) + if (!is_chunk_start(pga[i]->pg, &llch, &rc)) { + if (rc) { + CERROR("%s: Magic and csum OK, but sanity failed: "DFID": rc = %d\n", + obd_name, PFID(&aa->aa_oa->o_oi.oi_fid), + rc); + GOTO(out, rc); + } continue; + } + /* all chunks should be compressed with the same algorithm, but * we need to get the type and level from the data provided. * In the future it may be that the type is also changing in @@ -396,10 +412,13 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) * from storage and could potentially be corrupted) */ if (rpc_chunk_bits != chunk_bits) { + rc = -EUCLEAN; CERROR( - "chunk bits from storage (%d) and layout (%d) disagree\n", - rpc_chunk_bits, chunk_bits); - GOTO(out, rc = -EUCLEAN); + "%s: Chunk bits disagree %d != %d disagree: "DFID": rc = %d\n", + obd_name, + rpc_chunk_bits, chunk_bits, + PFID(&aa->aa_oa->o_oi.oi_fid), rc); + GOTO(out, rc); } CDEBUG(D_SEC, "chunk_size: %i, pages_per_chunk: %i\n", @@ -421,9 +440,11 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) * data, if not, the data from disk is probably corrupt */ if (compressed_pages > page_count - i) { - CERROR("compressed pages from disk %d don't match pages in rpc %d (at %d of %d pages)\n", - compressed_pages, page_count - i, i, page_count); - GOTO(out, rc = -EUCLEAN); + rc = -EUCLEAN; + CERROR("%s: compressed pages mismatch %d != %d (at %d of %d pages): object "DFID": rc = %d\n", + obd_name, compressed_pages, page_count - i, i, + page_count, PFID(&aa->aa_oa->o_oi.oi_fid), rc); + GOTO(out, rc); } CDEBUG(D_SEC, "Merge chunk [%i, %i], src: %px\n", i, @@ -436,9 +457,11 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) * CSDC currently (but could happen if there's bad data) */ if (src_size != compressed_pages * PAGE_SIZE) { - CERROR("buffer size from compressed pages (%u bytes) doesn't match number of compressed pages %d\n", - src_size, compressed_pages); - GOTO(out, rc = -EUCLEAN); + rc = -EUCLEAN; + CERROR("%s: buffer size from compressed pages (%u bytes) doesn't match number of compressed pages %d: object "DFID": rc = %d\n", + obd_name, src_size, compressed_pages, + PFID(&aa->aa_oa->o_oi.oi_fid), rc); + GOTO(out, rc); } dst_size = 2 * chunk_size; CDEBUG(D_SEC, "Compressed size %lu, type %i\n", @@ -460,9 +483,11 @@ int decompress_request(struct osc_brw_async_args *aa, int page_count) * data, if not, the data from disk is probably corrupt */ if (decompressed_pages > page_count - i) { - CERROR("decompressed pages from disk %d don't match pages in rpc %d (at %d of %d pages)\n", - decompressed_pages, page_count - i, i, page_count); - GOTO(out, rc = -EUCLEAN); + rc = -EUCLEAN; + CERROR("%s: decompressed pages from disk %d don't match pages in rpc %d (at %d of %d pages): object "DFID": rc = %d\n", + obd_name, decompressed_pages, page_count - i, i, + page_count, PFID(&aa->aa_oa->o_oi.oi_fid), rc); + GOTO(out, rc); } unmerge_chunk(pga, NULL, i, decompressed_pages, dst, dst_size, 0); diff --git a/lustre/tests/Makefile.am b/lustre/tests/Makefile.am index e385ef6..3c68b1f 100644 --- a/lustre/tests/Makefile.am +++ b/lustre/tests/Makefile.am @@ -12,6 +12,7 @@ noinst_DATA += ldiskfs_ost1_2_11.tar.bz2 ldiskfs_ost2_2_11.tar.bz2 noinst_DATA += zfs_mdt1_2_11.tar.bz2 zfs_mdt2_2_11.tar.bz2 noinst_DATA += zfs_ost1_2_11.tar.bz2 zfs_ost2_2_11.tar.bz2 noinst_DATA += AMSR_E_L3_DailyOcean_V05_20111003.hdf.bz2 +noinst_DATA += compressed.bin noinst_SCRIPTS = leak_finder.pl llmount.sh llmountcleanup.sh functions.sh noinst_SCRIPTS += test-framework.sh runvmstat runiozone runtests sanity.sh noinst_SCRIPTS += rundbench acceptance-small.sh compile.sh conf-sanity.sh diff --git a/lustre/tests/compressed.bin b/lustre/tests/compressed.bin new file mode 100644 index 0000000000000000000000000000000000000000..b303eff8362f9f63aa473a20e298fd8484d6995b GIT binary patch literal 20202 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≶uTq8#ir)TnKT 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�K~H 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 literal 0 HcmV?d00001 diff --git a/lustre/tests/sanity-compr.sh b/lustre/tests/sanity-compr.sh index 06fa48d..37a9e49 100644 --- a/lustre/tests/sanity-compr.sh +++ b/lustre/tests/sanity-compr.sh @@ -490,7 +490,6 @@ test_1002() { for bs in 3 4 7 32 97 128 130 192; do dd if=$tf bs=${bs}K of=$tf.3 || error "(28) dd with block size ${bs}K failed" - cmp -bl $tf.3 $tf.2 || error "(29) cmp failed" flush_and_compare $tf.3 $tf.2 "(30)" rm -f $tf.3 @@ -1183,6 +1182,102 @@ test_1008() { } run_test 1008 "validate directory space usage reduction with compression" +test_1008() { + (( MDS1_VERSION >= $(version_code 2.14.0-ddn121) )) || + skip "Need MDS version at least 2.14.0-ddn121" + + test_mkdir -p $DIR/$tdir + local tf=$DIR/$tdir/$tfile + local tf2=$TMP/$tfile.2 + local compressed=$LUSTRE/tests/compressed.bin + local broken_header=$TMP/$tfile.4 + + enable_compression + # Use pre-created compressed file + hexdump -C $compressed | head + cat $compressed > $tf + # The next data is needed for hole after first compressed block + dd if=/dev/urandom of=$tf bs=64k seek=1 count=1 + sync + cancel_lru_locks osc + echo 3 > /proc/sys/vm/drop_caches + + # In case the file has no compressed component, it can't be set + # after the data was written. Without such a component file is not + # decompressed at reading because there is + # clpage->cp_comp_type == LL_COMPR_TYPE_NONE check + # As a workaround, this check is skipped during the testing, + # if OBD_FAIL_OSC_FORCE_DECOMPR is set + # define OBD_FAIL_OSC_FORCE_DECOMPR 0x420 + $LCTL set_param fail_loc=0x420 + # Written data already has compression header + dd if=$tf of=$tf2 bs=64k count=1 || error "reading data" + hexdump -C $tf2 | head + + # Here is the compression header format to explain offsets + # struct ll_compr_hdr { + # __u64 llch_magic:48; /* LLCH_MAGIC */ + # __u8 llch_header_size; /* for future extensions */ + # __u8 llch_extra_flags; + # __u16 llch_flags; + # __u8 llch_compr_type; /* LLCH_COMP_GZIP, LLCH_COMP_LZ4, */ + # __u8 llch_compr_level:4, /* per-algorithm mapped level */ + # llch_chunk_log_bits:4; + # __u32 llch_compr_size; /* bytes of compressed data */ + # __u32 llch_reserved; /* unused, initialize to 0 */ + # __u32 llch_uncompr_csum; /* crc32 of raw data, or 0 */ + # __u32 llch_compr_csum; /* crc32 of compressed data, or 0 */ + # __u32 llch_hdr_csum; /* crc32 of magic..compr_csum, or 0 */ + #}; + + # set wrong llch_compr_size + # checksum should fail and chunk copied as is + cp $compressed $broken_header + printf '\xff\xff\xff\xff' | dd of=$broken_header bs=1 seek=12 conv=notrunc + cat $broken_header > $tf + # The next data is needed for hole after first compressed block + dd if=/dev/urandom of=$tf bs=64k seek=1 count=1 + sync + cancel_lru_locks osc + echo 3 > /proc/sys/vm/drop_caches + + # If the magic is found but most fields are invalid, it implies that the data + # is likely uncompressed and should be passed through unmodified. + dd if=$tf of=$tf2 bs=64k count=1 || error "reading data failed(0)" + hexdump -C $tf2 | head + + # fix checksum, so sanity check fail at wrong header size + printf '\x88\xb3\xf6\x81' | dd of=$broken_header bs=1 seek=28 conv=notrunc + cat $broken_header > $tf + # The next data is needed for hole after first compressed block + dd if=/dev/urandom of=$tf bs=64k seek=1 count=1 + sync + cancel_lru_locks osc + echo 3 > /proc/sys/vm/drop_caches + + # If the magic is valid but some fields are invalid, the reading + # must fail, because data is corrupted. + dd if=$tf of=$tf2 bs=64k count=1 && error "reading should fail" + hexdump -C $tf2 | head + + # Set checksum to 0, so sanity check fails at wrong header size, + # but chunk is returned as is without error + printf '\x0\x0\x0\x0' | dd of=$broken_header bs=1 seek=28 conv=notrunc + cat $broken_header > $tf + # The next data is needed for hole after first compressed block + dd if=/dev/urandom of=$tf bs=64k seek=1 count=1 + sync + cancel_lru_locks osc + echo 3 > /proc/sys/vm/drop_caches + + # If the magic is 0 and some fields are invalid, it still can be + # not corrupted chunk but coincidence. + dd if=$tf of=$tf2 bs=64k count=1 || error "reading data failed(0)" + hexdump -C $tf2 | head + $LCTL set_param fail_loc=0x0 +} +run_test 1008 "Compression header error tolerance" + complete_test $SECONDS check_and_cleanup_lustre declare -a logs=($ONLY) -- 1.8.3.1