使用crosvm创建开发沙箱

Lobsters Hottest 工具

摘要

关于使用crosvm创建隔离开发沙箱的指南,通过在虚拟机中运行开发工具和测试来应对软件供应链风险。

<p><a href="https://lobste.rs/s/mml3sh/creating_development_sandbox_with">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/02 04:07

# 使用 crosvm 创建开发沙箱 来源:https://www.erat.org/crosvm.html ## 目录 - [引言](#intro) - [背景](#background) - [编译 `crosvm`](#compiling) - [运行 `crosvm`](#running) - [宿主机网络配置](#host-networking) - [启动虚拟机](#starting-vm) - [虚拟机网络配置](#vm-networking) - [调整虚拟机磁盘大小](#resizing-disk) - [管理虚拟机](#managing-vm) - [开发流程](#development) - [连接虚拟机中的服务](#vm-servers) - [签名与推送更改](#signing-pushing) - [结论](#conclusion) ## 引言¶ 在同时存放电子邮件、密码和浏览器 cookie 等敏感信息的电脑上编写软件,会让我感到不安。就在过去几天里,我读到有人因克隆了错误的 GitHub 仓库而生活被颠覆,还有针对开发者的虚假面试骗局,试图诱骗开发者运行 `npm install` 来安装后门。某些工具链(例如 Go)至少能确保编译程序不会执行不可信代码,但测试仍然需要运行。我尽量少用第三方依赖(Go 的标准库在这方面帮了大忙),并在新版本发布后推迟升级库,但语言服务器、格式化工具和 linter 等开发工具仍然增加了供应链攻击的风险。解决这个问题的一种方法是使用完全独立的电脑进行开发,但随身携带第二台笔记本电脑的想法并不吸引人——我更愿意只操心一块硬件。 ## 背景¶ 长期以来,我主要通过在 Chromebook 上进行开发来应对这些担忧。ChromeOS 为其“主”操作系统(即 Chrome 浏览器和相关用户空间服务)使用了验证启动与只读操作系统分区相结合的方式,同时还提供了 Crostini——一个在主操作系统旁运行独立 Linux 安装的虚拟机系统。至少在理论上,虚拟机中运行的代码只能通过少数受限方式(如共享目录、通过 sommelier 转发的 Wayland 窗口以及音频输出)与主操作系统交互。我的开发流程通常涉及文本模式程序和 Web 服务器,因此通过终端窗口访问虚拟机非常合适。在 Crostini 虚拟机中,我进一步尝试通过使用 `bubblewrap` 将 Node.js 程序和其他第三方开发工具运行在沙箱环境中来限制其行为。 2025 年,谷歌宣布了一项模糊的计划,要将“ChromeOS 体验”迁移到 Android 上: > 因此,我们认为机会在于如何加速我们在 Android 上所做的所有 AI 进步,并将其尽快带到笔记本电脑形态,同时让笔记本电脑与 Android 生态系统无缝协作。所以,我们正在做的是,基本上把 ChromeOS 体验拿过来,并将其底层技术重新基于 Android。 2026 年初,我使用 ChromeOS 虚拟机时受到了几个 bug 的影响,这些 bug 花了几个月才修复:重启后虚拟机无法启动,以及终端中的字形偶尔被渲染为实心块。Reddit 上的用户为这两个问题都找到了变通方法,但这些 bug 最初发布并长期未修复的事实,对谷歌保持该项目的可靠性来说并不是好兆头。2026 年 5 月,谷歌宣布一种名为 Googlebook、运行另一种名为 Aluminium OS 的系统将取代 Chromebook。对生成式 AI 的承诺/威胁最终促使我开始寻找其他选择。(我的最新 Chromebook 搭载的是 2019 年的 CPU,这也是一个因素。) 我知道新的开发环境很可能是一台运行 Debian 的笔记本电脑,但我不确定如何解决我的安全问题。我曾考虑使用 `bubblewrap` 对所有开发进行沙箱化,但考虑到今年 Linux 内核 CVE 的数量,我对依赖命名空间来保证安全感到不安。crosvm——Crostini 的 hypervisor——以独立形式提供,并有相当多的文档,所以我决定尝试用它来复制之前在 ChromeOS 上做的事情。我知道使用名称中包含“cros”且由谷歌维护的软件,当对 ChromeOS 未来的担忧促使我走上这条路时,可能不是最安全的逃脱路线,但 `crosvm` 截至 2026 年中仍在积极开发中,而且从文档来看,它似乎在 ChromeOS 之外也获得了一定的关注: > 它现在已用于多个产品和平台,例如 Android 上的 TerminalApp、Cuttlefish 和 Windows。 如果 `crosvm` 在某个时候停止接收更新,我想我会尝试用其他 hypervisor 复制我的设置。(可能是 Firecracker?我很希望这里有一个非大型科技公司的选项,同样有强大的安全声明,并且在需要时支持图形和声音。) ## 编译 `crosvm`¶ [在 Linux 上构建 crosvm](https://crosvm.dev/book/building_crosvm/linux.html) 页面描述了如何检出并编译 `crosvm` 源码。我首先尝试按照“使用开发容器”的说明通过 Docker 构建,但过程失败并出现了无帮助的错误。我没有进一步调试,而是切换到 Podman: ``` $ sudo apt install --no-install-recommends podman passt uidmap libguestfs-tools $ ./tools/dev_container ./tools/build_release ``` 构建过程下载了数 GB 的数据,全部存放在 `~/.local/share/containers` 下。最终,编译好的 `crosvm` 可执行文件位于 `~/.local/share/containers/storage/overlay/5dec291d9e203d8cfa5fb69a301fadd5be2020fb9173e09420444622701788df/diff/scratch/cargo_target/crosvm/x86_64-unknown-linux-gnu/x86_64-unknown-linux-gnu/release/crosvm`。(可能那个 32 字节的十六进制 ID 不是固定的。)该可执行文件动态链接了 `libvirglrenderer.so.1`,看起来它由 Debian 软件包 `libvirglrenderer1` 提供。它还链接了 `libgfxstream_backend.so.0`,这导致了更多问题。我在任何地方都找不到这个库的预编译版本(我甚至尝试在谷歌提供的各种 Android 开发相关归档中翻找,因为那似乎是代码的来源)。源代码看起来在 https://github.com/google/gfxstream,但与其在不确定是否能成功运行 `crosvm` 的情况下花时间构建依赖,我决定暂时在没有图形支持的情况下重新构建。(后来我注意到 `crosvm` 构建过程似乎会自动将库编译到 `~/.local/share/containers/storage/overlay/26a34f735cfaeb84ad3969e2886048652ef2b21f818fe73a3d7b8660f18eaa9c/diff/usr/lib/x86_64-linux-gnu/libgfxstream_backend.so.0.1.2`,我本来可以直接使用那个副本。) `crosvm` 在其顶层的 `Cargo.toml` 文件中定义了一组默认特性: ``` default = ["audio", "balloon", "document-features", "gpu", "qcow", "usb", "libvda-stub", "net", "slirp"] ``` 目前似乎无法在 Cargo 中禁用单个默认特性,因此我进行了另一种构建,仅明确请求我认为可能需要的特性: ``` $ ./tools/dev_container cargo build --release --no-default-features --features audio,balloon,net,qcow,usb ``` 生成的 `crosvm` 二进制文件动态链接的库比之前少得多。这次它位于 `~/.local/share/containers/storage/overlay/5dec291d9e203d8cfa5fb69a301fadd5be2020fb9173e09420444622701788df/diff/scratch/cargo_target/release/crosvm`,我将其复制到了 `$PATH` 中的某个目录。 ## 运行 `crosvm`¶ ### 操作系统镜像 `crosvm` 文档包括[示例用法](https://crosvm.dev/book/running_crosvm/example_usage.html)和[高级用法](https://crosvm.dev/book/running_crosvm/advanced_usage.html)页面。根据这些说明,我从 Debian 官方云镜像网站下载了最新的 `debian-13-nocloud-amd64-XXXXXXXX-XXXX.tar.xz` 文件,并将其解压到 `~/temp/crosvm` 目录中,我计划在那里存储虚拟机的数据。我使用了 `guestfs-tools` 软件包中的 `virt-builder` 来提取内核和 initrd: ``` $ virt-builder --get-kernel ./disk.raw -o . ``` ### 宿主机网络配置¶ 为了让虚拟机能够访问网络,`crosvm` 可以与其共享一个 TAP 接口。我从 `crosvm` 的[网络](https://crosvm.dev/book/devices/net.html)文档中运行了这些命令,以创建用于 192.168.10.1/24 的 TAP 接口并路由进出流量: ``` $ sudo ip tuntap add mode tap user $USER vnet_hdr crosvm_tap $ sudo ip addr add 192.168.10.1/24 dev crosvm_tap $ sudo ip link set crosvm_tap up $ sudo sysctl net.ipv4.ip_forward=1 $ HOST_DEV=$(ip route get 8.8.8.8 | awk -- '{printf $5}') $ sudo iptables -t nat -A POSTROUTING -o "$HOST_DEV" -j MASQUERADE $ sudo iptables -A FORWARD -i "$HOST_DEV" -o crosvm_tap -m state --state RELATED,ESTABLISHED -j ACCEPT $ sudo iptables -A FORWARD -i crosvm_tap -o "$HOST_DEV" -j ACCEPT ``` 由于我(不情愿地)在电脑上使用了 NetworkManager,我需要某种方法使接口永久化。运行以下命令似乎与之前的 `ip` 命令产生相同的结果: ``` $ sudo nmcli con add type tun ifname crosvm_tap con-name crosvm_tap \ mode tap owner $(id -u $USER) \ ipv4.method manual ipv4.addresses 192.168.10.1/24 ``` `net.ipv4.ip_forward=1` 的 `sysctl` 设置可以通过创建 `/etc/sysctl.d/ip_forward.conf` 文件来永久生效: ``` # Enable forwarding for crosvm TAP networking. net.ipv4.ip_forward = 1 ``` 为了使 `iptables` 规则持久化,我还安装了 `iptables-persistent` 软件包。 ### 启动虚拟机¶ 此时,我已经准备好尝试启动虚拟机。在我的 `~/temp/crosvm` 目录中,我运行了以下命令: ``` $ crosvm run \ --socket ./crosvm.sock \ --cpus num-cores=4 \ --mem size=2048 \ --net tap-name=crosvm_tap \ --block ./disk.raw \ --initrd ./initrd.img-* \ --params "root=/dev/vda1" \ ./vmlinuz-* ``` 我看到了熟悉的 Linux 启动序列,随后是设置虚拟机时区和 root 密码的提示。使用上述命令,初始的 `crosvm` 进程会创建一个 `crosvm.sock` 套接字,后续的 `crosvm` 调用可以使用该套接字与正在运行的 hypervisor 交互。 ### 虚拟机网络配置¶ 以 root 身份登录(使用刚刚设置的密码)后,我运行了以下命令,将虚拟机中的网络接口配置为 192.168.10.2,网关使用 192.168.10.1: ``` # ip addr add 192.168.10.2/24 dev enp0s4 # ip route add default via 192.168.10.1 ``` 我还更新了虚拟机中的 `/etc/systemd/resolved.conf` 文件,添加以下内容: ``` [Resolve] DNS=8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.goo gle ``` 并运行了 `systemctl restart systemd-resolved`。此时,我已能从虚拟机内访问外部世界,也能从宿主机 ping 通虚拟机的 192.168.10.2。为了使网络更改永久生效,我在虚拟机中创建了一个 `/etc/systemd/network/10-lan.network` 文件,内容如下: ``` [Match] Name=e* Type=ether [Network] Address=192.168.10.2/24 Gateway=192.168.10.1 DNS=8.8.8.8 ``` 然后运行了 `systemctl enable systemd-networkd --now`。 ### 调整虚拟机磁盘大小¶ 虚拟机启动时使用的是 Debian 镜像提供的 3 GiB 磁盘。为了扩大根分区,我按照 `crosvm` 的[块设备](https://crosvm.dev/book/devices/block.html)文档中的说明操作。在虚拟机已经运行的情况下,我在宿主机上的 `~/temp/crosvm` 中运行了以下命令,将块设备大小调整为 20 GiB: ``` $ crosvm disk resize 0 21474836480 ./crosvm.sock ``` 然后,在虚拟机内部,我按照[这个随机页面](https://www.golinuxcloud.com/extend-resize-primary-partition-non-lvm-linux/#method-1-change-size-of-partition-using-parted-cli-utility)的方法,使用 `parted` 删除 `vda1` 根分区并重新创建(从同一扇区开始,使用剩余磁盘空间;幸运的是,我没有像该文档中描述的 swap 分区挡在中间),然后使用 `e2fsprogs` 中的 `resize2fs` 来扩展 `vda1` 以使用新空间。 **务必更新虚拟机中的 `/etc/fstab`,使其引用新分区的 `PARTUUID`(参见 `/dev/disk/by-partuuid`),而不是它的 `UUID`(来自 `/dev/disk/by-uuid`)。** 如果不这样做或弄错了,最终会得到一个只读的根分区。我通过运行 `mount -n -o remount,rw -t ext4 /dev/vda1 /` 来恢复,从而可以写入更新后的 `/etc/fstab`。 ### 管理虚拟机¶ 我想要一种直接的方式来启动和停止虚拟机,所以我编写了一个名为 `manage_crosvm` 的小脚本: ``` #!/bin/bash -e readonly NUM_CPU=6 readonly MEM=8192 readonly STOP_SEC=60 usage() { echo "Usage: $0 [start|stop] <dir>" exit 2 } [[ $# -ne 2 || $1 = -* ]] && usage readonly cmd=$1 readonly dir=$2 readonly socket=$dir/crosvm.sock name=$(basename "$dir") readonly name cd "$dir" case $cmd in start) if [[ -e $socket ]]; then pids=$(lsof -t "$socket") if [[ -n $pids ]]; then echo "Socket $socket already used by process(es) $pids" exit 1 fi echo "Deleting stal

相似文章

@ghumare64: https://x.com/ghumare64/status/2055329887431393309

X AI KOLs Timeline

深入探讨为何本地编码代理(如Claude Code和Codex)正逐渐转向libkrun而非Firecracker进行沙盒隔离,原因是Firecracker无法在macOS上原生运行。本文还介绍了iii-sandbox,一个基于libkrun构建的开源硬件隔离执行层。