From 3441f66b244f26b1031879b994f6896149ea3a50 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Sat, 11 Jul 2026 12:13:14 +0100 Subject: [PATCH] crtools/rpc/libcriu: Expose page compression The compression paths have no supported user or RPC controls, and clients cannot discover whether CRIU was built with LZ4. Expose per-page and region compression, LZ4 acceleration, and worker concurrency for LZ4 decoding and eligible large zero fills through the CLI, configuration files, RPC, and libcriu. Add compression to the feature-check API and document each interface. Define --decompress-threads=0 as automatic worker concurrency and one as the serial default with zero-fill workers disabled. Values above one bound aggregate worker concurrency. Automatic mode derives a useful width from CPU affinity and the work in each batch. Reduce explicit requests above the available CPU count with a warning. Document the independent two-slot encoded-input budget. A local restore overlaps its next read only when it can reserve the second slot without waiting; otherwise it keeps synchronous I/O. Use the compression mode as the source of truth and validate every related value. An explicit RPC OFF clears ambient compression settings and rejects conflicting fields. Reject combinations unsupported by page server, stream, TLS, and image-format paths, and cover option and RPC precedence in the tests. Assisted-by: Codex:GPT-5 Assisted-by: Claude:claude-fable-5 Signed-off-by: Radostin Stoyanov --- Documentation/criu.txt | 54 ++++++ Makefile | 1 + criu/config.c | 166 ++++++++++++++++++- criu/cr-check.c | 11 ++ criu/cr-service.c | 68 ++++++++ criu/crtools.c | 17 ++ images/rpc.proto | 18 ++ lib/c/criu.c | 86 ++++++++++ lib/c/criu.h | 28 ++++ scripts/ci/run-ci-tests.sh | 1 + test/others/compression/page-server/Makefile | 3 + test/others/compression/page-server/run.sh | 118 +++++++++++++ test/others/libcriu/run.sh | 3 + test/others/libcriu/test_feature_check.c | 10 ++ test/others/libcriu/test_rpc_config.c | 32 +++- 15 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 test/others/compression/page-server/Makefile create mode 100755 test/others/compression/page-server/run.sh diff --git a/Documentation/criu.txt b/Documentation/criu.txt index d764dbce3..38df6c98f 100644 --- a/Documentation/criu.txt +++ b/Documentation/criu.txt @@ -378,6 +378,60 @@ mount -t cgroup -o devices,freezer none devices,freezer Allows to link unlinked files back, if possible (modifies filesystem during *restore*). +*-c, --compress*:: + Enable LZ4 per-page compression of memory pages during *dump*. + Each system page is compressed as an independent LZ4 block. + Zero-filled pages are detected and stored with no data; pages that + do not compress well are stored raw to avoid decompression overhead + on *restore*. The compression choice made at *dump* time is recorded + in the inventory image and applied automatically on *restore*. + Compatible with *--page-server* when *--tls* is not used, *--stream*, + *--lazy-pages*, iterative checkpointing, *--auto-dedup*, and the *dedup* + command. + +*--compress-region* 'size':: + Enable LZ4 region compression of memory pages during *dump*. + A run of consecutive pages totalling 'size' bytes is compressed as + a single LZ4 block, which yields a better ratio and faster dump + on heap-shaped data than per-page compression at the cost of + slightly slower restore on partial reads. 'size' accepts K/M/G + suffixes (e.g. *256K*, *1M*); it must be a multiple of the system + page size and at most 4M. Mutually exclusive with *--compress*. + Currently supports only the local image path (no + *--page-server* / *--stream*). Hole-punching deduplication is + skipped for compressed images since compressed pages are + variably-sized. + +*--compress-acceleration* 'N':: + Set the LZ4 acceleration level for page compression (1 to 65537). + Controls how thoroughly the compressor searches for matching byte + sequences. The default value of 1 gives the best compression + ratio. Higher values skip more match candidates, resulting in + faster compression but larger output. Decompression speed is not + affected. Implies *--compress* unless *--compress-region* is set. + +*--decompress-threads* 'N':: + Set worker concurrency for LZ4 decoding and eligible large zero fills, + including the calling restore thread. The default value of 1 keeps each + LZ4 decode serial and handles zero blocks without a worker pool; + independent private, shmem, and memfd requests can still run concurrently. + A value of 0 lets CRIU choose the concurrency for each batch. CRIU starts + with the CPUs allowed by its affinity mask, then limits the width by the + number and decoded size of the blocks in that batch and by the restore-wide + CPU budget. Small batches and zero runs with fewer than two blocks remain + serial. Values above 1 set an upper bound on aggregate worker concurrency + instead of using the automatic CPU count. Explicit requests above the + available CPU count are reduced with a warning. The accepted range is 0 + through 1024; this validation ceiling does not determine the automatic + concurrency. + Encoded staging memory is bounded independently of the thread count. + Restore keeps at most two active 32 MiB encoded input buffers. When a + local reader can reserve a second buffer without waiting, its calling thread + reads the next payload while pool workers decode the current one. If + another restore request already owns the second buffer, reading remains + synchronous. The option can also be set through a configuration file + (*decompress-threads 0*). + *--timeout* 'number':: Set a time limit in seconds for collecting tasks during the dump operation. The timeout is 10 seconds by default. diff --git a/Makefile b/Makefile index 95a48175a..6680f2d66 100644 --- a/Makefile +++ b/Makefile @@ -479,6 +479,7 @@ shellcheck: shellcheck -x test/others/config-file/*.sh shellcheck -x test/others/action-script/*.sh shellcheck -x contrib/criu-service-client/test/*.sh + shellcheck -x test/others/compression/*/*.sh codespell: codespell diff --git a/criu/config.c b/criu/config.c index ea75701ad..f6f4fc79f 100644 --- a/criu/config.c +++ b/criu/config.c @@ -1,6 +1,8 @@ #include +#include #include #include +#include #include #include #include @@ -13,6 +15,7 @@ #include "cgroup.h" #include "cgroup-props.h" #include "common/bug.h" +#include "compression.h" #include "cpu.h" #include "crtools.h" #include "cr_options.h" @@ -551,6 +554,41 @@ static size_t parse_size(char *optarg) return (size_t)atol(optarg); } +static int parse_size_strict(const char *value, size_t *size) +{ + unsigned long long number, multiplier = 1; + char *end; + + if (!value[0] || value[0] < '0' || value[0] > '9') + return -1; + + errno = 0; + number = strtoull(value, &end, 10); + if (errno == ERANGE || end == value) + return -1; + + if (*end) { + switch (*end++) { + case 'K': + multiplier = 1024; + break; + case 'M': + multiplier = 1024 * 1024; + break; + case 'G': + multiplier = 1024ULL * 1024 * 1024; + break; + default: + return -1; + } + } + if (*end || number > SIZE_MAX / multiplier) + return -1; + + *size = (size_t)(number * multiplier); + return 0; +} + static int parse_join_ns(const char *ptr) { char *aux, *ns_file, *extra_opts = NULL; @@ -634,7 +672,7 @@ int parse_options(int argc, char **argv, bool *usage_error, bool *has_exec_cmd, "no-" OPT_NAME, no_argument, SAVE_TO, false \ } - static const char short_opts[] = "dSsRt:hD:o:v::x::Vr:jJ:lW:L:M:"; + static const char short_opts[] = "dSscRt:hD:o:v::x::Vr:jJ:lW:L:M:"; static struct option long_opts[] = { { "tree", required_argument, 0, 't' }, { "leave-stopped", no_argument, 0, 's' }, @@ -722,6 +760,10 @@ int parse_options(int argc, char **argv, bool *usage_error, bool *has_exec_cmd, BOOL_OPT("unprivileged", &opts.unprivileged), BOOL_OPT("ghost-fiemap", &opts.ghost_fiemap), BOOL_OPT(OPT_ALLOW_UPROBES, &opts.allow_uprobes), + { "compress", no_argument, 0, 'c' }, + { "compress-acceleration", required_argument, 0, 1102 }, + { "compress-region", required_argument, 0, 1103 }, + { "decompress-threads", required_argument, 0, 1104 }, {}, }; @@ -831,6 +873,58 @@ int parse_options(int argc, char **argv, bool *usage_error, bool *has_exec_cmd, } else opts.log_level++; break; + case 'c': + if (opts.compress_mode == COMPRESS_REGION) { + pr_err("--compress conflicts with --compress-region\n"); + return 1; + } + opts.compress_mode = COMPRESS_PER_PAGE; + break; + case 1102: { + char *endptr; + long accel = strtol(optarg, &endptr, 10); + + if (*endptr != '\0' || accel < 1 || accel > LZ4_MAX_ACCELERATION) { + pr_err("Invalid --compress-acceleration value '%s' (must be 1..%d)\n", + optarg, LZ4_MAX_ACCELERATION); + return 1; + } + opts.compress_acceleration = accel; + break; + } + case 1103: { + size_t sz; + + if (parse_size_strict(optarg, &sz) || sz == 0 || + sz % PAGE_SIZE != 0 || sz > MAX_REGION_SIZE) { + pr_err("Invalid --compress-region '%s' (must be a multiple of %lu, max %lu)\n", + optarg, (unsigned long)PAGE_SIZE, + MAX_REGION_SIZE); + return 1; + } + if (opts.compress_mode == COMPRESS_PER_PAGE) { + pr_err("--compress-region conflicts with --compress\n"); + return 1; + } + opts.compress_region_size = sz; + opts.compress_mode = COMPRESS_REGION; + break; + } + case 1104: { + char *endptr; + long n; + + errno = 0; + n = strtol(optarg, &endptr, 10); + if (errno == ERANGE || endptr == optarg || *endptr != '\0' || + n < 0 || n > 1024) { + pr_err("Invalid --decompress-threads value '%s' (must be 0..1024)\n", + optarg); + return 1; + } + opts.decompress_threads = (unsigned int)n; + break; + } case 1043: { int fd; @@ -1097,6 +1191,76 @@ bad_arg: int check_options(void) { + /* + * --compress-acceleration (CLI) or compress_acceleration (RPC) on + * their own imply per-page compression. Resolve that here rather + * than while parsing each option, so that passing acceleration + * before --compress-region is not mistaken for a conflict with an + * implicit --compress. + */ + if (opts.compress_acceleration && opts.compress_mode == COMPRESS_OFF) + opts.compress_mode = COMPRESS_PER_PAGE; + + /* + * Compression is selected by the dump client and encoded in each page + * server wire command. Letting the server select a mode independently + * can make the payload disagree with the inventory written by the client. + */ + if (opts.mode == CR_PAGE_SERVER && opts.compress_mode != COMPRESS_OFF) { + pr_err("Memory page compression options apply to the dump client, not the page server\n"); + return 1; + } + + if (opts.compress_mode) { +#ifndef CONFIG_LZ4 + pr_err("Memory page compression requires CRIU built with LZ4 support (CONFIG_LZ4)\n"); + return 1; +#else + if (opts.compress_mode == COMPRESS_REGION) { + if (opts.compress_region_size == 0) + opts.compress_region_size = DEFAULT_REGION_SIZE; + if (opts.compress_region_size % PAGE_SIZE != 0 || + opts.compress_region_size > MAX_REGION_SIZE) { + pr_err("Invalid compress region size %u\n", + opts.compress_region_size); + return 1; + } + pr_debug("Region compression of memory pages is enabled (region=%u bytes)\n", + opts.compress_region_size); + } else { + pr_debug("Per-page compression of memory pages is enabled\n"); + } +#endif + } + + /* + * Region compression is currently only implemented for the local + * dump and restore paths. The page-server + * and image-streamer wire formats are per-page; combining them with + * --compress-region would produce an image the receiver cannot read. + * Reject the combination early. + */ + if (opts.compress_mode == COMPRESS_REGION) { + if (opts.use_page_server || opts.addr) { + pr_err("--compress-region is not supported with --page-server\n"); + return 1; + } + if (opts.stream) { + pr_err("--compress-region is not supported with --stream\n"); + return 1; + } + } + + /* + * The compressed page-server sender writes its records straight to + * the socket and does not route them through the TLS helpers, so a + * TLS page-server would receive plaintext into the encrypted stream. + * Reject the combination until compressed sends learn to use TLS. + */ + if (opts.compress_mode && opts.tls && (opts.use_page_server || opts.addr)) { + pr_err("Memory page compression is not supported with a TLS page-server\n"); + return 1; + } if (opts.tcp_established_ok) pr_info("Will dump/restore TCP connections\n"); if (opts.tcp_skip_in_flight) diff --git a/criu/cr-check.c b/criu/cr-check.c index d7aa43076..67a49ee28 100644 --- a/criu/cr-check.c +++ b/criu/cr-check.c @@ -1809,6 +1809,16 @@ static int check_binfmt_misc_sandboxing(void) return 0; } +static int check_compress(void) +{ +#ifdef CONFIG_LZ4 + return 0; +#else + pr_info("LZ4 compression support is not compiled in\n"); + return -1; +#endif +} + struct feature_list { char *name; int (*func)(void); @@ -1860,6 +1870,7 @@ static struct feature_list feature_list[] = { { "overlayfs_maps", check_overlayfs_maps }, { "pagemap_scan_guard_pages", check_pagemap_scan_guard_pages }, { "binfmt_misc_sandboxing", check_binfmt_misc_sandboxing }, + { "compress", check_compress }, { NULL, NULL }, }; diff --git a/criu/cr-service.c b/criu/cr-service.c index f88934b61..dc4b9804d 100644 --- a/criu/cr-service.c +++ b/criu/cr-service.c @@ -47,6 +47,7 @@ #include "cr-errno.h" #include "namespaces.h" +#include "compression.h" unsigned int service_sk_ino = -1; @@ -837,6 +838,64 @@ static int setup_opts_from_req(int sk, CriuOpts *req) if (req->mntns_compat_mode) opts.mntns_compat_mode = true; + if (req->has_compress) { + if (req->compress > COMPRESS_REGION) { + pr_err("Invalid compress value %u\n", req->compress); + goto err; + } + opts.compress_mode = req->compress; + if (req->compress == COMPRESS_OFF) { + /* + * An explicit RPC setting has precedence over values loaded + * from the service configuration. libcriu clears the related + * request fields when compression is disabled; reject a raw + * RPC request which asks for both settings at once. + */ + if (req->has_compress_acceleration || + req->has_compress_region_size) { + pr_err("compress=off conflicts with compression tuning options\n"); + goto err; + } + opts.compress_acceleration = 0; + opts.compress_region_size = 0; + } + } + + if (req->has_compress_acceleration) { + if (req->compress_acceleration < 1 || + req->compress_acceleration > LZ4_MAX_ACCELERATION) { + pr_err("Invalid compress_acceleration value %u (must be 1..%d)\n", + req->compress_acceleration, LZ4_MAX_ACCELERATION); + goto err; + } + opts.compress_acceleration = req->compress_acceleration; + } + + if (req->has_compress_region_size) { + if (req->compress_region_size == 0 || + req->compress_region_size % PAGE_SIZE != 0 || + req->compress_region_size > MAX_REGION_SIZE) { + pr_err("Invalid compress_region_size value %u\n", + req->compress_region_size); + goto err; + } + if (opts.compress_mode == COMPRESS_PER_PAGE) { + pr_err("compress_region_size conflicts with compress=per-page\n"); + goto err; + } + opts.compress_region_size = req->compress_region_size; + opts.compress_mode = COMPRESS_REGION; + } + + if (req->has_decompress_threads) { + if (req->decompress_threads > 1024) { + pr_err("Invalid decompress_threads value %u (must be 0..1024)\n", + req->decompress_threads); + goto err; + } + opts.decompress_threads = req->decompress_threads; + } + if (check_options()) goto err; @@ -1227,6 +1286,8 @@ static int handle_feature_check(int sk, CriuReq *msg) feat.lazy_pages = false; feat.has_pidfd_store = 1; feat.pidfd_store = false; + feat.has_mem_compression = 1; + feat.mem_compression = false; pid = fork(); if (pid < 0) { @@ -1249,6 +1310,13 @@ static int handle_feature_check(int sk, CriuReq *msg) if ((msg->features->has_pidfd_store == 1) && (msg->features->pidfd_store == true)) feat.pidfd_store = kdat.has_pidfd_getfd && kdat.has_pidfd_open; + if ((msg->features->has_mem_compression == 1) && + (msg->features->mem_compression == true)) { +#ifdef CONFIG_LZ4 + feat.mem_compression = true; +#endif + } + resp.features = &feat; resp.type = msg->type; /* The feature check is working, actual results are in resp.features */ diff --git a/criu/crtools.c b/criu/crtools.c index 95c2c1f29..112b8e86d 100644 --- a/criu/crtools.c +++ b/criu/crtools.c @@ -556,6 +556,23 @@ usage: " will be punched from the image\n" " --pre-dump-mode splice - parasite based pre-dumping (default)\n" " read - process_vm_readv syscall based pre-dumping\n" +#ifdef CONFIG_LZ4 + " -c|--compress enable LZ4 per-page compression of memory pages\n" + " --compress-region size\n" + " enable memory page compression for given region size;\n" + " size accepts K/M/G suffixes (e.g. 256K, 1M);\n" + " valid range: page-size multiples up to 4M\n" + " --compress-acceleration N\n" + " LZ4 acceleration (default is 1; max is 65537).\n" + " Higher values favor speed over compression ratio.\n" + " Implies --compress unless --compress-region is set.\n" + " --decompress-threads N\n" + " worker concurrency for LZ4 decode and zero fill\n" + " (default: 1 = serial per request; 0 = auto;\n" + " N > 1 = aggregate worker limit).\n" + " Valid range: 0..1024; affinity, batch work, and the\n" + " restore-wide budget may reduce active width.\n" +#endif "\n" "Page/Service server options:\n" " --address ADDR address of server or service\n" diff --git a/images/rpc.proto b/images/rpc.proto index c63f33026..b7bf62ea4 100644 --- a/images/rpc.proto +++ b/images/rpc.proto @@ -151,6 +151,22 @@ message criu_opts { optional bool display_stats = 70; optional bool log_to_stderr = 71; optional criu_image_io_mode image_io_mode = 72 [default = IMAGE_IO_WRITEBACK]; + /* + * Memory page compression mode (enum compress_mode): + * 0 = off, 1 = per-page, 2 = region. Default 0. + */ + optional uint32 compress = 73; + optional uint32 compress_acceleration = 74; + optional uint32 compress_region_size = 75; + /* + * Worker concurrency for LZ4 decoding and eligible large zero fills. + * 0 selects automatic concurrency; 1, the default, keeps LZ4 decoding + * serial and disables zero-fill workers; values above 1 limit aggregate + * worker concurrency. Active concurrency also depends on available CPUs, + * useful batch work, and the restore-wide CPU budget. Valid range: 0 + * through 1024. Restore-time only. + */ + optional uint32 decompress_threads = 76 [default = 1]; /* optional bool check_mounts = 128; */ } @@ -198,6 +214,8 @@ message criu_features { optional bool mem_track = 1; optional bool lazy_pages = 2; optional bool pidfd_store = 3; + /* Memory page compression support is available in this CRIU build. */ + optional bool mem_compression = 4; } /* diff --git a/lib/c/criu.c b/lib/c/criu.c index 01afb829b..36b17f7c9 100644 --- a/lib/c/criu.c +++ b/lib/c/criu.c @@ -376,6 +376,85 @@ void criu_set_track_mem(bool track_mem) criu_local_set_track_mem(global_opts, track_mem); } +int criu_local_set_compress(criu_opts *opts, enum criu_compress_mode mode) +{ + if (mode < CRIU_COMPRESS_OFF || mode > CRIU_COMPRESS_REGION) + return -EINVAL; + + opts->rpc->has_compress = true; + opts->rpc->compress = mode; + if (mode != CRIU_COMPRESS_REGION) { + opts->rpc->has_compress_region_size = false; + opts->rpc->compress_region_size = 0; + } + if (mode == CRIU_COMPRESS_OFF) { + opts->rpc->has_compress_acceleration = false; + opts->rpc->compress_acceleration = 0; + } + return 0; +} + +int criu_set_compress(enum criu_compress_mode mode) +{ + return criu_local_set_compress(global_opts, mode); +} + +int criu_local_set_compress_acceleration(criu_opts *opts, + unsigned int acceleration) +{ + if (acceleration < 1 || acceleration > CRIU_COMPRESS_MAX_ACCELERATION) + return -EINVAL; + + opts->rpc->has_compress_acceleration = true; + opts->rpc->compress_acceleration = acceleration; + if (!opts->rpc->has_compress || + opts->rpc->compress == CRIU_COMPRESS_OFF) { + opts->rpc->has_compress = true; + opts->rpc->compress = CRIU_COMPRESS_PER_PAGE; + } + return 0; +} + +int criu_set_compress_acceleration(unsigned int acceleration) +{ + return criu_local_set_compress_acceleration(global_opts, acceleration); +} + +int criu_local_set_compress_region_size(criu_opts *opts, unsigned int bytes) +{ + long page_size = sysconf(_SC_PAGESIZE); + + if (page_size <= 0 || !bytes || bytes > CRIU_COMPRESS_MAX_REGION_SIZE || + bytes % (unsigned long)page_size) + return -EINVAL; + + opts->rpc->has_compress_region_size = true; + opts->rpc->compress_region_size = bytes; + opts->rpc->has_compress = true; + opts->rpc->compress = CRIU_COMPRESS_REGION; + return 0; +} + +int criu_set_compress_region_size(unsigned int bytes) +{ + return criu_local_set_compress_region_size(global_opts, bytes); +} + +int criu_local_set_decompress_threads(criu_opts *opts, unsigned int threads) +{ + if (threads > CRIU_DECOMPRESS_MAX_THREADS) + return -EINVAL; + + opts->rpc->has_decompress_threads = true; + opts->rpc->decompress_threads = threads; + return 0; +} + +int criu_set_decompress_threads(unsigned int threads) +{ + return criu_local_set_decompress_threads(global_opts, threads); +} + void criu_local_set_auto_dedup(criu_opts *opts, bool auto_dedup) { opts->rpc->has_auto_dedup = true; @@ -2007,6 +2086,10 @@ int criu_local_feature_check(criu_opts *opts, struct criu_feature_check *feature criu_features.has_pidfd_store = true; criu_features.pidfd_store = true; } + if (features_copy.mem_compression) { + criu_features.has_mem_compression = true; + criu_features.mem_compression = true; + } req.features = &criu_features; ret = send_req_and_recv_resp(opts, &req, &resp); @@ -2025,6 +2108,9 @@ int criu_local_feature_check(criu_opts *opts, struct criu_feature_check *feature if (resp->features->has_pidfd_store) { features_copy.pidfd_store = resp->features->pidfd_store; } + if (resp->features->has_mem_compression) { + features_copy.mem_compression = resp->features->mem_compression; + } memcpy(features, &features_copy, size); } else { ret = -EBADE; diff --git a/lib/c/criu.h b/lib/c/criu.h index 81bbfd5c8..8eaf95cbd 100644 --- a/lib/c/criu.h +++ b/lib/c/criu.h @@ -60,6 +60,17 @@ enum criu_image_io_mode { CRIU_IMAGE_IO_DIRECT = 1, }; +enum criu_compress_mode { + CRIU_COMPRESS_OFF = 0, + CRIU_COMPRESS_PER_PAGE = 1, + CRIU_COMPRESS_REGION = 2, +}; + +#define CRIU_COMPRESS_MAX_ACCELERATION 65537U +#define CRIU_COMPRESS_MAX_REGION_SIZE (4U * 1024U * 1024U) +/* Maximum explicit setting; automatic concurrency is not capped by it. */ +#define CRIU_DECOMPRESS_MAX_THREADS 1024U + int criu_set_service_address(const char *path); void criu_set_service_fd(int fd); int criu_set_service_binary(const char *path); @@ -90,6 +101,17 @@ void criu_set_unprivileged(bool unprivileged); void criu_set_orphan_pts_master(bool orphan_pts_master); void criu_set_file_locks(bool file_locks); void criu_set_track_mem(bool track_mem); +int criu_set_compress(enum criu_compress_mode mode); +int criu_set_compress_acceleration(unsigned int acceleration); +int criu_set_compress_region_size(unsigned int bytes); +/* + * Worker concurrency for LZ4 decoding and eligible large zero fills: + * 0 = auto, 1 = serial/no zero-fill workers (default), N > 1 = aggregate + * worker limit. + * CPU affinity, useful batch work, and the shared budget may reduce active + * concurrency. + */ +int criu_set_decompress_threads(unsigned int threads); void criu_set_auto_dedup(bool auto_dedup); void criu_set_force_irmap(bool force_irmap); void criu_set_link_remap(bool link_remap); @@ -253,6 +275,11 @@ void criu_local_set_skip_file_rwx_check(criu_opts *opts, bool skip_file_rwx_chec void criu_local_set_orphan_pts_master(criu_opts *opts, bool orphan_pts_master); void criu_local_set_file_locks(criu_opts *opts, bool file_locks); void criu_local_set_track_mem(criu_opts *opts, bool track_mem); +int criu_local_set_compress(criu_opts *opts, enum criu_compress_mode mode); +int criu_local_set_compress_acceleration(criu_opts *opts, unsigned int acceleration); +int criu_local_set_compress_region_size(criu_opts *opts, unsigned int bytes); +/* Uses the same worker-concurrency values as criu_set_decompress_threads(). */ +int criu_local_set_decompress_threads(criu_opts *opts, unsigned int threads); void criu_local_set_auto_dedup(criu_opts *opts, bool auto_dedup); void criu_local_set_force_irmap(criu_opts *opts, bool force_irmap); void criu_local_set_link_remap(criu_opts *opts, bool link_remap); @@ -327,6 +354,7 @@ struct criu_feature_check { bool mem_track; bool lazy_pages; bool pidfd_store; + bool mem_compression; }; int criu_feature_check(struct criu_feature_check *features, size_t size); diff --git a/scripts/ci/run-ci-tests.sh b/scripts/ci/run-ci-tests.sh index 073996373..424d4ca15 100755 --- a/scripts/ci/run-ci-tests.sh +++ b/scripts/ci/run-ci-tests.sh @@ -386,6 +386,7 @@ run_non_shardable_tests() { # config file parser and parameter testing make -C test/others/config-file run + make -C test/others/compression/page-server run # action script testing make -C test/others/action-script run diff --git a/test/others/compression/page-server/Makefile b/test/others/compression/page-server/Makefile new file mode 100644 index 000000000..594edc070 --- /dev/null +++ b/test/others/compression/page-server/Makefile @@ -0,0 +1,3 @@ +run: + ./run.sh +.PHONY: run diff --git a/test/others/compression/page-server/run.sh b/test/others/compression/page-server/run.sh new file mode 100755 index 000000000..45ff4f364 --- /dev/null +++ b/test/others/compression/page-server/run.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Compression is a dump-client setting carried by page-server wire commands. +# A page server must reject local compression settings instead of silently +# changing how ordinary PS_IOV_ADD_F payloads are stored. + +set -euo pipefail + +# shellcheck source=test/others/env.sh +source ../../env.sh + +LOG="page-server-options.$$.log" + +function cleanup { + rm -f "$LOG" +} + +trap cleanup EXIT + +function fail { + echo "FAIL: $*" + exit 1 +} + +function check_rejected { + local name="$1" + shift + local status + + rm -f "$LOG" + set +e + timeout --signal=KILL 5 "${CRIU}" page-server --no-default-config \ + -D . -o "$LOG" --port 54321 "$@" + status=$? + set -e + + if [ "$status" -ne 1 ]; then + fail "$name returned $status instead of rejecting the option" + fi + grep -q "compression options apply to the dump client" "$LOG" || \ + fail "$name did not report the page-server compression error" +} + +function check_invalid_region { + local value="$1" + local status + + rm -f "$LOG" + set +e + timeout --foreground --kill-after=1s 5s \ + "${CRIU}" page-server --no-default-config -D . \ + --port 54321 --compress-region="$value" > "$LOG" 2>&1 + status=$? + set -e + + if [ "$status" -ne 1 ]; then + fail "malformed region '$value' returned $status" + fi + grep -q "Invalid --compress-region" "$LOG" || \ + fail "malformed region '$value' was not rejected by its parser" +} + +function check_invalid_threads { + local value="$1" + local status + + rm -f "$LOG" + set +e + timeout --foreground --kill-after=1s 5s \ + "${CRIU}" page-server --no-default-config -D . --port 54321 \ + --decompress-threads="$value" > "$LOG" 2>&1 + status=$? + set -e + + if [ "$status" -ne 1 ]; then + fail "malformed decompression thread count '$value' returned $status" + fi + grep -q "Invalid --decompress-threads" "$LOG" || \ + fail "malformed decompression thread count '$value' was not rejected" +} + +function check_valid_threads { + local value="$1" + local port="$2" + local status + + rm -f "$LOG" + set +e + timeout --foreground --kill-after=1s 2s \ + "${CRIU}" page-server --no-default-config -D . --port "$port" \ + --decompress-threads="$value" > "$LOG" 2>&1 + status=$? + set -e + + # The option parses; the server waits for connections until the + # timeout ends it. + if [ "$status" -ne 124 ] && [ "$status" -ne 137 ]; then + fail "valid decompression thread count '$value' returned $status" + fi + if grep -q "Invalid --decompress-threads" "$LOG"; then + fail "valid decompression thread count '$value' was rejected" + fi +} + +check_rejected compress --compress +check_rejected acceleration --compress-acceleration 2 +check_rejected region --compress-region 64K +check_invalid_region 64Kjunk +check_invalid_region 64MgarbageK +check_invalid_region 64KB +check_invalid_region -64K +check_invalid_region 18446744073709551616K +check_invalid_threads "" +# Check both special values: 0 selects automatic concurrency and 1, the default, is serial per request. +check_valid_threads 0 54322 +check_valid_threads 1 54323 + +echo "Test PASSED" diff --git a/test/others/libcriu/run.sh b/test/others/libcriu/run.sh index 0278d08e3..8421c2435 100755 --- a/test/others/libcriu/run.sh +++ b/test/others/libcriu/run.sh @@ -51,6 +51,9 @@ run_test() { fi } +if criu check --feature compress > /dev/null; then + export CRIU_FEATURE_COMPRESS=1 +fi run_test test_sub run_test test_self run_test test_notify diff --git a/test/others/libcriu/test_feature_check.c b/test/others/libcriu/test_feature_check.c index d88e0de23..86e80cbae 100644 --- a/test/others/libcriu/test_feature_check.c +++ b/test/others/libcriu/test_feature_check.c @@ -17,10 +17,12 @@ int main(int argc, char **argv) bool mem_track = 0; bool lazy_pages = 0; bool pidfd_store = 0; + bool mem_compression = 0; struct criu_feature_check features = { .mem_track = true, .lazy_pages = true, .pidfd_store = true, + .mem_compression = true, }; printf("--- Start feature check ---\n"); @@ -39,6 +41,10 @@ int main(int argc, char **argv) if (env) { pidfd_store = true; } + env = getenv("CRIU_FEATURE_COMPRESS"); + if (env) { + mem_compression = true; + } ret = criu_feature_check(&features, sizeof(features) + 1); printf(" `- passing too large structure to libcriu should return -1: %d\n", ret); @@ -60,6 +66,10 @@ int main(int argc, char **argv) printf(" `- pidfd_store: %d - expected : %d\n", features.pidfd_store, pidfd_store); if (features.pidfd_store != pidfd_store) return -1; + printf(" `- mem_compression: %d - expected : %d\n", + features.mem_compression, mem_compression); + if (features.mem_compression != mem_compression) + return -1; return 0; } diff --git a/test/others/libcriu/test_rpc_config.c b/test/others/libcriu/test_rpc_config.c index 529f13637..5cf59fd8a 100644 --- a/test/others/libcriu/test_rpc_config.c +++ b/test/others/libcriu/test_rpc_config.c @@ -67,15 +67,18 @@ static int create_criu_config_file(void) } fprintf(fp, "log-file=%s\n", log_file); + fprintf(fp, "compress-acceleration=2\n"); fflush(fp); fclose(fp); return 0; } -static int check_log_file(void) +static int check_log_file(const char *forbidden) { struct stat st; + char line[512]; + FILE *fp; if (stat(log_file, &st) < 0) { perror("Config file does not exist"); @@ -87,6 +90,20 @@ static int check_log_file(void) return -1; } + fp = fopen(log_file, "r"); + if (!fp) { + perror("Failed to open log file"); + return -1; + } + while (forbidden && fgets(line, sizeof(line), fp)) { + if (strstr(line, forbidden)) { + fprintf(stderr, "Unexpected log message: %s", line); + fclose(fp); + return -1; + } + } + fclose(fp); + unlink(log_file); return 0; } @@ -169,6 +186,10 @@ int main(int argc, char **argv) printf("Setting dump RPC config file: %s\n", conf_file); criu_set_config_file(conf_file); criu_set_log_file("dump.log"); + if (criu_set_compress(CRIU_COMPRESS_OFF)) { + fprintf(stderr, "Failed to disable compression\n"); + goto cleanup; + } ret = criu_dump(); if (ret < 0) { @@ -181,13 +202,14 @@ int main(int argc, char **argv) printf(" `- Dump succeeded\n"); waitpid(pid, NULL, 0); - if (check_log_file()) { + if (check_log_file("compression of memory pages is enabled")) { printf("Error: log file not overwritten by RPC config file\n"); goto cleanup; } printf("--- Restore loop ---\n"); criu_init_opts(); + criu_set_service_binary(argv[1]); criu_set_images_dir_fd(img_fd); criu_set_log_level(CRIU_LOG_DEBUG); @@ -195,6 +217,10 @@ int main(int argc, char **argv) printf("Setting restore RPC config file: %s\n", conf_file); criu_set_config_file(conf_file); criu_set_log_file("restore.log"); + if (criu_set_compress(CRIU_COMPRESS_OFF)) { + fprintf(stderr, "Failed to disable compression\n"); + goto cleanup; + } pid = criu_restore_child(); if (pid <= 0) { @@ -206,7 +232,7 @@ int main(int argc, char **argv) printf(" `- Restore returned pid %d\n", pid); kill(pid, SIGUSR1); - if (check_log_file()) { + if (check_log_file("compression of memory pages is enabled")) { printf("Error: log file not overwritten by RPC config file\n"); goto cleanup; }