import,pull: add --zero-fill= switch to fill up the destination range with zeroes

When writing a raw image into a section of an existing file or block device
(i.e. with --offset= and --size-max=, as sysupdate does when updating a
partition), we so far wrote precisely as many bytes as the payload was
large, leaving whatever followed in the destination range untouched. For
some payloads that's a problem: a verity signature partition for example
must carry NUL bytes only after the JSON object, and if a shorter signature
is written over a longer one, the tail of the old one survives.

Add a --zero-fill= boolean switch to both "systemd-import raw" and
"systemd-pull raw", which fills up the remainder of the destination range
(from the end of the payload up to --size-max=) with zeroes once the payload
has been written, via blockdev_zero_out(). It defaults to off, and requires
both --offset= and --size-max= to be specified.

(cherry picked from commit aff5aff407d421fbedc9eb0229c40996ca5570a1)
This commit is contained in:
Lennart Poettering
2026-10-16 17:24:51 +01:00
committed by Luca Boccassi
parent c4e7a0fdfe
commit f9cfc019f9
8 changed files with 69 additions and 5 deletions
+24
View File
@@ -6,6 +6,7 @@
#include "sd-event.h"
#include "acl-util.h"
#include "blockdev-util.h"
#include "capability-util.h"
#include "compress.h"
#include "dirent-util.h"
@@ -432,3 +433,26 @@ int import_remove_tree(const char *path, int *userns_fd, ImportFlags flags) {
return 0;
}
int import_zero_fill(int fd, uint64_t offset, uint64_t size_max, uint64_t skip) {
int r;
assert(fd >= 0);
assert(offset != UINT64_MAX);
assert(size_max != UINT64_MAX);
/* Fill up whatever is left of the destination range (i.e. the range starting at 'offset' and
* 'size_max' bytes long, minus the first 'skip' bytes we already wrote) with zeroes, so that no
* remnants of previous, larger contents survive after our data. */
if (skip > size_max)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Number of bytes to skip larger than maximum size of destination range.");
if (offset > UINT64_MAX - skip)
return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Destination offset overflows.");
r = blockdev_zero_out(fd, offset + skip, size_max - skip);
if (r < 0)
return log_error_errno(r, "Failed to fill up destination with zeroes: %m");
return 0;
}
+8 -5
View File
@@ -16,16 +16,17 @@ typedef enum ImportFlags {
IMPORT_DIRECT = 1 << 6, /* import without rename games */
IMPORT_SYNC = 1 << 7, /* fsync() right before we are done */
IMPORT_FOREIGN_UID = 1 << 8, /* tar: go via nsresourced/mountfsd and make owned by foreign UID */
IMPORT_ZERO_FILL = 1 << 9, /* raw: fill up the remainder of the destination range (as defined by offset/size max) with zeroes */
/* When pulling these flags are defined too */
IMPORT_PULL_SETTINGS = 1 << 9, /* download .nspawn settings file */
IMPORT_PULL_ROOTHASH = 1 << 10, /* only for raw: download .roothash file for verity */
IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 11, /* only for raw: download .roothash.p7s file for verity */
IMPORT_PULL_VERITY = 1 << 12, /* only for raw: download .verity file for verity */
IMPORT_PULL_SETTINGS = 1 << 10, /* download .nspawn settings file */
IMPORT_PULL_ROOTHASH = 1 << 11, /* only for raw: download .roothash file for verity */
IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 12, /* only for raw: download .roothash.p7s file for verity */
IMPORT_PULL_VERITY = 1 << 13, /* only for raw: download .verity file for verity */
/* The supported flags for the tar and raw importing */
IMPORT_FLAGS_MASK_TAR = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_DIRECT|IMPORT_SYNC|IMPORT_FOREIGN_UID,
IMPORT_FLAGS_MASK_RAW = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_CONVERT_QCOW2|IMPORT_DIRECT|IMPORT_SYNC,
IMPORT_FLAGS_MASK_RAW = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_CONVERT_QCOW2|IMPORT_DIRECT|IMPORT_SYNC|IMPORT_ZERO_FILL,
/* The supported flags for the tar, raw, oci pulling */
IMPORT_PULL_FLAGS_MASK_TAR = IMPORT_FLAGS_MASK_TAR|IMPORT_PULL_KEEP_DOWNLOAD|IMPORT_PULL_SETTINGS,
@@ -49,3 +50,5 @@ int import_allocate_event_with_signals(sd_event **ret);
int import_make_foreign_userns(int *userns_fd);
int import_remove_tree(const char *path, int *userns_fd, ImportFlags flags);
int import_zero_fill(int fd, uint64_t offset, uint64_t size_max, uint64_t skip);
+7
View File
@@ -217,6 +217,12 @@ static int raw_import_finish(RawImport *i) {
assert(i);
assert(i->output_fd >= 0);
if (FLAGS_SET(i->flags, IMPORT_ZERO_FILL)) {
r = import_zero_fill(i->output_fd, i->offset, i->size_max, i->written_uncompressed);
if (r < 0)
return r;
}
/* Nothing of what is below applies to block devices */
if (S_ISBLK(i->output_stat.st_mode)) {
@@ -510,6 +516,7 @@ int raw_import_start(
assert(local);
assert(!(flags & ~IMPORT_FLAGS_MASK_RAW));
assert(offset == UINT64_MAX || FLAGS_SET(flags, IMPORT_DIRECT));
assert(!FLAGS_SET(flags, IMPORT_ZERO_FILL) || (offset != UINT64_MAX && size_max != UINT64_MAX));
if (!import_validate_local(local, flags))
return -EINVAL;
+10
View File
@@ -343,6 +343,13 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
SET_FLAG(arg_import_flags, IMPORT_SYNC, r);
break;
OPTION_LONG("zero-fill", "BOOL", "Controls whether to fill up the destination range with zeroes"):
r = parse_boolean_argument("--zero-fill=", opts.arg, NULL);
if (r < 0)
return r;
SET_FLAG(arg_import_flags, IMPORT_ZERO_FILL, r);
break;
OPTION_LONG("offset", "BYTES", "Offset to seek to in destination"): {
uint64_t u;
@@ -397,6 +404,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
if (arg_offset != UINT64_MAX && !FLAGS_SET(arg_import_flags, IMPORT_DIRECT))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File offset only supported in --direct mode.");
if (FLAGS_SET(arg_import_flags, IMPORT_ZERO_FILL) && (arg_offset == UINT64_MAX || arg_size_max == UINT64_MAX))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Zero filling only supported if both --offset= and --size-max= are specified.");
if (!arg_image_root) {
r = image_root_pick(arg_runtime_scope < 0 ? RUNTIME_SCOPE_SYSTEM : arg_runtime_scope, arg_class, /* runtime= */ false, &arg_image_root);
if (r < 0)
+7
View File
@@ -12,6 +12,7 @@
#include "fd-util.h"
#include "format-util.h"
#include "hexdecoct.h"
#include "import-common.h"
#include "io-util.h"
#include "log.h"
#include "parse-util.h"
@@ -458,6 +459,12 @@ static int pull_job_curl_on_finished(CurlSlot *slot, CURL *curl, CURLcode result
return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Checksum of downloaded resource does not match expected checksum, yikes."));
}
if (j->disk_fd >= 0 && j->zero_fill) {
r = import_zero_fill(j->disk_fd, j->offset, j->uncompressed_max, j->written_uncompressed);
if (r < 0)
return pull_job_finish(j, r);
}
/* Do a couple of finishing disk operations, but only if we are the sole owner of the file (i.e. no
* offset is specified, which indicates we only own the file partially) */
+1
View File
@@ -76,6 +76,7 @@ typedef struct PullJob {
bool sync;
bool force_memory;
bool zero_fill;
char *authentication_challenge;
} PullJob;
+2
View File
@@ -810,6 +810,7 @@ int raw_pull_start(
assert((verify < 0) || !iovec_is_set(checksum));
assert(!(flags & ~IMPORT_PULL_FLAGS_MASK_RAW));
assert(offset == UINT64_MAX || FLAGS_SET(flags, IMPORT_DIRECT));
assert(!FLAGS_SET(flags, IMPORT_ZERO_FILL) || (offset != UINT64_MAX && size_max != UINT64_MAX));
assert(!(flags & (IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY)) || !(flags & IMPORT_DIRECT));
assert(!(flags & (IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY)) || !iovec_is_set(checksum));
@@ -859,6 +860,7 @@ int raw_pull_start(
p->raw_job->uncompressed_max = size_max;
if (offset != UINT64_MAX)
p->raw_job->offset = p->offset = offset;
p->raw_job->zero_fill = FLAGS_SET(flags, IMPORT_ZERO_FILL);
if (!FLAGS_SET(flags, IMPORT_DIRECT)) {
r = pull_find_old_etags(url, p->image_root, DT_REG, ".raw-", ".raw", &p->raw_job->old_etags);
+10
View File
@@ -459,6 +459,13 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
SET_FLAG(arg_import_flags, IMPORT_SYNC, r);
break;
OPTION_LONG("zero-fill", "BOOL", "Controls whether to fill up the destination range with zeroes"):
r = parse_boolean_argument("--zero-fill=", opts.arg, NULL);
if (r < 0)
return r;
SET_FLAG(arg_import_flags, IMPORT_ZERO_FILL, r);
break;
OPTION_LONG("offset", "BYTES", "Offset to seek to in destination"): {
uint64_t u;
@@ -523,6 +530,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
if (arg_offset != UINT64_MAX && !FLAGS_SET(arg_import_flags, IMPORT_DIRECT))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File offset only supported in --direct mode.");
if (FLAGS_SET(arg_import_flags, IMPORT_ZERO_FILL) && (arg_offset == UINT64_MAX || arg_size_max == UINT64_MAX))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Zero filling only supported if both --offset= and --size-max= are specified.");
if (iovec_is_set(&arg_checksum) && (arg_import_flags & (IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY)) != 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Literal checksum verification only supported if no associated files are downloaded.");