[pve-devel] [PATCH qemu-server 4/9] api: monitor: improve permission handling

Fiona Ebner f.ebner at proxmox.com
Thu Jul 17 15:36:52 CEST 2025


While HMP (human monitor protocol) commands beside 'info' and 'help'
already require the 'Sys.Modify' permission on '/', certain commands
are better further restricted to be root-only.

Command list and descriptions taken from the output of 'help' and
shortened the descriptions where appropriate.

Many related commands for root-only commands were also made root-only,
for example 'drive_del', because 'drive_add' is or the NBD commands,
because 'nbd_server_start' is. That is being able to only do certain
parts of command groups that are not that useful by themselves. An
exception here is 'qom-get' which is just too useful to be root-only.

Signed-off-by: Fiona Ebner <f.ebner at proxmox.com>
---
 src/PVE/API2/Qemu.pm          |  32 ++++--
 src/PVE/API2/Qemu/HMPPerms.pm | 207 ++++++++++++++++++++++++++++++++++
 src/PVE/API2/Qemu/Makefile    |   2 +-
 3 files changed, 231 insertions(+), 10 deletions(-)
 create mode 100644 src/PVE/API2/Qemu/HMPPerms.pm

diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index dbc08737..82cdc742 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -56,6 +56,7 @@ use PVE::Network;
 use PVE::Firewall;
 use PVE::API2::Firewall::VM;
 use PVE::API2::Qemu::Agent;
+use PVE::API2::Qemu::HMPPerms;
 use PVE::VZDump::Plugin;
 use PVE::DataCenterConfig;
 use PVE::ProcFSTools;
@@ -5582,8 +5583,7 @@ __PACKAGE__->register_method({
     proxyto => 'node',
     description => "Execute QEMU monitor commands.",
     permissions => {
-        description =>
-            "Sys.Modify is required for (sub)commands which are not read-only ('info *' and 'help')",
+        description => PVE::API2::Qemu::HMPPerms::generate_description(),
         check => ['perm', '/vms/{vmid}', ['VM.Monitor']],
     },
     parameters => {
@@ -5604,14 +5604,28 @@ __PACKAGE__->register_method({
         my $rpcenv = PVE::RPCEnvironment::get();
         my $authuser = $rpcenv->get_user();
 
-        my $is_ro = sub {
-            my $command = shift;
-            return $command =~ m/^\s*info(\s+|$)/
-                || $command =~ m/^\s*help\s*$/;
-        };
+        my $command = $param->{command} or die "no command specified\n";
+        die "unexpected command '$command'\n" if $command !~ m/^\s*(\S+)/;
+        my $command_name = $1;
+        my $required_perm = $PVE::API2::Qemu::HMPPerms::hmp_command_perms->{$command_name};
+        if (!$required_perm) {
+            my $msg =
+                "command '$command_name' non-existent or not assigned a required permission"
+                . " yet, limiting to root user\n";
+            die $msg if $authuser ne 'root at pam';
+            warn $msg;
+            $required_perm = 'root';
+        }
 
-        $rpcenv->check_full($authuser, "/", ['Sys.Modify'])
-            if !&$is_ro($param->{command});
+        if ($required_perm eq 'root') {
+            die "root-only command '$command_name'\n" if $authuser ne 'root at pam';
+        } elsif ($required_perm eq 'Sys.Modify') {
+            $rpcenv->check_full($authuser, "/", ['Sys.Modify']);
+        } elsif ($required_perm eq 'none') {
+            # nothing to check
+        } else {
+            die "unexpected required permission '$required_perm' for command '$command_name'\n";
+        }
 
         my $vmid = $param->{vmid};
 
diff --git a/src/PVE/API2/Qemu/HMPPerms.pm b/src/PVE/API2/Qemu/HMPPerms.pm
new file mode 100644
index 00000000..f6b32891
--- /dev/null
+++ b/src/PVE/API2/Qemu/HMPPerms.pm
@@ -0,0 +1,207 @@
+package PVE::API2::Qemu::HMPPerms;
+
+use strict;
+use warnings;
+
+# List of monitor commands and associated required permission. Listed explicitly to be future-proof.
+#
+# Currently permissions are:
+# 'root' - for root-only commands
+# 'Sys.Modify' - commands that can be issued with 'Sys.Modify' on '/'
+# 'none' - no permissions required (i.e. help and info)
+our $hmp_command_perms = {
+    help => 'none', # show the help
+    '?' => 'none', # short-form of 'help'
+    info => 'none', # show various information about the system state
+
+    # root-only: backup to arbitrary target file (although currently, not overwriting existing file)
+    backup => 'root', # create a VM backup (VMA format).
+    # root-only: requires the stream source in the backing chain currently, but better be safe
+    block_stream => 'root', # copy data from a backing file into a block device
+    # root-only: allows changing the path a removable medium points to
+    change => 'root', # change a removable medium
+    # root-only: among others, there is a 'file' driver
+    'chardev-add' => 'root', # add chardev
+    # root-only: among others, there is a 'file' driver (e.g. modify backend for serial device)
+    'chardev-change' => 'root', # change chardev
+    # root-only: because chardev-add is
+    'chardev-remove' => 'root', # remove chardev
+    # root-only: after migration SPICE client will attempt to connect to arbitrarily set host
+    client_migrate_info => 'root', # set migration information for remote display
+    # root-only: like '-device' on the commandline
+    device_add => 'root', # add device, like -device on the command line
+    # root-only: because device_add is
+    device_del => 'root', # remove device
+    # root-only: like '-drive' on the commandline
+    drive_add => 'root', # add drive to PCI storage controller
+    # root-only: backup to arbitrary target file
+    drive_backup => 'root', # initiates a point-in-time copy for a device.
+    # root-only: because drive_add is
+    drive_del => 'root', # remove host block device
+    # root-only: mirror to arbitrary target file
+    drive_mirror => 'root', # initiates live storage migration for a device.
+    # root-only: dump guest memory into arbitrary target file
+    'dump-guest-memory' => 'root', # dump guest memory into file 'filename'.
+    # root-only: dumps into arbitrary target file
+    dumpdtb => 'root', # dump the FDT in dtb format to 'filename'
+    # root-only: starts GDB server on the host
+    gdbserver => 'root', # start gdbserver on given device (default 'tcp::1234'), stop with 'none'
+    # root-only: host information leak
+    gpa2hpa => 'Sys.Modify', # print the host physical address corresponding to a guest physical address
+    # root-only: host information leak
+    gpa2hva => 'Sys.Modify', # print the host virtual address corresponding to a guest physical address
+    # root-only: redirect TCP or UDP connections from host to guest
+    hostfwd_add => 'root', # redirect TCP or UDP connections from host to guest (requires -net user)
+    # root-only: because hostfwd_add is
+    hostfwd_remove => 'root', # remove host-to-guest TCP or UDP redirection
+    # root-only: read from IO adress space (e.g. PCI devices)
+    i => 'Sys.Modify', # I/O port read
+    # root-only: log to arbitrary target file
+    logfile => 'root', # output logs to 'filename'
+    # root-only: no guarantee there are no KVM bugs that could afffect the real CPU
+    mce => 'root', # inject a MCE on the given CPU [and broadcast to other CPUs with -b option]
+    # root-only: allows to save to arbitrary file
+    memsave => 'root', # save to disk virtual memory dump starting at 'addr' of size 'size'
+    # root-only: could specify arbitrary host, also there is 'exec' and 'file' migrations
+    migrate => 'root', # migrate to URI (using -d to not wait for completion)
+    # root-only: allows setting arbitrary URI
+    migrate_incoming => 'root', # Continue an incoming migration from an -incoming defer
+    # root-only: allows setting arbitrary URI
+    migrate_recover => 'root', # Continue a paused incoming postcopy migration
+    # root-only: because nbd_server_start is
+    nbd_server_add => 'root', # export a block device via NBD
+    # root-only: because nbd_server_start is
+    nbd_server_remove => 'root', # remove an export previously exposed via NBD
+    # root-only: start NBD server on the host
+    nbd_server_start => 'root', # serve block devices on the given host and port
+    # root-only: because nbd_server_start is
+    nbd_server_stop => 'root', # stop serving block devices using the NBD protocol
+    # root-only: add host network device
+    netdev_add => 'root', # add host network device
+    # root-only: because netdev_add is
+    netdev_del => 'root', # remove host network device
+    # root-only: no guarantee there are no KVM bugs that could afffect the real CPU
+    nmi => 'root', # inject an NMI
+    # root-only: write to IO adress space (e.g. PCI devices)
+    o => 'root', # I/O port write
+    # root-only: create arbitrary objects, e.g. serial
+    object_add => 'root', # create QOM object
+    # root-only: because object_del is
+    object_del => 'root', # destroy QOM object
+    # root-only: inject error on PCIe devices
+    pcie_aer_inject_error => 'root', # inject pcie aer error
+    # root-only: save to arbitrary file
+    pmemsave => 'root', # save to disk physical memory dump starting at 'addr' of size 'size'
+    # root-only: modify arbitrary object properties
+    'qom-set' => 'root', # set QOM property.
+    # root-only: because savevm-start is
+    'savevm-end' => 'root', # Resume VM after snaphot.
+    # root-only: save VM state to arbitrary target file
+    'savevm-start' => 'root', # Prepare for snapshot and halt VM. Save VM state to statefile.
+    # root-only: dump to arbitrary target file
+    screendump => 'root', # save screen
+    # root-only: allows specifying arbitrary target file
+    snapshot_blkdev => 'root', # initiates a live snapshot of device
+    # root-only: allows inject-nmi
+    watchdog_action => 'root', # change watchdog action
+    # root-only: saves to arbitrary target file
+    wavcapture => 'root', # capture audio to a wave file
+    # root-only: not relevant for Proxmox VE
+    'xen-event-inject' => 'root', # inject event channel
+    # root-only: not relevant for Proxmox VE
+    'xen-event-list' => 'root', # list event channel state
+
+    announce_self => 'Sys.Modify', # Trigger GARP/RARP announcements
+    backup_cancel => 'Sys.Modify', # cancel the current VM backup
+    balloon => 'Sys.Modify', # request VM to change its memory allocation (in MB)
+    block_job_cancel => 'Sys.Modify', # stop an active background block operation
+    block_job_complete => 'Sys.Modify', # stop an active background block operation
+    block_job_pause => 'Sys.Modify', # pause an active background block operation
+    block_job_resume => 'Sys.Modify', # resume a paused background block operation
+    block_job_set_speed => 'Sys.Modify', # set maximum speed for a background block operation
+    block_resize => 'Sys.Modify', # resize a block image
+    block_set_io_throttle => 'Sys.Modify', # change I/O throttle limits for a block drive
+    boot_set => 'Sys.Modify', # define new values for the boot device list
+    calc_dirty_rate => 'Sys.Modify', # start a round of guest dirty rate measurement
+    cancel_vcpu_dirty_limit => 'Sys.Modify', # cancel dirty page rate limit
+    'chardev-send-break' => 'Sys.Modify', # send a break on chardev
+    closefd => 'Sys.Modify', # close a file descriptor previously passed via SCM rights
+    commit => 'Sys.Modify', # commit changes to the disk images or backing files
+    cont => 'Sys.Modify', # resume emulation
+    c => 'Sys.Modify', # short-form of 'cont'
+    cpu => 'Sys.Modify', # set the default CPU
+    delvm => 'Sys.Modify', # delete a VM snapshot from its tag
+    eject => 'Sys.Modify', # eject a removable medium (use -f to force it)
+    exit_preconfig => 'Sys.Modify', # exit the preconfig state
+    expire_password => 'Sys.Modify', # set spice/vnc password expire-time
+    getfd => 'Sys.Modify', # receive a file descriptor via SCM rights and assign it a name
+    gva2gpa => 'Sys.Modify', # print the guest physical address corresponding to a guest virtual address
+    loadvm => 'Sys.Modify', # restore a VM snapshot from its tag
+    log => 'Sys.Modify', # activate logging of the specified items
+    migrate_cancel => 'Sys.Modify', # cancel the current VM migration
+    migrate_continue => 'Sys.Modify', # Continue migration from the given paused state
+    migrate_pause => 'Sys.Modify', # Pause an ongoing migration (postcopy-only)
+    migrate_set_capability => 'Sys.Modify', # Enable/Disable the usage of a capability for migration
+    migrate_set_parameter => 'Sys.Modify', # Set the parameter for migration
+    migrate_start_postcopy => 'Sys.Modify', # Switch the migration to postcopy mode.
+    mouse_button => 'Sys.Modify', # change mouse button state (1=L, 2=M, 4=R)
+    mouse_move => 'Sys.Modify', # send mouse move events
+    mouse_set => 'Sys.Modify', # set which mouse device receives events
+    'one-insn-per-tb' => 'Sys.Modify', # run emulation with one guest instruction per translation block
+    print => 'Sys.Modify', # print expression value (use $reg for CPU register access)
+    p => 'Sys.Modify', # alias for 'print'
+    'qemu-io' => 'Sys.Modify', # run a qemu-io command on a block device
+    # decidedly not root-only even if qom-set ist, because it is just too useful
+    'qom-get' => 'Sys.Modify', # print QOM property
+    'qom-list' => 'Sys.Modify', # list QOM properties
+    quit => 'Sys.Modify', # quit the emulator
+    q => 'Sys.Modify', # short-form of 'quit'
+    replay_break => 'Sys.Modify', # set breakpoint at the specified instruction count
+    replay_delete_break => 'Sys.Modify', # remove replay breakpoint
+    replay_seek => 'Sys.Modify', # replay execution to the specified instruction count
+    ringbuf_read => 'Sys.Modify', # Read from a ring buffer character device
+    ringbuf_write => 'Sys.Modify', # Write to a ring buffer character device
+    savevm => 'Sys.Modify', # save a VM snapshot. If no tag is provided, a new snapshot is created
+    sendkey => 'Sys.Modify', # send keys to the VM
+    set_link => 'Sys.Modify', # change the link status of a network adapter
+    set_password => 'Sys.Modify', # set spice/vnc password
+    set_vcpu_dirty_limit => 'Sys.Modify', # set dirty page rate limit
+    snapshot_blkdev_internal => 'Sys.Modify', # take an internal snapshot of device.
+    snapshot_delete_blkdev_internal => 'Sys.Modify', # delete an internal snapshot of device.
+    stopcapture => 'Sys.Modify', # stop capture
+    stop => 'Sys.Modify', # stop emulation
+    s => 'Sys.Modify', # short-form of 'stop'
+    sum => 'Sys.Modify', # compute the checksum of a memory region
+    'sync-profile' => 'Sys.Modify', # enable, disable or reset synchronization profiling.
+    system_powerdown => 'Sys.Modify', # send system power down event
+    system_reset => 'Sys.Modify', # reset the system
+    system_wakeup => 'Sys.Modify', # wakeup guest from suspend
+    'trace-event' => 'Sys.Modify', # changes status of a specific trace event
+    x => 'Sys.Modify', # virtual memory dump starting at 'addr'
+    x_colo_lost_heartbeat => 'Sys.Modify', # Tell COLO that heartbeat is lost
+    xp => 'Sys.Modify', # physical memory dump starting at 'addr'
+};
+
+sub generate_description {
+    my $cmd_by_priv = {};
+    for my $cmd (sort keys $hmp_command_perms->%*) {
+        push $cmd_by_priv->{$hmp_command_perms->{$cmd}}->@*, $cmd;
+    }
+    my $none_cmds = delete($cmd_by_priv->{none})
+        or die "internal error - no commands for 'none' found";
+    my $root_only_cmds = delete($cmd_by_priv->{'root'})
+        or die "internal error no commands for 'root' found";
+
+    my $text = '';
+    $text .= "The following commands do not require any additional privilege: "
+        . join(', ', $none_cmds->@*) . "\n\n";
+
+    for my $priv (sort keys $cmd_by_priv->%*) {
+        $text .= "The following commands require '$priv': "
+            . join(', ', $cmd_by_priv->{$priv}->@*) . "\n\n";
+    }
+
+    $text .= "The following commands are root-only: " . join(', ', $root_only_cmds->@*) . "\n";
+}
+
+1;
diff --git a/src/PVE/API2/Qemu/Makefile b/src/PVE/API2/Qemu/Makefile
index e64aa278..7c539702 100644
--- a/src/PVE/API2/Qemu/Makefile
+++ b/src/PVE/API2/Qemu/Makefile
@@ -2,7 +2,7 @@ DESTDIR=
 PREFIX=/usr
 PERLDIR=$(PREFIX)/share/perl5
 
-SOURCES=Agent.pm CPU.pm Machine.pm
+SOURCES=Agent.pm CPU.pm HMPPerms.pm Machine.pm
 
 .PHONY: install
 install:
-- 
2.47.2





More information about the pve-devel mailing list