Add qubes.VMExec call, for running a single command

With a VMExecGUI variant that waits for a session.

See QubesOS/qubes-issues#4850.
This commit is contained in:
Pawel Marczewski 2020-01-23 13:31:19 +01:00
parent c997008e2f
commit 738548a8e4
No known key found for this signature in database
GPG Key ID: DE42EE9B14F96465
8 changed files with 129 additions and 1 deletions

View File

@ -30,6 +30,8 @@ etc/qubes-rpc/qubes.SuspendPreAll
etc/qubes-rpc/qubes.ConnectTCP
etc/qubes-rpc/qubes.VMShell
etc/qubes-rpc/qubes.VMRootShell
etc/qubes-rpc/qubes.VMExec
etc/qubes-rpc/qubes.VMExecGUI
etc/qubes-rpc/qubes.WaitForSession
etc/qubes-rpc/qubes.GetDate
etc/qubes-suspend-module-blacklist
@ -43,6 +45,7 @@ etc/qubes/rpc-config/qubes.SelectDirectory
etc/qubes/rpc-config/qubes.StartApp
etc/qubes/rpc-config/qubes.InstallUpdatesGUI
etc/qubes/rpc-config/qubes.VMShell+WaitForSession
etc/qubes/rpc-config/qubes.VMExecGUI
etc/qubes/suspend-post.d/README
etc/qubes/suspend-post.d/*.sh
etc/qubes/suspend-pre.d/README
@ -94,6 +97,7 @@ usr/bin/qubes-desktop-run
usr/bin/qubes-open
usr/bin/qubes-session-autostart
usr/bin/qubes-run-terminal
usr/bin/qubes-vmexec
usr/bin/qvm-copy
usr/bin/qvm-copy-to-vm
usr/bin/qvm-features-request

View File

@ -52,6 +52,7 @@ install:
install -t $(DESTDIR)$(QUBESRPCCMDDIR) \
qubes.Filecopy qubes.OpenInVM qubes.VMShell \
qubes.VMRootShell \
qubes.VMExec \
qubes.OpenURL \
qubes.SuspendPre qubes.SuspendPost qubes.GetAppmenus \
qubes.SuspendPreAll \
@ -69,6 +70,7 @@ install:
qubes.GetDate \
qubes.ShowInTerminal \
qubes.ConnectTCP
ln -s qubes.VMExec $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.VMExecGUI
for config in *.config; do \
install -D -m 0644 "$$config" "$(DESTDIR)$(QUBESRPCCONFDIR)/$${config%.config}"; \
done

3
qubes-rpc/qubes.VMExec Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec /usr/bin/qubes-vmexec "$@"

View File

@ -0,0 +1 @@
wait-for-session=1

52
qubesagent/test_vmexec.py Normal file
View File

@ -0,0 +1,52 @@
from unittest import TestCase
from unittest.mock import patch, call
from qubesagent.vmexec import main, decode, DecodeError
class TestVmExec(TestCase):
def test_00_decode_simple(self):
self.assertEqual(decode('echo+Hello'), [b'echo', b'Hello'])
def test_01_decode_empty(self):
self.assertEqual(decode('echo+'), [b'echo', b''])
def test_02_decode_escaping(self):
self.assertEqual(decode('echo+Hello-20world'),
[b'echo', b'Hello world'])
self.assertEqual(decode('-0A-0D'),
[b'\n\r'])
self.assertEqual(decode('-2Fbin-2Fls'),
[b'/bin/ls'])
self.assertEqual(decode('ls+--la'),
[b'ls', b'-la'])
self.assertEqual(decode('ls+---61'),
[b'ls', b'-a'])
self.assertEqual(decode('ls+----help'),
[b'ls', b'--help'])
def test_03_decode_errors(self):
with self.assertRaises(DecodeError):
decode('illegal/slash')
with self.assertRaises(DecodeError):
decode('illegal-singledash')
with self.assertRaises(DecodeError):
decode('smalletters-0a-0d')
with self.assertRaises(DecodeError):
decode('incompletebyte-A')
with self.assertRaises(DecodeError):
decode('incomplete-Abyte')
with self.assertRaises(DecodeError):
decode('ls+---threeslashes')
def test_10_main_exec(self):
with patch('os.execvp') as mock_execvp:
main(['vmexec', 'ls+--la'])
self.assertEqual(mock_execvp.call_args_list, [
call(b'ls', [b'ls', b'-la'])])
def test_11_main_fail(self):
with self.assertRaises(SystemExit):
main(['vmexec'])
with self.assertRaises(SystemExit):
main(['vmexec', 'illegal/slash'])

59
qubesagent/vmexec.py Normal file
View File

@ -0,0 +1,59 @@
import sys
import os
import re
class DecodeError(ValueError):
pass
ESCAPE_RE = re.compile(br'--|-([A-F0-9]{2})')
def decode_part(part):
if not re.match(r'^[a-zA-Z0-9._-]*$', part):
raise DecodeError('illegal characters found')
part = part.encode('ascii')
# Check if no '-' remains outside of legal escape sequences.
if b'-' in ESCAPE_RE.sub(b'', part):
raise DecodeError("'-' can be used only in '-HH' or '--'")
def convert(m):
if m.group(0) == b'--':
return b'-'
num = int(m.group(1), 16)
return bytes([num])
return ESCAPE_RE.sub(convert, part)
def decode(arg):
'''
Decode the argument for executing. The format is as follows:
- individual parts are split by '+'
- bytes are escaped as '-HH' (where HH is hex code, capital letters only)
- literal '-' is encoded as '--'
- otherwise, only [a-zA-Z0-9._] are allowed
:param arg: argument, as a string
:returns: list of exec arguments (each as bytes)
'''
return [decode_part(part) for part in arg.split('+')]
def main(argv=sys.argv):
if len(argv) != 2:
print('This service requires exactly one argument', file=sys.stderr)
exit(1)
try:
command = decode(argv[1])
except DecodeError as e:
print('Decode error: {}'.format(e), file=sys.stderr)
exit(1)
os.execvp(command[0], command)
if __name__ == '__main__':
main()

View File

@ -549,6 +549,8 @@ rm -f %{name}-%{version}
%config(noreplace) /etc/qubes-rpc/qubes.GetAppmenus
%config(noreplace) /etc/qubes-rpc/qubes.ConnectTCP
%config(noreplace) /etc/qubes-rpc/qubes.VMShell
%config(noreplace) /etc/qubes-rpc/qubes.VMExec
%config(noreplace) /etc/qubes-rpc/qubes.VMExecGUI
%config(noreplace) /etc/qubes-rpc/qubes.VMRootShell
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPre
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPreAll
@ -574,6 +576,7 @@ rm -f %{name}-%{version}
%config(noreplace) /etc/qubes/rpc-config/qubes.StartApp
%config(noreplace) /etc/qubes/rpc-config/qubes.InstallUpdatesGUI
%config(noreplace) /etc/qubes/rpc-config/qubes.VMShell+WaitForSession
%config(noreplace) /etc/qubes/rpc-config/qubes.VMExecGUI
%dir /etc/qubes/autostart
%config(noreplace) /etc/default/grub.qubes
/etc/qubes/autostart/README.txt
@ -616,6 +619,7 @@ rm -f %{name}-%{version}
/usr/bin/qubes-session-autostart
/usr/bin/qvm-console
/usr/bin/qvm-connect-tcp
/usr/bin/qubes-vmexec
%dir /usr/lib/qubes
/usr/lib/qubes/prepare-suspend
/usr/lib/qubes/qfile-agent
@ -667,6 +671,8 @@ rm -f %{name}-%{version}
%{python3_sitelib}/qubesagent/__init__.py*
%{python3_sitelib}/qubesagent/firewall.py*
%{python3_sitelib}/qubesagent/test_firewall.py*
%{python3_sitelib}/qubesagent/vmexec.py*
%{python3_sitelib}/qubesagent/test_vmexec.py*
%{python3_sitelib}/qubesagent/xdg.py*
/usr/share/qubes/mime-override/globs

View File

@ -16,7 +16,8 @@ if __name__ == '__main__':
entry_points={
'console_scripts': [
'qubes-firewall = qubesagent.firewall:main'
'qubes-firewall = qubesagent.firewall:main',
'qubes-vmexec = qubesagent.vmexec:main',
],
}
)