public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Friedrich Weber <f.weber@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v3 container] lxc start: warn in case of conflicting lxc.idmap entries
Date: Mon, 15 May 2023 15:08:23 +0200	[thread overview]
Message-ID: <20230515130823.144871-1-f.weber@proxmox.com> (raw)

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

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





             reply	other threads:[~2023-05-15 13:08 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-05-15 13:08 Friedrich Weber [this message]
2023-05-25  7:11 ` [pve-devel] applied: " Wolfgang Bumiller

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230515130823.144871-1-f.weber@proxmox.com \
    --to=f.weber@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal