Good idea. Changed and tested - works.
Cleanliness, as a goal of the cleanup script, has nothing to do with particular drive type. It is just keeping the filesystem organized by removing unnecessary stuff. - Completely unrelated to the advertised “most effective anti-forensic solutions”.
file removing with discard SSD can definitely be called an anti-forensic tool
No, it cannot.
TRIM (discard) itself is a compromise of anti-forensic measures because it makes plausible deniability less efficient. Also TRIM itself does not erase data - firmware garbage collection does that. The latter is proprietary, so only the controller manufacturer knows how and when exactly that happens. It also matters how the drive is partitioned, overprovisioned (including the hidden OP by the manufacturer) etc.
But okay, specially for you and out of respect for you, I will edit the description of this section, specifying that simply deleting files in /var/log is not direct anti-forensics.
It is not any anti-forensics and this has nothing to do with me. It is just how hardware works. If you want no state in /var/log, mount it as tmpfs.
I’m not going to conduct this linguistically strange conversation. The cleaning script is “not the most effective solution”, but a supplement to ephemeral pools. We’re not discussing plausible deniability here, but anti-forensics. I know how SSDs work - it’s not so important when NVMe clears the data (after a minute, hour, or day), the main thing is that it immediately receives a signal to clear. Mounting /var/log/ in tmpfs can break the startup of all qubes, I’ve already done it and had to restore everything back in recovery mode. Just like mounting other metadata in tmpfs breaks app-menu. That’s why I only sent the journal to memory. It’s much more interesting to discuss how selectively, without breaking the fragile dom0 architecture, and without running dom0 in ram/encrypted overlay modes, to send all logs about removed dVMs to ram/ephemeral container.
All I am saying is that the script you copied does not ensure any anti-forensics and any claims in the opposite direction are incorrect. The additional technical details, I provided, are just the factual basis.
Regardless of what you or I may consider main/important, an expert analyst retrieves available evidence, and would not ignore a “planned-to-be-erased” or “maybe-erased” one.
Mounting /var/log/ in tmpfs can break the startup of all qubes, I’ve already done it and had to restore everything back in recovery mode. Just like mounting other metadata in tmpfs breaks app-menu.
All qubes? Dom0 included?
Can you post the details perhaps in another thread or in qubes-devel? Maybe we can get feedback from devs about why this is so.
@rustybird - you are an expert in FS matters. What do you think?
Yes, only dom0 was working. This broke qubesd. I disabled it in recovery mode and after rebooting everything worked. I also tried mount app-menu metadata to tmpfs, which also caused a crash. I also ran overlay in tmpfs for /var/logs. It more or less worked, but when I created a new qube, it wouldn’t show any available apps from the template - no apps at all, and no updates helped. Just an empty qube. I did it 3-4 months ago. After that, I took your script and add it into systemd with a command to clean old logs, and it works good every day
What license governs these scripts you have posted?
No license. You can publish this code wherever you want and do whatever you want with it
Cool, thanks!
So is it safe to assume you’re releasing the code in this thread as CC0/PD?
FYI, if you do not specify a license for your code, it stays All Rights Reserved by default, meaning no one can really do things with it.
Doesn’t the ZRam backed version introduce a new side channel attack? A VM can tell that some data is already stored based on the time it takes to write.
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:
- Choose how much RAM to allocate.
- Manually add the specific DVMs they want into the
zram-poolafterward.
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!
Here’s my version.
#!/bin/bash
set -euo pipefail
action="${1:-create}"
# --------------------------------------------------
pool_name="arq-zram-pool"
vg_name="arq_zram_vg"
zram_compression_ratio_expected_max=4
untouchable_mem=4096 # M
auto_dvm_enrollment_size_limit=256 # M
auto_dvm_ext="-zrdvm"
auto_ext="-zr"
lvm_local_conf="/etc/lvm/lvmlocal.conf"
lvm_marker="arqubes-zram-vms managed"
clone_dvm_concurrency="$(($(nproc)/2 < 1 ? 1 : $(nproc)/2))"
clone_named_disp_concurrency="$clone_dvm_concurrency"
# --------------------------------------------------
if [ "$EUID" -ne 0 ]; then
printf "Root required\n" >&2
exit 1
fi
print_zr_vms() {
qvm-ls --raw-data \
--fields NAME,PRIV-POOL \
"$@" 2>/dev/null \
| awk -F'|' "\$2 == \"$pool_name\" { print \$1 }"
}
destroy() {
local zram_dev="$(\
pvs --noheadings -o pv_name \
--select "vg_name=$vg_name" 2>/dev/null \
| awk 'NF { print $1; exit }' \
|| true
)"
printf "Shutting down VMs in pool %s...\n" "$pool_name" || true
print_zr_vms --running \
| xargs -r qvm-shutdown -v --wait --force --timeout 30 \
|| true
printf "Removing VMs in pool %s...\n" "$pool_name" || true
# remove named disps before their templates
print_zr_vms --class DispVM \
| xargs -r qvm-remove -v --force
print_zr_vms \
| xargs -r qvm-remove -v --force
if qvm-pool info "$pool_name" >/dev/null 2>&1; then
printf "Removing pool %s...\n" "$pool_name" || true
[ "$pool_name" = "vm-pool" ] && exit 99
qvm-pool remove "$pool_name"
fi
[ "$vg_name" = qubes_dom0 ] && exit 99
if vgs "$vg_name" >/dev/null 2>&1; then
printf "Removing volume %s...\n" "$vg_name" || true
vgchange -an "$vg_name"
vgremove -yff "$vg_name"
fi
if [[ "$zram_dev" == /dev/zram* ]]; then
printf "Removing zram %s...\n" "$zram_dev" || true
zramctl --reset "$zram_dev"
fi
}
convert_dvm_name() {
printf "%s\n" "${1%-dvm}$auto_dvm_ext"
}
convert_disp_name() {
printf "%s\n" "${1%-r}$auto_ext"
}
make_clone() {
local vm="$1"
local clone_name="$2"
[ -z "$vm" ] \
&& return
if qvm-check -q "$clone_name" 2>/dev/null; then
printf " VM already exists: %s\n" "$clone_name"
# qvm-kill "$clone_name" 2>/dev/null || true
# qvm-remove --force "$clone_name" 2>/dev/null || true
return 1
fi
if ! qvm-clone -P "$pool_name" "$vm" "$clone_name"; then
printf " Failed to clone: %32s -> %32s\n" "$vm" "$clone_name"
return 1
fi
printf " Cloned: %32s -> %32s\n" "$vm" "$clone_name" || true
}
ensure_lvm_zram_support() {
if grep -Rqs '"zram"' /etc/lvm/lvm.conf "$lvm_local_conf"; then
return 0
fi
printf "Enabling LVM support for zram devices in %s...\n" "$lvm_local_conf" || true
touch "$lvm_local_conf"
cat >> "$lvm_local_conf" <<EOF
# BEGIN $lvm_marker
devices {
types = [ "zram", 1 ]
}
# END $lvm_marker
EOF
}
create_pool() {
ensure_lvm_zram_support
get_outer_limit() {
local total_mem="$(free --mebi | awk '$1 ~ /Mem:/ { print $2 }' | grep -E '[0-9]+')"
[ "$total_mem" -le "$untouchable_mem" ] && printf "Not enough memory\n" >&2 && exit 5
local true_limit="$(($total_mem-$untouchable_mem))"
[ "$true_limit" -lt 64 ] && printf "Not enough memory\n" >&2 && exit 6
printf "%d\n" "$true_limit"
}
outer_limit_int="$(get_outer_limit)"
zram_outer_limit="${outer_limit_int}M"
zram_inner_size="$(($outer_limit_int*$zram_compression_ratio_expected_max))M"
unset outer_limit_int
printf "Creating zram device (%s)...\n" "$zram_inner_size" || true
modprobe zram
zram_dev="$(zramctl --find \
--size "$zram_inner_size" \
--algorithm lz4
)"
[ -z "$zram_dev" ] && printf "No zram dev\n" && exit 2
printf " Device: %s\n" "$zram_dev" || true
printf "%s\n" "$zram_outer_limit" > \
"/sys/block/${zram_dev##*/}/mem_limit"
printf "Creating LVM on %s...\n" "$zram_dev" || true
pvcreate -q "$zram_dev"
vgcreate -q "$vg_name" "$zram_dev"
lvcreate -qTn thin_pool -l "100%FREE" "$vg_name"
printf "Registering pool %s...\n" "$pool_name" || true
if qvm-pool info "$pool_name" >/dev/null 2>&1; then
printf "warning: residual pool still exists - removing...\n" || true
[ "$pool_name" = "vm-pool" ] && exit 99
qvm-pool remove "$pool_name" 2>/dev/null || true
sleep 1
fi
qvm-pool add "$pool_name" lvm_thin \
-o volume_group="$vg_name" \
-o thin_pool=thin_pool \
-o revisions_to_keep=-1
printf " Pool created:\n" || true
qvm-pool info "$pool_name"
}
make_all_clones() {
printf "Cloning VMs into zram pool...\n" || true
printf " Disposable templates:\n" || true
qvm-ls --no-spinner --raw-data \
--fields NAME,PRIV-POOL,PRIV-CURR,TEMPLATE_FOR_DISPVMS 2>/dev/null \
| awk -F'|' "\$4 == \"True\" && \$3 < $auto_dvm_enrollment_size_limit && \$2 != \"$pool_name\" { print \$1 }" \
| {
while read -r vm; do
while [ "$(jobs -pr | wc -l)" -ge "$clone_dvm_concurrency" ]; do
wait -n || true
done
make_clone "$vm" "$(convert_dvm_name "$vm")" &
done
wait
}
printf " Named disposables:\n" || true
qvm-ls --no-spinner --raw-data \
--class DispVM \
--fields NAME,PRIV-POOL,TEMPLATE 2>/dev/null \
| awk -F'|' "\$1 !~ /^disp[0-9]+$/ && \$2 != \"$pool_name\" { printf \"%s|%s\\n\", \$1, \$3 }" \
| {
while IFS="|" read -r vm template; do
while [ "$(jobs -pr | wc -l)" -ge "$clone_named_disp_concurrency" ]; do
wait -n || true
done
(
zr_template="$(convert_dvm_name "$template")"
qvm-check -q "$zr_template" 2>/dev/null \
|| exit 0
zr_disp="$(convert_disp_name "$vm")"
make_clone "$vm" "$zr_disp" \
|| exit 0
qvm-prefs "$zr_disp" template "$zr_template" \
&& exit 0
printf "Cloned named disposable %s -> %s but failed to reset template\n" "$vm" "$zr_disp" || true
qvm-remove --force "$zr_disp" 2>/dev/null \
|| printf "Failed to remove partial cloned named disposable %s -> %s\n" "$vm" "$zr_disp" >&2 || true
) &
done
wait
}
}
case "$action" in
create)
destroy
create_pool
make_all_clones
;;
create-pool)
destroy
create_pool
;;
destroy)
destroy
;;
*)
printf "usage %s {create,create-pool,destroy}\n" "$0" >&2
exit 1
;;
esac
Forensic survey on metadata and records from the dvm created in the zram-pool in the dom0
Looking through all the files in the dom0 in Qubes, after installing zram-pool and creating a dvm named anon-whonix-zram, filenames are created with the name anon-whonix-zram and the name anon-whonix-zram appears inside, written in several files…
Below is the list of directories where those records are:
In the case the user of this qube is user, then /home/user
/etc/libvirt/libxl/
/etc/lvm/archive/
/etc/lvm/backup/
/var/log/libvirt/libxl/
/var/log/qubes/
/var/log/xen/console/
/var/lib/qubes/appvms/
/var/lib/qubes/backup/
/home/user/.local/share/applications/
/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs/
/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs/apps/
/home/user/.local/share/desktop-directories/
/home/user/.local/state/wireplumber/
/run/udev/links/ #ignore runtime, volatile
/run/udev/data/ #ignore runtime, volatile
/tmp/ #by default it stays in tmpfs, ignore
/dev/zram_vg/ #this is the zram itself, ignore
Many of these files are records, netvm, when the netvm changes, when it’s turned on, when it’s turned off, and these are metadata that can be used to correlate online activity!
Example: a certain user logs into a social network and makes posts for time x1 to x2, on day xB!
Files in the dom0 may have metadata that matches that time and day, hour when they accessed, closed, and day!
If this repeats 10 or 100 times, it may be possible to infer that you are the user of that social network because it’s too much of a coincidence!
This metadata is eliminated when we use the dom0 100% in RAM and all of this is destroyed!
When the dom0 is not in RAM, apparently all of it stays there, but maybe there’s more…
To use zram-pool, it’s necessary to run the directories with the files where this metadata lives 100% anonymously in RAM using overlayfs or tmpfs!
This needs to be handled!
You can have more details
using on dom0 this script…
create the zram-pool, create a dvm or use a dvm that is in the zram-pool, turn it on, navigate a bit, turn it off.
Let’s say the name of this test dvm is dvm-test
then run the script inside dom0
sudo ./qubes-forensic-hunter.sh
put the dvm name, (dvm-test)
report mode: concise or detailed
After the report, it’s necessary to find out what each file does and whether they are accessed, modified, and record metadata such as start of use, end of use, time, day, etc… when the dvm-test and other dvm’s in the zram-pool are being used!
I didn’t have time to investigate each directory and file and exactly what each one does and other details…
After that, it is necessary to create overlayfs or tmpfs setup for all directories that contain the critical metadata we are discussing here!
qubes-forensic-hunter.sh (forensic script to trace appvm/dvm records in dom0
run inside dom0
#!/bin/bash
# ==============================================================================
# dom0-appvm-forensic-scan.sh v3
# Qubes OS dom0 — AppVM Forensic Scanner
#
# FILENAME SEARCH → entire filesystem (including /proc /sys /boot /dev)
# read-only, only the path string is checked, nothing opened
#
# CONTENT SEARCH → safe text/config/log files only (no binaries, no disk
# images, no media) — NO size limit, every line is checked
#
# TWO REPORT MODES:
# [1] CONCISE — filenames containing the AppVM name (path + metadata)
# + files where the name appears in content (path + count)
# [2] DETAILED — same as concise, plus every matching line with line number
#
# Report saved as: <appvm-name>-rastros.txt
# ==============================================================================
set -uo pipefail
# ──────────────────────────────────────────────────────────────────────────────
# ROOTS FOR CONTENT SCAN (safe — no /proc /sys /boot /dev)
# ──────────────────────────────────────────────────────────────────────────────
CONTENT_ROOTS=(
/etc
/var
/home
/root
/run
/tmp
/usr
/opt
/srv
)
# ──────────────────────────────────────────────────────────────────────────────
# ROOTS FOR FILENAME SCAN (entire filesystem — only path string is read)
# ──────────────────────────────────────────────────────────────────────────────
FILENAME_ROOTS=(
/etc
/var
/home
/root
/run
/tmp
/usr
/opt
/srv
/boot
/proc
/sys
/dev
/mnt
/media
)
# ──────────────────────────────────────────────────────────────────────────────
# EXTENSIONS SAFE FOR CONTENT SCAN (no size limit applied to these)
# ──────────────────────────────────────────────────────────────────────────────
SAFE_TEXT_EXTENSIONS=(
conf cfg ini xml json yaml yml toml
env profile rc
sh bash zsh fish py rb pl php js ts lua
log out err
txt md rst html htm css
pem crt key csr
service unit socket timer target mount
rules policy te
sql ks preseed xsl xslt
desktop
bak orig dpkg-old dpkg-dist rpmsave rpmnew
qube qubes policy2
)
# ──────────────────────────────────────────────────────────────────────────────
# EXTENSIONS TO SKIP FOR CONTENT SCAN (binary / media / archives / disk images)
# ──────────────────────────────────────────────────────────────────────────────
SKIP_CONTENT_EXTENSIONS=(
jpg jpeg png gif bmp ico tiff webp svg
mp3 mp4 avi mkv mov wav flac ogg opus webm
zip tar gz bz2 xz zst 7z rar
pdf doc docx xls xlsx ppt pptx odt ods odp
db sqlite sqlite3 img iso qcow2 vmdk vhd vhdx raw cow
rpm deb snap appimage
ttf otf woff woff2
so dll exe bin pyc pyo o a class jar elf
swp swo swap
)
# ──────────────────────────────────────────────────────────────────────────────
# HELPERS
# ──────────────────────────────────────────────────────────────────────────────
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
RED='\033[0;31m'; BOLD='\033[1m'; RESET='\033[0m'
log() { echo -e "${CYAN}[INFO]${RESET} $*"; }
ok() { echo -e "${GREEN}[ OK ]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
divider(){ printf '%0.s─' {1..80}; echo; }
in_array() {
local needle="$1"; shift
for item in "$@"; do [[ "${item}" == "${needle}" ]] && return 0; done
return 1
}
get_ext() {
local base
base=$(basename "$1" | tr '[:upper:]' '[:lower:]')
if [[ "${base}" == .* ]] && [[ "${base}" != *.*.* ]]; then
echo "${base#.}"
else
echo "${base##*.}"
fi
}
# Returns 0 = safe to scan content, 1 = skip
is_content_safe() {
local fpath="$1"
local ext
ext=$(get_ext "${fpath}")
in_array "${ext}" "${SKIP_CONTENT_EXTENSIONS[@]}" && return 1
in_array "${ext}" "${SAFE_TEXT_EXTENSIONS[@]}" && return 0
# Unknown extension: ask file(1) for MIME type
local mime
mime=$(file -b --mime-type "${fpath}" 2>/dev/null || echo "application/octet-stream")
case "${mime}" in
text/*|application/json|application/xml|application/x-sh|\
application/javascript|inode/x-empty)
return 0 ;;
*)
return 1 ;;
esac
}
# ──────────────────────────────────────────────────────────────────────────────
# INPUT
# ──────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BOLD}════════════════════════════════════════════════════════════════════${RESET}"
echo -e "${BOLD} Qubes OS dom0 — AppVM Forensic Scanner v3 ${RESET}"
echo -e "${BOLD}════════════════════════════════════════════════════════════════════${RESET}"
echo
[[ $EUID -ne 0 ]] && warn "Not root — some paths may be unreadable. Recommend: sudo $0" && echo
read -rp "$(echo -e "${BOLD}AppVM name to search for:${RESET} ")" APPVM_NAME
[[ -z "${APPVM_NAME}" ]] && echo -e "${RED}Name cannot be empty.${RESET}" && exit 1
echo
echo -e " ${BOLD}Report mode:${RESET}"
echo -e " ${CYAN}[1]${RESET} CONCISE — filenames containing the AppVM name + files where it"
echo -e " appears in content (path + occurrence count only)"
echo -e " ${CYAN}[2]${RESET} DETAILED — same as above, plus every matching line with line number"
echo
read -rp "$(echo -e "${BOLD}Choose mode [1/2]:${RESET} ")" MODE_CHOICE
case "${MODE_CHOICE}" in
1) REPORT_MODE="concise" ;;
2) REPORT_MODE="detailed" ;;
*) echo -e "${YELLOW}Invalid choice — defaulting to DETAILED.${RESET}"
REPORT_MODE="detailed" ;;
esac
REPORT_FILE="${APPVM_NAME}-rastros.txt"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
HOST_VAL=$(hostname)
echo
log "AppVM target : ${APPVM_NAME}"
log "Report mode : ${REPORT_MODE}"
log "Report file : ${REPORT_FILE}"
log "Started : ${TIMESTAMP}"
echo
# Counters
FNAME_HIT=0
CONTENT_HIT_FILES=0
CONTENT_HIT_LINES=0
FILES_SCANNED=0
FILES_SKIPPED=0
# ──────────────────────────────────────────────────────────────────────────────
# REPORT HEADER
# ──────────────────────────────────────────────────────────────────────────────
{
cat <<HEADER
================================================================================
QUBES OS dom0 — FORENSIC SCAN REPORT
================================================================================
AppVM Target : ${APPVM_NAME}
Report mode : $(echo "${REPORT_MODE}" | tr '[:lower:]' '[:upper:]')
Host : ${HOST_VAL}
Scan started : ${TIMESTAMP}
Run by : $(whoami)
================================================================================
HEADER
} > "${REPORT_FILE}"
# ──────────────────────────────────────────────────────────────────────────────
# SECTION 1 — FILENAME MATCHES
# Covers entire filesystem. Only the path string is examined — no file is
# opened, read, or modified. Safe for /proc /sys /boot /dev.
# ──────────────────────────────────────────────────────────────────────────────
log "SECTION 1 — Filename scan across entire filesystem..."
{
cat <<'S1'
================================================================================
SECTION 1 — FILES / DIRECTORIES WHOSE NAME CONTAINS THE APPVM NAME
================================================================================
Covers : /etc /var /home /root /run /tmp /usr /opt /srv
/boot /proc /sys /dev /mnt /media
Method : path string check only — no file is opened or modified
--------------------------------------------------------------------------------
S1
} >> "${REPORT_FILE}"
for ROOT in "${FILENAME_ROOTS[@]}"; do
[[ -e "${ROOT}" ]] || continue
# Limit depth on virtual filesystems to avoid hangs
DEPTH_ARG=()
[[ "${ROOT}" == /proc || "${ROOT}" == /sys || "${ROOT}" == /dev ]] \
&& DEPTH_ARG=(-maxdepth 4)
while IFS= read -r -d '' FPATH; do
FTYPE=$(file -b --mime-type "${FPATH}" 2>/dev/null || echo "unknown")
FSIZE=$(stat -c '%s' "${FPATH}" 2>/dev/null || echo "?")
FMOD=$(stat -c '%y' "${FPATH}" 2>/dev/null | cut -d'.' -f1 || echo "?")
OWNER=$(stat -c '%U:%G' "${FPATH}" 2>/dev/null || echo "?")
{
printf " PATH : %s\n" "${FPATH}"
printf " TYPE : %s\n" "${FTYPE}"
printf " SIZE : %s bytes\n" "${FSIZE}"
printf " MODIFIED : %s\n" "${FMOD}"
printf " OWNER : %s\n" "${OWNER}"
echo " ──────────────────────────────────────────────────────────────"
} >> "${REPORT_FILE}"
(( FNAME_HIT++ )) || true
done < <(find "${ROOT}" "${DEPTH_ARG[@]}" -name "*${APPVM_NAME}*" -print0 2>/dev/null)
done
[[ ${FNAME_HIT} -eq 0 ]] && echo " [NO FILENAME MATCHES FOUND]" >> "${REPORT_FILE}"
{
echo ""
printf " Total filename matches: %d\n" "${FNAME_HIT}"
echo ""
} >> "${REPORT_FILE}"
ok "Section 1 done — ${FNAME_HIT} filename match(es)."
# ──────────────────────────────────────────────────────────────────────────────
# SECTION 2 — CONTENT MATCHES
# Scans every text/config/log file in safe roots.
# No size limit — every line of every qualifying file is checked.
# ──────────────────────────────────────────────────────────────────────────────
log "SECTION 2 — Content scan (no size limit, line by line)..."
if [[ "${REPORT_MODE}" == "concise" ]]; then
FMT_NOTE=" FORMAT: FILE PATH | OCCURRENCE COUNT"
else
FMT_NOTE=" FORMAT: file header block + each matching line with its line number"
fi
{
cat <<S2
================================================================================
SECTION 2 — FILE CONTENT MATCHES
================================================================================
Scope : /etc /var /home /root /run /tmp /usr /opt /srv
Files : all text, config, log, script, xml, json, yaml, conf, sh, py ...
Skips : binaries, media, archives, disk images, compiled objects
Size : NO limit — every line of every qualifying file is checked
${FMT_NOTE}
--------------------------------------------------------------------------------
S2
} >> "${REPORT_FILE}"
TMP_CONTENT=$(mktemp /tmp/dom0-content-XXXXXX.tmp)
for ROOT in "${CONTENT_ROOTS[@]}"; do
[[ -d "${ROOT}" ]] || continue
while IFS= read -r -d '' FPATH; do
[[ -f "${FPATH}" ]] || continue
(( FILES_SCANNED++ )) || true
if ! is_content_safe "${FPATH}"; then
(( FILES_SKIPPED++ )) || true
continue
fi
# -i case-insensitive, -n line numbers, skip binary files automatically
GREP_OUT=$(grep -in "${APPVM_NAME}" "${FPATH}" 2>/dev/null) || continue
[[ -n "${GREP_OUT}" ]] || continue
LINE_COUNT=$(echo "${GREP_OUT}" | wc -l)
FTYPE=$(file -b --mime-type "${FPATH}" 2>/dev/null || echo "unknown")
FMOD=$(stat -c '%y' "${FPATH}" 2>/dev/null | cut -d'.' -f1 || echo "?")
if [[ "${REPORT_MODE}" == "concise" ]]; then
{
printf " %-72s | %d occurrence(s)\n" "${FPATH}" "${LINE_COUNT}"
} >> "${TMP_CONTENT}"
else
{
echo "════════════════════════════════════════════════════════════════════════════════"
printf " FILE NAME : %s\n" "$(basename "${FPATH}")"
printf " FULL PATH : %s\n" "${FPATH}"
printf " TYPE : %s\n" "${FTYPE}"
printf " LAST MODIFIED : %s\n" "${FMOD}"
printf " OCCURRENCES : %d line(s)\n" "${LINE_COUNT}"
echo " MATCHING LINES:"
while IFS= read -r MATCH; do
LNUM="${MATCH%%:*}"
LTEXT="${MATCH#*:}"
printf " LINE %-7s: %s\n" "${LNUM}" "${LTEXT}"
done <<< "${GREP_OUT}"
echo ""
} >> "${TMP_CONTENT}"
fi
(( CONTENT_HIT_FILES++ )) || true
(( CONTENT_HIT_LINES += LINE_COUNT )) || true
done < <(find "${ROOT}" -type f -print0 2>/dev/null)
done
if [[ ${CONTENT_HIT_FILES} -eq 0 ]]; then
echo " [NO CONTENT MATCHES FOUND]" >> "${REPORT_FILE}"
else
cat "${TMP_CONTENT}" >> "${REPORT_FILE}"
fi
rm -f "${TMP_CONTENT}"
{
echo ""
printf " Files examined for content : %d\n" "${FILES_SCANNED}"
printf " Files skipped (binary etc) : %d\n" "${FILES_SKIPPED}"
printf " Files with matches : %d\n" "${CONTENT_HIT_FILES}"
printf " Total matching lines : %d\n" "${CONTENT_HIT_LINES}"
echo ""
} >> "${REPORT_FILE}"
ok "Section 2 done — ${CONTENT_HIT_FILES} file(s), ${CONTENT_HIT_LINES} matching line(s)."
# ──────────────────────────────────────────────────────────────────────────────
# SECTION 3 — QUBES / XEN / LIBVIRT SPECIFIC RECORDS
# ──────────────────────────────────────────────────────────────────────────────
log "SECTION 3 — Qubes-specific records..."
{
cat <<'S3'
================================================================================
SECTION 3 — QUBES-SPECIFIC RECORDS
================================================================================
S3
} >> "${REPORT_FILE}"
# qvm-ls
{
echo ""
echo " ── qvm-ls ──────────────────────────────────────────────────────────────────"
} >> "${REPORT_FILE}"
if command -v qvm-ls &>/dev/null; then
OUT=$(qvm-ls 2>/dev/null | grep -i "${APPVM_NAME}" || true)
[[ -n "${OUT}" ]] \
&& printf "%s\n" "${OUT}" >> "${REPORT_FILE}" \
|| echo " [not found in qvm-ls]" >> "${REPORT_FILE}"
else
echo " [qvm-ls not available in this environment]" >> "${REPORT_FILE}"
fi
# qvm-prefs
{
echo ""
echo " ── qvm-prefs ───────────────────────────────────────────────────────────────"
} >> "${REPORT_FILE}"
if command -v qvm-prefs &>/dev/null; then
qvm-prefs "${APPVM_NAME}" 2>/dev/null >> "${REPORT_FILE}" \
|| echo " [could not retrieve prefs — VM may not exist or be inaccessible]" >> "${REPORT_FILE}"
else
echo " [qvm-prefs not available]" >> "${REPORT_FILE}"
fi
# qvm-firewall
{
echo ""
echo " ── qvm-firewall ────────────────────────────────────────────────────────────"
} >> "${REPORT_FILE}"
if command -v qvm-firewall &>/dev/null; then
qvm-firewall "${APPVM_NAME}" list 2>/dev/null >> "${REPORT_FILE}" \
|| echo " [no firewall rules found or VM inaccessible]" >> "${REPORT_FILE}"
else
echo " [qvm-firewall not available]" >> "${REPORT_FILE}"
fi
# journald
{
echo ""
echo " ── journald ────────────────────────────────────────────────────────────────"
} >> "${REPORT_FILE}"
if command -v journalctl &>/dev/null; then
JOUT=$(journalctl --no-pager -g "${APPVM_NAME}" 2>/dev/null || true)
if [[ -n "${JOUT}" ]]; then
JCOUNT=$(echo "${JOUT}" | wc -l)
if [[ "${REPORT_MODE}" == "concise" ]]; then
printf " %d journal line(s) matched.\n" "${JCOUNT}" >> "${REPORT_FILE}"
printf " To view: journalctl --no-pager -g '%s'\n" "${APPVM_NAME}" >> "${REPORT_FILE}"
else
echo "${JOUT}" >> "${REPORT_FILE}"
fi
else
echo " [no journal entries found]" >> "${REPORT_FILE}"
fi
else
echo " [journalctl not available]" >> "${REPORT_FILE}"
fi
echo "" >> "${REPORT_FILE}"
ok "Section 3 done."
# ──────────────────────────────────────────────────────────────────────────────
# SECTION 4 — SUMMARY
# ──────────────────────────────────────────────────────────────────────────────
END_TIME=$(date '+%Y-%m-%d %H:%M:%S')
{
cat <<SUMM
================================================================================
SECTION 4 — SUMMARY
================================================================================
AppVM searched : ${APPVM_NAME}
Report mode : $(echo "${REPORT_MODE}" | tr '[:lower:]' '[:upper:]')
Started : ${TIMESTAMP}
Finished : ${END_TIME}
[FILENAME SCAN]
Matches found : ${FNAME_HIT}
Searched in : /etc /var /home /root /run /tmp /usr /opt /srv
/boot /proc /sys /dev /mnt /media
[CONTENT SCAN]
Files examined : ${FILES_SCANNED}
Files skipped (binary etc) : ${FILES_SKIPPED}
Files with matches : ${CONTENT_HIT_FILES}
Total matching lines : ${CONTENT_HIT_LINES}
Searched in : /etc /var /home /root /run /tmp /usr /opt /srv
================================================================================
END OF REPORT
Saved to: ${REPORT_FILE}
================================================================================
SUMM
} >> "${REPORT_FILE}"
# ──────────────────────────────────────────────────────────────────────────────
# TERMINAL FINAL MESSAGE
# ──────────────────────────────────────────────────────────────────────────────
echo
divider
echo -e "${BOLD}${GREEN} Scan complete!${RESET}"
echo -e " Report file : ${BOLD}${REPORT_FILE}${RESET}"
echo -e " Mode : ${REPORT_MODE}"
echo -e " Filename hits: ${FNAME_HIT}"
echo -e " Content hits : ${CONTENT_HIT_FILES} file(s) — ${CONTENT_HIT_LINES} line(s)"
divider
echo
This handling must be done, because if this metadata exists, in case of a successful physical attack, correlation could deanonymize many users. With this handling, using the dom0 100% in RAM or in overlayfs with the appvms inside the varlibpool (of the dom0) continues to be the best strategy since this metadata will not exist after reboot and anti-cold-boot by dracut! The problem is: reboot, configure, use, and then go back to normal every time—this destroys usability for the user and wastes a lot of time. Hard to use for everyone, and that’s a problem!
Cool. Don’t stop, guys. Loving it. My main motivation for publishing guides is to motivate other users into discussions and guides of their own. Together we’ll unlock the full hidden super-potential of Qubes OS.
So, we can run dom0 in RAM or with ephemeral encryption. That’s settled. But is it possible to route VM metadata into RAM in a persistent dom0? I’ll take another crack at coming up with something clever for overlay on /var/log and other directories, but this time I’ll try to be really sneaky about it..
The ./zram-pool-suit.sh Script and the Challenge of Handling dom0 Metadata for Anti-Forensics
The script ./zram-pool-suit.sh accomplishes everything except placing the necessary directories into tmpfs, zram, or overlayfs.
It includes all functionalities to operate on the amnesiac zram_pool, fully automated and commented, with the following functions:
Main Menu Options
- Create ZRAM pool
- Remove everything
- Qubes DVM Clone Manager
- Start overlayfs on specific dom0 directories for anti-forensics
- Stop overlayfs on dom0 directories
- Start tmpfs on specific dom0 directories for anti-forensics
- Stop tmpfs on dom0 directories
- Check overlayfs/tmpfs status
Option 3: Sub-menu
Option 3 contains a submenu (needs further development—it currently does not return to the main menu):
- Add VM to clone registry
- Create all registered clones (to zram_pool)
- Create one registered clone (to zram_pool)
- Remove entry from registry
- Clear entire registry
- Delete specific DVM from zram_pool
- Delete ALL DVMs from zram_pool
- Check registry & zram_pool status
- Setup auto-clone service (boot)
- Check service status
Known Issues
I have tested and confirmed the core functionality works. However, metadata handling remains incomplete — many directories in dom0 retain traces of registered DVMs in zram_pool, including:
| Metadata Type | Description |
|---|---|
| Created filenames | Names of files created during DVM operation |
| Startup timestamps | Time when the DVM started |
| NetVM usage | Which network VM was used |
| Usage duration | When it was active |
| Shutdown timestamps | When it stopped running |
| Other metadata | Various system artifacts |
All of these must be completely eliminated! I have raised this issue at the Qubes OS forum:
Directories and files requiring analysis must be identified and selected to ensure 100% amnesiac operation in dom0. This investigation requires significant time, and unfortunately, I cannot dedicate myself fully to discovering and addressing all of it right now due to involvement with other projects. If anyone can solve this, please contribute with a complete solution!
Untested Functions
In the script below, I included functions with commented-out code intended to handle metadata cleanup. These are untested prototypes that likely contain errors or bugs:
| Function | Purpose |
|---|---|
overlayfs_dom0() |
Handles overlayfs-based amnesia for dom0 |
tmpfs_dom0() |
Handles tmpfs-based amnesia for dom0 |
The SUIT CLI is ready. The remaining work involves:
- Solving the metadata problem
- Testing thoroughly
- Integrating into the functions
- Using the SUIT CLI to call and execute with precision
Important Operational Note
Overlayfs or tmpfs treatment must be toggled ON/OFF for specific dom0 directories and files containing metadata.
Users will need to update dom0, or update another AppVM/DVM/template persistently. Certain files require persistent modification and registration. If these files need to remain amnesiac to prevent dom0 metadata leakage, you must:
- Disable amnesia mode
- Perform updates/configurations
- Re-enable amnesia mode
This hybrid workflow—part amnesiac, part persistent—allows Qubes OS operation without requiring reboot!
Technical Notes
This is a CLI for zram, but the same algorithm, logic, and organization can be adapted for tmpfs, overlayfs, etc. For my purposes, zram alone is sufficient.
Shell Script Template: zram-pool-suit.sh
#!/bin/bash
# =============================================================================
# OVERLAYFS DOM0 FUNCTION
# =============================================================================
overlayfs_dom0() {
echo "Function overlayfs_dom0"
echo "Function is in development — it is not ready yet!"
echo "See the code commented inside to understand the intended model and purpose."
echo "After development is complete, this function will be available for action in this program!"
: <<'COMMENTED_CODE'
[[ $EUID -ne 0 ]] && { echo "[!] ERROR: must run as root (dom0)." >&2; return 1; }
local action="${1:-}"
local dirs=(
"/etc/libvirt/libxl"
"/etc/lvm/archive"
"/etc/lvm/backup"
"/var/log/libvirt/libxl"
"/var/log/qubes"
"/var/log/xen/console"
"/var/lib/qubes/appvms"
"/var/lib/qubes/backup"
"/home/user/.local/share/applications"
"/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs"
"/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs/apps"
"/home/user/.local/share/desktop-directories"
"/home/user/.local/state/wireplumber"
"/tmp"
)
local base="/run/overlayfs-dom0"
case "$action" in
start|on)
echo ""
echo "==========================================================="
echo " OVERLAYFS DOM0 - START"
echo "==========================================================="
echo "[*] Directories entering overlayfs:"
printf ' %s\n' "${dirs[@]}"
echo "-----------------------------------------------------------"
mkdir -p "$base"
local dir safe upper work
for dir in "${dirs[@]}"; do
dir="${dir%/}"
[[ ! -d "$dir" ]] && { echo " SKIP $dir (does not exist)"; continue; }
mountpoint -q "$dir" && { echo " SKIP $dir (already mounted)"; continue; }
safe="$(echo "$dir" | sed 's|^/||; s|/|-|g')"
upper="${base}/upper-${safe}"
work="${base}/work-${safe}"
mkdir -p "$upper" "$work"
if mount -t overlay overlay -o "lowerdir=${dir},upperdir=${upper},workdir=${work}" "$dir"; then
echo " OK $dir -> overlay (upper: ${upper})"
else
echo " FAIL $dir (mount failed)"
fi
done
echo "-----------------------------------------------------------"
echo "[+] OVERLAYFS STARTED. Writes now go to /run (ephemeral)."
echo "[!] Will NOT survive reboot unless systemd service is enabled."
echo "==========================================================="
echo ""
;;
stop|off)
echo ""
echo "==========================================================="
echo " OVERLAYFS DOM0 - STOP"
echo "==========================================================="
echo "[*] Directories returning to normal:"
printf ' %s\n' "${dirs[@]}"
echo "-----------------------------------------------------------"
local dir fstype
for dir in "${dirs[@]}"; do
dir="${dir%/}"
mountpoint -q "$dir" || { echo " SKIP $dir (not a mountpoint)"; continue; }
fstype="$(findmnt -n -o FSTYPE "$dir" 2>/dev/null || true)"
if [[ "$fstype" == "overlay" ]]; then
if umount "$dir"; then
echo " OK $dir <- overlay removed"
else
echo " FAIL $dir (umount failed)"
fi
else
echo " SKIP $dir (not overlay — ${fstype})"
fi
done
rm -rf "$base" 2>/dev/null || true
echo "-----------------------------------------------------------"
echo "[+] OVERLAYFS STOPPED. Original directories restored."
echo "==========================================================="
echo ""
;;
status)
echo ""
echo "==========================================================="
echo " OVERLAYFS DOM0 - STATUS"
echo "==========================================================="
local dir fstype
for dir in "${dirs[@]}"; do
dir="${dir%/}"
if mountpoint -q "$dir"; then
fstype="$(findmnt -n -o FSTYPE "$dir" 2>/dev/null)"
echo " MOUNTED $dir (${fstype})"
else
echo " NORMAL $dir"
fi
done
echo "==========================================================="
echo ""
;;
*)
echo "[!] Usage: overlayfs_dom0 {start|stop|status}"
return 1
;;
esac
COMMENTED_CODE
}
# =============================================================================
# TMPFS DOM0 FUNCTION
# =============================================================================
tmpfs_dom0() {
echo "Function tmpfs_dom0"
echo "Function is in development — it is not ready yet!"
echo "See the code commented inside to understand the intended model and purpose."
echo "After development is complete, this function will be available for action in this program!"
: <<'COMMENTED_CODE'
[[ $EUID -ne 0 ]] && { echo "[!] ERROR: must run as root (dom0)." >&2; return 1; }
local action="${1:-}"
local size="${2:-512m}"
local dirs=(
"/etc/libvirt/libxl"
"/etc/lvm/archive"
"/etc/lvm/backup"
"/var/log/libvirt/libxl"
"/var/log/qubes"
"/var/log/xen/console"
"/var/lib/qubes/appvms"
"/var/lib/qubes/backup"
"/home/user/.local/share/applications"
"/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs"
"/home/user/.local/share/qubes-appmenus/anon-whonix-tmpfs/apps"
"/home/user/.local/share/desktop-directories"
"/home/user/.local/state/wireplumber"
"/tmp"
)
case "$action" in
start|on)
echo ""
echo "==========================================================="
echo " TMPFS DOM0 - START (size=${size})"
echo "==========================================================="
echo "[*] Directories entering tmpfs:"
printf ' %s\n' "${dirs[@]}"
echo "-----------------------------------------------------------"
local dir tmp_bind
for dir in "${dirs[@]}"; do
dir="${dir%/}"
[[ ! -d "$dir" ]] && { echo " SKIP $dir (does not exist)"; continue; }
mountpoint -q "$dir" && { echo " SKIP $dir (already mounted)"; continue; }
tmp_bind="/run/tmpfs-bind-$$"
mkdir -p "$tmp_bind"
if ! mount --bind "$dir" "$tmp_bind"; then
echo " FAIL $dir (bind for copy failed)"
umount "$tmp_bind" 2>/dev/null; rmdir "$tmp_bind" 2>/dev/null
continue
fi
if mount -t tmpfs -o "size=${size}" tmpfs "$dir"; then
cp -a "${tmp_bind}/." "$dir/" 2>/dev/null || true
echo " OK $dir -> tmpfs (size=${size}, content copied)"
else
echo " FAIL $dir (tmpfs mount failed)"
fi
umount "$tmp_bind" 2>/dev/null || true
rmdir "$tmp_bind" 2>/dev/null || true
done
echo "-----------------------------------------------------------"
echo "[+] TMPFS STARTED. All content is now ephemeral."
echo "[!] Everything will be LOST on unmount or reboot."
echo "==========================================================="
echo ""
;;
stop|off)
echo ""
echo "==========================================================="
echo " TMPFS DOM0 - STOP"
echo "==========================================================="
echo "[*] Directories returning to normal:"
printf ' %s\n' "${dirs[@]}"
echo "-----------------------------------------------------------"
local dir fstype
for dir in "${dirs[@]}"; do
dir="${dir%/}"
mountpoint -q "$dir" || { echo " SKIP $dir (not a mountpoint)"; continue; }
fstype="$(findmnt -n -o FSTYPE "$dir" 2>/dev/null || true)"
if [[ "$fstype" == "tmpfs" ]]; then
if umount "$dir"; then
echo " OK $dir <- tmpfs removed"
else
echo " FAIL $dir (umount failed)"
fi
else
echo " SKIP $dir (not tmpfs — ${fstype})"
fi
done
echo "-----------------------------------------------------------"
echo "[+] TMPFS STOPPED. Original directories restored."
echo "==========================================================="
echo ""
;;
status)
echo ""
echo "==========================================================="
echo " TMPFS DOM0 - STATUS"
echo "==========================================================="
local dir fstype
for dir in "${dirs[@]}"; do
dir="${dir%/}"
if mountpoint -q "$dir"; then
fstype="$(findmnt -n -o FSTYPE "$dir" 2>/dev/null)"
echo " MOUNTED $dir (${fstype})"
else
echo " NORMAL $dir"
fi
done
echo "==========================================================="
echo ""
;;
*)
echo "[!] Usage: tmpfs_dom0 {start|stop|status} [size e.g. 1g]"
return 1
;;
esac
COMMENTED_CODE
}
clone_dvm_zram_pool_manager()
{
# =============================================================================
# QUBES DVM CLONE MANAGER - COMPLETELY REWRITTEN WITH POOL DETECTION FIX
# =============================================================================
REGISTRY_FILE="/etc/qubes/zram-dvm-registry.conf"
ZRAM_POOL="zram_pool"
ZRAM_VG="zram_vg"
QUBES_APPVM_DIR="/var/lib/qubes/appvms"
# =============================================================================
# Helper Functions for Pool Detection
# =============================================================================
# Get list of all VMs that are currently in zram_pool by checking LVM devices
get_vms_in_zram_pool() {
local vms=""
# Method 1: Check qvm-volume list for all VMs
for vm in $(ls "$QUBES_APPVM_DIR" 2>/dev/null); do
if qvm-volume list "$vm" 2>/dev/null | grep -q "$ZRAM_POOL"; then
vms="$vms $vm"
fi
done
# Method 2: Check LVM devices in /dev/zram_vg (after VM started at least once)
if [[ -d "/dev/$ZRAM_VG" ]]; then
for dev in /dev/$ZRAM_VG/*; do
[[ -b "$dev" ]] || continue
# Extract VM name from device name like: vm-xxx-private, vm-yyy-volatile
local dev_name=$(basename "$dev")
if [[ "$dev_name" =~ ^vm-(.+)-private ]] || [[ "$dev_name" =~ ^vm-(.+)-volatile ]]; then
local vm_name="${BASH_REMATCH[1]}"
if ! echo "$vms" | grep -qw "$vm_name"; then
vms="$vms $vm_name"
fi
fi
done
fi
# Method 3: Check with findmnt for mounted pools
local mounts=$(findmnt -t lvm -n -o TARGET 2>/dev/null | grep "$ZRAM_VG" || true)
for mount in $mounts; do
# Extract VM path from mount point like: /var/lib/qubes/dom0/...
: # placeholder
done
# Method 4: Query qvm-pool info directly
local pool_vms=$(qvm-pool info "$ZRAM_POOL" 2>/dev/null | grep -E "^\s+[a-zA-Z0-9_-]+" | awk '{print $1}' || true)
for vm in $pool_vms; do
if ! echo "$vms" | grep -qw "$vm"; then
vms="$vms $vm"
fi
done
echo "$vms" | tr ' ' '\n' | sort -u | grep -v '^$'
}
# Check if a specific VM is in zram_pool
is_vm_in_zram_pool() {
local vm_name="$1"
local vms=$(get_vms_in_zram_pool)
echo "$vms" | grep -qx "$vm_name"
}
# =============================================================================
# List VMs
# =============================================================================
list_all_vms() {
echo "All AppVMs/DVMs available:"
echo "--------------------------"
ls "$QUBES_APPVM_DIR" 2>/dev/null | sort || echo "(none found)"
echo "--------------------------"
}
# =============================================================================
# Option 1: Add to Registry
# =============================================================================
add_entry() {
echo ""
echo "===== ADD VM TO CLONE REGISTRY ====="
mkdir -p "$(dirname "$REGISTRY_FILE")"
touch "$REGISTRY_FILE"
chmod 600 "$REGISTRY_FILE"
list_all_vms
read -p "Source VM name to clone: " SOURCE_VM
if [[ -z "$SOURCE_VM" ]]; then
echo "[!] ERROR: Source VM name cannot be empty!"
return 1
fi
if [[ ! -d "$QUBES_APPVM_DIR/$SOURCE_VM" ]]; then
echo "[!] ERROR: VM '$SOURCE_VM' not found!"
return 1
fi
read -p "Target clone name: " TARGET_NAME
if [[ -z "$TARGET_NAME" ]]; then
echo "[!] ERROR: Target name cannot be empty!"
return 1
fi
if grep -q "^${TARGET_NAME}:" "$REGISTRY_FILE" 2>/dev/null; then
echo "[!] ERROR: '$TARGET_NAME' already in registry!"
return 1
fi
echo "Available VMs for NetVM selection:"
echo "-----------------------------------"
ls "$QUBES_APPVM_DIR" 2>/dev/null | sort
echo "-----------------------------------"
echo "Press ENTER for NO network access"
echo "-----------------------------------"
read -p "Select NetVM: " NET_VM
if [[ -z "$NET_VM" ]]; then
NET_VM="none"
echo "[i] No NetVM selected — clone will have no network."
else
if [[ ! -d "$QUBES_APPVM_DIR/$NET_VM" ]]; then
echo "[!] ERROR: NetVM '$NET_VM' not found!"
return 1
fi
fi
echo "${TARGET_NAME}:${SOURCE_VM}:${NET_VM}" >> "$REGISTRY_FILE"
echo "[+] Added: $TARGET_NAME <- $SOURCE_VM (NetVM: $NET_VM)"
}
# =============================================================================
# Option 2: Create All Clones (WITH START/SHUTDOWN FOR REGISTRATION)
# =============================================================================
create_all_clones() {
echo ""
echo "===== CREATE ALL REGISTERED CLONES ====="
if [[ ! -s "$REGISTRY_FILE" ]]; then
echo "[!] Registry is empty. Nothing to clone!"
return 1
fi
echo "Entries in registry:"
cat "$REGISTRY_FILE" | nl
echo ""
local success=0
local fail=0
local skipped=0
local start_fail=0
while IFS=: read -r TARGET SOURCE NET; do
if [[ -z "$TARGET" ]]; then continue; fi
if qvm-check "$TARGET" >/dev/null 2>&1; then
echo "[SKIP] '$TARGET' already exists! (will not clone again)"
((skipped++)) || true
continue
fi
echo "[*] Cloning: $SOURCE -> $TARGET (pool: $ZRAM_POOL, NetVM: $NET)"
if qvm-clone -P="$ZRAM_POOL" "$SOURCE" "$TARGET" 2>/dev/null; then
if [[ "$NET" == "none" ]]; then
qvm-prefs "$TARGET" netvm ""
echo "[i] NetVM set to: NONE (no network)"
else
qvm-prefs "$TARGET" netvm "$NET"
echo "[i] NetVM set to: $NET"
fi
qvm-prefs "$TARGET" template_for_dispvms True
# ✅ CORREÇÃO: Start and shutdown to register volumes in LVM
echo "[*] Starting VM to register volumes in zram_pool..."
if qvm-start "$TARGET" 2>/dev/null; then
echo "[i] VM started successfully"
sleep 5
echo "[*] Shutting down VM..."
if qvm-shutdown --wait "$TARGET" 2>/dev/null; then
echo "[OK] VM shut down - volumes registered in /dev/$ZRAM_VG"
((success++)) || true
else
echo "[WARN] Shutdown failed! Trying force kill..."
qvm-kill "$TARGET" 2>/dev/null || true
sleep 2
echo "[WARN] Volume registration may be incomplete!"
((success++)) || true
((start_fail++)) || true
fi
else
echo "[WARN] Failed to start VM! Volume registration may be incomplete!"
echo "[i] Run 'qvm-start $TARGET' manually later to register volumes"
((success++)) || true
((start_fail++)) || true
fi
else
echo "[FAIL] Clone command failed!"
((fail++)) || true
fi
done < "$REGISTRY_FILE"
echo ""
echo "========================================"
echo "Results:"
echo " Created: $success"
echo " Skipped: $skipped (already existed)"
echo " Failed: $fail"
echo " Start issues: $start_fail (check volume registration)"
echo "========================================"
}
# =============================================================================
# Option 2.1: Create Single Registered Clone
# =============================================================================
create_single_clone() {
echo ""
echo "===== CREATE SINGLE REGISTERED CLONE ====="
if [[ ! -s "$REGISTRY_FILE" ]]; then
echo "[!] Registry is empty. Nothing to clone!"
return 1
fi
echo "Registered entries:"
echo "--------------------"
cat "$REGISTRY_FILE" | nl
echo "--------------------"
echo ""
read -p "Enter entry number to clone: " ENTRY_NUM
if ! [[ "$ENTRY_NUM" =~ ^[0-9]+$ ]]; then
echo "[!] ERROR: Invalid number!"
return 1
fi
# Extract specific line from registry
SELECTED_LINE=$(sed -n "${ENTRY_NUM}p" "$REGISTRY_FILE")
if [[ -z "$SELECTED_LINE" ]]; then
echo "[!] ERROR: Entry $ENTRY_NUM does not exist!"
return 1
fi
# Parse the selected entry
IFS=: read -r TARGET SOURCE NET <<< "$SELECTED_LINE"
if [[ -z "$TARGET" || -z "$SOURCE" ]]; then
echo "[!] ERROR: Invalid entry format!"
return 1
fi
# Check if already exists
if qvm-check "$TARGET" >/dev/null 2>&1; then
echo "[!] ERROR: '$TARGET' already exists! Cannot clone again."
echo "[i] Remove it first or choose a different entry."
return 1
fi
echo ""
echo "[*] Cloning: $SOURCE -> $TARGET (pool: $ZRAM_POOL)"
echo "[*] NetVM: $NET"
echo "------------------------------------------------------"
# Step 1: Clone with pool
if qvm-clone -P="$ZRAM_POOL" "$SOURCE" "$TARGET" 2>/dev/null; then
echo "[OK] VM cloned successfully"
# Step 2: Set NetVM
if [[ "$NET" == "none" ]]; then
qvm-prefs "$TARGET" netvm ""
echo "[i] NetVM set to: NONE (no network)"
else
qvm-prefs "$TARGET" netvm "$NET"
echo "[i] NetVM set to: $NET"
fi
# Step 3: Mark as DVM Template
qvm-prefs "$TARGET" template_for_dispvms True
echo "[i] Marked as DVM Template for Disposable VMs"
# Step 4: Start and shutdown to register volumes in LVM
echo ""
echo "[*] Starting VM to register volumes in zram_pool..."
if qvm-start "$TARGET" 2>/dev/null; then
echo "[i] VM started successfully"
sleep 5
echo "[*] Shutting down VM..."
if qvm-shutdown --wait "$TARGET" 2>/dev/null; then
echo "[OK] VM shut down - volumes registered in /dev/$ZRAM_VG"
echo ""
echo "========================================"
echo "[SUCCESS] Clone created successfully!"
echo "========================================"
else
echo "[WARN] Shutdown failed! Trying force kill..."
qvm-kill "$TARGET" 2>/dev/null || true
sleep 2
echo "[WARN] Volume registration may be incomplete!"
echo ""
echo "========================================"
echo "[SUCCESS] Clone created (partial registration)"
echo "========================================"
fi
else
echo "[WARN] Failed to start VM! Volume registration may be incomplete!"
echo "[i] Run 'qvm-start $TARGET' manually later to register volumes"
echo ""
echo "========================================"
echo "[SUCCESS] Clone created (manual start needed)"
echo "========================================"
fi
else
echo "[FAIL] Clone command failed!"
echo ""
echo "========================================"
echo "[FAILED] Could not create clone"
echo "========================================"
return 1
fi
}
# =============================================================================
# Option 3: Add More Entries
# =============================================================================
add_more_entries() {
add_entry
}
# =============================================================================
# Option 4: Remove Entry
# =============================================================================
remove_entry() {
echo ""
echo "===== REMOVE ENTRY FROM REGISTRY ====="
if [[ ! -s "$REGISTRY_FILE" ]]; then
echo "[!] Registry is empty!"
return 1
fi
echo "Current entries:"
cat "$REGISTRY_FILE" | nl
echo ""
read -p "Enter entry number to remove: " NUM
if ! [[ "$NUM" =~ ^[0-9]+$ ]]; then
echo "[!] Invalid number!"
return 1
fi
sed -i "${NUM}d" "$REGISTRY_FILE"
echo "[+] Entry removed!"
}
# =============================================================================
# Option 5: Clear Registry
# =============================================================================
clear_registry() {
echo ""
echo "===== CLEAR ENTIRE REGISTRY ====="
if [[ ! -s "$REGISTRY_FILE" ]]; then
echo "[!] Registry is already empty!"
return 1
fi
read -p "Are you sure? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "[!] Cancelled!"
return 1
fi
: > "$REGISTRY_FILE"
echo "[+] Registry cleared!"
}
# =============================================================================
# Option 6: Delete Specific DVM from zram_pool (DETECTION FIXED)
# =============================================================================
delete_specific_dvm() {
echo ""
echo "===== DELETE SPECIFIC DVM FROM zram_pool ====="
echo "Detecting VMs in zram_pool using multiple methods..."
echo "------------------------------------------------------"
local vms=$(get_vms_in_zram_pool)
if [[ -z "$vms" ]]; then
echo "[!] No VMs detected in zram_pool!"
echo "[i] Make sure at least one VM was started after cloning"
echo "[i] Try running 'qvm-start <vm-name>' first, then try again"
return 1
fi
echo "VMs found in zram_pool:"
echo "$vms" | nl
echo ""
echo "Note: If a VM doesn't appear here but exists,"
echo "it hasn't been started since being added to the pool."
echo "Try starting it: qvm-start <vm-name>"
echo "------------------------------------------------------"
echo ""
read -p "Enter DVM name to delete: " VM_NAME
if [[ -z "$VM_NAME" ]]; then
echo "[!] ERROR: VM name cannot be empty!"
return 1
fi
if ! qvm-check "$VM_NAME" >/dev/null 2>&1; then
echo "[!] ERROR: VM '$VM_NAME' not found in Qubes!"
return 1
fi
if ! echo "$vms" | grep -qx "$VM_NAME"; then
echo "[WARN] '$VM_NAME' was not detected in zram_pool by this script,"
echo "but will still try to delete it. It may not be in the correct pool."
read -p "Continue anyway? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "[!] Cancelled!"
return 1
fi
fi
read -p "Delete '$VM_NAME'? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "[!] Cancelled!"
return 1
fi
# Shutdown if running
if qvm-check --running "$VM_NAME" >/dev/null 2>&1; then
echo "[*] Shutting down $VM_NAME..."
qvm-shutdown --wait "$VM_NAME" 2>/dev/null || qvm-kill "$VM_NAME" 2>/dev/null
sleep 2
fi
qvm-remove --force "$VM_NAME"
echo "[+] '$VM_NAME' deleted!"
}
# =============================================================================
# Option 7: Delete ALL DVMs from zram_pool (DETECTION FIXED)
# =============================================================================
delete_all_dvms() {
echo ""
echo "===== DELETE ALL DVMS FROM zram_pool ====="
echo "Detecting VMs in zram_pool using multiple methods..."
echo "------------------------------------------------------"
local vms=$(get_vms_in_zram_pool)
if [[ -z "$vms" ]]; then
echo "[!] No VMs detected in zram_pool!"
echo "[i] Make sure at least one VM was started after cloning"
echo "[i] Try running 'qvm-start <vm-name>' first, then try again"
return 1
fi
echo "VMs found in zram_pool that will be DELETED:"
echo "$vms" | nl
echo "------------------------------------------------------"
echo ""
read -p "DELETE ALL THESE DVMS? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "[!] Cancelled!"
return 1
fi
local count=0
for vm in $vms; do
echo "Processing: $vm"
# Shutdown if running
if qvm-check --running "$vm" >/dev/null 2>&1; then
echo " [*] Shutting down..."
qvm-shutdown --wait "$vm" 2>/dev/null || qvm-kill "$vm" 2>/dev/null
sleep 2
fi
qvm-remove --force "$vm"
((count++)) || true
done
if [[ $count -eq 0 ]]; then
echo "[i] No VMs were deleted (none found or all already gone)"
else
echo "[+] Deleted $count VM(s) from zram_pool!"
fi
}
# =============================================================================
# Option 8: Check Status
# =============================================================================
check_status() {
echo ""
echo "===== REGISTRY & zram_pool STATUS ====="
echo "Registry entries:"
echo "------------------"
if [[ -s "$REGISTRY_FILE" ]]; then
cat "$REGISTRY_FILE" | nl
else
echo " [Empty]"
fi
echo ""
echo "VMs detected in zram_pool:"
echo "--------------------------"
local pool_count=0
local vms=$(get_vms_in_zram_pool)
if [[ -z "$vms" ]]; then
echo " [None detected]"
echo " Note: VMs must be started at least once to be visible in /dev/$ZRAM_VG"
else
for vm in $vms; do
local netvm=$(qvm-prefs "$vm" netvm 2>/dev/null || echo "None")
local disp=$(qvm-prefs "$vm" template_for_dispvms 2>/dev/null || echo "False")
local pool=$(qvm-prefs "$vm" default_volume_pool 2>/dev/null || echo "Unknown")
echo " $vm"
echo " ├─ Pool: $pool"
echo " ├─ NetVM: $netvm"
echo " └─ DispTemplate: $disp"
((pool_count++)) || true
done
fi
echo "--------------------------"
echo ""
echo "LVM Devices in /dev/$ZRAM_VG:"
echo "------------------------------"
if [[ -d "/dev/$ZRAM_VG" ]]; then
ls /dev/$ZRAM_VG/ 2>/dev/null | head -20 || echo " [Cannot read]"
else
echo " [Directory does not exist]"
fi
echo "------------------------------"
echo ""
echo "ZRAM pool size and DVM usage"
zramctl
echo
echo "Summary:"
echo " Registry entries: $(wc -l < "$REGISTRY_FILE" 2>/dev/null | tr -d ' ' || echo 0)"
echo " Active clones detected: $pool_count"
echo " ZRAM pool: $ZRAM_POOL"
}
# =============================================================================
# Option 9: Auto-Service Setup
# =============================================================================
setup_auto_service() {
echo ""
echo "===== SETUP AUTO-CLONE SERVICE ====="
cat > /etc/systemd/system/zram-dvm-auto.service << 'EOF'
[Unit]
Description=Auto-Clone DVMs from Registry
After=qubesd.service
Before=qubes-vm@sys-net.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/zram-dvm-clones.sh --auto
RemainAfterExit=yes
[Install]
WantedBy=qubes-post-installs.service
EOF
cat > /usr/local/bin/zram-dvm-clones.sh << 'AUTOSCRIPT'
#!/bin/bash
REGISTRY_FILE="/etc/qubes/zram-dvm-registry.conf"
ZRAM_POOL="zram_pool"
if [[ ! -s "$REGISTRY_FILE" ]]; then
echo "[i] No registry entries. Skipping auto-clone."
exit 0
fi
echo "[*] Starting auto-clone from registry..."
while IFS=: read -r TARGET SOURCE NET; do
if [[ -z "$TARGET" ]]; then continue; fi
if qvm-check "$TARGET" >/dev/null 2>&1; then
echo "[SKIP] $TARGET already exists"
continue
fi
if qvm-clone -P="$ZRAM_POOL" "$SOURCE" "$TARGET" 2>/dev/null; then
if [[ "$NET" == "none" ]]; then
qvm-prefs "$TARGET" netvm ""
echo "[OK] $TARGET cloned (no network)"
else
qvm-prefs "$TARGET" netvm "$NET"
echo "[OK] $TARGET cloned (NetVM: $NET)"
fi
qvm-prefs "$TARGET" template_for_dispvms True
# Start and shutdown to register volumes
echo "[*] Registering volumes in $ZRAM_POOL..."
if qvm-start "$TARGET" 2>/dev/null; then
sleep 5
qvm-shutdown --wait "$TARGET" 2>/dev/null || qvm-kill "$TARGET" 2>/dev/null
echo "[OK] Volumes registered"
else
echo "[WARN] Failed to start for volume registration"
fi
else
echo "[FAIL] Failed to clone $TARGET"
fi
done < "$REGISTRY_FILE"
echo "[*] Auto-clone completed."
AUTOSCRIPT
chmod +x /usr/local/bin/zram-dvm-clones.sh
systemctl daemon-reload
systemctl enable zram-dvm-auto.service 2>/dev/null || true
echo "[+] Service created and enabled!"
echo " File: /etc/systemd/system/zram-dvm-auto.service"
echo " Auto-script: /usr/local/bin/zram-dvm-clones.sh"
echo " Pool: $ZRAM_POOL"
}
# =============================================================================
# Option 10: Check Service Status
# =============================================================================
check_service() {
echo ""
echo "===== SERVICE STATUS ====="
echo "Service: zram-dvm-auto.service"
echo ""
if systemctl is-active --quiet zram-dvm-auto.service 2>/dev/null; then
echo "[OK] Status: ACTIVE"
else
echo "[WARN] Status: INACTIVE or not found"
fi
echo ""
echo "Enabled status:"
systemctl is-enabled zram-dvm-auto.service 2>/dev/null || echo " [Not enabled]"
echo ""
echo "Last 5 log entries:"
journalctl -u zram-dvm-auto.service -n 5 --no-pager 2>/dev/null || echo " [No logs found]"
}
# =============================================================================
# MAIN MENU
# =============================================================================
show_menu() {
clear
echo ""
echo "==========================================================="
echo " QUBES DVM CLONE MANAGER"
echo "==========================================================="
echo ""
echo " 1) Add VM to clone registry"
echo " 2) Create all registered clones (to zram_pool)"
echo " 3) Create one registered clone (to zram_pool)"
echo " 4) Remove entry from registry"
echo " 5) Clear entire registry"
echo " 6) Delete specific DVM from zram_pool"
echo " 7) Delete ALL DVMs from zram_pool"
echo " 8) Check registry & zram_pool status"
echo " 9) Setup auto-clone service (boot)"
echo " 10) Check service status"
echo " 0) Exit"
echo ""
echo "==========================================================="
}
# =============================================================================
# EXECUTION
# =============================================================================
while true; do
show_menu
read -p "Select an option: " choice
case "$choice" in
1) add_entry ;;
2) create_all_clones ;;
3) create_single_clone ;;
4) remove_entry ;;
5) clear_registry ;;
6) delete_specific_dvm ;;
7) delete_all_dvms ;;
8) check_status ;;
9) setup_auto_service ;;
10) check_service ;;
0) echo "[*] Exiting..."; exit 0 ;; #show_menu_main but do not come back do main menu
*) echo "[!] Invalid option!"; sleep 1 ;;
esac
echo ""
read -p "Press Enter to continue..."
done
}
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 "=========================================="
}
# =============================================================================
# MAIN MENU
# =============================================================================
show_menu_main() {
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) Qubes DVM Clone Manager"
echo " 4) Start overlayfs on specific dom0 directories for anti-forensics"
echo " 5) Stop overlayfs on dom0 directories"
echo " 6) Start tmpfs on specific dom0 directories for anti-forensics"
echo " 7) Stop tmpfs on dom0 directories"
echo " 8) Check overlayfs/tmpfs status"
echo " 0) Exit"
echo ""
echo "==========================================================="
}
# =============================================================================
# EXECUTION
# =============================================================================
while true; do
show_menu_main
read -p "Select an option: " choice
case "$choice" in
1)
echo "[*] Starting ZRAM pool creation..."
zram_pool
amnesic_logs_metadata_dom0
;;
2)
echo "[*] Removing ZRAM pool and all related artifacts..."
remove_zram_pool
;;
3)
echo "[*] Accessing DVM Clone Manager..."
clone_dvm_zram_pool_manager
;;
4)
echo "[*] Starting overlayfs on dom0 directories..."
sudo bash -c "$(declare -f overlayfs_dom0); overlayfs_dom0 start"
;;
5)
echo "[*] Stopping overlayfs on dom0 directories..."
sudo bash -c "$(declare -f overlayfs_dom0); overlayfs_dom0 stop"
;;
6)
echo "[*] Starting tmpfs on dom0 directories..."
read -p "Enter tmpfs size (e.g. 512m, 1g) [default: 512m]: " tsize
tsize="${tsize:-512m}"
sudo bash -c "$(declare -f tmpfs_dom0); tmpfs_dom0 start '${tsize}'"
;;
7)
echo "[*] Stopping tmpfs on dom0 directories..."
sudo bash -c "$(declare -f tmpfs_dom0); tmpfs_dom0 stop"
;;
8)
echo "[*] Checking overlayfs/tmpfs status..."
echo ""
overlayfs_dom0 status 2>/dev/null || sudo bash -c "$(declare -f overlayfs_dom0); overlayfs_dom0 status"
echo ""
tmpfs_dom0 status 2>/dev/null || sudo bash -c "$(declare -f tmpfs_dom0); tmpfs_dom0 status"
;;
0)
echo "[*] Exiting."
exit 0
;;
*)
echo "[!] Invalid option!"; sleep 1
;;
esac
echo ""
read -p "Press Enter to continue..."
done
show_menu_main
Cool! Definitely publish your method for complete removal of DVM metadata if you manage to pull it off. Also, just share your thoughts along the way. I think we’ll figure out a way to eliminate DVM metadata while preserving metadata for persistent VMs. I will add your script to the guide as soon as you publish the final release after all testing.
qubes-forensic-hunter.sh (forensic script to trace appvm/dvm records in dom0
This is good but still works on filesystem level only. A forensic analyst would look deeper and (depending on the case) may have access to manufacturer-supplied proprietary tools to do so. Consider all the existing technology as available.
Anti-forensics without properly addressing everything down to the hardware layer is a theatre. That’s why stateful hardware is simply not suitable for the purpose.
Could you share the details?
Also, regarding /var/logs on tmpfs - how exactly did you do that? Did you use /etc/fstab or a systemd unit (like tmp.mount)?
It is really strange that it breaks things because in qubesd.service:
After=qubes-db-dom0.service ...
which runs after qubes-db-dom0.service which runs after systemd-tmpfiles-setup.service, so everything regarding tmpfs should be set unless some contention slows down the latter service. If that is the case, then rescheduling the tmpfs mount might fix it.
No licence = no permission to use, so @Atrate is right.
Personally, I stand corrected too, so I am adding a public domain license to the original.
Also:
fstab. I don’t remember all the details anymore, it was a long time ago. I do remember that tmpfs caused major problems. After that, I started trying overlay - it worked better, but it also broke some features (new appvms didn’t have apps). I worked on it for a few days and then decided to just use metadata deletion + a volatile journal.
Okay. License:Unlicense Various Licenses and Comments about Them - GNU Project - Free Software Foundation in all my guides. Added it
Perhaps if you use a unit file it may work.
Also, what do you mean by overlay?