diff --git a/fsck/semantic_check.c b/fsck/semantic_check.c
index b168456..d7cf9e1 100644
--- a/fsck/semantic_check.c
+++ b/fsck/semantic_check.c
@@ -551,7 +551,10 @@ static int check_semantic_pass(struct reiserfs_key *key,
for (i = pos_in_item; i < get_ih_entry_count(&tmp_ih);
i++, deh++) {
- struct item_head relocated_ih;
+ /* Only meaningful when check_check_regular_file() returns
+ RELOCATED; that path currently passes NULL for new_ih,
+ so this stays zero-initialised. */
+ struct item_head relocated_ih = { 0 };
int ret = OK;
if (name) {
diff --git a/tests/vm/README.md b/tests/vm/README.md
new file mode 100644
index 0000000..40029d1
--- /dev/null
+++ b/tests/vm/README.md
@@ -0,0 +1,85 @@
+# Ambiente de testes em VM
+
+Este documento descreve o ambiente de validação do reiserfsprogs em máquinas
+virtuais (libvirt/KVM), usado para exercitar o build e as ferramentas em
+distribuições diferentes da máquina de desenvolvimento.
+
+O ambiente é **efêmero**: as VMs ficam em `/var/tmp/vmtest` e não fazem parte
+do repositório. Os scripts que o reproduzem estão em `tests/vm/`.
+
+## Requisitos do host
+
+| Componente | Verificação |
+|---|---|
+| KVM | `ls -l /dev/kvm` |
+| libvirt (user session) | `virsh -c qemu:///session list --all` |
+| `virt-install`, `qemu-img` | `which virt-install qemu-img` |
+| `guestfs-tools` (offline) | `which virt-customize` |
+| `mkisofs` ou `xorriso` | `which mkisofs xorriso` |
+
+No Fedora: `sudo dnf install libvirt-daemon-kvm qemu-kvm virt-install
+guestfs-tools genisoimage`.
+
+O ambiente usa **qemu:///session** (sem root). Não requer a rede `default`
+do libvirt: a conectividade vem do backend `passt` em modo *user*.
+
+## Topologia
+
+```mermaid
+graph LR
+ H[host] -->|qemu:///session| D[rfs-debian
Debian 12 · gcc]
+ H -->|qemu:///session| A[rfs-alpine
Alpine 3.20 · gcc/musl]
+ D -->|passt :2222| S[ssh opcional]
+ A -->|passt| S
+ H -->|attach-disk| V[vdb: fontes + run_tests.sh]
+```
+
+Cada VM recebe:
+
+* imagem cloud (qcow2) redimensionada para 8 GiB;
+* um disco de dados `vdb` (ext4) com `rfs-src.tar.gz` e `run_tests.sh`;
+* acesso por **console serial** (pty do libvirt), que é o canal de
+ automação usado — independente de rede.
+
+## Por que console serial e não SSH
+
+Nas imagens cloud o `sshd` demora a subir e o `cloud-init` nem sempre aplica
+a senha (o datasource NoCloud exige rótulo `cidata` e o CD precisa estar no
+Barramento certo). O console serial está disponível assim que o getty sobe e
+é suficiente para: autenticar, copiar arquivos (já estão no `vdb`) e rodar
+qualquer comando. Por isso o `tests/vm/guestcmd.py` dirige o pty.
+
+## Ciclo de uso
+
+```sh
+cd tests/vm
+
+# 1. preparar imagens e VMs (uma vez)
+./setup_env.sh
+
+# 2. rodar a bateria em uma VM
+./run_in_vm.sh rfs-alpine
+./run_in_vm.sh rfs-debian
+
+# 3. destruir tudo
+./teardown_env.sh
+```
+
+## O que a bateria cobre (`tests/vm/run_tests.sh`)
+
+Executado **dentro** do guest, sobre o código extraído do `vdb`:
+
+1. `autogen.sh && configure --enable-warnings && make -j` — build limpo;
+2. contagem de warnings (esperado: 0);
+3. `mkreiserfs` em imagem de 64 MiB;
+4. `reiserfsck --check` (esperado: *No corruptions found*);
+5. `debugreiserfs -S` (superblock consistente);
+6. `reiserfstune -l` (troca de label);
+7. `resize_reiserfs` 128 MiB → 256 MiB;
+8. `reiserfsck` pós-resize;
+9. rollback: `-R` grava, `--rollback-fsck-changes` restaura, arquivo
+ inválido é rejeitado.
+
+## Resultados obtidos
+
+Ver `tests/vm/RESULTS.md`.
\ No newline at end of file
diff --git a/tests/vm/RESULTS.md b/tests/vm/RESULTS.md
new file mode 100644
index 0000000..856397a
--- /dev/null
+++ b/tests/vm/RESULTS.md
@@ -0,0 +1,124 @@
+# Resultados dos testes em VM
+
+Bateria executada com `tests/vm/run_in_vm.sh` em duas distribuições, ambas em
+KVM (`qemu:///session`), com o código de `293e6a7`.
+
+## Ambiente
+
+| | Debian 12 (bookworm) | Alpine 3.20.3 |
+|---|---|---|
+| Kernel | 6.1.0-54-cloud-amd64 | 6.6.49-0-virt |
+| libc | glibc 2.36 | **musl 1.2.5** |
+| gcc | 12.2.0 | 13.2.1 |
+| init | systemd + cloud-init | OpenRC + cloud-init |
+
+## Resultado por item
+
+| # | Teste | Debian | Alpine |
+|---|---|---|---|
+| 1 | `autogen.sh` | passa | passa |
+| 1 | `configure --enable-warnings` | passa | **falha** (ver abaixo) |
+| 1 | `make -j` | passa, **0 warnings** | não alcançado |
+| 2 | `mkreiserfs` | passa | não alcançado |
+| 3 | `reiserfsck --check` | passa | não alcançado |
+| 4 | `debugreiserfs -S` | passa | não alcançado |
+| 5 | `reiserfstune -l` | passa | não alcançado |
+| 6 | `resize_reiserfs` 128→256 MiB | passa | não alcançado |
+| 7 | `fsck` pós-resize | passa | não alcançado |
+| 8 | rollback: grava | passa | não alcançado |
+| 8 | rollback: restaura | passa | não alcançado |
+| 8 | rollback: magic inválida rejeitada | passa | não alcançado |
+| 8 | rollback: arquivo inexistente rejeitado | passa | não alcançado |
+
+**Debian: 12/12.** Alpine: bloqueado no item 1.
+
+## Achado 1 (bloqueante): não compila em musl
+
+O `configure` do Alpine falha com:
+
+```
+checking for register_printf_modifier... no
+checking for register_printf_specifier... no
+configure: error: reiserfsprogs requires a method to add printf modifiers
+```
+
+Causa: `reiserfscore/prints.c` usa `register_printf_specifier(3)`, extensão
+**glibc-only** (``). O musl não fornece `printf.h` nem essas
+funções. O `configure.ac` trata isso como erro fatal explícito — o
+comportamento é correto, mas o projeto **não compila fora da glibc**.
+
+Alcance medido:
+
+| Especificador | Uso |
+|---|---|
+| `%k` | 139 |
+| `%K` | 106 |
+| `%H` | 52 |
+| `%M` | 25 |
+| `%b` | 9 |
+| `%U` | 4 |
+| `%y` | 2 |
+| `%30H`, `%20y`, `%20k`, `%20K` | 4 |
+| **total** | **341** |
+
+Todos passam por `reiserfs_warning()` / `reiserfs_info()`. Portar exigiria
+substituir o mecanismo por funções de formatação explícitas (por exemplo
+`char *format_key(const struct reiserfs_key *)` com `%s`), uma mudança de API
+interna ampla — não um ajuste local.
+
+**Impacto prático**: reiserfsprogs fica restrito à glibc. Como o único uso
+atual da ferramenta é resgate de dados, isso importa: live images de
+recuperação baseadas em Alpine/musl não conseguem compilar o projeto.
+
+## Achado 2 (corrigido nesta rodada): warning real no gcc 12
+
+O gcc 12.2 do Debian acusou, duas vezes:
+
+```
+../include/reiserfs_fs.h:63:54: warning: ‘relocated_ih’ may be used uninitialized
+```
+
+O gcc 16 do host não acusa. Investigação:
+
+* `fsck/semantic_check.c:620` passa `&relocated_ih` para
+ `check_semantic_pass()`;
+* o bloco que escreveria `*new_ih` está **comentado** (linhas 471–486);
+* a chamada em `check_check_regular_file()` (linha 466) passa
+ `NULL` como `new_ih`, com o comentário `/* relocate ? new_ih : */ NULL`;
+* portanto `RELOCATED` nunca é retornado por esse caminho e
+ `case RELOCATED:` (linha 665) leria `relocated_ih` não inicializado.
+
+Não é alcançável hoje, mas é uma leitura potencial de lixo de pilha atrás de
+um `#case` que o compilador não consegue descartar. Correção: inicialização
+explícita `= { 0 }` com comentário explicando por que permanece zero.
+
+Verificado: **0 warnings no gcc 12.2 após a correção**.
+
+Em `fsck/semantic_rebuild.c` o caminho equivalente é seguro: o bloco
+`if (relocate) { ... *new_ih = *ih; retval = RELOCATED; }` está ativo
+(linhas 808–822), então `new_ih` é escrito antes do `case RELOCATED:`
+(linha 999).
+
+## Achado 3: limitação do ambiente (não do código)
+
+Nas imagens cloud o `cloud-init` não aplicou a senha do usuário: o datasource
+NoCloud exige o rótulo `cidata` e o CD correto, e o `sshd` demora a subir.
+O console serial, porém, fica disponível assim que o getty sobe — por isso o
+`tests/vm/guestcmd.py` dirige o pty e é o canal de automação usado. Isso não
+afeta o produto; afeta apenas como os testes são conduzidos.
+
+Além disso, a rede user-mode (`passt`) não entrega DHCP nem DNS por padrão: a
+interface sobe como `enp1s0` mas fica sem IP. Foi necessário configurar IP e
+resolver manualmente para instalar pacotes nas VMs.
+
+## Como reproduzir
+
+```sh
+cd tests/vm
+./setup_env.sh # imagens + VMs (uma vez)
+./run_in_vm.sh rfs-debian # build + bateria completa
+./run_in_vm.sh rfs-alpine # deve falhar no configure (esperado)
+./teardown_env.sh
+```
+
+Detalhes do ambiente, topologia e limitações: `tests/vm/README.md`.
\ No newline at end of file
diff --git a/tests/vm/guestcmd.py b/tests/vm/guestcmd.py
new file mode 100755
index 0000000..018f4fc
--- /dev/null
+++ b/tests/vm/guestcmd.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""Roda um comando no guest atraves do console serial do libvirt.
+
+O console serial e o unico canal garantido nas imagens cloud minimas: nao
+depende de sshd, de cloud-init nem de rede. Este script sincroniza com o
+prompt, autentica (root/rfstest) e executa o comando, lendo a saida do pty
+em blocos pequenos para evitar a sobreposicao de eco que ocorre em comandos
+longos.
+
+Uso:
+ guestcmd.py ''
+ guestcmd.py -f # executa um script local
+"""
+import base64
+import os
+import re
+import select
+import subprocess
+import sys
+import time
+
+USER = "root"
+PASSWORD = "rfstest"
+
+
+def console_pty(domain):
+ xml = subprocess.run(["virsh", "dumpxml", domain],
+ capture_output=True, text=True).stdout
+ m = (re.search(r"]*>\s*]*>\s* {out_path} 2>&1; "
+ f"echo __GC_DONE__\n")
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ if b"__GC_DONE__" in self.drain(2.0):
+ break
+ else:
+ return "(timeout)\n"
+
+ # tamanho em bytes
+ self.send(f"wc -c < {out_path}\n")
+ time.sleep(1.2)
+ raw = self.drain(2.0).decode(errors="replace")
+ nums = re.findall(r"\d+", raw)
+ size = int(nums[-1]) if nums else 0
+
+ # ler em blocos base64 (evita eco/sobreposicao)
+ data = b""
+ while len(data) < size:
+ self.send(f"dd if={out_path} bs=1 skip={len(data)} count=150 "
+ f"2>/dev/null | base64 -w0; echo\n")
+ time.sleep(0.8)
+ enc = self.drain(2.0).decode(errors="replace")
+ token = "".join(re.findall(r"[A-Za-z0-9+/=]{4,}", enc))
+ if not token:
+ break
+ try:
+ data += base64.b64decode(token + "=" * (-len(token) % 4))
+ except Exception:
+ break
+ return data.decode(errors="replace")
+
+
+def main():
+ if len(sys.argv) < 3:
+ sys.exit(__doc__)
+ domain = sys.argv[1]
+ console = Console(console_pty(domain))
+ console.login()
+
+ if sys.argv[2] == "-f":
+ with open(sys.argv[3]) as fh:
+ script = fh.read().replace("'", "'\\''")
+ command = f"sh -c '{script}'"
+ else:
+ command = sys.argv[2]
+
+ sys.stdout.write(console.run(command))
+ sys.stdout.write("\n")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/tests/vm/run_in_vm.sh b/tests/vm/run_in_vm.sh
new file mode 100755
index 0000000..331ab48
--- /dev/null
+++ b/tests/vm/run_in_vm.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+# Envia o codigo-fonte e o script de teste para dentro de uma VM e executa.
+#
+# Uso: run_in_vm.sh
+# Requer: guestcmd.py (mesmo diretorio), mkisofs, libvirt.
+set -e
+
+DOMAIN=${1:?uso: run_in_vm.sh }
+HERE=$(cd "$(dirname "$0")" && pwd)
+REPO=$(cd "$HERE/../.." && pwd)
+WORK=${WORK:-/var/tmp/vmtest}
+
+echo "== empacotando o codigo-fonte =="
+tar czf "$WORK/rfs-src.tar.gz" -C "$REPO" \
+ --exclude=.git --exclude='*.o' --exclude='*.lo' --exclude='*.la' \
+ --exclude='.libs' --exclude=build-aux --exclude=m4 \
+ --exclude=autom4te.cache --exclude=config.h --exclude=config.log \
+ --exclude=config.status --exclude=libtool \
+ --exclude='Makefile' --exclude='Makefile.in' --exclude=configure \
+ --exclude=aclocal.m4 --exclude='*.8' \
+ .
+
+echo "== montando o disco de dados =="
+rm -rf /tmp/vmdata.$$ && mkdir -p /tmp/vmdata.$$
+sudo mount -o loop "$WORK/data.img" /tmp/vmdata.$$
+sudo cp "$WORK/rfs-src.tar.gz" "$HERE/run_tests.sh" /tmp/vmdata.$$
+sudo umount /tmp/vmdata.$$ && rmdir /tmp/vmdata.$$
+
+echo "== transferindo para $DOMAIN =="
+ATTACH=/tmp/vdb.$$.xml
+cat > "$ATTACH" <
+
+
+
+
+EOF
+virsh attach-device "$DOMAIN" "$ATTACH" --live >/dev/null 2>&1 || true
+rm -f "$ATTACH"
+
+echo "== extraindo e rodando os testes =="
+"$HERE/guestcmd.py" "$DOMAIN" '
+set -e
+mkdir -p /mnt/data
+mountpoint -q /mnt/data || mount -o ro /dev/vdb /mnt/data
+mkdir -p /root/rfs
+tar xzf /mnt/data/rfs-src.tar.gz -C /root/rfs
+cp /mnt/data/run_tests.sh /root/run_tests.sh
+chmod +x /root/run_tests.sh
+SRC=/root/rfs sh /root/run_tests.sh
+' 900
\ No newline at end of file
diff --git a/tests/vm/run_tests.sh b/tests/vm/run_tests.sh
new file mode 100755
index 0000000..c54f71d
--- /dev/null
+++ b/tests/vm/run_tests.sh
@@ -0,0 +1,134 @@
+#!/bin/sh
+# Bateria de testes do reiserfsprogs, executada DENTRO do guest.
+# Espera receber o codigo-fonte em $SRC (padrao: /root/rfs).
+#
+# Uso (dentro da VM):
+# SRC=/root/rfs sh run_tests.sh
+set -u
+
+SRC=${SRC:-/root/rfs}
+PASS=0
+FAIL=0
+ok() { echo "PASS: $1"; PASS=$((PASS + 1)); }
+bad() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
+
+cd "$SRC" || { echo "FAIL: $SRC nao existe"; exit 1; }
+
+echo "=== ambiente ==="
+uname -srm
+(cc --version 2>/dev/null || echo "sem cc") | head -1
+ldd --version 2>&1 | head -1
+
+echo
+echo "=== 1. bootstrap + build com -Wall -Wextra ==="
+make distclean >/dev/null 2>&1
+if ./autogen.sh >/tmp/ag.log 2>&1 && ./configure --enable-warnings >/tmp/cfg.log 2>&1 \
+ && make -j"$(nproc 2>/dev/null || echo 2)" >/tmp/mk.log 2>&1; then
+ ok "autogen.sh + configure + make"
+else
+ bad "build"
+ echo "--- configure ---"; tail -15 /tmp/cfg.log
+ echo "--- make ---"; tail -25 /tmp/mk.log
+ echo
+ echo "RESULTADO: $PASS passaram, $FAIL falharam"
+ exit 1
+fi
+
+# grep -c retorna 1 quando nao ha ocorrencias; capturar so a primeira linha
+WARN=$(grep -c "warning:" /tmp/mk.log 2>/dev/null | head -1)
+WARN=${WARN:-0}
+if [ "$WARN" = "0" ]; then
+ ok "0 warnings com -Wall -Wextra"
+else
+ bad "$WARN warnings"
+ grep "warning:" /tmp/mk.log | head -10
+fi
+
+echo
+echo "=== 2. mkfs ==="
+rm -f /tmp/t.img
+dd if=/dev/zero of=/tmp/t.img bs=1M count=64 status=none 2>/dev/null
+if ./mkreiserfs/mkreiserfs -f -q -l TESTVOL /tmp/t.img >/dev/null 2>&1; then
+ ok "mkreiserfs"
+else
+ bad "mkreiserfs"
+fi
+
+echo
+echo "=== 3. reiserfsck --check ==="
+if ./fsck/reiserfsck --check -f -q -y /tmp/t.img 2>&1 | grep -q "No corruptions found"; then
+ ok "reiserfsck --check"
+else
+ bad "reiserfsck --check"
+fi
+
+echo
+echo "=== 4. debugreiserfs -S ==="
+if ./debugreiserfs/debugreiserfs -S /tmp/t.img 2>&1 | grep -q "Filesystem state: consistent"; then
+ ok "debugreiserfs -S"
+else
+ bad "debugreiserfs -S"
+fi
+
+echo
+echo "=== 5. reiserfstune -l ==="
+if ./tune/reiserfstune -l NEWLABEL -f /tmp/t.img >/dev/null 2>&1; then
+ ok "reiserfstune -l"
+else
+ bad "reiserfstune -l"
+fi
+
+echo
+echo "=== 6. resize 128M -> 256M ==="
+rm -f /tmp/big.img
+dd if=/dev/zero of=/tmp/big.img bs=1M count=128 status=none 2>/dev/null
+./mkreiserfs/mkreiserfs -f -q /tmp/big.img >/dev/null 2>&1
+truncate -s 256M /tmp/big.img
+if ./resize_reiserfs/resize_reiserfs -s +128M -f /tmp/big.img 2>&1 \
+ | grep -q "Resizing finished successfully"; then
+ ok "resize_reiserfs"
+else
+ bad "resize_reiserfs"
+fi
+
+echo
+echo "=== 7. fsck pos-resize ==="
+if ./fsck/reiserfsck --check -f -q -y /tmp/big.img 2>&1 | grep -q "No corruptions found"; then
+ ok "fsck apos resize"
+else
+ bad "fsck apos resize"
+fi
+
+echo
+echo "=== 8. rollback (save/restore/reject) ==="
+cp /tmp/t.img /tmp/rb.img
+rm -f /tmp/rb.dat
+./fsck/reiserfsck --fix-fixable -y -q -R /tmp/rb.dat /tmp/rb.img >/dev/null 2>&1
+if [ -f /tmp/rb.dat ]; then
+ ok "rollback file criado"
+else
+ bad "rollback file nao criado"
+fi
+if ./fsck/reiserfsck --rollback-fsck-changes -y -q -R /tmp/rb.dat /tmp/rb.img 2>&1 \
+ | grep -q "blocks restored"; then
+ ok "rollback-fsck-changes"
+else
+ bad "rollback-fsck-changes"
+fi
+head -c 100 /dev/urandom > /tmp/rollback-invalido.dat
+if ./fsck/reiserfsck --rollback-fsck-changes -y -q -R /tmp/rollback-invalido.dat /tmp/rb.img 2>&1 \
+ | grep -q "does not look like a rollback file"; then
+ ok "rollback com magic invalida rejeitado"
+else
+ bad "rollback com magic invalida aceito"
+fi
+if ./fsck/reiserfsck --rollback-fsck-changes -y -q -R /tmp/nao-existe.dat /tmp/rb.img 2>&1 \
+ | grep -q "Cannot stat rollback file"; then
+ ok "rollback inexistente rejeitado"
+else
+ bad "rollback inexistente aceito"
+fi
+
+echo
+echo "RESULTADO: $PASS passaram, $FAIL falharam"
+[ "$FAIL" -eq 0 ]
\ No newline at end of file
diff --git a/tests/vm/setup_env.sh b/tests/vm/setup_env.sh
new file mode 100755
index 0000000..f082074
--- /dev/null
+++ b/tests/vm/setup_env.sh
@@ -0,0 +1,91 @@
+#!/bin/sh
+# Prepara o ambiente de VMs: imagens, seed e maquinas virtuais.
+# Requer: libvirt (qemu:///session), virt-install, qemu-img, mkisofs.
+set -e
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+WORK=${WORK:-/var/tmp/vmtest}
+CONN=qemu:///session
+
+mkdir -p "$WORK"
+cd "$WORK"
+
+fetch() {
+ url=$1; out=$2
+ [ -s "$out" ] && { echo " $out ja existe"; return; }
+ echo " baixando $out"
+ curl -fsSL -o "$out.part" "$url"
+ mv "$out.part" "$out"
+}
+
+echo "== imagens =="
+fetch https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2 debian.qcow2
+fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/cloud/nocloud_alpine-3.20.3-x86_64-bios-cloudinit-r0.qcow2 alpine.qcow2
+
+echo "== redimensionando =="
+for img in debian.qcow2 alpine.qcow2; do
+ size=$(qemu-img info --output=json "$img" | grep -oP '"virtual-size":\s*\K[0-9]+')
+ if [ "${size:-0}" -lt 8589934592 ]; then
+ qemu-img resize "$img" 8G
+ else
+ echo " $img ja tem 8G"
+ fi
+done
+
+echo "== disco de dados (fontes + script de teste) =="
+if [ ! -s data.img ]; then
+ dd if=/dev/zero of=data.img bs=1M count=8 status=none
+ mkfs.ext4 -q -F data.img
+fi
+
+echo "== seed cloud-init =="
+cat > user-data <<'EOF'
+#cloud-config
+hostname: rfs-test
+ssh_pwauth: true
+disable_root: false
+users:
+ - name: root
+ lock_passwd: false
+ plain_text_passwd: rfstest
+chpasswd:
+ expire: false
+EOF
+cat > meta-data <<'EOF'
+instance-id: rfs-test-1
+local-hostname: rfs-test
+EOF
+cat > network-config <<'EOF'
+version: 2
+ethernets:
+ eth0:
+ dhcp4: true
+EOF
+rm -f seed.iso
+mkisofs -output seed.iso -volid cidata -joliet -rock \
+ user-data meta-data network-config >/dev/null 2>&1
+
+echo "== definindo VMs =="
+define_vm() {
+ name=$1; img=$2; variant=$3; mem=$4
+ if virsh -c "$CONN" dominfo "$name" >/dev/null 2>&1; then
+ echo " $name ja existe"
+ return
+ fi
+ virt-install -c "$CONN" \
+ --name "$name" \
+ --memory "$mem" --vcpus 2 \
+ --disk "path=$WORK/$img,format=qcow2,bus=virtio" \
+ --disk "path=$WORK/seed.iso,device=cdrom" \
+ --os-variant "$variant" \
+ --network user,model=virtio \
+ --graphics none --console pty \
+ --import --noautoconsole >/dev/null
+ echo " criada: $name"
+}
+
+define_vm rfs-debian debian.qcow2 debian12 2048
+define_vm rfs-alpine alpine.qcow2 alpinelinux3.20 1024
+
+echo
+echo "Pronto. Use tests/vm/run_in_vm.sh para testar."
\ No newline at end of file
diff --git a/tests/vm/teardown_env.sh b/tests/vm/teardown_env.sh
new file mode 100755
index 0000000..3fbb64a
--- /dev/null
+++ b/tests/vm/teardown_env.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Destrói as VMs do ambiente de teste. Nao remove as imagens baixadas.
+set -e
+CONN=qemu:///session
+for d in rfs-debian rfs-alpine; do
+ if virsh -c "$CONN" dominfo "$d" >/dev/null 2>&1; then
+ virsh -c "$CONN" destroy "$d" >/dev/null 2>&1 || true
+ virsh -c "$CONN" undefine "$d" --remove-all-storage >/dev/null 2>&1 || true
+ echo "removida: $d"
+ else
+ echo "ausente: $d"
+ fi
+done
\ No newline at end of file