[PATCH v1 1/6] tools/migration: introduce PAGE_DATA_LZ4 stream record type

Marcus Granado <[email protected]>
Newsgroups org.xenproject.lists.xen-devel
Message-ID <[email protected]>
Allocate a new migration stream record type REC_TYPE_PAGE_DATA_LZ4
(0x00000013) to transmit PAGE_DATA when payload is LZ4-compressed. The
record use the same xc_sr_rec_page_data_header as REC_TYPE_PAGE_DATA
but the page data section type changes from page_data to data_lz4.

Signed-off-by: Marcus Granado <[email protected]>
---
 docs/specs/libxc-migration-stream.pandoc | 71 ++++++++++++++++-
 tools/libs/guest/xg_sr_common.c          |  4 +
 tools/libs/guest/xg_sr_stream_format.h   | 13 ++++
 tools/python/xen/migration/libxc.py      | 90 ++++++++++++++++++++-
 tools/python/xen/migration/tests.py      | 99 +++++++++++++++++++++++-
 5 files changed, 271 insertions(+), 6 deletions(-)

diff --git a/docs/specs/libxc-migration-stream.pandoc b/docs/specs/libxc-migration-stream.pandoc
index 1319ce1f1e..7469c95139 100644
--- a/docs/specs/libxc-migration-stream.pandoc
+++ b/docs/specs/libxc-migration-stream.pandoc
@@ -3,7 +3,8 @@
   Andrew Cooper <<[email protected]>>
   Wen Congyang <<[email protected]>>
   Yang Hongyang <<[email protected]>>
-% Revision 3
+  Marcus Granado <<[email protected]>>
+% Revision 4
 
 Introduction
 ============
@@ -39,8 +40,6 @@ Not Yet Included
 The following features are not yet fully specified and will be
 included in a future draft.
 
-* Page data compression.
-
 * ARM
 
 
@@ -233,7 +232,9 @@ type         0x00000000: END
 
              0x00000012: X86_MSR_POLICY
 
-             0x00000013 - 0x7FFFFFFF: Reserved for future _mandatory_
+             0x00000013: PAGE_DATA_LZ4
+
+             0x00000014 - 0x7FFFFFFF: Reserved for future _mandatory_
              records.
 
              0x80000000 - 0xFFFFFFFF: Reserved for future _optional_
@@ -359,6 +360,68 @@ tail.
 
 \clearpage
 
+PAGE_DATA_LZ4
+-------------
+
+A PAGE_DATA_LZ4 record carries exactly the same information as a
+PAGE_DATA record, but with the page contents LZ4-compressed.  The saver
+may emit it in place of a PAGE_DATA record when LZ4 compression has been
+requested.
+
+     0     1     2     3     4     5     6     7 octet
+    +-----------------------+-------------------------+
+    | count (C)             | (reserved)              |
+    +-----------------------+-------------------------+
+    | pfn[0]                                          |
+    +-------------------------------------------------+
+    ...
+    +-------------------------------------------------+
+    | pfn[C-1]                                        |
+    +-----------+-------------------------------------+
+    | clen[0]   | page_data_lz4[0]...                 |
+    +-----------+-------------------------------------+
+    ...
+    +-----------+-------------------------------------+
+    | clen[N-1] | page_data_lz4[N-1]...               |
+    ...
+    +-------------------------------------------------+
+
+--------------------------------------------------------------------
+Field       Description
+----------- --------------------------------------------------------
+count       Number of pages described in this record.
+
+pfn         An array of count PFNs and their types, with the same
+            layout and page types as in a PAGE_DATA record.
+
+data_lz4    The compressed page contents, as one sub-block per page
+            set as present in the pfn array, in pfn-array order (i.e.
+            N sub-blocks, with N as in a PAGE_DATA record: N <= C).
+            Each sub-block is a `uint16` little-endian length `clen`
+            followed by either an LZ4 block or a raw page.  When
+            `clen > 0`, page_data_lz4 is `clen` octets of a raw LZ4
+            block (per the LZ4 block format) whose decompressed output
+            is one page_size page.  When `clen == 0`, page_data_lz4 is
+            page_size octets of a raw, uncompressed page.
+            A compressed `clen` is at most page_size - 1, occupying
+            only the low 12 bits; the top 4 bits are reserved for
+            future use and must be 0.
+--------------------------------------------------------------------
+
+The `count` (C) and `pfn` fields are identical in meaning and
+constraints to those of a PAGE_DATA record, and N (the number of
+present pages, N <= C) is as defined there.  Unlike PAGE_DATA, a
+PAGE_DATA_LZ4 record always has N >= 1 (at least one sub-block):
+the saver emits the LZ4 variant only when there is page data to
+compress, and a restoring side rejects a PAGE_DATA_LZ4 record with
+N == 0.  When a batch has no present pages (all pfns of invalid
+types) the saver emits a plain PAGE_DATA record instead.
+
+PAGE_DATA_LZ4 is a _mandatory_ record: a restoring side that does not
+support it must fail the migration.
+
+\clearpage
+
 X86_PV_INFO
 -----------
 
diff --git a/tools/libs/guest/xg_sr_common.c b/tools/libs/guest/xg_sr_common.c
index 9b2782b5cf..8b0a59fa4c 100644
--- a/tools/libs/guest/xg_sr_common.c
+++ b/tools/libs/guest/xg_sr_common.c
@@ -39,6 +39,7 @@ static const char *const mandatory_rec_types[] =
     [REC_TYPE_STATIC_DATA_END]              = "Static data end",
     [REC_TYPE_X86_CPUID_POLICY]             = "x86 CPUID policy",
     [REC_TYPE_X86_MSR_POLICY]               = "x86 MSR policy",
+    [REC_TYPE_PAGE_DATA_LZ4]                = "Page data (lz4)",
 };
 
 const char *rec_type_to_str(uint32_t type)
@@ -154,6 +155,9 @@ static void __attribute__((unused)) build_assertions(void)
     BUILD_BUG_ON(sizeof(struct xc_sr_rec_x86_tsc_info)      != 24);
     BUILD_BUG_ON(sizeof(struct xc_sr_rec_hvm_params_entry)  != 16);
     BUILD_BUG_ON(sizeof(struct xc_sr_rec_hvm_params)        != 8);
+
+    /* clen's low bits hold the compressed length; reserved bits stay clear */
+    BUILD_BUG_ON((PAGE_SIZE - 1) & PAGE_DATA_LZ4_CLEN_RESERVED_MASK);
 }
 
 /*
diff --git a/tools/libs/guest/xg_sr_stream_format.h b/tools/libs/guest/xg_sr_stream_format.h
index 99dca5d490..e12833b2cd 100644
--- a/tools/libs/guest/xg_sr_stream_format.h
+++ b/tools/libs/guest/xg_sr_stream_format.h
@@ -76,6 +76,7 @@ struct xc_sr_rhdr
 #define REC_TYPE_STATIC_DATA_END            0x00000010U
 #define REC_TYPE_X86_CPUID_POLICY           0x00000011U
 #define REC_TYPE_X86_MSR_POLICY             0x00000012U
+#define REC_TYPE_PAGE_DATA_LZ4              0x00000013U
 
 #define REC_TYPE_OPTIONAL             0x80000000U
 
@@ -90,6 +91,18 @@ struct xc_sr_rec_page_data_header
 #define PAGE_DATA_PFN_MASK  0x000fffffffffffffULL
 #define PAGE_DATA_TYPE_MASK 0xf000000000000000ULL
 
+/*
+ * PAGE_DATA_LZ4 payload: one sub-block per present page (pfn order), each a
+ * uint16 LE length clen then clen octets.  clen == PAGE_DATA_LZ4_CLEN_RAW
+ * means a raw PAGE_SIZE page follows.  A compressed length is at most
+ * PAGE_SIZE - 1, so it occupies only the low 12 bits; the top 4 bits
+ * (PAGE_DATA_LZ4_CLEN_RESERVED_MASK) are reserved for future use and must
+ * be 0.
+ */
+#define PAGE_DATA_LZ4_CLEN_SIZE          (sizeof(uint16_t))
+#define PAGE_DATA_LZ4_CLEN_RAW           0x0000U
+#define PAGE_DATA_LZ4_CLEN_RESERVED_MASK 0xF000U
+
 /* X86_PV_INFO */
 struct xc_sr_rec_x86_pv_info
 {
diff --git a/tools/python/xen/migration/libxc.py b/tools/python/xen/migration/libxc.py
index e52e632cb1..0b161749ea 100644
--- a/tools/python/xen/migration/libxc.py
+++ b/tools/python/xen/migration/libxc.py
@@ -58,6 +58,7 @@ REC_TYPE_checkpoint_dirty_pfn_list  = 0x0000000f
 REC_TYPE_static_data_end            = 0x00000010
 REC_TYPE_x86_cpuid_policy           = 0x00000011
 REC_TYPE_x86_msr_policy             = 0x00000012
+REC_TYPE_page_data_lz4              = 0x00000013
 
 rec_type_to_str = {
     REC_TYPE_end                        : "End",
@@ -79,6 +80,7 @@ rec_type_to_str = {
     REC_TYPE_static_data_end            : "Static data end",
     REC_TYPE_x86_cpuid_policy           : "x86 CPUID policy",
     REC_TYPE_x86_msr_policy             : "x86 MSR policy",
+    REC_TYPE_page_data_lz4              : "Page data (lz4)",
 }
 
 # page_data
@@ -101,6 +103,10 @@ PAGE_DATA_TYPE_BROKEN        = (0xd << PAGE_DATA_TYPE_SHIFT) # Broken
 PAGE_DATA_TYPE_XALLOC        = (0xe << PAGE_DATA_TYPE_SHIFT) # Allocate-only
 PAGE_DATA_TYPE_XTAB          = (0xf << PAGE_DATA_TYPE_SHIFT) # Invalid
 
+# PAGE_DATA_LZ4 per-page sub-block: uint16 LE clen; top 4 bits reserved (must
+# be 0); see tools/libs/guest/xg_sr_stream_format.h.
+PAGE_DATA_LZ4_CLEN_RESERVED_MASK = 0xf000
+
 # x86_pv_info
 X86_PV_INFO_FORMAT        = "BBHI"
 
@@ -217,7 +223,7 @@ class VerifyLibxc(VerifyBase):
         contentsz = (length + 7) & ~7
         content = self.rdexact(contentsz)
 
-        if rtype != REC_TYPE_page_data:
+        if rtype not in (REC_TYPE_page_data, REC_TYPE_page_data_lz4):
 
             if self.squashed_pagedata_records > 0:
                 self.info("Squashed %d Page Data records together" %
@@ -476,6 +482,85 @@ class VerifyLibxc(VerifyBase):
                               (contentsz, sz))
 
 
+    def verify_record_page_data_lz4(self, content):
+        """ Page Data (lz4) record """
+        # Structure-only check: this verifier does not link liblz4 and so does
+        # not decompress data_lz4.  Decompression integrity (e.g. a corrupt
+        # block) is enforced by the C restore (LZ4_decompress_safe,
+        # xg_sr_restore.c) and by the end-to-end migration tests, not here.
+        minsz = calcsize(PAGE_DATA_FORMAT)
+
+        if len(content) <= minsz:
+            raise RecordError(
+                "PAGE_DATA_LZ4 record must be at least %d bytes long"
+                % (minsz, ))
+
+        count, res1 = unpack(PAGE_DATA_FORMAT, content[:minsz])
+
+        if res1 != 0:
+            raise StreamError(
+                "Reserved bits set in PAGE_DATA_LZ4 record 0x%04x" % (res1, ))
+
+        pfnsz = count * 8
+        if (len(content) - minsz) < pfnsz:
+            raise RecordError(
+                "PAGE_DATA_LZ4 record must contain a pfn record for each count")
+
+        pfns = list(unpack("=%dQ" % (count, ), content[minsz:minsz + pfnsz]))
+
+        nr_pages = 0
+        for idx, pfn in enumerate(pfns):
+
+            if pfn & PAGE_DATA_PFN_RESZ_MASK:
+                raise RecordError("Reserved bits set in pfn[%d]: 0x%016x" %
+                                  (idx, pfn & PAGE_DATA_PFN_RESZ_MASK))
+
+            if pfn >> PAGE_DATA_TYPE_SHIFT in (5, 6, 7, 8):
+                raise RecordError("Invalid type value in pfn[%d]: 0x%016x" %
+                                  (idx, pfn & PAGE_DATA_TYPE_LTAB_MASK))
+
+            # We expect page data for each normal page or pagetable
+            if PAGE_DATA_TYPE_NOTAB <= (pfn & PAGE_DATA_TYPE_LTABTYPE_MASK) \
+                    <= PAGE_DATA_TYPE_L4TAB:
+                nr_pages += 1
+
+        if nr_pages == 0:
+            raise RecordError(
+                "PAGE_DATA_LZ4 record with no pages of data "
+                "(zero-payload LZ4 records are rejected on restore)")
+
+        # data_lz4 follows as nr_pages per-page sub-blocks, in pfn order: a
+        # uint16 LE length clen then clen octets (clen == 0 => a raw 4096-byte
+        # page; clen's top 4 bits reserved).  Walk the framing exactly as the C
+        # restore does (xg_sr_restore.c); this does not decompress.
+        data_lz4 = content[minsz + pfnsz:]
+        comprsz = len(data_lz4)
+        off = 0
+        for idx in range(nr_pages):
+            if off + 2 > comprsz:
+                raise RecordError(
+                    "PAGE_DATA_LZ4 sub-block[%d]: truncated clen prefix" % idx)
+            clen = data_lz4[off] | (data_lz4[off + 1] << 8)
+            off += 2
+
+            if clen & PAGE_DATA_LZ4_CLEN_RESERVED_MASK:
+                raise RecordError(
+                    "PAGE_DATA_LZ4 sub-block[%d]: reserved clen bits 0x%04x" %
+                    (idx, clen))
+
+            blocksz = 4096 if clen == 0 else clen
+            if blocksz > comprsz - off:
+                raise RecordError(
+                    "PAGE_DATA_LZ4 sub-block[%d]: overruns payload" % idx)
+            off += blocksz
+
+        if off != comprsz:
+            raise RecordError(
+                "PAGE_DATA_LZ4 record has %u trailing octets "
+                "after %u sub-blocks"
+                % (comprsz - off, nr_pages))
+
+
 record_verifiers = {
     REC_TYPE_end:
         VerifyLibxc.verify_record_end,
@@ -525,4 +610,7 @@ record_verifiers = {
         VerifyLibxc.verify_record_x86_cpuid_policy,
     REC_TYPE_x86_msr_policy:
         VerifyLibxc.verify_record_x86_msr_policy,
+
+    REC_TYPE_page_data_lz4:
+        VerifyLibxc.verify_record_page_data_lz4,
     }
diff --git a/tools/python/xen/migration/tests.py b/tools/python/xen/migration/tests.py
index fcf94b0bb2..9f8a4e2a0d 100644
--- a/tools/python/xen/migration/tests.py
+++ b/tools/python/xen/migration/tests.py
@@ -4,9 +4,10 @@
 Unit tests for migration v2 streams
 """
 
+import io
 import unittest
 
-from struct import calcsize
+from struct import calcsize, pack
 
 from xen.migration import libxc, libxl
 
@@ -31,6 +32,102 @@ class TestLibxc(unittest.TestCase):
             self.assertEqual(calcsize(fmt), sz)
 
 
+    def test_page_data_lz4(self):
+        """ REC_TYPE_PAGE_DATA_LZ4 verifier: accept valid per-page framing,
+        reject malformed framing """
+
+        v = libxc.VerifyLibxc(lambda *_: None, lambda *_: b"")
+
+        def hdr(count, res=0):
+            return pack(libxc.PAGE_DATA_FORMAT, count, res)
+
+        def clen(n):
+            return pack("<H", n)            # uint16 LE sub-block length prefix
+
+        NOTAB = libxc.PAGE_DATA_TYPE_NOTAB | 0x1   # a normal page (type 0)
+        XTAB = libxc.PAGE_DATA_TYPE_XTAB | 0x1     # invalid type, no data
+        PAGE = b"\x00" * 4096                       # a raw page (clen == 0)
+
+        # Positive: one normal page stored raw (clen == 0 => 4096 octets).
+        v.verify_record_page_data_lz4(
+            hdr(1) + pack("=Q", NOTAB) + clen(0) + PAGE)
+
+        # Positive: one normal page "compressed" (clen > 0 => clen octets).  The
+        # bytes are arbitrary -- the verifier checks framing, not decompression.
+        v.verify_record_page_data_lz4(
+            hdr(1) + pack("=Q", NOTAB) + clen(4) + b"\xde\xad\xbe\xef")
+
+        # Positive: two pages, compressed then raw, back-to-back sub-blocks.
+        v.verify_record_page_data_lz4(
+            hdr(2) + pack("=QQ", NOTAB, NOTAB)
+            + clen(3) + b"\x01\x02\x03" + clen(0) + PAGE)
+
+        # Negative: no pages of data (all-invalid pfns) -- rejected to match the
+        # restore side, which rejects a zero-payload PAGE_DATA_LZ4 record.
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(hdr(1) + pack("=Q", XTAB))
+
+        # Negative: pages present but no payload at all (truncated clen prefix).
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(hdr(1) + pack("=Q", NOTAB))
+
+        # Negative: truncated clen prefix (1 octet where 2 are needed).
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB) + b"\x04")
+
+        # Negative: a compressed sub-block whose clen overruns the payload.
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB) + clen(8) + b"\x01\x02")
+
+        # Negative: a raw sub-block (clen == 0) without a full 4096-octet page.
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB) + clen(0) + b"\x00" * 100)
+
+        # Negative: reserved bits set in clen (top 4 bits).
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB) + clen(0x8000) + PAGE)
+
+        # Negative: trailing octets after the last sub-block.
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB) + clen(4) + b"\xde\xad\xbe\xef"
+                + b"\x99\x99")
+
+        # Negative: reserved bits set in the record header.
+        with self.assertRaises(libxc.StreamError):
+            v.verify_record_page_data_lz4(
+                hdr(1, 1) + pack("=Q", NOTAB) + clen(0) + PAGE)
+
+        # Negative: reserved bits set in a pfn.
+        with self.assertRaises(libxc.RecordError):
+            v.verify_record_page_data_lz4(
+                hdr(1) + pack("=Q", NOTAB | libxc.PAGE_DATA_PFN_RESZ_MASK)
+                + clen(0) + PAGE)
+
+
+    def test_page_data_lz4_dispatch(self):
+        """ REC_TYPE_PAGE_DATA_LZ4 routed through verify_record() end-to-end:
+        exercises the record_verifiers registry, the page-data squash branch,
+        and the trailing-padding check (which the direct-call test bypasses) """
+        NOTAB = libxc.PAGE_DATA_TYPE_NOTAB | 0x1
+
+        # One normal page + a compressed sub-block, 8-byte-aligned body.
+        body = (pack(libxc.PAGE_DATA_FORMAT, 1, 0)
+                + pack("=Q", NOTAB)
+                + pack("<H", 4) + b"\xde\xad\xbe\xef")
+        padding = b"\x00" * ((8 - len(body) % 8) % 8)
+        record = (pack(libxc.RH_FORMAT, libxc.REC_TYPE_page_data_lz4, len(body))
+                  + body + padding)
+
+        stream = io.BytesIO(record)
+        v = libxc.VerifyLibxc(lambda *_: None, stream.read)
+        self.assertEqual(v.verify_record(), libxc.REC_TYPE_page_data_lz4)
+
+
 class TestLibxl(unittest.TestCase):
 
     def test_format_sizes(self):

base-commit: a7fd7d4cbd5e793d31d61c25e08526b330edd7f8
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.