Ephemeral DVMs in fully ephemeral thin pools (ephemeral encryption, zram-disk, tmpfs). Removing DVM logs and metadata

Setting up a zram-pool and placing all DVMs inside it is not feasible when the user’s Qubes installation contains DVMs that total around 50 GB, while the chosen total RAM is only 4 GB.

So I created this CLI tool/suite with an option that lets the user:

  1. Choose how much RAM to allocate.
  2. Manually add the specific DVMs they want into the zram-pool afterward.

Additionally, I implemented a function to check in dom0:

  • zram status
  • which DVMs are currently using the zram-pool
  • and related outputs to confirm correct usage.

Copying/persisting only what is needed

The ideal behavior is for the user to decide whether to copy all DVMs into the zram-pool or not.

This is useful because it allows selective persistence:

  • some DVMs can remain persistent (e.g., heavy ones that would not fit in the available RAM)
  • other DVMs can be moved to zram for performance

If the system contains many heavy DVMs, the user can choose only the ones they really want in zram, while leaving the rest persistent—otherwise RAM won’t be enough.

Script and current testing

This is the script I created with this mode (installation/uninstallation is also available). The script is as follows:

#!/bin/bash

zram_pool()
{
echo ""
echo "==========================================================="
echo "  [!] ZRAM AMNESIC POOL - WARNING"
echo "==========================================================="
echo "[i] Recommendation: reserve 50-60% of your total RAM"
echo "    Example: if you have 16GB -> use 8G or 10G"
echo ""
echo "Expected format: 4G, 6G, 8G, 10G, 12G, etc."
echo ""
read -p "Enter ZRAM pool size (e.g. 4G): " zram_pool_size

if [ -z "$zram_pool_size" ]; then
    echo "[!] No size entered. Aborting."
    exit 1
fi

echo ""
echo "[*] ZRAM_SIZE set to: ${zram_pool_size}"
echo ""

sudo tee /etc/systemd/system/zram-pool.service << 'EOF'
[Unit]
Description=ZRAM Ephemeral Pool
After=qubesd.service
Before=qubes-vm@sys-net.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/zram-pool-create.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF


sudo tee /usr/local/bin/zram-pool-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

POOL_NAME="zram_pool"
ZRAM_SIZE="$zram_pool_size"
VG_NAME="zram_vg"

EXTRA_VMS=(
    #"whonix"
    #"sys-whonix"
)

ROOT_DEV=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")

if echo "$ROOT_DEV" | grep -qE "(overlay|/dev/zram0)"; then
    echo "[*] Detected amnesiac mode: $ROOT_DEV"
    echo "    Cleaning up old VMs..."

    for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Stopping: $vm"
            qvm-shutdown --wait --timeout 10 "$vm" 2>/dev/null || \
                qvm-kill "$vm" 2>/dev/null || true
        fi
    done

    for vm in $(qvm-ls --raw-list 2>/dev/null); do
        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Removing: $vm"
            qvm-remove --force "$vm" 2>/dev/null || true
        fi
    done

    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    vgchange -an "${VG_NAME}" 2>/dev/null || true
    vgremove -f "${VG_NAME}" 2>/dev/null || true

    for dev in /dev/zram*; do
        [ -b "$dev" ] || continue
        zramctl --reset "$dev" 2>/dev/null || true
    done

    echo "[+] Cleanup completed."
    exit 0
fi

if [ "$EUID" -ne 0 ]; then
    echo "[!] Root privileges required"
    exit 1
fi

echo "[*] Creating zram device (${ZRAM_SIZE})..."
modprobe zram 2>/dev/null || true

ZRAM_DEV=""
for dev in /dev/zram*; do
    [ -b "$dev" ] || continue
    if ! zramctl "$dev" 2>/dev/null | grep -q "mounted\|active"; then
        ZRAM_DEV="$dev"
        break
    fi
done

if [ -z "$ZRAM_DEV" ]; then
    ZRAM_DEV=$(zramctl --find --size "$ZRAM_SIZE" --algorithm lz4)
else
    zramctl --reset "$ZRAM_DEV" 2>/dev/null || true
    ZRAM_DEV=$(zramctl --find --size "$ZRAM_SIZE" --algorithm lz4)
fi

echo "    Device: $ZRAM_DEV"

echo "[*] Removing VMs from pool ${POOL_NAME}..."
for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        echo "    -> Stopping: $vm"
        qvm-shutdown --wait --timeout 30 "$vm" 2>/dev/null || {
            echo "    -> Force killing: $vm"
            qvm-kill "$vm" 2>/dev/null || true
        }
    fi
done

for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        echo "    -> Removing: $vm"
        qvm-remove --force "$vm" 2>/dev/null || true
    fi
done

echo "[*] Cleaning up old infrastructure..."
qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
vgchange -an "${VG_NAME}" 2>/dev/null || true
vgremove -f "${VG_NAME}" 2>/dev/null || true

# Detach old loops on zram
for loopdev in $(losetup -a 2>/dev/null | grep "$ZRAM_DEV" | cut -d: -f1); do
    echo "    -> Detaching loop: $loopdev"
    losetup -d "$loopdev" 2>/dev/null || true
done

echo "[*] Creating loop on ${ZRAM_DEV}..."
LOOP_DEV=$(losetup -f --show "$ZRAM_DEV")
echo "    Loop: $LOOP_DEV"

echo "[*] Creating LVM on ${LOOP_DEV}..."
pvcreate -q "$LOOP_DEV"
vgcreate -q "${VG_NAME}" "$LOOP_DEV"
lvcreate -q -T -n "thin_pool" -l +100%FREE "${VG_NAME}"

echo "[*] Registering pool ${POOL_NAME}..."
if qvm-pool list 2>/dev/null | grep -q "^${POOL_NAME}"; then
    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    sleep 1
fi

qvm-pool add "${POOL_NAME}" lvm_thin --option volume_group="${VG_NAME}" --option thin_pool=thin_pool 2>/dev/null || \
qvm-pool add "${POOL_NAME}" lvm_thin -o volume_group="${VG_NAME}",thin_pool=thin_pool

echo "    Pool created:"
qvm-pool info "${POOL_NAME}"
EOF

# [!] Modifying /usr/local/bin/zram-pool-create.sh: replacing $zram_pool_size with real size
sed -i 's/ZRAM_SIZE="\$zram_pool_size"/ZRAM_SIZE="'$zram_pool_size'"/' /usr/local/bin/zram-pool-create.sh

sudo chmod +x /usr/local/bin/zram-pool-create.sh
echo "ZRAM pool activated"
echo "Add only DVMs to it for amnesic anti-forensic mode"
echo "AppVMs do not work — never create an AppVM inside zram_pool"
sudo systemctl daemon-reload
sudo systemctl enable --now zram-pool.service
}

amnesic_logs_metadata_dom0()
{
sudo tee /etc/systemd/journald.conf << EOF
[Journal]
Storage=volatile
EOF
sudo systemctl restart systemd-journald

sudo tee /etc/systemd/system/clean.service << 'EOF'
[Unit]
Description=Clean logs of removed Qubes VMs
After=qubesd.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/clean.sh
RemainAfterExit=no

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/clean.sh << 'EOF'
#!/bin/bash

set -euo pipefail

readonly LOGDIR='/var/log'
readonly TEMPDIR_ROOT='/home/user/tmp'
readonly MENUDIR='/home/user/.config/menus/applications-merged'


existing_qubes=$(qvm-ls --fields=name --raw-data | sort)


all_qube_names=''


all_qube_names+=$(find "${LOGDIR}/libvirt/libxl/" \
    -type f \
    -regextype posix-egrep \
    -regex '.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
    -exec basename "{}" \; \
    | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
    | sed -r 's/^(guid|qrexec|qubesdb)\.//g' \
    | sort | uniq)$'\n'


all_qube_names+=$(find "${LOGDIR}/qubes/" \
    -type f \
    -regextype posix-egrep \
    -regex '.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
    -exec basename "{}" \; \
    | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
    | sed -r 's/^(guid|qrexec|qubesdb)\.//g' \
    | sort | uniq)$'\n'


all_qube_names+=$(find "${LOGDIR}/xen/console/" \
    -type f \
    -regextype posix-egrep \
    -regex '.*\/guest-.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
    -exec basename "{}" \; \
    | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
    | sed -r 's/^guest-//g' \
    | sort | uniq)$'\n'


set +e
ram_pools=$(qvm-pool list | grep -Eio '^ram_pool_[^ ]+' | sort | uniq)
set -e
all_qube_names+=$(echo "${ram_pools}" | sed -r 's/^ram_pool_//g')$'\n'


if [ -d "${TEMPDIR_ROOT}" ]; then
    all_qube_names+=$(find "${TEMPDIR_ROOT}" \
        -mindepth 1 -maxdepth 1 -type d \
        -exec basename "{}" \; \
        | sort | uniq)$'\n'
fi


all_qube_names+=$(find "${MENUDIR}" \
    -regextype posix-egrep \
    -regex '.*\/user-qubes-.*\.menu$' \
    -exec basename "{}" \; \
    | sed -r 's/\.menu$//g' \
    | sed -r 's/^user-qubes-(disp)?vm-directory(_|-)//g' \
    | sort | uniq)$'\n'


all_qube_names=$(echo "${all_qube_names}" \
    | sed -r 's/^(Domain-0|libxl-driver)$//g' \
    | sed -r '/^\s*$/d' \
    | sort | uniq)


set +e
qubes_to_remove=$(diff --new-line-format='' --unchanged-line-format='' \
    <(echo "${all_qube_names}") <(echo "${existing_qubes}") \
    | sed -r '/^\s*$/d')
set -e


for qube_name in ${qubes_to_remove}; do
    decoded_qube_name=$(echo "${qube_name}" \
        | sed -r 's/_d/-/g' \
        | sed -r 's/_u/_/g')

    log_pattern="${qube_name}\.log((\.old)|(-[0-9]{8}))?(\.gz)?"
    menu_pattern="user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu"

    declare -A targets
    targets=(["${LOGDIR}/libvirt/libxl"]="${log_pattern}"
             ["${LOGDIR}/qubes"]="((guid|qrexec|qubesdb)\.)?${log_pattern}"
             ["${LOGDIR}/xen/console"]="guest-${log_pattern}"
             ["${MENUDIR}"]="${menu_pattern}")

    if [ -d "${TEMPDIR_ROOT}" ]; then
        targets+=("${TEMPDIR_ROOT}"="${qube_name}")
    fi

    for search_dir in "${!targets[@]}"; do
        mapfile -d $'\0' found_files < <(find "${search_dir}" \
            -regextype posix-egrep \
            -regex ".*\/${targets[${search_dir}]}$" \
            -print0)
        for file in "${found_files[@]}"; do
            [ -z "${file}" ] && continue
            rm -rf "${file}"
        done
    done
done


for pool_name in ${ram_pools}; do
    qube_name=$(echo "${pool_name}" | sed -r 's/^ram_pool_//')
    if ! echo "${existing_qubes}" | grep -qx "${qube_name}"; then
        pool_mountpoint=$(qvm-pool info "${pool_name}" \
            | grep -E '^dir_path' \
            | sed -r 's/^dir_path\s+//g')
        qvm-pool remove "${pool_name}" 2>/dev/null || true
        umount "${pool_mountpoint}" 2>/dev/null || true
        rm -rf "${pool_mountpoint}" 2>/dev/null || true
    fi
done

find "${LOGDIR}/qubes/" -maxdepth 1 -type f -name '*.log.old' -delete
EOF

sudo chmod +x /usr/local/bin/clean.sh
echo "Turn on amnesic logs and metadata in dom0 for zram-pool DVMs"
sudo systemctl daemon-reload
sudo systemctl enable clean.service
}


remove_zram_pool()
{
# Step 1: Stop and disable the systemd service
sudo systemctl stop zram-pool.service
sudo systemctl disable zram-pool.service

# Step 2: Remove all VMs from zram_pool
echo "[*] Removing VMs from zram_pool..."
for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "zram_pool"; then
        echo "    -> Removing: $vm"
        qvm-kill "$vm" 2>/dev/null || true
        sleep 1
        qvm-remove --force "$vm" 2>/dev/null || true
        sleep 1
    fi
done

# Step 3: Remove the Qubes storage pool
echo "[*] Removing zram_pool..."
qvm-pool remove zram_pool 2>/dev/null || true

# Step 4: Deactivate and remove LVM volume group
echo "[*] Cleaning up LVM..."
vgchange -an zram_vg 2>/dev/null || true
vgremove -f zram_vg 2>/dev/null || true

# Step 5: Detach loop device from zram
echo "[*] Detaching loop devices..."
for loopdev in $(losetup -a 2>/dev/null | grep "/dev/zram" | cut -d: -f1); do
    echo "    -> Detaching: $loopdev"
    losetup -d "$loopdev" 2>/dev/null || true
done

# Step 6: Reset zram device
echo "[*] Resetting zram device..."
for dev in /dev/zram*; do
    [ -b "$dev" ] || continue
    zramctl --reset "$dev" 2>/dev/null || true
done

echo "[+] zram pool completely removed. Reboot to clear all traces from memory."
}

# Function: Check ZRAM Pool and DVM Memory Status
check_zram_amnesic_status()
{
echo "=========================================="
echo "ZRAM AMNESIC POOL STATUS CHECK"
echo "=========================================="
echo ""

# 1. ZRAM DEVICE CHECK
echo "========== 1. ZRAM DEVICE CHECK =========="
echo "ZRAM devices present:"
zramctl 2>/dev/null || echo "  [!] No ZRAM devices found!"
echo ""

echo "ZRAM compression algorithm:"
cat /sys/block/zram0/comp_algorithm 2>/dev/null || echo "  [!] Cannot read zram0 algorithm"
echo ""

# 2. ZRAM POOL REGISTRY CHECK
echo "========== 2. QUBES STORAGE POOL CHECK =========="
echo "Available pools in Qubes:"
qvm-pool list 2>/dev/null || echo "  [!] qvm-pool command failed"
echo ""

echo "zram_pool details:"
qvm-pool info zram_pool 2>/dev/null || echo "  [!] zram_pool not found or not accessible"
echo ""

# 3. DVM VOLUME LOCATION CHECK
echo "========== 3. DVM VOLUME LOCATION CHECK =========="
echo "All DVMs and their volume locations:"
for vm in $(qvm-ls --raw-list 2>/dev/null); do
    volumes=$(qvm-volume list "$vm" 2>/dev/null | grep "root\|private" | awk '{print $2}' | tr '\n' ' ')
    echo "  $vm: $volumes"
done
echo ""

echo "Checking which VMs use zram_pool:"
for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "zram_pool"; then
        echo "  [OK] $vm uses zram_pool"
    fi
done
echo ""

# 4. TMPFS MOUNTS CHECK (for ZRAM-backed pools)
echo "========== 4. TMPFS/ZRAM MOUNT CHECK =========="
echo "All tmpfs filesystems (includes ZRAM):"
mount | grep tmpfs | grep -v "snapshots" || echo "  [!] No tmpfs mounts found"
echo ""

echo "Detailed findmnt for tmpfs:"
findmnt -t tmpfs -o TARGET,SOURCE,FSTYPE,SIZE 2>/dev/null || echo "  [!] findmnt failed"
echo ""

# 5. LVM THIN POOL CHECK
echo "========== 5. LVM THIN POOL CHECK =========="
echo "Volume groups:"
vgdisplay 2>/dev/null | grep -E "VG Name|VG Size|Free" || echo "  [!] Cannot read VG info"
echo ""

echo "Logical volumes:"
lvdisplay 2>/dev/null | grep -E "LV Name|LV Path|LV Size" | head -20 || echo "  [!] Cannot read LV info"
echo ""

# 6. SERVICE STATUS CHECK
echo "========== 6. SYSTEMD SERVICE STATUS =========="
echo "ZRAM pool service:"
systemctl is-active zram-pool.service 2>/dev/null || echo "  [!] Service not active or not found"
echo ""

echo "Clean service (log cleaning):"
systemctl is-active clean.service 2>/dev/null || echo "  [!] Service not active or not found"
echo ""

# 7. JOURNALD CONFIGURATION CHECK
echo "========== 7. JOURNALD VOLATILE CHECK =========="
echo "Current journald storage setting:"
grep -E "^Storage=" /etc/systemd/journald.conf 2>/dev/null || echo "  [!] journald.conf not modified or missing"
echo ""

echo "Journald actual storage (runtime):"
journalctl -b | head -5 2>/dev/null && echo "  [INFO] Journal is running (storage may be volatile)" || echo "  [!] Cannot read journal"
echo ""

# 8. MEMORY USAGE SUMMARY
echo "========== 8. MEMORY USAGE SUMMARY =========="
echo "Total RAM and swap:"
free -h 2>/dev/null || echo "  [!] free command failed"
echo ""

# 9. VERIFICATION SUMMARY
echo "========== 9. VERIFICATION SUMMARY =========="
errors=0

if ! zramctl &>/dev/null; then
    echo "[FAIL] ZRAM device not found"
    errors=$((errors+1))
else
    echo "[PASS] ZRAM device detected"
fi

if ! qvm-pool list 2>/dev/null | grep -q "zram_pool"; then
    echo "[FAIL] zram_pool not registered in Qubes"
    errors=$((errors+1))
else
    echo "[PASS] zram_pool registered"
fi

if [ "$(systemctl is-active zram-pool.service 2>/dev/null)" != "active" ]; then
    echo "[WARN] zram-pool.service not active (may need manual start)"
else
    echo "[PASS] zram-pool.service active"
fi

if ! grep -q "Storage=volatile" /etc/systemd/journald.conf 2>/dev/null; then
    echo "[WARN] journald may not be set to volatile"
else
    echo "[PASS] journald configured for volatile storage"
fi

echo ""
echo "Total issues found: $errors"
echo ""

if [ "$errors" -eq 0 ]; then
    echo "[SUCCESS] All checks passed! Your DVMs should be fully amnesic."
else
    echo "[WARNING] Some checks failed. Review the output above."
fi

echo ""
echo "=========================================="
echo "END OF ZRAM AMNESIC CHECK"
echo "=========================================="
}



show_menu() {
    clear
    echo ""
    echo "==========================================================="
    echo "  [!] ZRAM AMNESIC POOL - WARNING"
    echo "==========================================================="
    echo ""
    echo "This tool creates a ZRAM pool where ALL DVMs will live 100%"
    echo "IN RAM — fully amnesic and anti-forensic."
    echo ""
    echo "[!] THIS MEANS:"
    echo "    - Data NEVER touches the physical disk"
    echo "    - After REBOOTING the Qubes HOST, ALL data will be GONE"
    echo "    - Any files saved in these VMs will be PERMANENTLY LOST"
    echo "    - ONLY DVMs are supported — AppVMs will NOT work"
    echo ""
    echo "==========================================================="
    echo ""
    echo "  1) Create ZRAM pool"
    echo "  2) Remove everything"
    echo "  3) Check ZRAM Amnesic Status (verify DVMs are in RAM)"
    echo "  0) Exit"
    echo ""
    
    while true; do
        read -p "Select an option: " choice
        
        case "$choice" in
            1)
                echo "[*] Starting ZRAM pool creation..."
                zram_pool
                amnesic_logs_metadata_dom0
                break
                ;;
            2)
                echo "[*] Removing ZRAM pool and all related artifacts..."
                remove_zram_pool
                break
                ;;
            3)
                echo "[*] Checking ZRAM Amnesic Status..."
                check_zram_amnesic_status
                break
                ;;
            0)
                echo "[*] Exiting."
                exit 0
                ;;
            *)
                echo "[!] Invalid option. Try again."
                ;;
        esac
    done
}
show_menu

I cloned an AppVM (anon-whonix) into anon-whonix-zram for the zram pool (zram-pool). Then I ran it and executed option 3. The main outputs were:

========== 1. ZRAM DEVICE CHECK ==========
ZRAM devices present:
NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
/dev/zram1 lz4 2G 403,2M 266,9M 271,8M
/dev/zram0 lzo-rle 3,8G 4K 80B 12K [SWAP]

Checking which VMs use zram_pool:
[OK] anon-whonix-zram uses zram_pool

to check where is the files of the dvm when it is running, do

cd /dev
ls #to find zram_vg
cd /dev/zram_vg

after I am running anon-whonix-zram

ls /dev/zram_vg
#output
vm-anon-whonix-zram-private
vm-anon-whonix-zram-private-1784587540-back
vm-anon-whonix-zram-private-snap
vm-anon-whonix-zram-volatile

the dvm is in zram_vg, in zram-pool

### Questions
So my question to **LinuxUser1** (and the moderator too):
Is this approach actually correct / does it work in the way I expect on Qubes?

WARNING: Logs in dom0

/var/log/libvirt/libxl/.log
/var/log/qubes/
.log
/var/log/xen/console/*.log

These logs allow access to metadata about when a DVM starts or stops, which NetVM it uses, and more? If so, a lot of metadata can be collected because they are not in ZRAM or RAM, right? They are written to disk and can be recovered through a forensic attack! This enables activity correlation and reveals user behavior if physical access is successfully obtained. So, if possible, it would be better to configure all these .log files and others to use bind mounts in dom0 pointing to a volatile folder in tmpfs or ZRAM.

Thank you very much to help us!

2 Likes