[pve-devel] [PATCH container] lxc start: warn in case of conflicting lxc.idmap entries

Wolfgang Bumiller w.bumiller at proxmox.com
Thu Mar 16 14:59:36 CET 2023


On Thu, Feb 23, 2023 at 06:03:02PM +0100, Friedrich Weber wrote:
> Users can customize the mapping between host and container uids/gids
> by providing `lxc.idmap` entries in the container config. The syntax
> is described in lxc.container.conf(5). One source of errors are
> conflicting entries for one or more uid/gids. An example:
> 
>     ...
>     lxc.idmap: u 0 100000 65536
>     lxc.idmap: u 1000 1000 10
>     ...
> 
> Assuming `root:1000:10` is correctly added to /etc/subuid, starting
> the container fails with an error that is hard to interpret:
> 
>     lxc_map_ids: 3701 newuidmap failed to write mapping
>     "newuidmap: write to uid_map failed: Invalid argument":
>     newuidmap 67993 0 100000 65536 1000 1000 10
> 
> In order to simplify troubleshooting, validate the mapping before
> starting the container and print a warning if conflicting uid/gid
> mappings are detected. For the above mapping:
> 
>     lxc.idmap: invalid map entry 'u 1000 1000 10': container uid 1000
>     is already mapped by entry 'u 0 100000 65536'
> 
> The warning appears in the task log and in the output of `pct start`.
> 
> A failed validation check only prints a warning instead of erroring
> out, to make sure potentially buggy (or outdated) validation code does
> not prevent containers from starting.
> 
> The validation algorithm is quite straightforward and has quadratic
> runtime in the worst case. The kernel allows at most 340 lines in
> uid_map (see user_namespaces(7)), which the algorithm should be able
> to handle in well under 0.5s, but to be safe, skip validation for more
> than 100 entries.
> 
> Note that validation does not take /etc/sub{uid,gid} into account,
> which, if misconfigured, could still prevent the container from
> starting with an error like
> 
>     "newuidmap: uid range [1000-1010) -> [1000-1010) not allowed"
> 
> If needed, validating /etc/sub{uid,gid} could be added in the future.
> 
> Signed-off-by: Friedrich Weber <f.weber at proxmox.com>
> ---
>  The warning could of course be even more detailed, e.g., "container uid range
>  [1000...1009] is already mapped to [101000...101009] by entry 'u 0 100000
>  65536'". But this would require a more sophisticated algorithm, and I'm not
>  sure if the added complexity is worth it -- happy to add it if wanted, though.
> 
>  src/PVE/LXC.pm              |  97 ++++++++++++++++++
>  src/test/Makefile           |   5 +-
>  src/test/idmap-test.pm      | 197 ++++++++++++++++++++++++++++++++++++
>  src/test/run_idmap_tests.pl |  10 ++
>  4 files changed, 308 insertions(+), 1 deletion(-)
>  create mode 100644 src/test/idmap-test.pm
>  create mode 100755 src/test/run_idmap_tests.pl
> 
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 54ee0d9..141f195 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -2313,6 +2313,93 @@ sub parse_id_maps {
>      return ($id_map, $rootuid, $rootgid);
>  }
>  
> +# parse custom id mapping and throw a human-readable error if any
> +# host/container uid/gid is mapped twice. note that the algorithm has quadratic
> +# worst-case runtime, which may be problematic with >1000 mapping entries.
> +# we're unlikely to exceed this because the kernel only allows 340 entries (see
> +# user_namespaces(7)), but if we do, this could be implemented more efficiently
> +# (see e.g. "interval trees").

Both seem a bit excessive to me.

Let's look at the data:
We have a set of ranges consisting of a type, 2 starts and a count.
The types are uids and gids, so we can view those as 2 separate
instances of sets of [ct_start, host_start, count].
Since neither the container nor the host sides must overlap we can -
again - view these as separate sets of container side [start, count] and
host side [start, count].
In other words, we can see the entire id map as just 4 sets of [start,
count] ranges which must not overlap.

So I think all we need to do is sort these by the 'start' value, and for
each element make sure that

    prevous_start + previous_count <= current_start

And yes, that means we need to sort $id_maps twice, once by ct id, once
by host id, and then iterate and do the above check.

Should be much shorter (and faster).

> +sub validate_id_maps {
> +    my ($id_map) = @_;
> +
> +    # keep track of already-mapped uids/gid ranges on the container and host
> +    # sides. each range is an array [mapped_entry, first_id, last_id], where
> +    # mapped_entry is the original config entry (stored for generating error
> +    # messages), and [first_id, last_id] is an (inclusive) interval of mapped
> +    # ids. Ranges are sorted ascendingly by first_id for more efficient
> +    # traversal, and they must not overlap (this would be an invalid mapping).
> +    my $sides_ranges = {
> +	host => { u => [], g => [] },
> +	container => { u => [], g => [] },
> +    };
> +
> +    # try to update the mapping with the requested range.
> +    # return (1, undef, undef) on success and (0, $mapped_entry, $id) on failure,
> +    # where $mapped_entry is the conflicting map entry that already maps $id.
> +    my $map_range = sub {
> +	my ($requested_entry, $mapped_ranges, $requested_first, $requested_count) = @_;
> +	my $requested_last = $requested_first + $requested_count - 1;
> +
> +	# fallback case: insert the requested range at the end
> +	my $mapped_length = scalar(@$mapped_ranges);
> +	my $insert_before = $mapped_length;
> +
> +	foreach my $i (0 .. $mapped_length - 1) {
> +	    my ($mapped_entry, $mapped_first, $mapped_last) = @$mapped_ranges[$i]->@*;
> +
> +	    if ($requested_last < $mapped_first) {
> +		# mapped:          [---]
> +		# requested: [---]
> +		# as the ranges are sorted, the requested range cannot overlap
> +		# with any subsequent mapped range, so we can stop iterating
> +		$insert_before = $i;
> +		last;
> +	    }
> +
> +	    # two shapes of overlapping ranges are possible.
> +	    # mapped:  [---]
> +	    # requested: [...
> +	    #            ^- conflicting id
> +	    return (0, $mapped_entry, $requested_first)
> +		if $mapped_first <= $requested_first <= $mapped_last;
> +
> +	    # mapped:      [...
> +	    # requested: [---]
> +	    #              ^- conflicting id
> +	    return (0, $mapped_entry, $mapped_first)
> +		if $requested_first <= $mapped_first <= $requested_last;
> +	}
> +
> +	my $new_entry = [$requested_entry, $requested_first, $requested_last];
> +	splice @$mapped_ranges, $insert_before, 0, $new_entry;
> +	return (1, undef, undef);
> +    };
> +
> +    my $validate_and_map_range = sub {
> +	my ($map_entry, $side, $type, $first_id, $count) = @_;
> +	my $mapped_ranges = $sides_ranges->{$side}->{$type};
> +
> +	my ($ok, $already_mapped_entry, $already_mapped) = $map_range->(
> +	    $map_entry,
> +	    $mapped_ranges,
> +	    $first_id,
> +	    $count,
> +	);
> +	die "invalid map entry '@$map_entry': " .
> +	    "$side ${type}id $already_mapped is already mapped by entry '@$already_mapped_entry'\n"
> +	    if !$ok;
> +    };
> +
> +    foreach my $map_entry (@$id_map) {
> +	my ($type, $first_ct_id, $first_host_id, $count) = @$map_entry;
> +
> +	$validate_and_map_range->($map_entry, 'container', $type, int($first_ct_id), int($count));
> +	$validate_and_map_range->($map_entry, 'host', $type, int($first_host_id), int($count));
> +    }
> +
> +    return $sides_ranges;
> +}
> +
>  sub userns_command {
>      my ($id_map) = @_;
>      if (@$id_map) {
>...





More information about the pve-devel mailing list