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

Friedrich Weber f.weber at proxmox.com
Mon May 15 15:08:23 CEST 2023


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 a conflict is detected.
For the above mapping:

    lxc.idmap: invalid map entry 'u 1000 1000 10':
    container uid 1000 is also mapped by entry 'u 0 100000 65536'

The warning appears in the task log and in the output of `pct start`.

The validation subroutine considers uid and gid mappings separately.
For each of the two types, it makes one pass to detect container id
conflicts and one pass to detect host id conflicts. The subroutine
dies with the first detected conflict.

A failed validation only prints a warning instead of erroring out, to
make sure buggy (or outdated) validation logic does not prevent
containers from starting.

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>
---

Notes:
    Changes from v2:
     - fix style issues: $h->{a}{b} should be $h->{a}->{b}
     - introduce $sides variable to shorten two lines that would now have
       exceeded the 100 character limit
    
    Changes from v1:
     - implemented simpler sorting algorithm suggested by Wolfgang
     - removed "< 100 entries" safeguard. Due to the more efficient
       algorithm it is not needed anymore.
    
     (v2 concluded with a lengthy discussion of algorithm limitations
     which I skip here)

 src/PVE/LXC.pm              |  41 +++++++++
 src/test/Makefile           |   5 +-
 src/test/idmap-test.pm      | 174 ++++++++++++++++++++++++++++++++++++
 src/test/run_idmap_tests.pl |  10 +++
 4 files changed, 229 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 d138161..fe1a5cf 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -2324,6 +2324,41 @@ sub parse_id_maps {
     return ($id_map, $rootuid, $rootgid);
 }
 
+sub validate_id_maps {
+    my ($id_map) = @_;
+
+    # $mappings->{$type}->{$side} = [ { line => $line, start => $start, count => $count }, ... ]
+    #   $type: either "u" or "g"
+    #   $side: either "container" or "host"
+    #   $line: index of this mapping in @$id_map
+    #   $start, $count: interval of this mapping
+    my $mappings = { u => {}, g => {} };
+    for (my $i = 0; $i < scalar(@$id_map); $i++) {
+	my ($type, $ct_start, $host_start, $count) = $id_map->[$i]->@*;
+	my $sides = $mappings->{$type};
+	push $sides->{host}->@*, { line => $i, start => $host_start, count => $count };
+	push $sides->{container}->@*, { line => $i, start => $ct_start, count => $count };
+    }
+
+    # find the first conflict between two consecutive mappings when sorted by their start id
+    for my $type (qw(u g)) {
+	for my $side (qw(container host)) {
+	    my @entries = sort { $a->{start} <=> $b->{start} } $mappings->{$type}->{$side}->@*;
+	    for my $idx (1..scalar(@entries) - 1) {
+		my $previous = $entries[$idx - 1];
+		my $current = $entries[$idx];
+		if ($previous->{start} + $previous->{count} > $current->{start}) {
+		    my $conflict = $current->{start};
+		    my @previous_line = $id_map->[$previous->{line}]->@*;
+		    my @current_line = $id_map->[$current->{line}]->@*;
+		    die "invalid map entry '@current_line': $side ${type}id $conflict "
+		       ."is also mapped by entry '@previous_line'\n";
+		}
+	    }
+	}
+    }
+}
+
 sub userns_command {
     my ($id_map) = @_;
     if (@$id_map) {
@@ -2406,6 +2441,12 @@ sub vm_start {
 
     update_lxc_config($vmid, $conf);
 
+    eval {
+	my ($id_map, undef, undef) = PVE::LXC::parse_id_maps($conf);
+	PVE::LXC::validate_id_maps($id_map);
+    };
+    warn "lxc.idmap: $@" if $@;
+
     my $skiplock_flag_fn = "/run/lxc/skiplock-$vmid";
 
     if ($skiplock) {
diff --git a/src/test/Makefile b/src/test/Makefile
index 8734879..82e8967 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -2,7 +2,7 @@ RUN_USERNS := lxc-usernsexec -m "u:0:`id -u`:1" -m "g:0:`id -g`:1" --
 
 all: test
 
-test: test_setup test_snapshot test_bindmount
+test: test_setup test_snapshot test_bindmount test_idmap
 
 test_setup: run_setup_tests.pl
 	$(RUN_USERNS) ./run_setup_tests.pl
@@ -13,5 +13,8 @@ test_snapshot: run_snapshot_tests.pl
 test_bindmount: bindmount_test.pl
 	$(RUN_USERNS) ./bindmount_test.pl
 
+test_idmap: run_idmap_tests.pl
+	./run_idmap_tests.pl
+
 clean:
 	rm -rf tmprootfs
diff --git a/src/test/idmap-test.pm b/src/test/idmap-test.pm
new file mode 100644
index 0000000..db12aa5
--- /dev/null
+++ b/src/test/idmap-test.pm
@@ -0,0 +1,174 @@
+package PVE::LXC::Test;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use Test::More;
+use Time::HiRes qw (gettimeofday tv_interval);
+
+use PVE::LXC;
+
+subtest 'valid: default config (unprivileged)' => sub {
+    plan tests => 1;
+
+    my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+	    unprivileged => 1,
+	    lxc => [ ['rootfs', 'xyz'] ],
+    });
+
+    $@ = undef;
+    eval {
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    is($@, "", "no error");
+};
+
+subtest 'valid: mapping one user/group to host' => sub {
+    plan tests => 1;
+
+    my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+	    lxc => [
+		['lxc.idmap', 'u 0 100000 1005'],
+		['lxc.idmap', 'g 0 100000 1007'],
+		['lxc.idmap', 'u 1005 1005 1'],
+		['lxc.idmap', 'g 1007 1007 1'],
+		['lxc.idmap', 'u 1006 101006 64530'],
+		['lxc.idmap', 'g 1008 101008 64528'],
+	    ],
+    });
+
+    $@ = undef;
+    eval {
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    is($@, "", "no error");
+};
+
+subtest 'valid: mapping user/group ranges to host' => sub {
+    plan tests => 1;
+
+    my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+	    lxc => [
+		['lxc.idmap', 'u 3000 103000 60000'],
+		['lxc.idmap', 'u 2000 1000 1000'],
+		['lxc.idmap', 'u 0 100000 2000'],
+		['lxc.idmap', 'g 2000 102000 63534'],
+		['lxc.idmap', 'g 1000 2000 1000'],
+		['lxc.idmap', 'g 0 100000 1000'],
+		['lxc.idmap', 'u 63000 263000 2536'],
+		['lxc.idmap', 'g 65534 365534 2'],
+	    ],
+    });
+
+    $@ = undef;
+    eval {
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    is($@, "", "no error");
+};
+
+subtest 'invalid: ambiguous mappings' => sub {
+    plan tests => 10;
+
+    $@ = undef;
+    eval {
+	my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+		lxc => [
+		    ['lxc.idmap', 'u 0 100000 1005'],
+		    ['lxc.idmap', 'u 1005 1005 2'], # maps host uid 1005
+		    ['lxc.idmap', 'u 1007 101007 992'],
+		    ['lxc.idmap', 'u 11000 1000 10'], # maps host uid 1005 again
+		],
+	});
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    like($@, qr/invalid map entry 'u 1005 1005 2'/, '$@ correct');
+    like($@, qr/host uid 1005 is also mapped by entry 'u 11000 1000 10'/, '$@ correct');
+
+    $@ = undef;
+    eval {
+	my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+		lxc => [
+		    ['lxc.idmap', 'u 0 100000 65536'], # maps container uid 1005
+		    ['lxc.idmap', 'u 1005 1005 1'], # maps container uid 1005 again
+		    ['lxc.idmap', 'u 1006 201006 64530'],
+		],
+	});
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+
+    like($@, qr/invalid map entry 'u 1005 1005 1'/, '$@ correct');
+    like($@, qr/container uid 1005 is also mapped by entry 'u 0 100000 65536'/, '$@ correct');
+
+    $@ = undef;
+    eval {
+	my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+		lxc => [
+		    ['lxc.idmap', 'u 5 100000 6'], # 5..10
+		    ['lxc.idmap', 'u 0 200000 11'], # 0..10
+		    ['lxc.idmap', 'u 3 300000 2'], # 3..4
+		],
+	});
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+
+    # this flags line 2 and 3. the input is [ 0..10, 3..4, 5..10 ], and the
+    # algorithm misses the conflict between 0..10 and 5..10.
+    like($@, qr/invalid map entry 'u 3 300000 2'/, '$@ correct');
+    like($@, qr/container uid 3 is also mapped by entry 'u 0 200000 11'/, '$@ correct');
+
+    $@ = undef;
+    eval {
+	my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+		lxc => [
+		    ['lxc.idmap', 'g 0 100000 1001'], # maps container gid 1000
+		    ['lxc.idmap', 'g 1000 1000 10'], # maps container gid 1000 again
+		],
+	});
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    like($@, qr/invalid map entry 'g 1000 1000 10'/, '$@ correct');
+    like($@, qr/container gid 1000 is also mapped by entry 'g 0 100000 1001'/, '$@ correct');
+
+    $@ = undef;
+    eval {
+	my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({
+		lxc => [
+		    ['lxc.idmap', 'g 0 100000 1000'], # maps host gid 100000
+		    ['lxc.idmap', 'g 2000 102000 1000'],
+		    ['lxc.idmap', 'g 3500 99500 5000'], # maps host gid 100000 again
+		],
+	});
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    like($@, qr/invalid map entry 'g 0 100000 1000'/, '$@ correct');
+    like($@, qr/host gid 100000 is also mapped by entry 'g 3500 99500 5000'/, '$@ correct');
+};
+
+subtest 'check performance' => sub {
+    plan tests => 1;
+
+    # generate mapping with 1000 entries
+    my $entries = [];
+    foreach my $i (0..999) {
+	my $first_container_uid = $i * 10;
+	my $first_host_uid = 100000 + $first_container_uid;
+	push @$entries, ['lxc.idmap', "u $first_container_uid $first_host_uid 10"]
+    }
+
+    my ($id_maps, undef, undef) = PVE::LXC::parse_id_maps({ lxc => $entries });
+
+    my $start_time = [ gettimeofday() ];
+    $@ = undef;
+    eval {
+	PVE::LXC::validate_id_maps($id_maps);
+    };
+    my $elapsed = tv_interval($start_time);
+
+    is($@, "", "no error");
+    diag("validation took $elapsed seconds");
+};
+
+done_testing();
diff --git a/src/test/run_idmap_tests.pl b/src/test/run_idmap_tests.pl
new file mode 100755
index 0000000..38bb494
--- /dev/null
+++ b/src/test/run_idmap_tests.pl
@@ -0,0 +1,10 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use TAP::Harness;
+
+my $harness = TAP::Harness->new( { "verbosity" => -2 });
+my $res = $harness->runtests( "idmap-test.pm");
+exit -1 if $res->{failed};
-- 
2.30.2






More information about the pve-devel mailing list