* [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping
@ 2026-02-23 13:04 Filip Schauer
2026-02-23 13:04 ` [PATCH common 1/8] tools: export O_CLOEXEC constant Filip Schauer
` (7 more replies)
0 siblings, 8 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Add support for configuring UID/GID mappings on individual container
mount points without affecting the global container mapping.
A new "idmap" mount point option accepts space-separated mappings:
```
idmap=type:ct:host:len type:ct:host:len ...
```
type: can be either 'u' or 'g'
ct: ID as seen inside the container
host: corresponding ID on the host
len: number of consecutive IDs to map
Unmapped ranges inherit the container's ID mapping.
Example to pass through the host UID & GID 1005:
```
mp0: /mnt/data,mp=/data,idmap=u:1005:1005:1 g:1005:1005:1
```
This allows, for example, passing through a directory owned by a
specific user on the host to a specific user inside the container,
without changing the ownership on the host or configuring an ID mapping
for the entire container.
Build/Bump order:
* pve-common
* pve-container
* pve-manager
pve-common:
Filip Schauer (3):
tools: export O_CLOEXEC constant
syscall: add missing mount attribute constants
tools: add mount_setattr syscall
src/PVE/Syscall.pm | 3 +++
src/PVE/Tools.pm | 10 ++++++++++
2 files changed, 13 insertions(+)
pve-container:
Filip Schauer (4):
namespaces: relax prototype of run_in_userns
namespaces: refactor run_in_userns
namespaces: add helper to create user namespace from idmap
implement per-mountpoint uid/gid mapping
src/PVE/LXC.pm | 85 ++++++++++++++++++++++++++++++++++++++-
src/PVE/LXC/Config.pm | 7 ++++
src/PVE/LXC/Namespaces.pm | 63 ++++++++++++++++++++++++-----
src/lxc-pve-prestart-hook | 14 +++++++
4 files changed, 157 insertions(+), 12 deletions(-)
pve-manager:
Filip Schauer (1):
ui: lxc/MPEdit: add "idmap" option
www/manager6/lxc/MPEdit.js | 203 +++++++++++++++++++++++++++++++++++++
1 file changed, 203 insertions(+)
Summary over all repositories:
7 files changed, 373 insertions(+), 12 deletions(-)
--
Generated by git-murpp 0.6.0
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH common 1/8] tools: export O_CLOEXEC constant
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH common 2/8] syscall: add missing mount attribute constants Filip Schauer
` (6 subsequent siblings)
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/Tools.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index 39c7155..d226fb6 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -55,6 +55,7 @@ our @EXPORT_OK = qw(
file_copy
get_host_arch
O_PATH
+ O_CLOEXEC
O_TMPFILE
AT_EMPTY_PATH
AT_FDCWD
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH common 2/8] syscall: add missing mount attribute constants
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-23 13:04 ` [PATCH common 1/8] tools: export O_CLOEXEC constant Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH common 3/8] tools: add mount_setattr syscall Filip Schauer
` (5 subsequent siblings)
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/Syscall.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/PVE/Syscall.pm b/src/PVE/Syscall.pm
index 7a931d7..a5753d9 100644
--- a/src/PVE/Syscall.pm
+++ b/src/PVE/Syscall.pm
@@ -67,6 +67,8 @@ BEGIN {
MOUNT_ATTR_NOATIME => 0x0000_0010,
MOUNT_ATTR_STRICTATIME => 0x0000_0020,
MOUNT_ATTR_NODIRATIME => 0x0000_0080,
+ MOUNT_ATTR_IDMAP => 0x0010_0000,
+ MOUNT_ATTR_NOSYMFOLLOW => 0x0020_0000,
FSPICK_CLOEXEC => 0x0000_0001,
FSPICK_SYMLINK_NOFOLLOW => 0x0000_0002,
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH common 3/8] tools: add mount_setattr syscall
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-23 13:04 ` [PATCH common 1/8] tools: export O_CLOEXEC constant Filip Schauer
2026-02-23 13:04 ` [PATCH common 2/8] syscall: add missing mount attribute constants Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH container 4/8] namespaces: relax prototype of run_in_userns Filip Schauer
` (4 subsequent siblings)
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
The mount_setattr syscall can change attributes of an existing mount.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/Syscall.pm | 1 +
src/PVE/Tools.pm | 9 +++++++++
2 files changed, 10 insertions(+)
diff --git a/src/PVE/Syscall.pm b/src/PVE/Syscall.pm
index a5753d9..53e0f4b 100644
--- a/src/PVE/Syscall.pm
+++ b/src/PVE/Syscall.pm
@@ -27,6 +27,7 @@ BEGIN {
renameat2 => &SYS_renameat2,
open_tree => &SYS_open_tree,
move_mount => &SYS_move_mount,
+ mount_setattr => &SYS_mount_setattr,
fsopen => &SYS_fsopen,
fsconfig => &SYS_fsconfig,
fsmount => &SYS_fsmount,
diff --git a/src/PVE/Tools.pm b/src/PVE/Tools.pm
index d226fb6..5b15874 100644
--- a/src/PVE/Tools.pm
+++ b/src/PVE/Tools.pm
@@ -1682,6 +1682,15 @@ sub move_mount($$$$$) {
);
}
+sub mount_setattr($$$$$$$) {
+ my ($dirfd, $path, $flags, $attr_set, $attr_clr, $propagation, $userns_fd) = @_;
+
+ my $attr = pack("Q4", $attr_set, $attr_clr, $propagation, $userns_fd);
+ return 0 ==
+ syscall(&PVE::Syscall::mount_setattr, int($dirfd), $path, int($flags), $attr,
+ length($attr));
+}
+
sub fsopen($$) {
my ($fsname, $flags) = @_;
return PVE::Syscall::file_handle_result(syscall(&PVE::Syscall::fsopen, $fsname, int($flags)));
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH container 4/8] namespaces: relax prototype of run_in_userns
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
` (2 preceding siblings ...)
2026-02-23 13:04 ` [PATCH common 3/8] tools: add mount_setattr syscall Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH container 5/8] namespaces: refactor run_in_userns Filip Schauer
` (3 subsequent siblings)
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Allow passing a coderef stored in a private variable as the $code
argument. This fixes the following compile-time error:
Type of arg 1 to PVE::LXC::Namespaces::run_in_userns must be block or
sub {} (not private variable) at PVE/LXC/Create.pm line 736
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC/Namespaces.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PVE/LXC/Namespaces.pm b/src/PVE/LXC/Namespaces.pm
index aa62659..3b86262 100644
--- a/src/PVE/LXC/Namespaces.pm
+++ b/src/PVE/LXC/Namespaces.pm
@@ -25,7 +25,7 @@ my sub set_id_map($$) {
PVE::Tools::run_command(['newuidmap', $pid, @uid_args]) if scalar(@uid_args);
}
-sub run_in_userns(&;$) {
+sub run_in_userns($;$) {
my ($code, $id_map) = @_;
socketpair(my $sp, my $sc, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
or die "socketpair: $!\n";
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH container 5/8] namespaces: refactor run_in_userns
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
` (3 preceding siblings ...)
2026-02-23 13:04 ` [PATCH container 4/8] namespaces: relax prototype of run_in_userns Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH container 6/8] namespaces: add helper to create user namespace from idmap Filip Schauer
` (2 subsequent siblings)
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC/Namespaces.pm | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/PVE/LXC/Namespaces.pm b/src/PVE/LXC/Namespaces.pm
index 3b86262..477d0ac 100644
--- a/src/PVE/LXC/Namespaces.pm
+++ b/src/PVE/LXC/Namespaces.pm
@@ -25,6 +25,19 @@ my sub set_id_map($$) {
PVE::Tools::run_command(['newuidmap', $pid, @uid_args]) if scalar(@uid_args);
}
+my sub sync_send {
+ my ($fh, $msg) = @_;
+
+ syswrite($fh, $msg) == length($msg) or die "sync write failed: $!\n";
+}
+
+my sub sync_recv {
+ my ($fh, $expect) = @_;
+
+ my $received = <$fh>;
+ die "sync read failed\n" if $received ne $expect;
+}
+
sub run_in_userns($;$) {
my ($code, $id_map) = @_;
socketpair(my $sp, my $sc, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
@@ -32,25 +45,23 @@ sub run_in_userns($;$) {
my $child = sub {
close($sp);
PVE::Tools::unshare(CLONE_NEWUSER | CLONE_NEWNS) or die "unshare(NEWUSER|NEWNS): $!\n";
- syswrite($sc, "1\n") == 2 or die "write: $!\n";
+ sync_send($sc, "1\n");
shutdown($sc, 1);
- my $two = <$sc>;
- die "failed to sync with parent process\n" if $two ne "2\n";
+ sync_recv($sc, "2\n");
close($sc);
$! = undef;
($(, $)) = (0, 0);
- die "$!\n" if $!;
+ die "setgid(0): $!\n" if $!;
($<, $>) = (0, 0);
- die "$!\n" if $!;
+ die "setuid(0): $!\n" if $!;
return $code->();
};
my $parent = sub {
my ($pid) = @_;
close($sc);
- my $one = <$sp>;
- die "failed to sync with userprocess\n" if $one ne "1\n";
+ sync_recv($sp, "1\n");
set_id_map($pid, $id_map);
- syswrite($sp, "2\n") == 2 or die "write: $!\n";
+ sync_send($sp, "2\n");
close($sp);
};
PVE::Tools::run_fork($child, { afterfork => $parent });
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH container 6/8] namespaces: add helper to create user namespace from idmap
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
` (4 preceding siblings ...)
2026-02-23 13:04 ` [PATCH container 5/8] namespaces: refactor run_in_userns Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-23 13:04 ` [PATCH container 7/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-23 13:04 ` [PATCH manager 8/8] ui: lxc/MPEdit: add "idmap" option Filip Schauer
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC/Namespaces.pm | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/PVE/LXC/Namespaces.pm b/src/PVE/LXC/Namespaces.pm
index 477d0ac..7836d06 100644
--- a/src/PVE/LXC/Namespaces.pm
+++ b/src/PVE/LXC/Namespaces.pm
@@ -3,10 +3,10 @@ package PVE::LXC::Namespaces;
use strict;
use warnings;
-use Fcntl qw(O_WRONLY);
+use Fcntl qw(O_WRONLY O_RDONLY);
use Socket;
-use PVE::Tools qw(CLONE_NEWNS CLONE_NEWUSER);
+use PVE::Tools qw(CLONE_NEWNS CLONE_NEWUSER O_CLOEXEC);
my sub set_id_map($$) {
my ($pid, $id_map) = @_;
@@ -67,4 +67,34 @@ sub run_in_userns($;$) {
PVE::Tools::run_fork($child, { afterfork => $parent });
}
+# Create a new user namespace with the provided idmap applied.
+# Returns a file handle to the namespace.
+sub new_userns($) {
+ my ($id_map) = @_;
+ socketpair(my $sp, my $sc, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
+ or die "socketpair: $!\n";
+ my $userns_fh;
+ my $child = sub {
+ close($sp);
+ PVE::Tools::unshare(CLONE_NEWUSER) or die "unshare(NEWUSER): $!\n";
+ sync_send($sc, "1\n");
+ shutdown($sc, 1);
+ sync_recv($sc, "2\n");
+ close($sc);
+ };
+ my $parent = sub {
+ my ($pid) = @_;
+ close($sc);
+ sync_recv($sp, "1\n");
+ set_id_map($pid, $id_map);
+ sysopen($userns_fh, "/proc/$pid/ns/user", O_RDONLY | O_CLOEXEC)
+ or die "Failed to open user namespace of child: $!\n";
+ sync_send($sp, "2\n");
+ close($sp);
+ };
+ PVE::Tools::run_fork($child, { afterfork => $parent });
+
+ return $userns_fh;
+}
+
1;
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
` (5 preceding siblings ...)
2026-02-23 13:04 ` [PATCH container 6/8] namespaces: add helper to create user namespace from idmap Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
2026-02-27 14:39 ` Maximiliano Sandoval
2026-02-27 15:34 ` Wolfgang Bumiller
2026-02-23 13:04 ` [PATCH manager 8/8] ui: lxc/MPEdit: add "idmap" option Filip Schauer
7 siblings, 2 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Add support for customizing UID/GID mappings on individual mount points
without affecting the entire container.
A new "idmap" mount point option accepts space-separated mappings:
```
idmap=type:ct:host:len type:ct:host:len ...
```
type: can be either 'u' or 'g'
ct: ID as seen inside the container
host: corresponding ID on the host
len: number of consecutive IDs to map
Unmapped ranges inherit the container's ID mapping.
Example to pass through the host UID & GID 1005:
```
mp0: /mnt/data,mp=/data,idmap=u:1005:1005:1 g:1005:1005:1
```
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC.pm | 85 ++++++++++++++++++++++++++++++++++++++-
src/PVE/LXC/Config.pm | 7 ++++
src/lxc-pve-prestart-hook | 14 +++++++
3 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 2c02e9a..ec7cb01 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -11,6 +11,7 @@ use File::Path;
use File::Spec;
use IO::Poll qw(POLLIN POLLHUP);
use IO::Socket::UNIX;
+use List::Util qw(max min);
use POSIX qw(EINTR);
use Socket;
use Time::HiRes qw (gettimeofday);
@@ -43,6 +44,7 @@ use PVE::Syscall qw(:fsmount);
use PVE::LXC::CGroup;
use PVE::LXC::Config;
use PVE::LXC::Monitor;
+use PVE::LXC::Namespaces;
use PVE::LXC::Tools;
my $have_sdn;
@@ -2438,7 +2440,12 @@ sub device_passthrough_hotplug : prototype($$$) {
sub mountpoint_hotplug : prototype($$$$$) {
my ($vmid, $conf, $opt, $mp, $storage_cfg) = @_;
- my (undef, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
+ my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
+ my $mp_userns_fh;
+ if ($mp->{idmap}) {
+ my $mp_id_map = parse_mountpoint_idmap($id_map, $mp);
+ $mp_userns_fh = PVE::LXC::Namespaces::new_userns($mp_id_map);
+ }
# We do the rest in a fork with an unshared mount namespace:
# -) change our apparmor profile to 'pve-container-mounthotplug', which is '/usr/bin/lxc-start'
@@ -2474,6 +2481,18 @@ sub mountpoint_hotplug : prototype($$$$$) {
my $mount_fd = mountpoint_stage($mp, $dir, $storage_cfg, undef, $root_uid, $root_gid);
+ if ($mp_userns_fh) {
+ PVE::Tools::mount_setattr(
+ fileno($mount_fd),
+ '',
+ PVE::Tools::AT_EMPTY_PATH,
+ &PVE::Syscall::MOUNT_ATTR_IDMAP,
+ 0,
+ 0,
+ fileno($mp_userns_fh),
+ ) or die "mount_setattr: $!\n";
+ }
+
PVE::Tools::setns(fileno($ct_mnt_ns), PVE::Tools::CLONE_NEWNS);
chdir('/')
or die "failed to change root directory within the container's mount namespace: $!\n";
@@ -2989,6 +3008,70 @@ sub map_ct_gid_to_host {
return map_ct_id_to_host($gid, $id_map, 'g');
}
+sub parse_mountpoint_idmap {
+ my ($id_map, $mp) = @_;
+
+ die "mount point does not specify an idmap\n" if !$mp->{idmap};
+
+ # Parse the user-friendly mount-specific ID map
+ # This maps IDs as seen in the container to IDs as seen on disk.
+ my $user_mp_id_map = [];
+ for my $entry (split(' ', $mp->{idmap})) {
+ $entry =~ /^([ug]):(\d+):(\d+):(\d+)$/ or die "failed to parse mount point idmap: $entry\n";
+ push @$user_mp_id_map, [$1, $2, $3, $4];
+ }
+
+ validate_id_maps($user_mp_id_map);
+
+ # Convert the user friendly mp.idmap to the actual mapping to be applied via mount_setattr.
+ # Provided by the config:
+ # lxc.idmap: ID in Container --> ID on Host
+ # mp.idmap: ID in Container --> ID on Disk
+ #
+ # Convert to: ID on Disk --> ID on Host
+ my $result = [];
+ for my $type ('u', 'g') {
+ my @ct_chunks = grep { $_->[0] eq $type } @$id_map;
+ next if !@ct_chunks;
+
+ my @exceptions = sort { $a->[1] <=> $b->[1] } grep { $_->[0] eq $type } @$user_mp_id_map;
+
+ for my $chunk (@ct_chunks) {
+ my (undef, $ct_start, $host_start, $len) = @$chunk;
+ my $ct_end = $ct_start + $len;
+
+ # Find exceptions that fall within this specific lxc.idmap chunk
+ my @chunk_exc = grep { $_->[1] < $ct_end && $_->[1] + $_->[3] > $ct_start } @exceptions;
+ push @chunk_exc, [$type, $ct_end, undef, 0]; # ensure the trailing gap is mapped
+
+ my $ct = $ct_start;
+ for my $exc (@chunk_exc) {
+ my (undef, $exc_ct, $exc_disk, $exc_len) = @$exc;
+
+ my $clamped_ct = max($exc_ct, $ct_start);
+ my $clamped_len = min($exc_ct + $exc_len, $ct_end) - $clamped_ct;
+
+ # Identity mapping for unmapped ranges
+ if ($ct < $clamped_ct) {
+ my $host = $host_start + ($ct - $ct_start);
+ push @$result, [$type, $host, $host, $clamped_ct - $ct];
+ }
+
+ # Map the IDs on Disk to the Host IDs.
+ if ($clamped_len > 0) {
+ my $disk = $exc_disk + $clamped_ct - $exc_ct;
+ my $host = $host_start + $clamped_ct - $ct_start;
+ push @$result, [$type, $disk, $host, $clamped_len];
+ }
+
+ $ct = $clamped_ct + $clamped_len;
+ }
+ }
+ }
+
+ return $result;
+}
+
sub userns_command {
my ($id_map) = @_;
if (@$id_map) {
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index 5442586..9f56bc7 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -369,6 +369,13 @@ my $rootfs_desc = {
format_description => 'opt[;opt...]',
pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
},
+ idmap => {
+ optional => 1,
+ type => 'string',
+ description => 'Map specific UIDs/GIDs to specific host UIDs/GIDs for this mount point',
+ format_description => 'id-type:id-mount:id-host:id-range id-type:id-mount:...',
+ pattern => qr/^(?:[ug]:[0-9]+:[0-9]+:[1-9][0-9]*)(?: [ug]:[0-9]+:[0-9]+:[1-9][0-9]*)*$/,
+ },
ro => {
type => 'boolean',
description => 'Read-only mount point',
diff --git a/src/lxc-pve-prestart-hook b/src/lxc-pve-prestart-hook
index 9862509..6e500a8 100755
--- a/src/lxc-pve-prestart-hook
+++ b/src/lxc-pve-prestart-hook
@@ -95,6 +95,20 @@ PVE::LXC::Tools::lxc_hook(
$mountpoint, $dir, $storage_cfg, undef, $root_uid, $root_gid,
);
+ if ($mountpoint->{idmap}) {
+ my $mp_id_map = PVE::LXC::parse_mountpoint_idmap($id_map, $mountpoint);
+ my $usernsfh = PVE::LXC::Namespaces::new_userns($mp_id_map);
+ PVE::Tools::mount_setattr(
+ fileno($mount_fd),
+ '',
+ PVE::Tools::AT_EMPTY_PATH,
+ &PVE::Syscall::MOUNT_ATTR_IDMAP,
+ 0,
+ 0,
+ fileno($usernsfh),
+ ) or die "mount_setattr: $!\n";
+ }
+
my ($dest_dir, $dest_base_fd, $keep_attrs);
if ($rootdir_fd) {
# Mount relative to the rootdir fd.
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH manager 8/8] ui: lxc/MPEdit: add "idmap" option
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
` (6 preceding siblings ...)
2026-02-23 13:04 ` [PATCH container 7/8] implement per-mountpoint uid/gid mapping Filip Schauer
@ 2026-02-23 13:04 ` Filip Schauer
7 siblings, 0 replies; 17+ messages in thread
From: Filip Schauer @ 2026-02-23 13:04 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
www/manager6/lxc/MPEdit.js | 203 +++++++++++++++++++++++++++++++++++++
1 file changed, 203 insertions(+)
diff --git a/www/manager6/lxc/MPEdit.js b/www/manager6/lxc/MPEdit.js
index 4ed2d07b..b07f8882 100644
--- a/www/manager6/lxc/MPEdit.js
+++ b/www/manager6/lxc/MPEdit.js
@@ -48,6 +48,7 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
setMPOpt('acl', values.acl);
setMPOpt('replicate', values.replicate);
setMPOpt('keepattrs', values.keepattrs);
+ setMPOpt('idmap', values.idmap);
let res = {};
res[confid] = PVE.Parser.printLxcMountPoint(me.mp);
@@ -132,6 +133,12 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
me.getViewModel().set('type', rec.data.type);
},
},
+ 'grid proxmoxintegerfield': {
+ change: 'idmapChanged',
+ },
+ 'field[name=idmap]': {
+ change: 'setGridData',
+ },
},
init: function (view) {
let _me = this;
@@ -154,6 +161,90 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
view.setVMConfig(view.vmconfig);
}
},
+ idmapChanged: function (field, value) {
+ let me = this;
+ if (value === null) {
+ return;
+ }
+ let record = field.getWidgetRecord();
+ if (record === undefined) {
+ return;
+ }
+ let col = field.getWidgetColumn();
+ record.set(col.dataIndex, value);
+ record.commit();
+
+ me.updateIDMapField();
+ },
+ idmapTypeChanged: function (button, newValue) {
+ let me = this;
+ let record = button.getWidgetRecord();
+ if (record === undefined || record.get('type') === newValue) {
+ return;
+ }
+ record.set('type', newValue);
+ record.commit();
+ me.updateIDMapField();
+ },
+ addIDMap: function () {
+ let me = this;
+ me.lookup('idmaps').getStore().add({
+ type: 'u',
+ ct: '',
+ host: '',
+ length: '',
+ });
+
+ me.updateIDMapField();
+ },
+ removeIDMap: function (field) {
+ let me = this;
+ let record = field.getWidgetRecord();
+ if (record === undefined) {
+ return;
+ }
+
+ me.lookup('idmaps').getStore().remove(record);
+ me.updateIDMapField();
+ },
+ updateIDMapField: function () {
+ let me = this;
+
+ let idmaps = [];
+ me.lookup('idmaps')
+ .getStore()
+ .each((rec) => {
+ let { type, ct, host, length } = rec.data;
+ idmaps.push(`${type}:${ct}:${host}:${length}`);
+ });
+
+ let field = me.lookup('idmap');
+ field.suspendEvent('change');
+ field.setValue(idmaps.join(' '));
+ field.resumeEvent('change');
+ },
+ parseIDMap: function (idmap) {
+ let [type, ct, host, length] = idmap.split(':');
+
+ let record = {
+ type,
+ ct,
+ host,
+ length,
+ };
+
+ return record;
+ },
+ setGridData: function (field, value) {
+ let me = this;
+ if (!value) {
+ return;
+ }
+
+ value = value.split(' ');
+ let records = value.map((idmap) => me.parseIDMap(idmap));
+ me.lookup('idmaps').getStore().setData(records);
+ },
},
viewModel: {
@@ -354,6 +445,118 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
},
},
],
+
+ advancedColumnB: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('ID Mapping'),
+ },
+ {
+ xtype: 'fieldcontainer',
+ items: [
+ {
+ xtype: 'grid',
+ height: 200,
+ scrollable: true,
+ reference: 'idmaps',
+ viewConfig: {
+ emptyText: gettext('No ID maps configured'),
+ },
+ store: {
+ fields: ['type', 'ct', 'host', 'length'],
+ data: [],
+ },
+ columns: [
+ {
+ text: gettext('ID Type'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'type',
+ widget: {
+ xtype: 'segmentedbutton',
+ items: [
+ {
+ text: 'UID',
+ value: 'u',
+ },
+ {
+ text: 'GID',
+ value: 'g',
+ },
+ ],
+ listeners: {
+ change: 'idmapTypeChanged',
+ },
+ },
+ flex: 1,
+ },
+ {
+ text: gettext('Container'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'ct',
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ margin: '4 0',
+ emptyText: gettext('Container'),
+ isFormField: false,
+ allowBlank: false,
+ minValue: 0,
+ },
+ flex: 1,
+ },
+ {
+ text: gettext('Host'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'host',
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ margin: '4 0',
+ emptyText: gettext('Host'),
+ isFormField: false,
+ allowBlank: false,
+ minValue: 0,
+ },
+ flex: 1,
+ },
+ {
+ text: gettext('Length'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'length',
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ margin: '4 0',
+ emptyText: gettext('Length'),
+ isFormField: false,
+ allowBlank: false,
+ minValue: 1,
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'widgetcolumn',
+ width: 40,
+ widget: {
+ xtype: 'button',
+ margin: '4 0',
+ iconCls: 'fa fa-trash-o',
+ handler: 'removeIDMap',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'button',
+ text: gettext('Add'),
+ iconCls: 'fa fa-plus-circle',
+ handler: 'addIDMap',
+ },
+ {
+ xtype: 'hidden',
+ reference: 'idmap',
+ name: 'idmap',
+ },
+ ],
});
Ext.define('PVE.lxc.MountPointEdit', {
--
2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-23 13:04 ` [PATCH container 7/8] implement per-mountpoint uid/gid mapping Filip Schauer
@ 2026-02-27 14:39 ` Maximiliano Sandoval
2026-02-27 15:01 ` Daniel Kral
2026-02-27 15:34 ` Wolfgang Bumiller
1 sibling, 1 reply; 17+ messages in thread
From: Maximiliano Sandoval @ 2026-02-27 14:39 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
Filip Schauer <f.schauer@proxmox.com> writes:
> Add support for customizing UID/GID mappings on individual mount points
> without affecting the entire container.
>
> A new "idmap" mount point option accepts space-separated mappings:
> ```
> idmap=type:ct:host:len type:ct:host:len ...
> ```
>
> type: can be either 'u' or 'g'
> ct: ID as seen inside the container
> host: corresponding ID on the host
> len: number of consecutive IDs to map
>
> Unmapped ranges inherit the container's ID mapping.
>
> Example to pass through the host UID & GID 1005:
> ```
> mp0: /mnt/data,mp=/data,idmap=u:1005:1005:1 g:1005:1005:1
> ```
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> src/PVE/LXC.pm | 85 ++++++++++++++++++++++++++++++++++++++-
> src/PVE/LXC/Config.pm | 7 ++++
> src/lxc-pve-prestart-hook | 14 +++++++
> 3 files changed, 105 insertions(+), 1 deletion(-)
>
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 2c02e9a..ec7cb01 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -11,6 +11,7 @@ use File::Path;
> use File::Spec;
> use IO::Poll qw(POLLIN POLLHUP);
> use IO::Socket::UNIX;
> +use List::Util qw(max min);
> use POSIX qw(EINTR);
> use Socket;
> use Time::HiRes qw (gettimeofday);
> @@ -43,6 +44,7 @@ use PVE::Syscall qw(:fsmount);
> use PVE::LXC::CGroup;
> use PVE::LXC::Config;
> use PVE::LXC::Monitor;
> +use PVE::LXC::Namespaces;
> use PVE::LXC::Tools;
>
> my $have_sdn;
> @@ -2438,7 +2440,12 @@ sub device_passthrough_hotplug : prototype($$$) {
> sub mountpoint_hotplug : prototype($$$$$) {
> my ($vmid, $conf, $opt, $mp, $storage_cfg) = @_;
>
> - my (undef, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
> + my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
> + my $mp_userns_fh;
> + if ($mp->{idmap}) {
> + my $mp_id_map = parse_mountpoint_idmap($id_map, $mp);
> + $mp_userns_fh = PVE::LXC::Namespaces::new_userns($mp_id_map);
> + }
>
> # We do the rest in a fork with an unshared mount namespace:
> # -) change our apparmor profile to 'pve-container-mounthotplug', which is '/usr/bin/lxc-start'
> @@ -2474,6 +2481,18 @@ sub mountpoint_hotplug : prototype($$$$$) {
>
> my $mount_fd = mountpoint_stage($mp, $dir, $storage_cfg, undef, $root_uid, $root_gid);
>
> + if ($mp_userns_fh) {
> + PVE::Tools::mount_setattr(
> + fileno($mount_fd),
> + '',
> + PVE::Tools::AT_EMPTY_PATH,
> + &PVE::Syscall::MOUNT_ATTR_IDMAP,
> + 0,
> + 0,
> + fileno($mp_userns_fh),
> + ) or die "mount_setattr: $!\n";
> + }
> +
> PVE::Tools::setns(fileno($ct_mnt_ns), PVE::Tools::CLONE_NEWNS);
> chdir('/')
> or die "failed to change root directory within the container's mount namespace: $!\n";
> @@ -2989,6 +3008,70 @@ sub map_ct_gid_to_host {
> return map_ct_id_to_host($gid, $id_map, 'g');
> }
>
> +sub parse_mountpoint_idmap {
> + my ($id_map, $mp) = @_;
> +
> + die "mount point does not specify an idmap\n" if !$mp->{idmap};
> +
> + # Parse the user-friendly mount-specific ID map
> + # This maps IDs as seen in the container to IDs as seen on disk.
> + my $user_mp_id_map = [];
> + for my $entry (split(' ', $mp->{idmap})) {
> + $entry =~ /^([ug]):(\d+):(\d+):(\d+)$/ or die "failed to parse mount point idmap: $entry\n";
> + push @$user_mp_id_map, [$1, $2, $3, $4];
> + }
> +
> + validate_id_maps($user_mp_id_map);
> +
> + # Convert the user friendly mp.idmap to the actual mapping to be applied via mount_setattr.
> + # Provided by the config:
> + # lxc.idmap: ID in Container --> ID on Host
> + # mp.idmap: ID in Container --> ID on Disk
> + #
> + # Convert to: ID on Disk --> ID on Host
> + my $result = [];
> + for my $type ('u', 'g') {
> + my @ct_chunks = grep { $_->[0] eq $type } @$id_map;
> + next if !@ct_chunks;
> +
> + my @exceptions = sort { $a->[1] <=> $b->[1] } grep { $_->[0] eq $type } @$user_mp_id_map;
> +
> + for my $chunk (@ct_chunks) {
> + my (undef, $ct_start, $host_start, $len) = @$chunk;
> + my $ct_end = $ct_start + $len;
> +
> + # Find exceptions that fall within this specific lxc.idmap chunk
> + my @chunk_exc = grep { $_->[1] < $ct_end && $_->[1] + $_->[3] > $ct_start } @exceptions;
> + push @chunk_exc, [$type, $ct_end, undef, 0]; # ensure the trailing gap is mapped
> +
> + my $ct = $ct_start;
> + for my $exc (@chunk_exc) {
> + my (undef, $exc_ct, $exc_disk, $exc_len) = @$exc;
> +
> + my $clamped_ct = max($exc_ct, $ct_start);
> + my $clamped_len = min($exc_ct + $exc_len, $ct_end) - $clamped_ct;
> +
> + # Identity mapping for unmapped ranges
> + if ($ct < $clamped_ct) {
> + my $host = $host_start + ($ct - $ct_start);
> + push @$result, [$type, $host, $host, $clamped_ct - $ct];
> + }
> +
> + # Map the IDs on Disk to the Host IDs.
> + if ($clamped_len > 0) {
> + my $disk = $exc_disk + $clamped_ct - $exc_ct;
> + my $host = $host_start + $clamped_ct - $ct_start;
> + push @$result, [$type, $disk, $host, $clamped_len];
> + }
> +
> + $ct = $clamped_ct + $clamped_len;
> + }
> + }
> + }
> +
> + return $result;
> +}
> +
> sub userns_command {
> my ($id_map) = @_;
> if (@$id_map) {
> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
> index 5442586..9f56bc7 100644
> --- a/src/PVE/LXC/Config.pm
> +++ b/src/PVE/LXC/Config.pm
> @@ -369,6 +369,13 @@ my $rootfs_desc = {
> format_description => 'opt[;opt...]',
> pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
> },
> + idmap => {
> + optional => 1,
> + type => 'string',
> + description => 'Map specific UIDs/GIDs to specific host
> UIDs/GIDs for this mount point',
I think this would benefit from a verbose_description, it is not clear
to me whether the syntax 123:456:1 maps 123 from the root to 456 on the
container or the other way around. Perhaps an explicit example would be
helpful.
> + format_description => 'id-type:id-mount:id-host:id-range id-type:id-mount:...',
> + pattern => qr/^(?:[ug]:[0-9]+:[0-9]+:[1-9][0-9]*)(?: [ug]:[0-9]+:[0-9]+:[1-9][0-9]*)*$/,
> + },
> ro => {
> type => 'boolean',
> description => 'Read-only mount point',
> diff --git a/src/lxc-pve-prestart-hook b/src/lxc-pve-prestart-hook
> index 9862509..6e500a8 100755
> --- a/src/lxc-pve-prestart-hook
> +++ b/src/lxc-pve-prestart-hook
> @@ -95,6 +95,20 @@ PVE::LXC::Tools::lxc_hook(
> $mountpoint, $dir, $storage_cfg, undef, $root_uid, $root_gid,
> );
>
> + if ($mountpoint->{idmap}) {
> + my $mp_id_map = PVE::LXC::parse_mountpoint_idmap($id_map, $mountpoint);
> + my $usernsfh = PVE::LXC::Namespaces::new_userns($mp_id_map);
> + PVE::Tools::mount_setattr(
> + fileno($mount_fd),
> + '',
> + PVE::Tools::AT_EMPTY_PATH,
> + &PVE::Syscall::MOUNT_ATTR_IDMAP,
> + 0,
> + 0,
> + fileno($usernsfh),
> + ) or die "mount_setattr: $!\n";
> + }
> +
> my ($dest_dir, $dest_base_fd, $keep_attrs);
> if ($rootdir_fd) {
> # Mount relative to the rootdir fd.
--
Maximiliano
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-27 14:39 ` Maximiliano Sandoval
@ 2026-02-27 15:01 ` Daniel Kral
0 siblings, 0 replies; 17+ messages in thread
From: Daniel Kral @ 2026-02-27 15:01 UTC (permalink / raw)
To: Maximiliano Sandoval, Filip Schauer; +Cc: pve-devel
On Fri Feb 27, 2026 at 3:39 PM CET, Maximiliano Sandoval wrote:
> Filip Schauer <f.schauer@proxmox.com> writes:
>> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
>> index 5442586..9f56bc7 100644
>> --- a/src/PVE/LXC/Config.pm
>> +++ b/src/PVE/LXC/Config.pm
>> @@ -369,6 +369,13 @@ my $rootfs_desc = {
>> format_description => 'opt[;opt...]',
>> pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
>> },
>> + idmap => {
>> + optional => 1,
>> + type => 'string',
>> + description => 'Map specific UIDs/GIDs to specific host
>> UIDs/GIDs for this mount point',
>
>
> I think this would benefit from a verbose_description, it is not clear
> to me whether the syntax 123:456:1 maps 123 from the root to 456 on the
> container or the other way around. Perhaps an explicit example would be
> helpful.
+1, especially that each field's purpose is clarified once more there as
the user/group id mappings can be confusing at first for users.
>
>> + format_description => 'id-type:id-mount:id-host:id-range id-type:id-mount:...',
nit: id-range-size might be a bit clearer that it specifies a single
integer, which is the size of the id map range.
I guess the string could also be abbreviated to
<type>:<mount>:<host>:<range-size> if it's clear to the user that all of
these refer to the user/group ids for a idmap.
Might also make sense to make it more apparent that that string can be
repeated:
format_description => '<type>:<mount>:<host>:<range-size>{ <type>:<mount>:<host>:<range-size>}*',
On another note, do we have other properties where we use space as a
separator? It makes sense here as it's used like that in the underlying
API but might make our API inconsistent, but might be a bit pedantic
from my side.
>> + pattern => qr/^(?:[ug]:[0-9]+:[0-9]+:[1-9][0-9]*)(?: [ug]:[0-9]+:[0-9]+:[1-9][0-9]*)*$/,
>> + },
>> ro => {
>> type => 'boolean',
>> description => 'Read-only mount point',
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-23 13:04 ` [PATCH container 7/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-27 14:39 ` Maximiliano Sandoval
@ 2026-02-27 15:34 ` Wolfgang Bumiller
2026-03-02 16:37 ` Filip Schauer
2026-03-03 13:59 ` Filip Schauer
1 sibling, 2 replies; 17+ messages in thread
From: Wolfgang Bumiller @ 2026-02-27 15:34 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Mon, Feb 23, 2026 at 02:04:53PM +0100, Filip Schauer wrote:
> Add support for customizing UID/GID mappings on individual mount points
> without affecting the entire container.
>
> A new "idmap" mount point option accepts space-separated mappings:
> ```
> idmap=type:ct:host:len type:ct:host:len ...
> ```
While I don't have time for a detailed review today, some thoughts:
We probably want a way to just say "undo the container user
namespace". The pre-start hook gets a `$namespaces` hash passed as 3rd
parameter, we can just open the user namespace fd there for this
purpose.
Eg. `idmap=passthrough` or something, although the rust-side of the
schema might not like that...
some more inline...
>
> type: can be either 'u' or 'g'
> ct: ID as seen inside the container
> host: corresponding ID on the host
> len: number of consecutive IDs to map
>
> Unmapped ranges inherit the container's ID mapping.
>
> Example to pass through the host UID & GID 1005:
> ```
> mp0: /mnt/data,mp=/data,idmap=u:1005:1005:1 g:1005:1005:1
> ```
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> src/PVE/LXC.pm | 85 ++++++++++++++++++++++++++++++++++++++-
> src/PVE/LXC/Config.pm | 7 ++++
> src/lxc-pve-prestart-hook | 14 +++++++
> 3 files changed, 105 insertions(+), 1 deletion(-)
>
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 2c02e9a..ec7cb01 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -11,6 +11,7 @@ use File::Path;
> use File::Spec;
> use IO::Poll qw(POLLIN POLLHUP);
> use IO::Socket::UNIX;
> +use List::Util qw(max min);
> use POSIX qw(EINTR);
> use Socket;
> use Time::HiRes qw (gettimeofday);
> @@ -43,6 +44,7 @@ use PVE::Syscall qw(:fsmount);
> use PVE::LXC::CGroup;
> use PVE::LXC::Config;
> use PVE::LXC::Monitor;
> +use PVE::LXC::Namespaces;
> use PVE::LXC::Tools;
>
> my $have_sdn;
> @@ -2438,7 +2440,12 @@ sub device_passthrough_hotplug : prototype($$$) {
> sub mountpoint_hotplug : prototype($$$$$) {
> my ($vmid, $conf, $opt, $mp, $storage_cfg) = @_;
>
> - my (undef, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
> + my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
> + my $mp_userns_fh;
> + if ($mp->{idmap}) {
> + my $mp_id_map = parse_mountpoint_idmap($id_map, $mp);
> + $mp_userns_fh = PVE::LXC::Namespaces::new_userns($mp_id_map);
> + }
>
> # We do the rest in a fork with an unshared mount namespace:
> # -) change our apparmor profile to 'pve-container-mounthotplug', which is '/usr/bin/lxc-start'
> @@ -2474,6 +2481,18 @@ sub mountpoint_hotplug : prototype($$$$$) {
>
> my $mount_fd = mountpoint_stage($mp, $dir, $storage_cfg, undef, $root_uid, $root_gid);
>
> + if ($mp_userns_fh) {
> + PVE::Tools::mount_setattr(
> + fileno($mount_fd),
> + '',
> + PVE::Tools::AT_EMPTY_PATH,
> + &PVE::Syscall::MOUNT_ATTR_IDMAP,
> + 0,
> + 0,
> + fileno($mp_userns_fh),
> + ) or die "mount_setattr: $!\n";
> + }
> +
> PVE::Tools::setns(fileno($ct_mnt_ns), PVE::Tools::CLONE_NEWNS);
> chdir('/')
> or die "failed to change root directory within the container's mount namespace: $!\n";
> @@ -2989,6 +3008,70 @@ sub map_ct_gid_to_host {
> return map_ct_id_to_host($gid, $id_map, 'g');
> }
>
> +sub parse_mountpoint_idmap {
> + my ($id_map, $mp) = @_;
> +
> + die "mount point does not specify an idmap\n" if !$mp->{idmap};
> +
> + # Parse the user-friendly mount-specific ID map
> + # This maps IDs as seen in the container to IDs as seen on disk.
> + my $user_mp_id_map = [];
> + for my $entry (split(' ', $mp->{idmap})) {
> + $entry =~ /^([ug]):(\d+):(\d+):(\d+)$/ or die "failed to parse mount point idmap: $entry\n";
> + push @$user_mp_id_map, [$1, $2, $3, $4];
> + }
> +
> + validate_id_maps($user_mp_id_map);
> +
> + # Convert the user friendly mp.idmap to the actual mapping to be applied via mount_setattr.
> + # Provided by the config:
> + # lxc.idmap: ID in Container --> ID on Host
> + # mp.idmap: ID in Container --> ID on Disk
> + #
> + # Convert to: ID on Disk --> ID on Host
> + my $result = [];
> + for my $type ('u', 'g') {
> + my @ct_chunks = grep { $_->[0] eq $type } @$id_map;
> + next if !@ct_chunks;
> +
> + my @exceptions = sort { $a->[1] <=> $b->[1] } grep { $_->[0] eq $type } @$user_mp_id_map;
> +
> + for my $chunk (@ct_chunks) {
> + my (undef, $ct_start, $host_start, $len) = @$chunk;
> + my $ct_end = $ct_start + $len;
> +
> + # Find exceptions that fall within this specific lxc.idmap chunk
> + my @chunk_exc = grep { $_->[1] < $ct_end && $_->[1] + $_->[3] > $ct_start } @exceptions;
> + push @chunk_exc, [$type, $ct_end, undef, 0]; # ensure the trailing gap is mapped
> +
> + my $ct = $ct_start;
> + for my $exc (@chunk_exc) {
> + my (undef, $exc_ct, $exc_disk, $exc_len) = @$exc;
> +
> + my $clamped_ct = max($exc_ct, $ct_start);
> + my $clamped_len = min($exc_ct + $exc_len, $ct_end) - $clamped_ct;
> +
> + # Identity mapping for unmapped ranges
> + if ($ct < $clamped_ct) {
> + my $host = $host_start + ($ct - $ct_start);
> + push @$result, [$type, $host, $host, $clamped_ct - $ct];
> + }
> +
> + # Map the IDs on Disk to the Host IDs.
> + if ($clamped_len > 0) {
> + my $disk = $exc_disk + $clamped_ct - $exc_ct;
> + my $host = $host_start + $clamped_ct - $ct_start;
> + push @$result, [$type, $disk, $host, $clamped_len];
> + }
> +
> + $ct = $clamped_ct + $clamped_len;
> + }
> + }
> + }
> +
> + return $result;
> +}
> +
> sub userns_command {
> my ($id_map) = @_;
> if (@$id_map) {
> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
> index 5442586..9f56bc7 100644
> --- a/src/PVE/LXC/Config.pm
> +++ b/src/PVE/LXC/Config.pm
> @@ -369,6 +369,13 @@ my $rootfs_desc = {
> format_description => 'opt[;opt...]',
> pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
> },
> + idmap => {
> + optional => 1,
> + type => 'string',
> + description => 'Map specific UIDs/GIDs to specific host UIDs/GIDs for this mount point',
> + format_description => 'id-type:id-mount:id-host:id-range id-type:id-mount:...',
> + pattern => qr/^(?:[ug]:[0-9]+:[0-9]+:[1-9][0-9]*)(?: [ug]:[0-9]+:[0-9]+:[1-9][0-9]*)*$/,
> + },
> ro => {
> type => 'boolean',
> description => 'Read-only mount point',
> diff --git a/src/lxc-pve-prestart-hook b/src/lxc-pve-prestart-hook
> index 9862509..6e500a8 100755
> --- a/src/lxc-pve-prestart-hook
> +++ b/src/lxc-pve-prestart-hook
> @@ -95,6 +95,20 @@ PVE::LXC::Tools::lxc_hook(
> $mountpoint, $dir, $storage_cfg, undef, $root_uid, $root_gid,
> );
>
> + if ($mountpoint->{idmap}) {
> + my $mp_id_map = PVE::LXC::parse_mountpoint_idmap($id_map, $mountpoint);
> + my $usernsfh = PVE::LXC::Namespaces::new_userns($mp_id_map);
^ We may want to cache the file descriptors to not create too many user
namespaces as that's quite expensive and wasteful.
An alternative would be to, instead of defining arbitrary maps in the
container configs, define a central map somewhere and refer to the
mappings by name. We could then bind-mount the namespaces as, for
instance, `/run/pve/user-ns-mappings/<name>-<hash>`. That way an
existing mapping can just be `open()`ed, the namespace can be easily
accessed during debugging...
Downside: more work if this should be exposed in the UI ;-)
Although I do believe the "most used" thing would be the "passthrough"
value (?).
> + PVE::Tools::mount_setattr(
> + fileno($mount_fd),
> + '',
> + PVE::Tools::AT_EMPTY_PATH,
> + &PVE::Syscall::MOUNT_ATTR_IDMAP,
> + 0,
> + 0,
> + fileno($usernsfh),
> + ) or die "mount_setattr: $!\n";
> + }
> +
> my ($dest_dir, $dest_base_fd, $keep_attrs);
> if ($rootdir_fd) {
> # Mount relative to the rootdir fd.
> --
> 2.47.3
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-27 15:34 ` Wolfgang Bumiller
@ 2026-03-02 16:37 ` Filip Schauer
2026-03-02 18:05 ` Filip Schauer
2026-03-03 13:59 ` Filip Schauer
1 sibling, 1 reply; 17+ messages in thread
From: Filip Schauer @ 2026-03-02 16:37 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 27/02/2026 16:33, Wolfgang Bumiller wrote:
> We probably want a way to just say "undo the container user
> namespace". The pre-start hook gets a `$namespaces` hash passed as 3rd
> parameter, we can just open the user namespace fd there for this
> purpose.
When `lxc.hook.version = 0` (which seems to be the default), $namespaces
remains empty. So yes, we could try to get the namespace fd from
$namespaces, but we would just have to fall back to obtaining the
namespace manually unless `lxc.hook.version = 1` is set explicitly.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-03-02 16:37 ` Filip Schauer
@ 2026-03-02 18:05 ` Filip Schauer
2026-03-03 11:52 ` Wolfgang Bumiller
0 siblings, 1 reply; 17+ messages in thread
From: Filip Schauer @ 2026-03-02 18:05 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 02/03/2026 17:37, Filip Schauer wrote:
> On 27/02/2026 16:33, Wolfgang Bumiller wrote:
>> We probably want a way to just say "undo the container user
>> namespace". The pre-start hook gets a `$namespaces` hash passed as 3rd
>> parameter, we can just open the user namespace fd there for this
>> purpose.
>
> When `lxc.hook.version = 0` (which seems to be the default), $namespaces
> remains empty. So yes, we could try to get the namespace fd from
> $namespaces, but we would just have to fall back to obtaining the
> namespace manually unless `lxc.hook.version = 1` is set explicitly.
Or we could fix `PVE::LXC::Tools::lxc_hook`, such that it always finds
the namespaces.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-03-02 18:05 ` Filip Schauer
@ 2026-03-03 11:52 ` Wolfgang Bumiller
0 siblings, 0 replies; 17+ messages in thread
From: Wolfgang Bumiller @ 2026-03-03 11:52 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Mon, Mar 02, 2026 at 07:05:09PM +0100, Filip Schauer wrote:
> On 02/03/2026 17:37, Filip Schauer wrote:
> > On 27/02/2026 16:33, Wolfgang Bumiller wrote:
> > > We probably want a way to just say "undo the container user
> > > namespace". The pre-start hook gets a `$namespaces` hash passed as 3rd
> > > parameter, we can just open the user namespace fd there for this
> > > purpose.
> >
> > When `lxc.hook.version = 0` (which seems to be the default), $namespaces
> > remains empty. So yes, we could try to get the namespace fd from
> > $namespaces, but we would just have to fall back to obtaining the
> > namespace manually unless `lxc.hook.version = 1` is set explicitly.
>
> Or we could fix `PVE::LXC::Tools::lxc_hook`, such that it always finds
> the namespaces.
That.
Alternatively, I'm not sure containers really "work fine" with PVE if
people override `lxc.hook.*` manually, so maybe we should consider
dropping those from the list of valid custom keys and just force version
1?
If that's not an option, maybe we should add `lxc.hook.${hook}.version`
settings to lxc for per-hook versioning...
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-02-27 15:34 ` Wolfgang Bumiller
2026-03-02 16:37 ` Filip Schauer
@ 2026-03-03 13:59 ` Filip Schauer
2026-03-03 16:16 ` Wolfgang Bumiller
1 sibling, 1 reply; 17+ messages in thread
From: Filip Schauer @ 2026-03-03 13:59 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 27/02/2026 16:33, Wolfgang Bumiller wrote:
> The pre-start hook gets a `$namespaces` hash passed as 3rd
> parameter, we can just open the user namespace fd there for this
> purpose.
I just realized that this won't work. The pre-start hook cannot access
the container's user namespace, since the container's init process
wasn't even started yet.
We could however still reuse the container namespace when hot-plugging.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH container 7/8] implement per-mountpoint uid/gid mapping
2026-03-03 13:59 ` Filip Schauer
@ 2026-03-03 16:16 ` Wolfgang Bumiller
0 siblings, 0 replies; 17+ messages in thread
From: Wolfgang Bumiller @ 2026-03-03 16:16 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Tue, Mar 03, 2026 at 02:59:11PM +0100, Filip Schauer wrote:
> On 27/02/2026 16:33, Wolfgang Bumiller wrote:
> > The pre-start hook gets a `$namespaces` hash passed as 3rd
> > parameter, we can just open the user namespace fd there for this
> > purpose.
>
> I just realized that this won't work. The pre-start hook cannot access
> the container's user namespace, since the container's init process
> wasn't even started yet.
We could see if a `start-host` hook applying the idmapping after the
fact could do the job, but I'm not a big fan of splitting the mounting
into phases like this.
>
> We could however still reuse the container namespace when hot-plugging.
Yeah, if we want to special case this.
Other than that, we could at least cache the namespaces somewhere via
bind mounts so we can reuse them in hotplugging, too, but that can be
added as a follow up as well, since that's rather the exception, not the
rule. For the regular setup code, simply caching the fds in a hash is
enough.
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-03-03 16:15 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-23 13:04 [PATCH common/container/manager 0/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-23 13:04 ` [PATCH common 1/8] tools: export O_CLOEXEC constant Filip Schauer
2026-02-23 13:04 ` [PATCH common 2/8] syscall: add missing mount attribute constants Filip Schauer
2026-02-23 13:04 ` [PATCH common 3/8] tools: add mount_setattr syscall Filip Schauer
2026-02-23 13:04 ` [PATCH container 4/8] namespaces: relax prototype of run_in_userns Filip Schauer
2026-02-23 13:04 ` [PATCH container 5/8] namespaces: refactor run_in_userns Filip Schauer
2026-02-23 13:04 ` [PATCH container 6/8] namespaces: add helper to create user namespace from idmap Filip Schauer
2026-02-23 13:04 ` [PATCH container 7/8] implement per-mountpoint uid/gid mapping Filip Schauer
2026-02-27 14:39 ` Maximiliano Sandoval
2026-02-27 15:01 ` Daniel Kral
2026-02-27 15:34 ` Wolfgang Bumiller
2026-03-02 16:37 ` Filip Schauer
2026-03-02 18:05 ` Filip Schauer
2026-03-03 11:52 ` Wolfgang Bumiller
2026-03-03 13:59 ` Filip Schauer
2026-03-03 16:16 ` Wolfgang Bumiller
2026-02-23 13:04 ` [PATCH manager 8/8] ui: lxc/MPEdit: add "idmap" option Filip Schauer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox