]> git.proxmox.com Git - pve-container.git/commitdiff
lxc start: warn in case of conflicting lxc.idmap entries
authorFriedrich Weber <f.weber@proxmox.com>
Mon, 15 May 2023 13:08:23 +0000 (15:08 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Thu, 25 May 2023 07:10:56 +0000 (09:10 +0200)
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@proxmox.com>
src/PVE/LXC.pm
src/test/Makefile
src/test/idmap-test.pm [new file with mode: 0644]
src/test/run_idmap_tests.pl [new file with mode: 0755]

index d138161dbf7cec10dd5eb0e42f326905e9ba28da..fe1a5cfd51c03dab0fbb9abfb70daf3fffe07cd2 100644 (file)
@@ -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) {
index 8734879647844609b33356650a6351e4ba045acc..82e8967fbf1481886cde5535738cffcb1d5ce0a8 100644 (file)
@@ -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 (file)
index 0000000..db12aa5
--- /dev/null
@@ -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 (executable)
index 0000000..38bb494
--- /dev/null
@@ -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};