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 7BED91FF0C0 for ; Wed, 04 Nov 2026 20:06:24 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 2BC8D216F8; Wed, 04 Nov 2026 20:05:23 +0100 (CET) From: =?UTF-8?q?Michael=20K=C3=B6ppl?= To: pve-devel@lists.proxmox.com Subject: [PATCH e2e-tests 17/38] tests: container: verify device passthrough Date: Wed, 4 Nov 2026 20:03:24 +0100 Message-ID: <20261104190345.2573464-18-m.koeppl@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261104190345.2573464-1-m.koeppl@proxmox.com> References: <20261104190345.2573464-1-m.koeppl@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793819034892 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.226 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: FAVL667NU4LLN7LTLN5JURIU75BVZFC2 X-Message-ID-Hash: FAVL667NU4LLN7LTLN5JURIU75BVZFC2 X-MailFrom: m.koeppl@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 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Pass two loop devices through to a container, one of them with 'deny-write' set, and check mode, read and write access inside the container. Run it for an unprivileged container and for a privileged one with the 'unmanaged' OS type. Signed-off-by: Michael Köppl --- tests/pve_ct_device_passthrough.pl | 162 +++++++++++++++++++++++++++++ tests/testcases.ron | 9 ++ 2 files changed, 171 insertions(+) create mode 100755 tests/pve_ct_device_passthrough.pl diff --git a/tests/pve_ct_device_passthrough.pl b/tests/pve_ct_device_passthrough.pl new file mode 100755 index 0000000..15ca4ab --- /dev/null +++ b/tests/pve_ct_device_passthrough.pl @@ -0,0 +1,162 @@ +#!/usr/bin/perl + +=head1 NAME + +C - Test passing host devices through to a container. + +=head1 DESCRIPTION + +This testcase attaches two loop devices on the host, each backed by a small +scratch file, and passes them through to a container, the second one with write +access denied. It verifies that both devices show up inside the container with +the configured mode, that data written to the writable one can be read back, +and that writes to the other one are refused while reads still work. + +Loop devices are used because a container cannot access them unless they are +passed through and they are easy to set up. + +It runs once for an unprivileged container and once for a privileged container +with the 'unmanaged' OS type, for which LXC ships no OS-specific config. + +=cut + +use strict; +use warnings; + +use Test::More; + +use lib '.'; +use Proxmox::Test::Helper qw(CONTAINER_TEMPLATE get_pve_instance); +use Proxmox::Test::Testcase; + +my $pve = get_pve_instance('pve1'); +my $node = $pve->get_nodename(); +my $client = $pve->client(); + +my $backing_dir = '/var/lib/ct-passthrough'; + +# The device type and number of $dev as reported by stat on the host. +my $host_device_id = sub { + my ($dev) = @_; + + my ($out) = $pve->run_command("stat -c '%F %t:%T' $dev"); + chomp($out); + return $out; +}; + +Proxmox::Test::Testcase->new('pve_ct_device_passthrough')->with([ + { + name => 'unprivileged', + unprivileged => 1, + }, + { + name => 'privileged-unmanaged', + unprivileged => 0, + ostype => 'unmanaged', + }, +])->step( + 'attach loop devices on the host', + sub { + my ($testcase) = @_; + + $pve->run_command("mkdir -p $backing_dir"); + for my $kind ('writable', 'readonly') { + my $file = "$backing_dir/$kind.img"; + $pve->run_command("truncate -s 1M $file"); + my ($dev) = $pve->run_command("losetup --find --show $file"); + chomp($dev); + $testcase->{"${kind}_dev"} = $dev; + } + }, + sub { + my ($testcase) = @_; + + for my $kind ('writable', 'readonly') { + my $dev = $testcase->{"${kind}_dev"} // next; + $pve->run_command("losetup --detach $dev"); + } + $pve->run_command("rm -rf $backing_dir"); + }, +)->step( + 'create container with passthrough devices', + sub { + my ($testcase) = @_; + + $testcase->{vmid} = $client->get('/cluster/nextid'); + my $upid = $client->post( + "/nodes/$node/lxc", + { + vmid => $testcase->{vmid}, + ostemplate => CONTAINER_TEMPLATE, + rootfs => 'local-lvm:2', + storage => 'local-lvm', + memory => 256, + unprivileged => $testcase->{unprivileged}, + features => 'nesting=1', + hostname => "ct-dev-$testcase->{name}", + ($testcase->{ostype} ? (ostype => $testcase->{ostype}) : ()), + dev0 => "$testcase->{writable_dev},mode=0660", + dev1 => "$testcase->{readonly_dev},mode=0640,deny-write=1", + }, + ); + $pve->task_ok($upid, "created $testcase->{name} container $testcase->{vmid}") + or die "creating container failed\n"; + }, + sub { + my ($testcase) = @_; + $pve->destroy_guest('lxc', $testcase->{vmid}) if defined($testcase->{vmid}); + }, +)->step( + 'start container', + sub { + my ($testcase) = @_; + my $vmid = $testcase->{vmid}; + + $pve->start_guest('lxc', $vmid); + }, +)->step( + 'verify devices inside container', + sub { + my ($testcase) = @_; + my $vmid = $testcase->{vmid}; + my $writable_dev = $testcase->{writable_dev}; + my $readonly_dev = $testcase->{readonly_dev}; + + is( + $pve->ct_exec($vmid, "stat -c '%F %t:%T %a' $writable_dev"), + $host_device_id->($writable_dev) . " 660\n", + "$writable_dev is passed through with the configured mode", + ); + is( + $pve->ct_exec($vmid, "stat -c '%F %t:%T %a' $readonly_dev"), + $host_device_id->($readonly_dev) . " 640\n", + "$readonly_dev is passed through with the configured mode", + ); + + my $data = 'test'; + + ok( + eval { $pve->ct_exec($vmid, "sh -c 'printf $data > $writable_dev'"); 1 }, + "$writable_dev is writable", + ); + is( + $pve->ct_exec($vmid, "head -c " . length($data) . " $writable_dev"), + $data, + "data written to $writable_dev can be read back", + ); + + # Opening for writing should already fail because the device cgroup checks access when the + # device is opened. + ok( + !eval { $pve->ct_exec($vmid, "sh -c 'printf $data > $readonly_dev'"); 1 }, + "writing to $readonly_dev is denied", + ); + is( + $pve->ct_exec($vmid, "sh -c 'head -c 4 $readonly_dev | wc -c'"), + "4\n", + "$readonly_dev is still readable", + ); + }, +)->run(); + +done_testing(); diff --git a/tests/testcases.ron b/tests/testcases.ron index c0369e4..1d5b7ad 100644 --- a/tests/testcases.ron +++ b/tests/testcases.ron @@ -169,6 +169,15 @@ TestcaseConfig( ], tag: "extended", ), + Testcase( + name: "pve_ct_device_passthrough", + run: "./tests/pve_ct_device_passthrough.pl", + timeout: 600, + instances: [ + "pve1", + ], + tag: "extended", + ), Testcase( name: "pve_ha_resource_config", run: "./tests/pve_ha_resource_config.pl", -- 2.47.3