all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping
@ 2026-03-30 14:10 Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 1/7] namespaces: relax prototype of run_in_userns Filip Schauer
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 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 semicolon-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
```

To identity-map the entire range of ids, "passthrough" can be used:
```
idmap=passthrough
```

Mount point idmapping only works for unprivileged containers. Privileged
containers are unaffected.

Build/Bump order:
* pve-container
* pve-manager

Changes since v1:
* Include $msg in error message in sync_send/sync_recv
* Switch idmap from space-separated to semicolon-separated
* Improve documentation of idmap option
* Add 'idmap=passthrough'
* Cache user namespace file descriptors in memory at container startup
  to avoid overhead of re-creating duplicates

pve-container:

Filip Schauer (5):
  namespaces: relax prototype of run_in_userns
  namespaces: refactor run_in_userns
  d/control: update versioned dependency for libpve-common-perl
  namespaces: add helper to create user namespace from idmap
  implement per-mountpoint uid/gid mapping

 debian/control            |  2 +-
 src/PVE/LXC.pm            | 96 +++++++++++++++++++++++++++++++++++++--
 src/PVE/LXC/Config.pm     | 39 ++++++++++++++++
 src/PVE/LXC/Namespaces.pm | 63 ++++++++++++++++++++-----
 src/lxc-pve-prestart-hook | 23 ++++++++++
 5 files changed, 206 insertions(+), 17 deletions(-)


pve-manager:

Filip Schauer (2):
  ui: lxc/MPEdit: remove duplicate "mp" assignment
  ui: lxc/MPEdit: add "idmap" option

 www/manager6/lxc/MPEdit.js | 213 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 212 insertions(+), 1 deletion(-)


Summary over all repositories:
  6 files changed, 418 insertions(+), 18 deletions(-)

-- 
Generated by git-murpp 0.6.0




^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH container v2 1/7] namespaces: relax prototype of run_in_userns
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 2/7] namespaces: refactor run_in_userns Filip Schauer
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 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] 8+ messages in thread

* [PATCH container v2 2/7] namespaces: refactor run_in_userns
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 1/7] namespaces: relax prototype of run_in_userns Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 3/7] d/control: update versioned dependency for libpve-common-perl Filip Schauer
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 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..0859bee 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 of message \"$msg\" failed: $!\n";
+}
+
+my sub sync_recv {
+    my ($fh, $expect) = @_;
+
+    my $received = <$fh>;
+    die "sync read failed (expected message \"$expect\")\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] 8+ messages in thread

* [PATCH container v2 3/7] d/control: update versioned dependency for libpve-common-perl
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 1/7] namespaces: relax prototype of run_in_userns Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 2/7] namespaces: refactor run_in_userns Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 4/7] namespaces: add helper to create user namespace from idmap Filip Schauer
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 UTC (permalink / raw)
  To: pve-devel

To ensure that O_CLOEXEC and mount_setattr are available for idmapped
mount points.

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 debian/control | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/debian/control b/debian/control
index 0150db4..259c081 100644
--- a/debian/control
+++ b/debian/control
@@ -26,7 +26,7 @@ Depends: binutils,
          isc-dhcp-client,
          libpve-access-control (>= 8.0.0~),
          libpve-cluster-perl,
-         libpve-common-perl (>= 9.1.8),
+         libpve-common-perl (>= 9.1.9),
          libpve-guest-common-perl (>= 5.1.3),
          libpve-rs-perl (>= 0.11~),
          libpve-storage-perl (>= 8.3.5),
-- 
2.47.3





^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH container v2 4/7] namespaces: add helper to create user namespace from idmap
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
                   ` (2 preceding siblings ...)
  2026-03-30 14:10 ` [PATCH container v2 3/7] d/control: update versioned dependency for libpve-common-perl Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH container v2 5/7] implement per-mountpoint uid/gid mapping Filip Schauer
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 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 0859bee..c50afa6 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] 8+ messages in thread

* [PATCH container v2 5/7] implement per-mountpoint uid/gid mapping
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
                   ` (3 preceding siblings ...)
  2026-03-30 14:10 ` [PATCH container v2 4/7] namespaces: add helper to create user namespace from idmap Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH manager v2 6/7] ui: lxc/MPEdit: remove duplicate "mp" assignment Filip Schauer
  2026-03-30 14:10 ` [PATCH manager v2 7/7] ui: lxc/MPEdit: add "idmap" option Filip Schauer
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 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 semicolon-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
```

To identity-map the entire range of ids, "passthrough" can be used:
```
idmap=passthrough
```

Mount point idmapping only works for unprivileged containers. Privileged
containers are unaffected.

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 src/PVE/LXC.pm            | 96 +++++++++++++++++++++++++++++++++++++--
 src/PVE/LXC/Config.pm     | 39 ++++++++++++++++
 src/lxc-pve-prestart-hook | 23 ++++++++++
 3 files changed, 153 insertions(+), 5 deletions(-)

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 6f0dec4..0285d72 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;
@@ -2470,7 +2472,24 @@ 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);
+    # Pin the container pid longer, we also need to get its monitor/parent:
+    my ($ct_pid, $ct_pidfd) = open_lxc_pid($vmid)
+        or die "failed to open pidfd of container $vmid\'s init process\n";
+
+    my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
+    my $mp_userns_fh;
+    if ($mp->{idmap}) {
+        if (!@$id_map) {
+            PVE::RESTEnvironment::log_warn(
+                "'$opt' - ignoring 'idmap' option unsupported by privileged container");
+        } elsif ($mp->{idmap} eq "passthrough") {
+            # Optimization: Reuse the container userns to avoid the overhead of creating a new ns
+            $mp_userns_fh = $get_container_namespace->($vmid, $ct_pid, 'user');
+        } else {
+            my $mp_id_map = resolve_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'
@@ -2479,10 +2498,6 @@ sub mountpoint_hotplug : prototype($$$$$) {
     #     namespace, then mount it.
 
     PVE::Tools::run_fork(sub {
-        # Pin the container pid longer, we also need to get its monitor/parent:
-        my ($ct_pid, $ct_pidfd) = open_lxc_pid($vmid)
-            or die "failed to open pidfd of container $vmid\'s init process\n";
-
         my ($monitor_pid, $monitor_pidfd) = open_ppid($ct_pid)
             or die "failed to open pidfd of container $vmid\'s monitor process\n";
 
@@ -2506,6 +2521,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";
@@ -3021,6 +3048,65 @@ sub map_ct_gid_to_host {
     return map_ct_id_to_host($gid, $id_map, 'g');
 }
 
+sub resolve_mountpoint_idmap {
+    my ($id_map, $mp) = @_;
+
+    die "mount point does not specify an idmap\n" if !$mp->{idmap};
+
+    return $id_map if $mp->{idmap} eq "passthrough";
+
+    my $mp_ct_idmap = $mp->{idmap};
+    validate_id_maps($mp_ct_idmap);
+
+    # 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 } @$mp_ct_idmap;
+
+        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..924a98c 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -369,6 +369,27 @@ 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 container UIDs/GIDs to underlying disk UIDs/GIDs for this mount point',
+        verbose_description =>
+            "Customize UID/GID mappings that override the container's `lxc.idmap` for this mount "
+            . "point. Accepts a semicolon-separated list of `type:container:disk:range-size` "
+            . "entries.\n"
+            . "`type` is `u` for UID or `g` for GID.\n"
+            . "`container` is the first ID as seen inside the container.\n"
+            . "`disk` is the first corresponding ID on the underlying filesystem.\n"
+            . "`range-size` is the number of consecutive IDs to map.\n"
+            . "Unmapped IDs fall back to the container's `lxc.idmap`.\n"
+            . "Example: `u:123:456:1` maps UID 123 in the container to UID 456 on the disk. "
+            . "Files owned by UID 456 on the disk will appear as UID 123 inside the container.",
+        format_description =>
+            'type:container:disk:range-size[;type:container:disk:range-size;...]',
+        pattern =>
+            qr/^(?:passthrough|[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',
@@ -1315,6 +1336,8 @@ sub update_pct_config {
             $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
             my $mp = $class->parse_volume($opt, $value);
             $check_content_type->($mp) if ($mp->{type} eq 'volume');
+            PVE::LXC::validate_id_maps($mp->{idmap})
+                if defined($mp->{idmap}) && $mp->{idmap} ne 'passthrough';
         } elsif ($opt eq 'hookscript') {
             PVE::GuestHelpers::check_hookscript($value);
         } elsif ($opt eq 'nameserver') {
@@ -1439,6 +1462,16 @@ my $parse_ct_mountpoint_full = sub {
 
     $res->{type} = $class->classify_mountpoint($res->{volume});
 
+    if (defined($res->{idmap}) && $res->{idmap} ne 'passthrough') {
+        my $mp_ct_idmap = [];
+        for my $entry (split(';', $res->{idmap})) {
+            $entry =~ /^([ug]):(\d+):(\d+):(\d+)$/
+                or die "failed to parse mount point idmap: $entry\n";
+            push @$mp_ct_idmap, [$1, $2, $3, $4];
+        }
+        $res->{idmap} = $mp_ct_idmap;
+    }
+
     return $res;
 };
 
@@ -1446,6 +1479,12 @@ sub print_ct_mountpoint {
     my ($class, $info, $nomp) = @_;
     my $skip = ['type'];
     push @$skip, 'mp' if $nomp;
+
+    if (defined($info->{idmap}) && $info->{idmap} ne 'passthrough') {
+        $info = {%$info}; # Shallow copy to avoid mutating the caller's hashref
+        $info->{idmap} = join ';', map { join ':', @$_ } @{ $info->{idmap} };
+    }
+
     return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
 }
 
diff --git a/src/lxc-pve-prestart-hook b/src/lxc-pve-prestart-hook
index 9862509..2bfce31 100755
--- a/src/lxc-pve-prestart-hook
+++ b/src/lxc-pve-prestart-hook
@@ -87,6 +87,7 @@ PVE::LXC::Tools::lxc_hook(
         };
 
         my $rootdir_fd = undef;
+        my $userns_cache = {};
         my $setup_mountpoint = sub {
             my ($opt, $mountpoint) = @_;
 
@@ -95,6 +96,28 @@ PVE::LXC::Tools::lxc_hook(
                 $mountpoint, $dir, $storage_cfg, undef, $root_uid, $root_gid,
             );
 
+            if ($mountpoint->{idmap}) {
+                if (@$id_map) {
+                    my $mp_id_map = PVE::LXC::resolve_mountpoint_idmap($id_map, $mountpoint);
+                    my $cache_key = join(';', map { join(':', @$_) } @$mp_id_map);
+                    my $usernsfh = $userns_cache->{$cache_key} //=
+                        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";
+                } else {
+                    $log_warn->(
+                        "'$opt' - ignoring 'idmap' option unsupported by privileged container");
+                }
+            }
+
             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] 8+ messages in thread

* [PATCH manager v2 6/7] ui: lxc/MPEdit: remove duplicate "mp" assignment
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
                   ` (4 preceding siblings ...)
  2026-03-30 14:10 ` [PATCH container v2 5/7] implement per-mountpoint uid/gid mapping Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  2026-03-30 14:10 ` [PATCH manager v2 7/7] ui: lxc/MPEdit: add "idmap" option Filip Schauer
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 www/manager6/lxc/MPEdit.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/www/manager6/lxc/MPEdit.js b/www/manager6/lxc/MPEdit.js
index 4ed2d07b..b1f67741 100644
--- a/www/manager6/lxc/MPEdit.js
+++ b/www/manager6/lxc/MPEdit.js
@@ -41,7 +41,6 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
         setMPOpt('mp', values.mp);
         let mountOpts = (values.mountoptions || []).join(';');
         setMPOpt('mountoptions', values.mountoptions, mountOpts);
-        setMPOpt('mp', values.mp);
         setMPOpt('backup', values.backup);
         setMPOpt('quota', values.quota);
         setMPOpt('ro', values.ro);
-- 
2.47.3





^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH manager v2 7/7] ui: lxc/MPEdit: add "idmap" option
  2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
                   ` (5 preceding siblings ...)
  2026-03-30 14:10 ` [PATCH manager v2 6/7] ui: lxc/MPEdit: remove duplicate "mp" assignment Filip Schauer
@ 2026-03-30 14:10 ` Filip Schauer
  6 siblings, 0 replies; 8+ messages in thread
From: Filip Schauer @ 2026-03-30 14:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 www/manager6/lxc/MPEdit.js | 212 +++++++++++++++++++++++++++++++++++++
 1 file changed, 212 insertions(+)

diff --git a/www/manager6/lxc/MPEdit.js b/www/manager6/lxc/MPEdit.js
index b1f67741..1227ebc0 100644
--- a/www/manager6/lxc/MPEdit.js
+++ b/www/manager6/lxc/MPEdit.js
@@ -47,6 +47,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);
@@ -131,6 +132,41 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
                     me.getViewModel().set('type', rec.data.type);
                 },
             },
+            'grid proxmoxintegerfield,grid segmentedbutton': {
+                change: function (widget, value) {
+                    let me = this;
+                    let record = widget.getWidgetRecord();
+                    let column = widget.getWidgetColumn();
+                    if (!record || !column) {
+                        return;
+                    }
+                    record.set(column.dataIndex, value);
+                    record.commit();
+                    me.updateIdMapField();
+                },
+            },
+            'field[name=idmap]': {
+                change: function (field, value) {
+                    let me = this;
+                    let mode = !value ? 'none' : value === 'passthrough' ? value : 'custom';
+                    let modeBtn = me.lookup('idmapMode');
+                    modeBtn.suspendEvent('change');
+                    modeBtn.setValue(mode);
+                    modeBtn.resumeEvent('change');
+                    me.updateIdMapUI(mode, value);
+                },
+            },
+            'segmentedbutton[reference=idmapMode]': {
+                change: function (button, mode) {
+                    let me = this;
+                    let value = mode === 'passthrough' ? mode : '';
+                    let field = me.lookup('idmap');
+                    field.suspendEvent('change');
+                    field.setValue(value);
+                    field.resumeEvent('change');
+                    me.updateIdMapUI(mode, value);
+                },
+            },
         },
         init: function (view) {
             let _me = this;
@@ -153,6 +189,50 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
                 view.setVMConfig(view.vmconfig);
             }
         },
+        updateIdMapUI: function (mode, value) {
+            let me = this;
+            let isCustom = mode === 'custom';
+            me.lookup('idmaps').setVisible(isCustom);
+            me.lookup('addIdmapButton').setVisible(isCustom);
+
+            let store = me.lookup('idmaps').getStore();
+            if (isCustom && value) {
+                store.setData(
+                    value.split(';').map((v) => {
+                        let [type, ct, host, length] = v.split(':');
+                        return { type, ct, host, length };
+                    }),
+                );
+            } else {
+                store.removeAll();
+            }
+        },
+        addIdMap: function () {
+            let me = this;
+            me.lookup('idmaps').getStore().add({ type: 'u', ct: '', host: '', length: '' });
+            me.updateIdMapField();
+        },
+        removeIdMap: function (button) {
+            let me = this;
+            let record = button.getWidgetRecord();
+            me.lookup('idmaps').getStore().remove(record);
+            me.updateIdMapField();
+        },
+        updateIdMapField: function () {
+            let me = this;
+
+            let value = me
+                .lookup('idmaps')
+                .getStore()
+                .getRange()
+                .map(({ data: { type, ct, host, length } }) => `${type}:${ct}:${host}:${length}`)
+                .join(';');
+
+            let field = me.lookup('idmap');
+            field.suspendEvent('change');
+            field.setValue(value);
+            field.resumeEvent('change');
+        },
     },
 
     viewModel: {
@@ -353,6 +433,138 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
             },
         },
     ],
+
+    advancedColumnB: [
+        {
+            xtype: 'fieldcontainer',
+            fieldLabel: gettext('ID Mapping'),
+            items: [
+                {
+                    xtype: 'segmentedbutton',
+                    reference: 'idmapMode',
+                    defaults: {
+                        allowDepress: false,
+                    },
+                    items: [
+                        {
+                            text: gettext('None'),
+                            value: 'none',
+                            pressed: true,
+                        },
+                        {
+                            text: gettext('Passthrough'),
+                            value: 'passthrough',
+                            tooltip: gettext('UIDs/GIDs in the container match those on disk'),
+                        },
+                        {
+                            text: gettext('Custom'),
+                            value: 'custom',
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            xtype: 'grid',
+            height: 170,
+            scrollable: true,
+            reference: 'idmaps',
+            hidden: true,
+            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',
+                            },
+                        ],
+                    },
+                    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('Range Size'),
+                    xtype: 'widgetcolumn',
+                    dataIndex: 'length',
+                    widget: {
+                        xtype: 'proxmoxintegerfield',
+                        margin: '4 0',
+                        emptyText: gettext('Range Size'),
+                        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',
+            reference: 'addIdmapButton',
+            text: gettext('Add'),
+            iconCls: 'fa fa-plus-circle',
+            handler: 'addIdMap',
+            hidden: true,
+        },
+        {
+            xtype: 'hidden',
+            reference: 'idmap',
+            name: 'idmap',
+        },
+    ],
 });
 
 Ext.define('PVE.lxc.MountPointEdit', {
-- 
2.47.3





^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-03-30 14:13 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-30 14:10 [PATCH container/manager v2 0/7] implement per-mountpoint uid/gid mapping Filip Schauer
2026-03-30 14:10 ` [PATCH container v2 1/7] namespaces: relax prototype of run_in_userns Filip Schauer
2026-03-30 14:10 ` [PATCH container v2 2/7] namespaces: refactor run_in_userns Filip Schauer
2026-03-30 14:10 ` [PATCH container v2 3/7] d/control: update versioned dependency for libpve-common-perl Filip Schauer
2026-03-30 14:10 ` [PATCH container v2 4/7] namespaces: add helper to create user namespace from idmap Filip Schauer
2026-03-30 14:10 ` [PATCH container v2 5/7] implement per-mountpoint uid/gid mapping Filip Schauer
2026-03-30 14:10 ` [PATCH manager v2 6/7] ui: lxc/MPEdit: remove duplicate "mp" assignment Filip Schauer
2026-03-30 14:10 ` [PATCH manager v2 7/7] ui: lxc/MPEdit: add "idmap" option Filip Schauer

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal