Skip to content

File Exchange

See Under the Hood: File Exchange for how CIFS and 9p mounts work at the QEMU level.

All examples below assume:

python
from quicksand import Sandbox, Mount, NetworkMode

Boot-time mounts

Declare directories to share when creating the sandbox:

python
async with Sandbox(
    image="ubuntu",
    mounts=[
        Mount("/host/code", "/mnt/code"),
        Mount("/host/data", "/mnt/data", readonly=True),
    ],
) as sb:
    result = await sb.execute("ls /mnt/code")
    await sb.execute("python3 /mnt/code/main.py")

Files appear inside the VM at the specified guest path. Changes to non-readonly mounts are visible on both sides immediately.

Git repositories on CIFS mounts

CIFS mounts present synthesized Unix permissions rather than each host file's executable bit. As a result, git status inside the guest can report mode-only changes for otherwise unchanged files.

Disable executable-bit comparisons for Git commands against the mounted repository:

python
result = await sb.execute("git -c core.filemode=false -C /mnt/code status")

The -c option applies only to that command; it does not change the host repository's Git configuration. Content changes are still reported, but executable-bit changes are ignored.

File locking on CIFS mounts

The bundled SMB server does not implement server-side byte-range locks. Quicksand mounts its shares with nobrl, allowing flock and SQLite to use local locking inside the guest instead.

These locks coordinate processes using the same mount in one sandbox. They do not coordinate with host processes, other sandboxes, or independent mounts of the same host directory. Do not concurrently access a writable SQLite database from those locations. The optional native SMB backend retains server-side locking.

Dynamic hot-mounts

Share a directory into an already-running sandbox:

python
async with Sandbox(image="ubuntu") as sb:
    # Mount at any time after start
    handle = await sb.mount("/host/project", "/mnt/project")

    await sb.execute("ls /mnt/project")
    await sb.execute("echo 'new file' > /mnt/project/output.txt")

    # Unmount when done
    await sb.unmount(handle)

Hot-mounts use the same CIFS file-sharing protocol as boot-time mounts. New shares can be added at any time. No restart is needed.

Read-only mounts

Prevent the guest from modifying host files:

python
# Boot-time
Mount("/host/data", "/mnt/data", readonly=True)

# Dynamic
handle = await sb.mount("/host/data", "/mnt/data", readonly=True)

Getting files out without mounts

If you just need to read a small file from the guest, execute() works:

python
result = await sb.execute("cat /etc/hostname")
hostname = result.stdout.strip()

To write a file into the guest:

python
await sb.execute("echo 'hello' > /tmp/greeting.txt")

For larger file transfers, mounts are more efficient.

Listing active mounts

python
for handle in sb.active_mounts:
    print(f"{handle.host_path}{handle.guest_path} (readonly={handle.readonly})")

9p mounts (no network)

If you need file sharing with NetworkMode.NONE (no network at all), use 9p:

python
sb = Sandbox(
    image="ubuntu",
    network_mode=NetworkMode.NONE,
    mounts=[Mount("/host/data", "/mnt/data", type="9p")],
)

9p mounts are configured at launch time and can't be hot-added. They use a virtual device instead of the network stack.

Which mount type to use

CIFS (default)9p
Hot-mountableYesNo (boot-time only)
Works with NONENoYes
Works with MOUNTS_ONLY / FULLYesYes
ProtocolNetwork (SMB3)Virtual device