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) {
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) {
--- /dev/null
+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();