From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 1DAFA1FF0AB for ; Wed, 07 Oct 2026 10:37:59 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 6FFBF215FC; Wed, 07 Oct 2026 10:37:42 +0200 (CEST) From: Elias Huhsovitz To: pve-devel@lists.proxmox.com Subject: [PATCH container 2/2] fix #8093: console: bind console sessions to container lifetime via systemd scopes Date: Wed, 7 Oct 2026 10:37:26 +0200 Message-ID: <20261007083726.26067-3-e.huhsovitz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261007083726.26067-1-e.huhsovitz@proxmox.com> References: <20261007083726.26067-1-e.huhsovitz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1791362257487 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.543 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: WTB3BMYPJOMZ6HNUJQFDLMR7H54UTZTO X-Message-ID-Hash: WTB3BMYPJOMZ6HNUJQFDLMR7H54UTZTO X-MailFrom: e.huhsovitz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Elias Huhsovitz X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Console processes can outlive their useful session when a container stops or disappears while a console is attached. The previous cleanup ran in pvestatd, which polled every 10s: forked lxc-info for every running container, and killed orphaned processes by their PID. Manage console lifetime in the container lifecycle instead. Add wrap_in_console_scope() to start console commands in a systemd scope. Bind each scope with PartOf= to the respective pve-container service. Use the wrapper for pct console/enter/exec, and the API console endpoints. When the container service stops, systemd stops the related console scopes. Signed-off-by: Elias Huhsovitz --- src/PVE/API2/LXC.pm | 6 +++--- src/PVE/CLI/pct.pm | 10 +++++++--- src/PVE/LXC.pm | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm index 5f94d5a..3fd2bf8 100644 --- a/src/PVE/API2/LXC.pm +++ b/src/PVE/API2/LXC.pm @@ -1015,7 +1015,7 @@ __PACKAGE__->register_method({ my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath, $port); my $conf = PVE::LXC::Config->load_config($vmid, $node); - my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1); + my $concmd = PVE::LXC::get_console_command_scoped($vmid, $conf, -1); my $shcmd = [ '/usr/bin/dtach', @@ -1146,7 +1146,7 @@ __PACKAGE__->register_method({ my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath, $port); my $conf = PVE::LXC::Config->load_config($vmid, $node); - my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1); + my $concmd = PVE::LXC::get_console_command_scoped($vmid, $conf, -1); my $shcmd = [ '/usr/bin/dtach', @@ -1288,7 +1288,7 @@ __PACKAGE__->register_method({ die "CT $vmid not running\n" if !PVE::LXC::check_running($vmid); - my $concmd = PVE::LXC::get_console_command($vmid, $conf); + my $concmd = PVE::LXC::get_console_command_scoped($vmid, $conf); my $shcmd = [ '/usr/bin/dtach', diff --git a/src/PVE/CLI/pct.pm b/src/PVE/CLI/pct.pm index ceb0bea..11e85bc 100755 --- a/src/PVE/CLI/pct.pm +++ b/src/PVE/CLI/pct.pm @@ -153,7 +153,7 @@ __PACKAGE__->register_method({ # test if container exists on this node my $conf = PVE::LXC::Config->load_config($param->{vmid}); - my $cmd = PVE::LXC::get_console_command($param->{vmid}, $conf, $param->{escape}); + my $cmd = PVE::LXC::get_console_command_scoped($param->{vmid}, $conf, $param->{escape}); exec(@$cmd); }, }); @@ -207,7 +207,9 @@ __PACKAGE__->register_method({ my @lxc_attach_cmd = ('lxc-attach', '-n', $vmid); push @lxc_attach_cmd, $keep_env ? '--keep-env' : '--clear-env'; - exec(@lxc_attach_cmd); + + my $scoped_cmd = PVE::LXC::wrap_in_console_scope($vmid, \@lxc_attach_cmd); + exec(@$scoped_cmd); }, }); @@ -250,7 +252,9 @@ __PACKAGE__->register_method({ my @lxc_attach_cmd = ('lxc-attach', '-n', $vmid); push @lxc_attach_cmd, $keep_env ? '--keep-env' : '--clear-env'; push @lxc_attach_cmd, '--', @{ $param->{'extra-args'} }; - exec(@lxc_attach_cmd); + + my $scoped_cmd = PVE::LXC::wrap_in_console_scope($vmid, \@lxc_attach_cmd); + exec(@$scoped_cmd); }, }); diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm index dee073d..14bc6e4 100644 --- a/src/PVE/LXC.pm +++ b/src/PVE/LXC.pm @@ -954,6 +954,39 @@ sub verify_searchdomain_list { return join(' ', @list); } +=head2 wrap_in_console_scope + +Wraps a command array in a transient systemd scope and binds it to the +container's main systemd service. This ensures the console process is +automatically terminated by systemd when the container stops, preventing +orphaned processes without requiring manual cleanup scripts. + +=cut + +sub wrap_in_console_scope { + my ($vmid, $cmd) = @_; + + my $session_id = "$$-" . time(); + my $unit_name = "pve-lxc-console-$vmid-$session_id"; + + my $service = "pve-container\@$vmid.service"; + # service name appends "-debug" when container is started in debug mode + my $debug_service = "pve-container-debug\@$vmid.service"; + + my @scope_cmd = ( + 'systemd-run', + '--scope', + '--unit', $unit_name, + '--description', "PVE LXC Console for $vmid", + '--property', "PartOf=$service $debug_service", + '--quiet', + ); + + push @scope_cmd, @$cmd; + + return \@scope_cmd; +} + sub get_console_command { my ($vmid, $conf, $escapechar) = @_; @@ -978,6 +1011,14 @@ sub get_console_command { return $cmd; } +sub get_console_command_scoped { + my ($vmid, $conf, $escapechar) = @_; + + my $cmd = get_console_command($vmid, $conf, $escapechar); + + return wrap_in_console_scope($vmid, $cmd) +} + sub get_primary_ips { my ($conf) = @_; -- 2.47.3