diff --git a/scripts/ci/run-ci-tests.sh b/scripts/ci/run-ci-tests.sh index f99924fd8..1d46721d0 100755 --- a/scripts/ci/run-ci-tests.sh +++ b/scripts/ci/run-ci-tests.sh @@ -360,6 +360,13 @@ run_non_shardable_tests() { echo "Skipping hugetlb compression tests" fi + # Incremental compression parent chains. + if criu/criu check --feature compress && criu/criu check --feature mem_dirty_track; then + make -C test/others/compression/incremental run + else + echo "Skipping compression/incremental test" + fi + ./test/zdtm.py run -t zdtm/transition/pid_reuse --pre 2 # start time based pid reuse detection ./test/zdtm.py run -t zdtm/transition/pidfd_store_sk --rpc --pre 2 # pidfd based pid reuse detection diff --git a/test/others/compression/incremental/.gitignore b/test/others/compression/incremental/.gitignore new file mode 100644 index 000000000..5c2b9704b --- /dev/null +++ b/test/others/compression/incremental/.gitignore @@ -0,0 +1 @@ +dump-*/ diff --git a/test/others/compression/incremental/Makefile b/test/others/compression/incremental/Makefile new file mode 100644 index 000000000..594edc070 --- /dev/null +++ b/test/others/compression/incremental/Makefile @@ -0,0 +1,3 @@ +run: + ./run.sh +.PHONY: run diff --git a/test/others/compression/incremental/image_tool.py b/test/others/compression/incremental/image_tool.py new file mode 100644 index 000000000..ae7865c1a --- /dev/null +++ b/test/others/compression/incremental/image_tool.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys + +import pycriu.images + + +PE_PRESENT = 4 +EXPECTED_REGION_SIZE = 64 * 1024 + + +def inventory_compression(directory, mode, version): + expected_mode = {"plain": 0, "page": 1, "region": 2}[mode] + expected_version = int(version) + + with open(os.path.join(directory, "inventory.img"), "rb") as image: + inventory = pycriu.images.load(image)["entries"][0] + + if inventory["img_version"] != expected_version: + print( + "FAIL: %s inventory version is %s, expected %s" + % (directory, inventory["img_version"], expected_version) + ) + return 1 + if int(inventory.get("compress", 0)) != expected_mode: + print( + "FAIL: %s inventory compression mode is %s, expected %s" + % (directory, inventory.get("compress"), expected_mode) + ) + return 1 + + region_size = inventory.get("compress_region_size") + if expected_mode == 2: + if int(region_size or 0) != EXPECTED_REGION_SIZE: + print( + "FAIL: %s inventory region size is %s, expected 65536" + % (directory, region_size) + ) + return 1 + elif region_size is not None: + print( + "FAIL: %s has unexpected compression region size %s" + % (directory, region_size) + ) + return 1 + + return 0 + + +def present_payload(directory, pid): + path = os.path.join(directory, "pagemap-%s.img" % pid) + with open(path, "rb") as image: + entries = pycriu.images.load(image)["entries"][1:] + + if not any(int(entry.get("flags", 0)) & PE_PRESENT for entry in entries): + print("FAIL: %s contains no present page payload" % path) + return 1 + + return 0 + + +def toggle_page(directory, pid, address_file): + path = os.path.join(directory, "pagemap-%s.img" % pid) + if os.path.exists(address_file): + with open(address_file) as saved: + address = int(saved.read().strip(), 16) + else: + with open(path, "rb") as image: + entries = pycriu.images.load(image)["entries"][1:] + present = [ + entry + for entry in entries + if int(entry.get("flags", 0)) & PE_PRESENT + ] + if not present: + print("FAIL: no present mapping available to modify in %s" % path) + return 1 + entry = max(present, key=lambda item: int(item.get("nr_pages", 0))) + address = int(entry["vaddr"]) + with open(address_file, "w") as saved: + saved.write("%x\n" % address) + + with open("/proc/%s/mem" % pid, "r+b", buffering=0) as memory: + value = os.pread(memory.fileno(), 1, address) + if len(value) != 1: + raise RuntimeError("short read from workload memory") + if os.pwrite(memory.fileno(), bytes([value[0] ^ 0xFF]), address) != 1: + raise RuntimeError("short write to workload memory") + + return 0 + + +def main(): + parser = argparse.ArgumentParser( + description="Inspect incremental compression images and toggle test memory" + ) + subparsers = parser.add_subparsers(dest="command") + + inventory = subparsers.add_parser("inventory-compression") + inventory.add_argument("directory") + inventory.add_argument("mode", choices=("plain", "page", "region")) + inventory.add_argument("version", type=int) + + payload = subparsers.add_parser("present-payload") + payload.add_argument("directory") + payload.add_argument("pid") + + toggle = subparsers.add_parser("toggle-page") + toggle.add_argument("directory") + toggle.add_argument("pid") + toggle.add_argument("address_file") + + args = parser.parse_args() + if args.command is None: + parser.error("a command is required") + if args.command == "inventory-compression": + return inventory_compression(args.directory, args.mode, args.version) + if args.command == "present-payload": + return present_payload(args.directory, args.pid) + return toggle_page(args.directory, args.pid, args.address_file) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/others/compression/incremental/run.sh b/test/others/compression/incremental/run.sh new file mode 100755 index 000000000..058c8756c --- /dev/null +++ b/test/others/compression/incremental/run.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# Test incremental checkpoint chains that change compression mode by layer. +# +# Exercise both ways of mixing compressed and uncompressed iterative +# checkpoint images. Readers must use the compression metadata from their +# own pagemap entries, not from the top image inventory. + +set -euo pipefail + +# shellcheck source=test/others/env.sh +source ../../env.sh + +function fail { + echo "FAIL: $*" + exit 1 +} + +set -x + +CRIU_CMD=("${CRIU}" --no-default-config) +ZDTM_DIR="../../../zdtm/static" +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +IMAGE_TOOL="${SCRIPT_DIR}/image_tool.py" +PID="" + +function cleanup { + if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then + kill -TERM "$PID" 2>/dev/null || true + for _ in $(seq 1 50); do + kill -0 "$PID" 2>/dev/null || break + sleep 0.1 + done + if kill -0 "$PID" 2>/dev/null; then + kill -KILL "$PID" 2>/dev/null || true + fi + fi + rm -rf dump-* +} + +trap cleanup EXIT + +function start_test { + ( + cd "$ZDTM_DIR" || exit 1 + make compress_pages00.cleanout + make compress_pages00 + make compress_pages00.pid || exit 1 + ) + + PID=$(cat "$ZDTM_DIR/compress_pages00.pid") + kill -0 "$PID" || fail "Test didn't start" +} + +function stop_test { + ( + cd "$ZDTM_DIR" || exit 1 + make compress_pages00.stop + grep PASS compress_pages00.out || exit 1 + ) || fail "$1: memory content verification failed" + PID="" +} + +function run_image_tool { + PYTHONPATH="${BASE_DIR}/lib${PYTHONPATH:+:${PYTHONPATH}}" \ + python3 "$IMAGE_TOOL" "$@" +} + +function assert_inventory_compression { + local directory="$1" + local expected_mode="$2" + local expected_version="$3" + + run_image_tool inventory-compression \ + "$directory" "$expected_mode" "$expected_version" +} + +function assert_present_payload { + local directory="$1" + + run_image_tool present-payload "$directory" "$PID" +} + +function toggle_test_page { + local directory="$1" + local address_file="$2" + + # Pick the largest present mapping from the preceding image. Toggling its + # first byte after each pre-dump makes the next layer contain real payload; + # the second toggle restores the byte expected by the ZDTM workload. + run_image_tool toggle-page "$directory" "$PID" "$address_file" +} + +function validate_compression_mode { + local chain_name="$1" + local stage="$2" + local mode="$3" + + case "$mode" in + plain | page | region) ;; + *) fail "$chain_name: unknown $stage compression mode $mode" ;; + esac +} + +function create_checkpoint_layer { + local chain_name="$1" + local stage="$2" + local operation="$3" + local directory="$4" + local previous="$5" + local compression_mode="$6" + local expected_version="$7" + local args=("$operation" -D "$directory" -o dump.log -t "$PID" -v4) + + args+=(--track-mem) + if [ "$operation" = "pre-dump" ]; then + args+=(-R) + fi + if [ -n "$previous" ]; then + args+=(--prev-images-dir="$previous") + fi + + case "$compression_mode" in + page) args+=(--compress) ;; + region) args+=(--compress-region=64K) ;; + plain) ;; + esac + + echo "=== $chain_name: $stage ===" + mkdir "$directory" + "${CRIU_CMD[@]}" "${args[@]}" || fail "$chain_name: $stage failed" + assert_present_payload "$directory" || \ + fail "$chain_name: $stage contains no payload" + assert_inventory_compression \ + "$directory" "$compression_mode" "$expected_version" || \ + fail "$chain_name: $stage inventory compression state is invalid" +} + +function run_chain { + local name="$1" + local predump_compress="$2" + local dump_compress="$3" + local imgdir="dump-$name" + local parent_version=2 + local final_version + + validate_compression_mode "$name" "pre-dump" "$predump_compress" + validate_compression_mode "$name" "final dump" "$dump_compress" + + if [ "$predump_compress" != "plain" ]; then + parent_version=3 + fi + + final_version=$parent_version + if [ "$dump_compress" != "plain" ]; then + final_version=3 + fi + + rm -rf "$imgdir" + mkdir "$imgdir" + + start_test + + create_checkpoint_layer "$name" "pre-dump 1" pre-dump \ + "$imgdir/1/" "" "$predump_compress" "$parent_version" + toggle_test_page "$imgdir/1/" "$imgdir/toggle-address" \ + || fail "$name: unable to modify workload" + + create_checkpoint_layer "$name" "pre-dump 2" pre-dump \ + "$imgdir/2/" "../1/" "$predump_compress" "$parent_version" + toggle_test_page "$imgdir/2/" "$imgdir/toggle-address" \ + || fail "$name: unable to restore workload byte" + + create_checkpoint_layer "$name" "final dump" dump \ + "$imgdir/3/" "../2/" "$dump_compress" "$final_version" + + echo "=== $name: restore ===" + "${CRIU_CMD[@]}" restore -D "$imgdir/3/" -o restore.log -v4 -d \ + || fail "$name: restore failed" + + stop_test "$name" + rm -rf "$imgdir" +} + +run_chain "plain-parents-compressed-final" plain page +run_chain "compressed-parents-plain-final" page plain +run_chain "plain-parents-region-final" plain region +run_chain "region-parents-plain-final" region plain + +echo "Test PASSED"