Qubes User Data - utilize `vm.config` to execute scripts at Qube launch, even in DispVMs

Thank you for @ddevz for the inspiration to write this tiny, general-purpose utility!

This script is a more “generic” version of what is presented in this thread: Customize a named disposable using the vm-config feature

It allows you to add any* script to any VM (including DispVMs) and have it executed at every start. The script can be plaintext or base64-encoded, like in AWS EC2 user-data.

More information here: GitHub - Atrate/qubes-user-data: Execute dom0-configured scripts in Disposable VMs (and other VMs) in QubesOS

* Unsure if there’s length limits, but that’s quite possible.

5 Likes

On my dom0, the bash command-line length limit is:

[user@dom0 ~]$ getconf ARG_MAX
2097152

… which is exactly 2 Mib. You have to deduct another 20-30 bytes for the rest of the command-line, but all in all, that is AMPLE space for your startup script! :smile:

Edit: there may be other, more stringent, restrictions for the qvm-features command :thinking:

You could compress the args and uncompress before use :melting_face:

Yeah, well … I was thinking about the command-line used by qvm-features to store the script (see the git repo) :
qvm-features QUBENAME vm-config.user-data ONELINER/BASE64ENCODEDSCRIPT

And the base64 encoded script is not compressible while staying command-line friendly.

Thanks @Atrate, your solution is very good!

I changed the ExecStart line to run the following python script instead. The initial idea was to be able to provide a python script. So my solution is the following:

  • if it’s a clear text, run it directly (I have not properly tested this with complex oneliner!)
  • if it’s encoded, save it in a file and run the file, any supported script with a correct shebang should work

For this to happen, one need:

  1. to set the feature vm-config.scripts with a list of “scripts” names, separated by spaces
  2. to set a oneliner or a base64 encoded script for each provided name as a feature named vm-config.scripts.<NAME> where <NAME> is equal to the name in vm-config.scripts.
#!/usr/bin/env python3
import base64
import binascii
import subprocess
import shlex

from pathlib import Path


import qubesdb

FEATURE_NAME = '/vm-config/scripts'
SCRIPTS_ROOT = Path('/tmp/vm-config-script')

db = qubesdb.QubesDB()

def run_file(name: str, content: bytes):
    path = SCRIPTS_ROOT / name
    SCRIPTS_ROOT.mkdir(exist_ok=True)
    with open(path, 'wb') as script_file:
        script_file.write(content)

    path.chmod(0o700)
    subprocess.call(path)

def run_oneliner(content: bytes):
    subprocess.run(shlex.split(content.decode()))

def run_script(name: str):
    feature_name = f'{FEATURE_NAME}.{name}'
    content = db.read(feature_name)
   
    if content is None:
        print(f"Impossible to read {feature_name}")
        return
   
    # The script content might be encoded in base64
    decoded = base64.b64decode(content)
    if base64.b64encode(decoded) == content:
        run_file(name, base64.decodebytes(content))
    else:
        run_oneliner(content)


    

def main():
    try:
        raw_scripts = db.read(FEATURE_NAME).decode()
    except AttributeError:
        return

    for script_name in raw_scripts.split():
        print(f"Run script {script_name}")
        run_script(script_name)

if __name__ == '__main__':
    main()
2 Likes

My method to check if something was base64-encoded was wrong under some circumstances, I edited my reply:

-    try:
+    decoded = base64.b64decode(content)
+    if base64.b64encode(decoded) == content:
         run_file(name, base64.decodebytes(content))
-    except binascii.Error:
+    else:
         run_oneliner(content)