Whonix in live mode in Qubes OS! Amnesia and anti-forensics mode

Patrick, this is fantastic. These guys have learned to run Qubes in live mode! I checked, and indeed the data is not saved. This allows the use of Whonix and Kicksecure in Qubes in amnesia mode. I would like to draw the attention of developers and enthusiasts to get involved in this topic and develop this wonderful project. Perhaps it should be added to the Whonix wiki. Let more users learn about it

so, you recommend using whonix exactly like this for maximum protection and anonymity?

Yes, this is the most effective solution. Patrick has been suggesting adding a live mode to Qubes for a long time implement live boot by porting grub-live to Qubes - amnesia / non-persistent boot / anti-forensics · Issue #4982 · QubesOS/qubes-issues · GitHub, and these guys implemented it several years ago. This solution has excellent reviews. I checked it and amnesia works. Therefore, the best options for using Whonix:
Live Kicksecure host + Whonix and Live Qubes with Whonix and Kicksecure VMs

I am not so excited yet.

  • Lengthy instructed hosted in a forum. I am not a fan of lengthy documentation in forums as it tends bit rot over time, cannot be updated collaboratively as if it was in a wiki or source code and requires reading the whole forum discussion.
  • Not officially supported by Qubes.
  • Anti-forensic Claims

I published a comment that they updated. I agree that it’s not very convenient - perhaps it’s worth suggesting to them to create a repository on GitHub-GitLab.

I don’t know why the Qubes team doesn’t officially support this - the project has been around for 3 years. Many users have long been asking for an amnesia mode for Qubes - many topics have been created about it. Perhaps it hasn’t been included in the official support because it’s just dracut settings. Dracut is very flexible and can be configured in various ways.

I think we can write in the wiki: “Some Qubes users create their own solutions for running live mode, which can enhance the protection of Whonix VMs” or something like that. I believe these users deserve attention. Their solution has been working on several versions of Qubes since 2022. This could attract other developers to the very important topic of anti-forensic protection and it will enhance the security of Whonix users in Qubes

Hello. I tested it. It works great! but you need to specify the correct configuration for maximum protection against forensics:

  1. Follow this instruction and make dom0 run in memory Qubes in tmpfs 🤫 - #30 by qstateless - Community Guides - Qubes OS Forum
    This will make only dom0 amnesic!
  2. Then it is desirable to increase the size of dom0 (I highly doubt that 20 GB will be enough after the 3 point):
sudo lvresize --size 30G /dev/mapper/qubes_dom0-root
sudo resize2fs /dev/mapper/qubes_dom0-root
  1. Follow this instruction and recreate the qubes so that all this is stored exactly in dom0 Qubes in tmpfs 🤫 - #56 by qstateless - Community Guides - Qubes OS Forum
  • a. Instead of using AppQubes based on TemplateQubes, you could alternatively create StandaloneQubes in the ‘varlibqubes’ storage pool, which appear to store ALL data in traditional image files within Dom0’s ‘/var/lib/qubes’ directory. This is very costly in Dom0 RAM, as it copies your entire TemplateQube’s OS into Dom0 RAM space for each StandaloneQube you make, whether it is actively running or not, which is usually multiple extra GBs per qube, in addition to the RAM it takes to store any user files and the RAM it takes to run and operate the qube’s OS & apps.

  • b. You could create a new TemplateQube from the previous TemplateQube you want to use, but store that new TemplateQube in the ‘varlibqubes’ storage pool. Then create a new AppQubes based on this new TemplateQube, and store this new AppQube in the ‘varlibqubes’ storage pool too. Now, when you use this new AppQube, it appears to store ALL data in traditional image files within Dom0’s ‘/var/lib/qubes’ directory. This is as costly as the other method for the first AppQube, but you do not have to copy & store the entire TemplateQube OS root filesystem for every AppQube you want to make with it, so this saves a lot of RAM space for using more than one qube.

  • c. Like “b”, you could create a new TemplateQube from the previous TemplateQube you want to use, but store that new TemplateQube in the ‘varlibqubes’ storage pool. Then create a Disposable Template by creating new AppQubes based on this new TemplateQube, and store this new AppQube in the ‘varlibqubes’ storage pool too. After creation, in the settings of this AppQube, under the “Advanced” tab, you can check to turn on “Disposable template” and after applying also select “Default disposable template” to either be “(none)” or that very same AppQube itself. Now, you can use both this TemplateQube and Disposable Template AppQube to create new AppQubes and DisposableQubes fully within the ‘varlibqubes’ storage pool. This is likely to generally be the most desirable approach for most people.

Here is an example implementation of approach “c”:

Let’s say you want to base some of your fully stateless qubes on the ‘debian-12-xfce’ persistent template.

    1. In Persistent mode: Create & Configure a new TemplateQube named ‘debian-12-xfce-stateless’ based on ‘debian-12-xfce’ and choose to store it in storage pool ‘varlibqubes’ (Advanced tab).
    1. In Persistent mode: Create & Configure a new AppQube named ‘debian-12-xfce-stateless-dvm’ based on ‘debian-12-xfce-stateless’ and choose to store it in storage pool ‘varlibqubes’ (Advanced tab).
    1. In Persistent mode: After creation, for the AppQube ‘debian-12-xfce-stateless-dvm’, change the ‘Advanced’ tab setting ‘Disposable template’ to be checked as turned on (click Apply), then the ‘Default disposable template’ to either be ‘(none)’ or ‘debian-12-xfce-stateless-dvm’ itself.
    1. In Persistent mode: Create & Configure any new AppQubes based on ‘debian-12-xfce-stateless’ that you want to exist across multiple stateless boot sessions and choose to store them in storage pool ‘varlibqubes’ (Advanced tab).
    1. In Stateless mode: You are free to now use any AppQubes based on ‘debian-12-xfce-stateless’ and DisposableQubes based on ‘debian-12-xfce-stateless-dvm’, which appear to remain fully stateless by storing ALL data in traditional image files within Dom0’s ‘/var/lib/qubes’ directory (that directory gets wiped and reset back to match the state of your last persistent session once your stateless session is powered down).

I made an amnesic template Kicksecure and created all secure qubes based on it. I also changed all Whonix templates. I created a Fedora-live template for cubes with gimp, strawberry, vlc. In the end, it works great. There is no saving in either appvm or dvm qubes. For good performance, I recommend having at least 32 GB of memory (I recommend the same amount for running Whonix from the Kicksecure-live host). 32 GB in Qubes allows you to run many virtual machines and it is convenient. If you have 16 GB of memory, only dvm is used in amnesic mode. Otherwise, you will quickly run out of free space. By the way, some Whonix forum moderators are impressed with this solution. It is really amazing, but the devs write that it can be improved and they are happy with new ideas and solutions. It’s a shame that this is published on the forum and not on GitHub. You have to read all the messages to not miss the relevant information. But I am very satisfied with this solution. I hope it will continue to develop.

1 Like

Your instruction is very good!
I think this solution will work for a long time. It’s independent of Qubes, so it should work with other distros as well. This solution is also more efficient in terms of memory usage for Qubes compared to grub-live. I have a laptop with 62GB of memory. In Kicksecure live, I have 31GB of free disk space. In Qubes live, I allocated 50GB for dom0, and all of it is available for running VMs. That’s really cool. Grub-live would require 128GB of memory to work normally with Qubes live. Maybe someone will come up with updates to this solution to require even less memory for Qubes, but I’m very satisfied with it now.

But if you only use whonix-workstation, whonix-workstation-dvm, and, for example, mullvad-dvm, it won’t require a lot of memory. Computers with 16GB of memory will be sufficient for such amnesic sessions

:exclamation:If you are copying your amnesic VMs, always choose ‘varlibqubes’! Otherwise, it will lose amnesia! I conducted a test and copied an amnesic AppVM based on an amnesic template, but chose ‘vm-pool’. This almost completely removed amnesia - only newly added programs in the Qubes Manager were not saved.

Patrick, maybe we should edit this text:

At time of writing, there is no shortcut, substitute or workaround available that users can easily use to get Qubes Anti-Forensics capabilities. This is elaborated in developers chapter Forensic Considerations and Anti-forensics Claims.

Disposables are not amnesic. In practice this means traces of their activity can be left on storage or in memory, making them vulnerable to forensic operations. [20]

I read this a long time ago and it upset me then. I think it’s worth mentioning this solution. For example, “Experienced Qubes users have created a workaround for anti-forensic protection…”

Did anyone consider Forensic Considerations and Anti-forensics Claims?

If you have a lot of memory and plan to create only amnesic varlibqubes, then during the Qubes installation, do not select the installation of templates. You can choose only sys-net and sys-firewall. The installation will proceed very quickly.
After logging into the system, open the dom0 terminal and change the default Storage pool:
qubes-prefs default_pool varlibqubes
After that, install the Whonix templates How-to: Install Qubes-Whonix It will be installed directly in dom0. Then install other templates.
After that, proceed with this instruction:
Qubes in tmpfs 🤫 - #30 by qstateless - Community Guides - Qubes OS Forum

1 Like

In Qubes OS, you cannot perform a TRIM operation on varlibqubes to increase free disk space. This is very important for live mode. You can easily perform a TRIM operation as follows: create a copy of the templates in the vm-pool → start a terminal in the whonix-gateway-clone-1 (vm-pool) → clear cache and unnecessary packages → then enter sudo fstrim -av → create a copy of the templates in the varlibqubes. To perform a TRIM operation in a workstation, do the following: open the terminal in dom0 and enter qvm-console-dispvm whonix-workstation-17-clone-1 (vm-pool) → then log in as root → clear cache and unnecessary packages → write sudo fstrim -av → create a copy of the templates in the varlibqubes. With these actions, I was able to remove more than 3 GB of trash!

PS: You may encounter error copy from a dirty volume when testing it in live mode. But in persistent mode, everything should work fine. I made a trim in Debian, Fedora, and Whonix templates without any problems.

[quote=“Patrick, post:10, topic:21912”]
Patrick, I have studied Qubes live and grub-live a bit. It seems that no one has been able to port grub-live to Qubes due to very high memory requirements. These guys changed the approach to this solution - you can implement live only for dom0 storage. This saves a lot of memory. I think we can try to implement grub-live only for dom0. But it should work differently than in Kicksecure. I have half of my memory on disk in Kicksecure Live. If I have 16 GB of memory, I won’t be able to run Whonix from KVM/VirtualBox in live mode due to quick memory exhaustion. But this solution allows running sys-whonix, anon-whonix, and whonix-dvm in memory - I asked a friend to check it on a laptop with 16 GB of memory. One of the authors of this project thoroughly studied the amnesic properties of this solution. No data remains if both the template and appvm are created in varlibqubes. I decided to check this myself and also did not find any traces of the session - snapshots, images, and metadata are stored in dom0, so they disappear. Is this an ideal solution for forensic protection? We don’t know. Is grub-live ideal for it? We also don’t know. You say you are not a forensic expert, but you implemented live mode to protect your users. You know that this will greatly enhance their protection. You are a very kind person and I admire you. I think this solution for Whonix on Qubes is very good and also greatly enhances forensic protection. But I also think it can be significantly improved. Essentially, live Qubes works according to this script:

mkdir /mnt
umount /sysroot
mount /dev/mapper/qubes_dom0-root /mnt
modprobe zram
echo 10G > /sys/block/zram0/disksize
/mnt/usr/sbin/mkfs.ext2 /dev/zram0
mount /dev/zram0 /sysroot
cp -a /mnt/* /sysroot
exit 0

It just copies the dom0 partition to zram. Perhaps this can be improved with overlayfs. It seems you are familiar with this (or someone from the Whonix team). I think it would be great to improve this and recommend it to Whonix and Kicksecure on Whonix users. Maybe you will immediately see where improvements can be made in this small code. I am ready to test any new ideas about it. Maybe it can be implemented through overlayfs. But only so that memory is not halved, as in Kicksecure

A post was split to a new topic: Grub-live improvement - overlay-mount.sh - add, increase size= mount command parameter

This needs to be contributed upstream. Qubes amnesia development is fully independent from Whonix. This here is the wrong place to discuss this.

Unfortunately, not. Only Qubes developers can comment on that.

1 Like

Perhaps size=100% parameter will help implement this suggest (implement live boot by porting grub-live to Qubes - amnesia / non-persistent boot / anti-forensics · Issue #4982 · QubesOS/qubes-issues · GitHub). For example, my laptop has 32 GB of memory. I had 15 GB in the default live mode in Kicksecure host. Now I have 31 GB in live mode. It allows me to run Whonix gateway and Whonix workstation, Kicksecure VM, Ubuntu KDE, and Fedora KDE simultaneously in KVM (provided I use fstrim -av in the virtual machines). These are full-fledged distributions, not lightweight Qubes templates. We will always have only half of the memory in overlayfs-tmpfs if we do not specify the size.
Copying dom0 to zram is a very simple solution. It is efficient, but the boot time increases. Perhaps this can be implemented using overlayfs for dom0. I’m not an expert in this, but I would be interested in trying it next week

1 Like

Wow, size=100% works! Thank you! And I don’t notice any issues with RAM shortage - I opened 10 tabs in browser and ran KeepassXC, Thunar, VLC, and everything works without problems :slightly_smiling_face:

1 Like

Whonix and Kicksecure in Live Mode with OverlayFS in Qubes OS

Qubes in tmpfs 🤫 - #30 by qstateless - Community Guides - Qubes OS Forum - Method of copying to zram is very slow, less secure and has more limitations on memory usage in live mode than OverlayFS. With OverlayFS / filesystem will be read-only and launching in live mode will be very fast. and you will also have more free disk space. The original live mode in Whonix and Kicksecure works on OverlayFS.

Instructions ( do it in dom0):

  1. Create a folder 90overlay-root in /usr/lib/dracut/modules.
    sudo mkdir /usr/lib/dracut/modules.d/90overlay-root

  2. Create two files module-setup.sh and overlay-mount.sh in this folder and make it executable.

sudo touch /usr/lib/dracut/modules.d/90overlay-root/module-setup.sh
sudo touch /usr/lib/dracut/modules.d/90overlay-root/overlay-mount.sh
sudo chmod 755 /usr/lib/dracut/modules.d/90overlay-root/module-setup.sh
sudo chmod 755 /usr/lib/dracut/modules.d/90overlay-root/overlay-mount.sh
  1. Open module-setup.sh
    sudo nano /usr/lib/dracut/modules.d/90overlay-root/module-setup.sh
    Add this code:
#!/bin/bash

check() {
    # do not add modules if the kernel does not have overlayfs support
    [ -d /lib/modules/$kernel/kernel/fs/overlayfs ] || return 1
}

depends() {
    # We do not depend on any modules - just some root
    return 0
}

# called by dracut
installkernel() {
    hostonly='' instmods overlay
}

install() {
    inst_hook pre-pivot 10 "$moddir/overlay-mount.sh"
}

Press Ctrl + O and Enter
Press Ctrl + X to exit nano editor.

  1. Open overlay-mount.sh
    sudo nano /usr/lib/dracut/modules.d/90overlay-root/overlay-mount.sh
    Add this code:
#!/bin/sh

# make a read-only nfsroot writeable by using overlayfs
# the nfsroot is already mounted to $NEWROOT
# add the parameter rootovl to the kernel, to activate this feature

. /lib/dracut-lib.sh

if ! getargbool 0 rootovl ; then
    return
fi

modprobe overlay

# a little bit tuning
mount -o remount,nolock,noatime $NEWROOT

# Move root
# --move does not always work. Google >mount move "wrong fs"< for
#     details
mkdir -p /live/image
mount --bind $NEWROOT /live/image
umount $NEWROOT

# Create tmpfs
mkdir /cow
mount -n -t tmpfs -o mode=0755,size=100% tmpfs /cow
mkdir /cow/work /cow/rw

# Merge both to new Filesystem
mount -t overlay -o noatime,lowerdir=/live/image,upperdir=/cow/rw,workdir=/cow/work,default_permissions overlay $NEWROOT

# Let filesystems survive pivot
mkdir -p $NEWROOT/live/cow
mkdir -p $NEWROOT/live/image
mount --bind /cow/rw $NEWROOT/live/cow
umount /cow
mount --bind /live/image $NEWROOT/live/image
umount /live/image

Press Ctrl + O and Enter
Press Ctrl + X to exit nano editor.

  1. Create a file dracut-lib.sh in lib and make it executable
sudo touch lib/dracut-lib.sh
sudo chmod 755 lib/dracut-lib.sh

And add this code:

#!/bin/sh

type wait_for_dev > /dev/null 2>&1 || . /lib/dracut-dev-lib.sh

export DRACUT_SYSTEMD
export NEWROOT
if [ -n "$NEWROOT" ]; then
    [ -d "$NEWROOT" ] || mkdir -p -m 0755 "$NEWROOT"
fi

# shellcheck disable=SC2153
if [ -z "$PREFIX" ]; then
    if ! [ -d /run/initramfs ]; then
        mkdir -p -m 0755 /run/initramfs/log
        ln -sfn /run/initramfs/log /var/log
    fi

    [ -d /run/lock ] || mkdir -p -m 0755 /run/lock
    [ -d /run/log ] || mkdir -p -m 0755 /run/log
fi

debug_off() {
    set +x
}

debug_on() {
    [ "$RD_DEBUG" = "yes" ] && set -x
}

# returns OK if $1 contains literal string $2 (and isn't empty)
strstr() {
    [ "${1##*"$2"*}" != "$1" ]
}

# returns OK if $1 matches (completely) glob pattern $2
# An empty $1 will not be considered matched, even if $2 is * which technically
# matches; as it would match anything, it's not an interesting case.
strglob() {
    [ -n "$1" -a -z "${1##$2}" ]
}

# returns OK if $1 contains (anywhere) a match of glob pattern $2
# An empty $1 will not be considered matched, even if $2 is * which technically
# matches; as it would match anything, it's not an interesting case.
strglobin() {
    [ -n "$1" -a -z "${1##*$2*}" ]
}

# returns OK if $1 contains literal string $2 at the beginning, and isn't empty
str_starts() {
    [ "${1#"$2"*}" != "$1" ]
}

# returns OK if $1 contains literal string $2 at the end, and isn't empty
str_ends() {
    [ "${1%*"$2"}" != "$1" ]
}

trim() {
    local var="$*"
    var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
    var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
    printf "%s" "$var"
}

if [ -z "$DRACUT_SYSTEMD" ]; then

    warn() {
        check_quiet
        echo "<28>dracut Warning: $*" > /dev/kmsg
        echo "dracut Warning: $*" >&2
    }

    info() {
        check_quiet
        echo "<30>dracut: $*" > /dev/kmsg
        if [ "$DRACUT_QUIET" != "yes" ]; then
            echo "dracut: $*" >&2
        fi
    }

else

    warn() {
        echo "Warning: $*" >&2
    }

    info() {
        echo "$*"
    }

fi

vwarn() {
    while read -r line || [ -n "$line" ]; do
        warn "$line"
    done
}

vinfo() {
    while read -r line || [ -n "$line" ]; do
        info "$line"
    done
}

killall_proc_mountpoint() {
    local _pid
    local _killed=0
    for _pid in /proc/*; do
        _pid=${_pid##/proc/}
        case $_pid in
            *[!0-9]*) continue ;;
        esac
        [ -e "/proc/$_pid/exe" ] || continue
        [ -e "/proc/$_pid/root" ] || continue
        if strstr "$(ls -l -- "/proc/$_pid" "/proc/$_pid/fd" 2> /dev/null)" "$1"; then
            kill -9 "$_pid"
            _killed=1
        fi
    done
    return $_killed
}

getcmdline() {
    local _line
    local _i
    local CMDLINE_ETC_D
    local CMDLINE_ETC
    local CMDLINE_PROC
    unset _line

    if [ -e /etc/cmdline ]; then
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_ETC="$CMDLINE_ETC $_line"
        done < /etc/cmdline
    fi
    for _i in /etc/cmdline.d/*.conf; do
        [ -e "$_i" ] || continue
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_ETC_D="$CMDLINE_ETC_D $_line"
        done < "$_i"
    done
    if [ -e /proc/cmdline ]; then
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_PROC="$CMDLINE_PROC $_line"
        done < /proc/cmdline
    fi
    CMDLINE="$CMDLINE_ETC_D $CMDLINE_ETC $CMDLINE_PROC"
    printf "%s" "$CMDLINE"
}

getarg() {
    debug_off
    local _deprecated _newoption
    CMDLINE=$(getcmdline)
    export CMDLINE
    while [ $# -gt 0 ]; do
        case $1 in
            -d)
                _deprecated=1
                shift
                ;;
            -y)
                if dracut-getarg "$2" > /dev/null; then
                    if [ "$_deprecated" = "1" ]; then
                        if [ -n "$_newoption" ]; then
                            warn "Kernel command line option '$2' is deprecated, use '$_newoption' instead."
                        else
                            warn "Option '$2' is deprecated."
                        fi
                    fi
                    echo 1
                    debug_on
                    return 0
                fi
                _deprecated=0
                shift 2
                ;;
            -n)
                if dracut-getarg "$2" > /dev/null; then
                    echo 0
                    if [ "$_deprecated" = "1" ]; then
                        if [ -n "$_newoption" ]; then
                            warn "Kernel command line option '$2' is deprecated, use '$_newoption=0' instead."
                        else
                            warn "Option '$2' is deprecated."
                        fi
                    fi
                    debug_on
                    return 1
                fi
                _deprecated=0
                shift 2
                ;;
            *)
                if [ -z "$_newoption" ]; then
                    _newoption="$1"
                fi
                if dracut-getarg "$1"; then
                    if [ "$_deprecated" = "1" ]; then
                        if [ -n "$_newoption" ]; then
                            warn "Kernel command line option '$1' is deprecated, use '$_newoption' instead."
                        else
                            warn "Option '$1' is deprecated."
                        fi
                    fi
                    debug_on
                    return 0
                fi
                _deprecated=0
                shift
                ;;
        esac
    done
    debug_on
    return 1
}

# getargbool <defaultval> <args...>
# False if "getarg <args...>" returns "0", "no", or "off".
# True if getarg returns any other non-empty string.
# If not found, assumes <defaultval> - usually 0 for false, 1 for true.
# example: getargbool 0 rd.info
#   true: rd.info, rd.info=1, rd.info=xxx
#   false: rd.info=0, rd.info=off, rd.info not present (default val is 0)
getargbool() {
    local _b
    unset _b
    local _default
    _default="$1"
    shift
    _b=$(getarg "$@") || _b=${_b:-"$_default"}
    if [ -n "$_b" ]; then
        [ "$_b" = "0" ] && return 1
        [ "$_b" = "no" ] && return 1
        [ "$_b" = "off" ] && return 1
    fi
    return 0
}

isdigit() {
    case "$1" in
        *[!0-9]* | "") return 1 ;;
    esac

    return 0
}

# getargnum <defaultval> <minval> <maxval> <arg>
# Will echo the arg if it's in range [minval - maxval].
# If it's not set or it's not valid, will set it <defaultval>.
# Note all values are required to be >= 0 here.
# <defaultval> should be with [minval -maxval].
getargnum() {
    local _b
    unset _b
    local _default _min _max
    _default="$1"
    shift
    _min="$1"
    shift
    _max="$1"
    shift
    _b=$(getarg "$1") || _b=${_b:-"$_default"}
    if [ -n "$_b" ]; then
        isdigit "$_b" && _b=$((_b)) \
            && [ $_b -ge "$_min" ] && [ $_b -le "$_max" ] && echo $_b && return
    fi
    echo "$_default"
}

getargs() {
    debug_off
    CMDLINE=$(getcmdline)
    export CMDLINE
    local _val _i _gfound _deprecated
    unset _val
    unset _gfound
    _newoption="$1"
    for _i in "$@"; do
        if [ "$_i" = "-d" ]; then
            _deprecated=1
            continue
        fi

        if _val="$(dracut-getargs "$_i")"; then
            if [ "$_deprecated" = "1" ]; then
                if [ -n "$_newoption" ]; then
                    warn "Option '$_i' is deprecated, use '$_newoption' instead."
                else
                    warn "Option $_i is deprecated!"
                fi
            fi
            if [ -n "$_val" ]; then
                printf '%s\n' "$_val"
            fi
            _gfound=1
        fi
        _deprecated=0
    done
    if [ -n "$_gfound" ]; then
        debug_on
        return 0
    fi
    debug_on
    return 1
}

# Prints value of given option.  If option is a flag and it's present,
# it just returns 0.  Otherwise 1 is returned.
# $1 = options separated by commas
# $2 = option we are interested in
#
# Example:
# $1 = cipher=aes-cbc-essiv:sha256,hash=sha256,verify
# $2 = hash
# Output:
# sha256
getoptcomma() {
    local line=",$1,"
    local opt="$2"
    local tmp

    case "${line}" in
        *,${opt}=*,*)
            tmp="${line#*,${opt}=}"
            echo "${tmp%%,*}"
            return 0
            ;;
        *,${opt},*) return 0 ;;
    esac
    return 1
}

# Splits given string 'str' with separator 'sep' into variables 'var1', 'var2',
# 'varN'.  If number of fields is less than number of variables, remaining are
# not set.  If number of fields is greater than number of variables, the last
# variable takes remaining fields.  In short - it acts similary to 'read'.
#
# splitsep sep str var1 var2 varN
#
# example:
#   splitsep ':' 'foo:bar:baz' v1 v2
# in result:
#   v1='foo', v2='bar:baz'
#
# TODO: ':' inside fields.
splitsep() {
    debug_off
    local sep="$1"
    local str="$2"
    shift 2
    local tmp

    while [ -n "$str" -a "$#" -gt 1 ]; do
        tmp="${str%%$sep*}"
        eval "$1='${tmp}'"
        str="${str#"$tmp"}"
        str="${str#$sep}"
        shift
    done
    [ -n "$str" -a -n "$1" ] && eval "$1='$str'"
    debug_on
    return 0
}

setdebug() {
    [ -f /usr/lib/initrd-release ] || return
    if [ -z "$RD_DEBUG" ]; then
        if [ -e /proc/cmdline ]; then
            RD_DEBUG=no
            if getargbool 0 rd.debug -d -y rdinitdebug -d -y rdnetdebug; then
                RD_DEBUG=yes
                [ -n "$BASH" ] \
                    && export PS4='${BASH_SOURCE}@${LINENO}(${FUNCNAME[0]-}): '
            fi
        fi
        export RD_DEBUG
    fi
    debug_on
}

setdebug

source_all() {
    local f
    local _dir
    _dir=$1
    shift
    [ "$_dir" ] && [ -d "/$_dir" ] || return
    for f in "/$_dir"/*.sh; do
        if [ -e "$f" ]; then
            # shellcheck disable=SC1090
            # shellcheck disable=SC2240
            . "$f" "$@"
        fi
    done
}

hookdir=/lib/dracut/hooks
export hookdir

source_hook() {
    local _dir
    _dir=$1
    shift
    source_all "/lib/dracut/hooks/$_dir" "$@"
}

check_finished() {
    local f
    for f in "$hookdir"/initqueue/finished/*.sh; do
        [ "$f" = "$hookdir/initqueue/finished/*.sh" ] && return 0
        # shellcheck disable=SC1090
        { [ -e "$f" ] && (. "$f"); } || return 1
    done
    return 0
}

source_conf() {
    local f
    [ "$1" ] && [ -d "/$1" ] || return
    # shellcheck disable=SC1090
    for f in "/$1"/*.conf; do [ -e "$f" ] && . "$f"; done
}

die() {
    {
        echo "<24>dracut: FATAL: $*"
        echo "<24>dracut: Refusing to continue"
    } > /dev/kmsg

    {
        echo "warn dracut: FATAL: \"$*\""
        echo "warn dracut: Refusing to continue"
    } >> $hookdir/emergency/01-die.sh
    [ -d /run/initramfs ] || mkdir -p -- /run/initramfs

    : > /run/initramfs/.die

    if getargbool 0 "rd.shell"; then
        emergency_shell
    else
        source_hook "shutdown-emergency"
    fi

    if [ -n "$DRACUT_SYSTEMD" ]; then
        systemctl --no-block --force halt
    fi

    exit 1
}

check_quiet() {
    if [ -z "$DRACUT_QUIET" ]; then
        DRACUT_QUIET="yes"
        getargbool 0 rd.info -d -y rdinfo && DRACUT_QUIET="no"
        getargbool 0 rd.debug -d -y rdinitdebug && DRACUT_QUIET="no"
        getarg quiet || DRACUT_QUIET="yes"
        a=$(getarg loglevel=)
        [ -n "$a" ] && [ "$a" -ge 28 ] && DRACUT_QUIET="yes"
        export DRACUT_QUIET
    fi
}

check_occurances() {
    # Count the number of times the character $ch occurs in $str
    # Return 0 if the count matches the expected number, 1 otherwise
    local str="$1"
    local ch="$2"
    local expected="$3"
    local count=0

    while [ "${str#*$ch}" != "${str}" ]; do
        str="${str#*$ch}"
        count=$((count + 1))
    done

    [ $count -eq "$expected" ]
}

incol2() {
    debug_off
    local check
    local file="$1"
    local str="$2"

    [ -z "$file" ] && return 1
    [ -z "$str" ] && return 1

    while read -r _ check _ || [ -n "$check" ]; do
        if [ "$check" = "$str" ]; then
            debug_on
            return 0
        fi
    done < "$file"
    debug_on
    return 1
}

udevsettle() {
    # shellcheck disable=SC2086
    udevadm settle --exit-if-exists=$hookdir/initqueue/work $settle_exit_if_exists
}

udevproperty() {
    for i in "$@"; do
        udevadm control --property="$i"
    done
}

find_mount() {
    local dev wanted_dev
    wanted_dev="$(readlink -e -q "$1")"
    while read -r dev _ || [ -n "$dev" ]; do
        [ "$dev" = "$wanted_dev" ] && echo "$dev" && return 0
    done < /proc/mounts
    return 1
}

# usage: ismounted <mountpoint>
# usage: ismounted /dev/<device>
if command -v findmnt > /dev/null; then
    ismounted() {
        findmnt "$1" > /dev/null 2>&1
    }
else
    ismounted() {
        if [ -b "$1" ]; then
            find_mount "$1" > /dev/null && return 0
            return 1
        fi

        while read -r _ m _ || [ -n "$m" ]; do
            [ "$m" = "$1" ] && return 0
        done < /proc/mounts
        return 1
    }
fi

# Create udev rule match for a device with its device name, or the udev property
# ID_FS_UUID or ID_FS_LABEL
#
# example:
#   udevmatch LABEL=boot
# prints:
#   ENV{ID_FS_LABEL}="boot"
#
# TODO: symlinks
udevmatch() {
    case "$1" in
        UUID=????????-????-????-????-???????????? | LABEL=* | PARTLABEL=* | PARTUUID=????????-????-????-????-????????????)
            printf 'ENV{ID_FS_%s}=="%s"' "${1%%=*}" "${1#*=}"
            ;;
        UUID=*)
            printf 'ENV{ID_FS_UUID}=="%s*"' "${1#*=}"
            ;;
        PARTUUID=*)
            printf 'ENV{ID_FS_PARTUUID}=="%s*"' "${1#*=}"
            ;;
        /dev/?*) printf -- 'KERNEL=="%s"' "${1#/dev/}" ;;
        *) return 255 ;;
    esac
}

label_uuid_to_dev() {
    local _dev
    _dev="${1#block:}"
    case "$_dev" in
        LABEL=*)
            echo "/dev/disk/by-label/$(echo "${_dev#LABEL=}" | sed 's,/,\\x2f,g;s, ,\\x20,g')"
            ;;
        PARTLABEL=*)
            echo "/dev/disk/by-partlabel/$(echo "${_dev#PARTLABEL=}" | sed 's,/,\\x2f,g;s, ,\\x20,g')"
            ;;
        UUID=*)
            echo "/dev/disk/by-uuid/${_dev#UUID=}"
            ;;
        PARTUUID=*)
            echo "/dev/disk/by-partuuid/${_dev#PARTUUID=}"
            ;;
        *)
            echo "$_dev"
            ;;
    esac
}

# Prints unique path for potential file inside specified directory.  It consists
# of specified directory, prefix and number at the end which is incremented
# until non-existing file is found.
#
# funiq dir prefix
#
# example:
# # ls /mnt
# cdrom0 cdrom1
#
# # funiq /mnt cdrom
# /mnt/cdrom2
funiq() {
    local dir="$1"
    local prefix="$2"
    local i=0

    [ -d "${dir}" ] || return 1

    while [ -e "${dir}/${prefix}$i" ]; do
        i=$((i + 1)) || return 1
    done

    echo "${dir}/${prefix}$i"
}

# Creates unique directory and prints its path.  It's using funiq to generate
# path.
#
# mkuniqdir subdir new_dir_name
mkuniqdir() {
    local dir="$1"
    local prefix="$2"
    local retdir
    local retdir_new

    [ -d "${dir}" ] || mkdir -m 0755 -p "${dir}" || return 1

    retdir=$(funiq "${dir}" "${prefix}") || return 1
    until mkdir -m 0755 "${retdir}" 2> /dev/null; do
        retdir_new=$(funiq "${dir}" "${prefix}") || return 1
        [ "$retdir_new" = "$retdir" ] && return 1
        retdir="$retdir_new"
    done

    echo "${retdir}"
}

# Copy the contents of SRC into DEST, merging the contents of existing
# directories (kinda like rsync, or cpio -p).
# Creates DEST if it doesn't exist. Overwrites files with the same names.
#
# copytree SRC DEST
copytree() {
    local src="$1" dest="$2"
    [ -d "$src" ] || return 1
    mkdir -p "$dest" || return 1
    dest=$(readlink -e -q "$dest") || return 1
    (
        cd "$src" || exit 1
        cp -af . -t "$dest"
    )
}

# Evaluates command for UUIDs either given as arguments for this function or all
# listed in /dev/disk/by-uuid.  UUIDs doesn't have to be fully specified.  If
# beginning is given it is expanded to all matching UUIDs.  To pass full UUID to
# your command use '$___' as a place holder.  Remember to escape '$'!
#
# foreach_uuid_until [ -p prefix ] command UUIDs
#
# prefix - string to put just before $___
# command - command to be evaluated
# UUIDs - list of UUIDs separated by space
#
# The function returns after *first successful evaluation* of the given command
# with status 0.  If evaluation fails for every UUID function returns with
# status 1.
#
# Example:
# foreach_uuid_until "mount -U \$___ /mnt; echo OK; umount /mnt" \
#       "01234 f512 a235567f-12a3-c123-a1b1-01234567abcb"
foreach_uuid_until() (
    cd /dev/disk/by-uuid || return 1

    [ "$1" = -p ] && local prefix="$2" && shift 2
    local cmd="$1"
    shift
    local uuids_list="$*"
    local uuid
    local full_uuid
    local ___

    [ -n "${cmd}" ] || return 1

    for uuid in ${uuids_list:-*}; do
        for full_uuid in "${uuid}"*; do
            [ -e "${full_uuid}" ] || continue
            # shellcheck disable=SC2034
            ___="${prefix}${full_uuid}"
            eval "${cmd}" && return 0
        done
    done

    return 1
)

# Get kernel name for given device.  Device may be the name too (then the same
# is returned), a symlink (full path), UUID (prefixed with "UUID=") or label
# (prefixed with "LABEL=").  If just a beginning of the UUID is specified or
# even an empty, function prints all device names which UUIDs match - every in
# single line.
#
# NOTICE: The name starts with "/dev/".
#
# Example:
#   devnames UUID=123
# May print:
#   /dev/dm-1
#   /dev/sdb1
#   /dev/sdf3
devnames() {
    local dev="$1"
    local d
    local names

    case "$dev" in
        UUID=*)
            # shellcheck disable=SC2016
            dev="$(foreach_uuid_until '! blkid -U $___' "${dev#UUID=}")" \
                && return 255
            [ -z "$dev" ] && return 255
            ;;
        LABEL=*) dev="$(blkid -L "${dev#LABEL=}")" || return 255 ;;
        /dev/?*) ;;
        *) return 255 ;;
    esac

    for d in $dev; do
        names="$names
$(readlink -e -q "$d")" || return 255
    done

    echo "${names#
}"
}

usable_root() {
    local _i

    [ -d "$1" ] || return 1

    for _i in "$1"/usr/lib*/ld-*.so "$1"/lib*/ld-*.so; do
        [ -e "$_i" ] && return 0
    done

    for _i in proc sys dev; do
        [ -e "$1"/$_i ] || return 1
    done

    return 0
}

inst_hook() {
    local _hookname _unique _name _job _exe
    while [ $# -gt 0 ]; do
        case "$1" in
            --hook)
                _hookname="/$2"
                shift
                ;;
            --unique)
                _unique="yes"
                ;;
            --name)
                _name="$2"
                shift
                ;;
            *)
                break
                ;;
        esac
        shift
    done

    if [ -z "$_unique" ]; then
        _job="${_name}$$"
    else
        _job="${_name:-$1}"
        _job=${_job##*/}
    fi

    _exe=$1
    shift

    [ -x "$_exe" ] || _exe=$(command -v "$_exe")

    if [ -n "$onetime" ]; then
        {
            # shellcheck disable=SC2016
            echo '[ -e "$_job" ] && rm -f -- "$_job"'
            echo "$_exe $*"
        } > "/tmp/$$-${_job}.sh"
    else
        echo "$_exe $*" > "/tmp/$$-${_job}.sh"
    fi

    mv -f "/tmp/$$-${_job}.sh" "$hookdir/${_hookname}/${_job}.sh"
}

# inst_mount_hook <mountpoint> <prio> <name> <script>
#
# Install a mount hook with priority <prio>,
# which executes <script> as soon as <mountpoint> is mounted.
inst_mount_hook() {
    local _prio="$2" _jobname="$3" _script="$4"
    local _hookname
    _hookname="mount-$(str_replace "$1" '/' '\\x2f')"
    [ -d "$hookdir/${_hookname}" ] || mkdir -p "$hookdir/${_hookname}"
    inst_hook --hook "$_hookname" --unique --name "${_prio}-${_jobname}" "$_script"
}

# wait_for_mount <mountpoint>
#
# Installs a initqueue-finished script,
# which will cause the main loop only to exit,
# if <mountpoint> is mounted.
wait_for_mount() {
    local _name
    _name="$(str_replace "$1" '/' '\\x2f')"
    printf '. /lib/dracut-lib.sh\nismounted "%s"\n' "$1" \
        >> "$hookdir/initqueue/finished/ismounted-${_name}.sh"
    {
        printf 'ismounted "%s" || ' "$1"
        printf 'warn "\"%s\" is not mounted"\n' "$1"
    } >> "$hookdir/emergency/90-${_name}.sh"
}

killproc() {
    debug_off
    local _exe
    _exe="$(command -v "$1")"
    local _sig=$2
    local _i
    [ -x "$_exe" ] || return 1
    for _i in /proc/[0-9]*; do
        [ "$_i" = "/proc/1" ] && continue
        if [ -e "$_i"/_exe ] && [ "$_i/_exe" -ef "$_exe" ]; then
            kill "$_sig" "${_i##*/}"
        fi
    done
    debug_on
}

need_shutdown() {
    : > /run/initramfs/.need_shutdown
}

wait_for_loginit() {
    [ "$RD_DEBUG" = "yes" ] || return
    [ -e /run/initramfs/loginit.pipe ] || return
    debug_off
    echo "DRACUT_LOG_END"
    exec 0<> /dev/console 1<> /dev/console 2<> /dev/console
    # wait for loginit
    i=0
    while [ $i -lt 10 ]; do
        if [ ! -e /run/initramfs/loginit.pipe ]; then
            j=$(jobs)
            [ -z "$j" ] && break
            [ -z "${j##*Running*}" ] || break
        fi
        sleep 0.1
        i=$((i + 1))
    done

    if [ $i -eq 10 ]; then
        kill %1 > /dev/null 2>&1
        kill "$(while read -r line || [ -n "$line" ]; do echo "$line"; done < /run/initramfs/loginit.pid)"
    fi

    setdebug
    rm -f -- /run/initramfs/loginit.pipe /run/initramfs/loginit.pid
}

# pidof version for root
if ! command -v pidof > /dev/null 2> /dev/null; then
    pidof() {
        debug_off
        local _cmd
        local _exe
        local _rl
        local _ret=1
        local i
        _cmd="$1"
        if [ -z "$_cmd" ]; then
            debug_on
            return 1
        fi
        _exe=$(command -v "$1")
        for i in /proc/*/exe; do
            [ -e "$i" ] || continue
            if [ -n "$_exe" ]; then
                [ "$i" -ef "$_exe" ] || continue
            else
                _rl=$(readlink -f "$i")
                [ "${_rl%/$_cmd}" != "$_rl" ] || continue
            fi
            i=${i%/exe}
            echo "${i##/proc/}"
            _ret=0
        done
        debug_on
        return $_ret
    }
fi

_emergency_shell() {
    local _name="$1"
    if [ -n "$DRACUT_SYSTEMD" ]; then
        : > /.console_lock
        echo "PS1=\"$_name:\\\${PWD}# \"" > /etc/profile
        systemctl start dracut-emergency.service
        rm -f -- /etc/profile
        rm -f -- /.console_lock
    else
        debug_off
        source_hook "$hook"
        echo
        /sbin/rdsosreport
        echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
        echo 'after mounting them and attach it to a bug report.'
        if ! RD_DEBUG='' getargbool 0 rd.debug -d -y rdinitdebug -d -y rdnetdebug; then
            echo
            echo 'To get more debug information in the report,'
            echo 'reboot with "rd.debug" added to the kernel command line.'
        fi
        echo
        echo 'Dropping to debug shell.'
        echo
        export PS1="$_name:\${PWD}# "
        [ -e /.profile ] || : > /.profile

        _ctty="$(RD_DEBUG='' getarg rd.ctty=)" && _ctty="/dev/${_ctty##*/}"
        if [ -z "$_ctty" ]; then
            _ctty=console
            while [ -f /sys/class/tty/$_ctty/active ]; do
                read -r _ctty < /sys/class/tty/$_ctty/active
                _ctty=${_ctty##* } # last one in the list
            done
            _ctty=/dev/$_ctty
        fi
        [ -c "$_ctty" ] || _ctty=/dev/tty1
        case "$(/usr/bin/setsid --help 2>&1)" in *--ctty*) CTTY="--ctty" ;; esac
        setsid $CTTY /bin/sh -i -l 0<> $_ctty 1<> $_ctty 2<> $_ctty
    fi
}

emergency_shell() {
    local _ctty
    set +e
    local _rdshell_name="dracut" action="Boot" hook="emergency"
    local _emergency_action

    if [ "$1" = "-n" ]; then
        _rdshell_name=$2
        shift 2
    elif [ "$1" = "--shutdown" ]; then
        _rdshell_name=$2
        action="Shutdown"
        hook="shutdown-emergency"
        shift 2
    fi

    echo
    echo
    warn "$*"
    echo

    _emergency_action=$(getarg rd.emergency)
    [ -z "$_emergency_action" ] \
        && [ -e /run/initramfs/.die ] \
        && _emergency_action=halt

    if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
        _emergency_shell "$_rdshell_name"
    else
        source_hook "$hook"
        warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
        [ -z "$_emergency_action" ] && _emergency_action=halt
    fi

    case "$_emergency_action" in
        reboot)
            reboot || exit 1
            ;;
        poweroff)
            poweroff || exit 1
            ;;
        halt)
            halt || exit 1
            ;;
    esac
}

# Retain the values of these variables but ensure that they are unexported
# This is a POSIX-compliant equivalent of bash's "export -n"
export_n() {
    local var
    local val
    for var in "$@"; do
        eval val=\$$var
        unset $var
        [ -n "$val" ] && eval "$var=\"$val\""
    done
}

# returns OK if list1 contains all elements of list2, i.e. checks if list2 is a
# sublist of list1.  An order and a duplication doesn't matter.
#
# $1 = separator
# $2 = list1
# $3 = list2
# $4 = ignore values, separated by $1
listlist() {
    local _sep="$1"
    local _list="${_sep}${2}${_sep}"
    local _sublist="$3"
    [ -n "$4" ] && local _iglist="${_sep}${4}${_sep}"
    local IFS="$_sep"
    local _v

    [ "$_list" = "$_sublist" ] && return 0

    for _v in $_sublist; do
        if [ -n "$_v" ] && ! ([ -n "$_iglist" ] && strstr "$_iglist" "$_v"); then
            strstr "$_list" "$_v" || return 1
        fi
    done

    return 0
}

# returns OK if both lists contain the same values.  An order and a duplication
# doesn't matter.
#
# $1 = separator
# $2 = list1
# $3 = list2
# $4 = ignore values, separated by $1
are_lists_eq() {
    listlist "$1" "$2" "$3" "$4" && listlist "$1" "$3" "$2" "$4"
}

setmemdebug() {
    if [ -z "$DEBUG_MEM_LEVEL" ]; then
        DEBUG_MEM_LEVEL=$(getargnum 0 0 5 rd.memdebug)
        export DEBUG_MEM_LEVEL
    fi
}

setmemdebug

# parameters: func log_level prefix msg [trace_level:trace]...
make_trace_mem() {
    local log_level prefix msg msg_printed
    local trace trace_level trace_in_higher_levels insert_trace

    msg=$1
    shift

    prefix='[debug_mem]'
    log_level=$DEBUG_MEM_LEVEL

    if [ -z "$log_level" ] || [ "$log_level" -le 0 ]; then
        return
    fi

    # FIXME? useless echo?
    # shellcheck disable=SC2116
    msg=$(echo "$msg")

    msg_printed=0
    while [ $# -gt 0 ]; do
        trace=${1%%:*}
        trace_level=${trace%%+}
        [ "$trace" != "$trace_level" ] && trace_in_higher_levels="yes"
        trace=${1##*:}

        if [ -z "$trace_level" ]; then
            trace_level=0
        fi

        insert_trace=0
        if [ -n "$trace_in_higher_levels" ]; then
            if [ "$log_level" -ge "$trace_level" ]; then
                insert_trace=1
            fi
        else
            if [ "$log_level" -eq "$trace_level" ]; then
                insert_trace=1
            fi
        fi

        if [ $insert_trace -eq 1 ]; then
            if [ $msg_printed -eq 0 ]; then
                echo "$prefix $msg"
                msg_printed=1
            fi
            show_memstats "$trace"
        fi
        shift
    done
}

# parameters: type
show_memstats() {
    case $1 in
        shortmem)
            while read -r line || [ -n "$line" ]; do
                str_starts "$line" "MemFree" \
                    || str_starts "$line" "Cached" \
                    || str_starts "$line" "Slab" \
                    || continue
                echo "$line"
            done < /proc/meminfo
            ;;
        mem)
            cat /proc/meminfo
            ;;
        slab)
            cat /proc/slabinfo
            ;;
        iomem)
            cat /proc/iomem
            ;;
    esac
}

remove_hostonly_files() {
    rm -fr /etc/cmdline /etc/cmdline.d/*.conf "$hookdir/initqueue/finished"
    if [ -f /lib/dracut/hostonly-files ]; then
        while read -r line || [ -n "$line" ]; do
            [ -e "$line" ] || [ -h "$line" ] || continue
            rm -f "$line"
        done < /lib/dracut/hostonly-files
    fi
}

# parameter: kernel_module [filesystem_name]
# returns OK if kernel_module is loaded
# modprobe fails if /lib/modules is not available (--no-kernel use case)
load_fstype() {
    strstr "$(cat /proc/filesystems)" "${2:-$1}" || modprobe "$1"
}

  1. Update dracut
    sudo dracut --force

  2. Open /etc/default/grub
    sudo nano /etc/default/grub
    add rootovl to GRUB_CMDLINE_LINUX, then change dom0_mem=max: and add 90% of your available RAM (you can find this info with command xl info | grep total_memory in terminal). For example, if you have 15 GB of available RAM, write dom0_mem=max:13312 (if you have 64 GB of RAM, you can write dom0_mem=max:59392).
    Then update grub
    sudo grub2-mkconfig -o /boot/grub2/grub.cfg

You will now always launch dom0 in live mode!

Next, copy Whonix and Kicksecure templates and select varlibqubes in the advanced settings. Then create new AppVMs based on the varlibqubes templates and also select varlibqubes in the advanced settings.

Templates in vm-pool will be in persistent mode (but metadata from dom0 will be deleted, so if you create a new qube in vm-pool in live mode, it will not be saved after rebooting).

To launch persistent mode for dom0 and perform updates or install new programs, do the following:

  • Press E in grub menu and remove rootovl, then press F10. This will launch a persistent session for one time.
  • To completely remove live mode, remove rootovl in /etc/default/grub and update grub in persistent mode.

You can choose not to create a vm-pool and only have dom0 and varlibqubes during Qubes installation, which will result in a complete disk amnesia in live mode. In this case, you will need to always launch persistent mode to save persistent files. This will require a significant amount of RAM for normal Qubes operation in live mode (at least 64 GB, but preferably 128 GB).

1 Like

Now the Qubes team can easily port grub-live and add a live mode to the GRUB menu, just like it’s implemented in Whonix and Kicksecure! implement live boot by porting grub-live to Qubes - amnesia / non-persistent boot / anti-forensics · Issue #4982 · QubesOS/qubes-issues · GitHub You can write to the Qubes team on GitHub and suggest Patrick’s solution again (I’m not registered on GitHub)

So, I’ve conducted tests. Here are my results:

Overlayfs consumes significantly more memory when running virtual machines, but it provides more free space for downloading files.

Copying dom0 to zram run slowly and has less free disk space, but it hardly loads the memory - I couldn’t even get the memory to 100% load, even with 17 concurrently running varlibqubes qubes!! :flushed: Overlayfs ran out of memory after loading 10 varlibqubes qubes.

These two modes can be used simultaneously, and you can choose one of them when launching the system, depending on your needs.