Merge with upstream 2025-06-09

7a930a814e e2e: virtio: net: add e2e cases for virtio-net
5274caf47b e2e_tests: uprev guest under test
1458989b4c rutabaga_gfx: fix kumquat build

https://chromium.googlesource.com/crosvm/crosvm/+log/7083e31d219cdcd57866c70144e1b39ddc008f0f..7a930a814ee5e36bbfe5ba6957251891a4659e7a

BUG=420964652

Change-Id: I57b19447e0f16a7749eb18bafbc82aaf3870634e
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/6631626
Bot-Commit: crosvm LUCI CI <crosvm-luci-ci-builder@crosvm-infra.iam.gserviceaccount.com>
Commit-Queue: Yuan Yao <yuanyaogoog@chromium.org>
diff --git a/e2e_tests/guest_under_test/Makefile b/e2e_tests/guest_under_test/Makefile
index 27de098..1edae9f 100644
--- a/e2e_tests/guest_under_test/Makefile
+++ b/e2e_tests/guest_under_test/Makefile
@@ -13,12 +13,14 @@
   KERNEL_ARCH=x86
   KERNEL_BINARY=bzImage
   DOCKER_ARCH=amd64
+  DOCKER_PLATFORM="linux/amd64"
   CROSS_COMPILE=
   RUSTFLAGS=
 else ifeq ($(ARCH), aarch64)
   KERNEL_ARCH=arm64
   KERNEL_BINARY=Image
   DOCKER_ARCH=arm64v8
+  DOCKER_PLATFORM="linux/arm64"
   CROSS_COMPILE=aarch64-linux-gnu-
   RUSTFLAGS="-Clinker=aarch64-linux-gnu-ld"
 else
@@ -69,8 +71,8 @@
 # Build rootfs from Dockerfile and export into squashfs
 $(TARGET)/rootfs: $(TARGET)/rootfs-build/delegate $(TARGET)/rootfs-build/readclock rootfs/Dockerfile
 	# Build image from Dockerfile
-	DOCKER_BUILDKIT=1 docker build -t crosvm_e2e_test_guest $(TARGET)/rootfs-build \
-		-f rootfs/Dockerfile --build-arg ARCH=$(DOCKER_ARCH)
+	docker buildx build -t crosvm_e2e_test_guest $(TARGET)/rootfs-build \
+		-f rootfs/Dockerfile --build-arg ARCH=$(DOCKER_ARCH) --platform $(DOCKER_PLATFORM)
 	# Make sure tar2sqfs is available. If not, print a help message.
 	tar2sqfs --help > /dev/null || \
 		{ \
diff --git a/e2e_tests/guest_under_test/PREBUILT_VERSION b/e2e_tests/guest_under_test/PREBUILT_VERSION
index 36ad70f..c553121 100644
--- a/e2e_tests/guest_under_test/PREBUILT_VERSION
+++ b/e2e_tests/guest_under_test/PREBUILT_VERSION
@@ -1,2 +1,2 @@
-r0015
+r0016
 
diff --git a/e2e_tests/guest_under_test/initramfs/Containerfile b/e2e_tests/guest_under_test/initramfs/Containerfile
index de318f6..39f6388 100644
--- a/e2e_tests/guest_under_test/initramfs/Containerfile
+++ b/e2e_tests/guest_under_test/initramfs/Containerfile
@@ -11,13 +11,13 @@
 
 # Download rust static coreutils
 WORKDIR /root
-RUN wget https://github.com/uutils/coreutils/releases/download/0.0.20/coreutils-0.0.20-x86_64-unknown-linux-musl.tar.gz -O coreutils.tar.gz
+RUN wget https://github.com/uutils/coreutils/releases/download/0.1.0/coreutils-0.1.0-x86_64-unknown-linux-musl.tar.gz -O coreutils.tar.gz
 RUN tar -zxvf coreutils.tar.gz
 
 # Download source code and build util-linux
 RUN git clone https://github.com/util-linux/util-linux.git
 WORKDIR /root/util-linux
-RUN git checkout v2.39.1
+RUN git checkout v2.41
 RUN apt-get build-dep -y util-linux
 RUN meson setup build -D static-programs="losetup, mount, umount" -D build-python=disabled -D cryptsetup=disabled -D build-chfn-chsh=disabled -D build-su=disabled -D build-runuser=disabled
 RUN ninja -C build
@@ -25,12 +25,12 @@
 FROM scratch
 
 # Create /bin directory manually to avoid potential problems caused by implicit directory creation with COPY
-COPY --from=builder /root/coreutils-0.0.20-x86_64-unknown-linux-musl/coreutils /coreutils
+COPY --from=builder /root/coreutils-0.1.0-x86_64-unknown-linux-musl/coreutils /coreutils
 RUN ["/coreutils", "mkdir", "/bin"]
 
 # Start populating /bin directory with bash and coreutils
 COPY --from=builder /bin/bash-static /bin/bash
-COPY --from=builder /root/coreutils-0.0.20-x86_64-unknown-linux-musl/coreutils /bin/coreutils
+COPY --from=builder /root/coreutils-0.1.0-x86_64-unknown-linux-musl/coreutils /bin/coreutils
 
 # Link /bin/bash to /bin/sh so podman can accept shell style RUN statements
 RUN ["/coreutils", "ln", "-s", "/bin/bash", "/bin/sh"]
diff --git a/e2e_tests/guest_under_test/rootfs/Dockerfile b/e2e_tests/guest_under_test/rootfs/Dockerfile
index 0cf26ce..98f616d5 100644
--- a/e2e_tests/guest_under_test/rootfs/Dockerfile
+++ b/e2e_tests/guest_under_test/rootfs/Dockerfile
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 ARG ARCH
-FROM ${ARCH}/debian:bookworm
+FROM docker.io/${ARCH}/debian:bookworm
 
 RUN --mount=type=cache,target=/var/cache/apt,sharing=private \
     --mount=type=cache,target=/var/lib/apt,sharing=private \
diff --git a/e2e_tests/tests/net.rs b/e2e_tests/tests/net.rs
new file mode 100644
index 0000000..3bc7ce7
--- /dev/null
+++ b/e2e_tests/tests/net.rs
@@ -0,0 +1,470 @@
+// Copyright 2025 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Testing virtio-net.
+#![cfg(any(target_os = "android", target_os = "linux"))]
+
+use std::fs::File;
+use std::io::ErrorKind;
+use std::path::Path;
+use std::process::Command;
+use std::process::Stdio;
+use std::time::Duration;
+
+use anyhow::anyhow;
+use fixture::utils::retry_with_delay;
+use fixture::vhost_user::CmdType;
+use fixture::vhost_user::Config as VuConfig;
+use fixture::vhost_user::VhostUserBackend;
+use fixture::vm::Config as VmConfig;
+use fixture::vm::TestVm;
+use tempfile::NamedTempFile;
+const VIRTIO_NET_F_MRG_RXBUF: usize = 15;
+const NCAT_RETRIES: usize = 15;
+const NCAT_RETRY_DELAY: Duration = Duration::from_millis(300);
+
+pub fn create_vu_net_config(socket: &Path, host_net_name: String, mrg_rxbuf: bool) -> VuConfig {
+    let socket_path = socket.to_str().unwrap();
+    let mut args = vec![
+        "net".to_string(),
+        "--tap-name".to_string(),
+        format!("{socket_path},{host_net_name}").to_string(),
+    ];
+    if mrg_rxbuf {
+        args.push("--mrg-rxbuf".to_string());
+    }
+    VuConfig::new(CmdType::Device, "net").extra_args(args)
+}
+
+fn create_guest_with_virtio_net_backend(
+    config: VmConfig,
+    host_ip_with_mask: String,
+    host_net_name: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<(Option<VhostUserBackend>, TestVm)> {
+    // Del crosvm_tap if exist
+    Command::new("sudo")
+        .args(["ip", "tuntap", "del", "mode", "tap", &host_net_name])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to del {}", host_net_name));
+    // Enable crosvm_tap in backend and up
+    Command::new("sudo")
+        .args([
+            "ip",
+            "tuntap",
+            "add",
+            "mode",
+            "tap",
+            "user",
+            "crosvm",
+            &host_net_name,
+        ])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to create {}", host_net_name));
+    Command::new("sudo")
+        .args([
+            "ip",
+            "addr",
+            "add",
+            &host_ip_with_mask,
+            "dev",
+            &host_net_name,
+        ])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to set {} address", host_net_name));
+    Command::new("sudo")
+        .args(["ip", "link", "set", &host_net_name, "up"])
+        .output()
+        .unwrap_or_else(|_| panic!("Fail to up {}", host_net_name));
+
+    let (vu, cfg) = if vhost_user_mode {
+        // Start a vhost-user-net backend firstly
+        let socket = NamedTempFile::new().unwrap();
+        let vu_config = create_vu_net_config(socket.path(), host_net_name, mrg_rxbuf);
+        let vu_device = VhostUserBackend::new_sudo(vu_config).unwrap();
+        (
+            Some(vu_device),
+            config
+                .extra_args(vec!["--mem".to_owned(), "512".to_owned()])
+                .with_vhost_user("net", socket.path()),
+        )
+    } else {
+        let mut extra_args = vec!["--mem".to_owned(), "512".to_owned(), "--net".to_owned()];
+        if mrg_rxbuf {
+            extra_args.push(format!("tap-name={},mrg-rxbuf", host_net_name))
+        } else {
+            extra_args.push(format!("tap-name={}", host_net_name))
+        }
+        (None, config.extra_args(extra_args))
+    };
+
+    let guest_vm = TestVm::new_sudo(cfg).expect("fail to create guest vm");
+    if vhost_user_mode {
+        Ok((vu, guest_vm))
+    } else {
+        Ok((None, guest_vm))
+    }
+}
+
+/// Configure guest virtio-net device, return virtio name
+fn network_configure_in_guest(
+    vm: &mut TestVm,
+    host_ip: String,
+    guest_ip: String,
+) -> anyhow::Result<String> {
+    // mount sysfs to check details
+    vm.exec_in_guest("mount -t sysfs sysfs /sys")
+        .expect("fail to mount sysfs in vm");
+
+    // Parse virtio device list and find out virtio-net id
+    let result = vm
+        .exec_in_guest("cat /sys/bus/virtio/devices/*/device")
+        .expect("Can't get virtio devices id");
+
+    let virtio_id_list: Vec<&str> = result.stdout.split("\n").collect();
+    let virtio_net_id = virtio_id_list.iter().position(|&x| x == "0x0001");
+    // The name of virtio-net driver is virtioX
+    let virtio_name = if let Some(id) = virtio_net_id {
+        format!("virtio{}", id)
+    } else {
+        return Err(anyhow!("fail to find virtio net driver"));
+    };
+
+    // Find the ethernet interface name in guest
+    let guest_dev = vm
+        .exec_in_guest(&format!("ls /sys/bus/virtio/devices/{}/net", virtio_name))
+        .expect("Can not find the name of virtio-net")
+        .stdout
+        .trim_end()
+        .to_string();
+
+    // set ip address in guest
+    vm.exec_in_guest(&format!("ip addr add {}/24 dev {}", guest_ip, guest_dev))
+        .expect("fail to configure net device address");
+    // up network device
+    vm.exec_in_guest(&format!("ip link set {} up", guest_dev))
+        .expect("fail to up net device");
+    // route information add
+    vm.exec_in_guest(&format!("ip route add default via {}", host_ip))
+        .expect("fail to configure net device address");
+
+    vm.exec_in_guest("ip route show")
+        .expect("fail to configure net device address");
+    Ok(virtio_name)
+}
+
+/// Check whether MRG_RXBUF feature bit is configured in guest driver
+/// vm: guest test VM
+/// virtio_name: the virtio driver name in guest (e.g. virtio0)
+fn check_driver_negotiated_features_with_mrg_rxbuf(
+    vm: &mut TestVm,
+    virtio_name: String,
+) -> anyhow::Result<bool> {
+    let binding = vm
+        .exec_in_guest(&format!(
+            "cat /sys/bus/virtio/devices/{}/features",
+            virtio_name
+        ))
+        .expect("Can not get the features of virtio-net");
+    // Find the ethernet interface name in guest
+    let features = binding.stdout.trim_end();
+
+    Ok(features.chars().nth(VIRTIO_NET_F_MRG_RXBUF).unwrap() == '1')
+}
+
+fn test_net_connection(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    let packets_num = "5";
+    let host_ping_guest_result = Command::new("ping")
+        .args([&guest_ip, "-c", packets_num])
+        .output()
+        .expect("fail to ping guest")
+        .stdout;
+
+    assert!(String::from_utf8(host_ping_guest_result)
+        .unwrap()
+        .contains(&format!(
+            "{} packets transmitted, {} received",
+            packets_num, packets_num
+        )));
+    let guest_ping_host_result = vm
+        .exec_in_guest(&format!("ping {} -c {}", host_ip.clone(), packets_num))
+        .expect("fail to ping host")
+        .stdout;
+    assert!(guest_ping_host_result.contains(&format!(
+        "{} packets transmitted, {} received",
+        packets_num, packets_num
+    )));
+    Command::new("sudo")
+        .args(["ip", "link", "set", &host_net_name.clone(), "down"])
+        .output()
+        .expect("fail to set device down");
+    Ok(())
+}
+
+#[test]
+fn virtio_net_ping_test() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.10.1".to_owned(),
+        "crosvm_tap0".to_owned(),
+        "192.168.10.2".to_owned(),
+        false,
+        false,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn virtio_net_ping_test_with_mrg_rxbuf() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.11.1".to_owned(),
+        "crosvm_tap1".to_owned(),
+        "192.168.11.2".to_owned(),
+        true,
+        false,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ping_test() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.12.1".to_owned(),
+        "crosvm_tap2".to_owned(),
+        "192.168.12.2".to_owned(),
+        false,
+        true,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ping_test_with_mrg_rxbuf() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_net_connection(
+        vm_config,
+        "192.168.13.1".to_owned(),
+        "crosvm_tap3".to_owned(),
+        "192.168.13.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}
+
+fn guest_to_host_ncat_test(vm: &mut TestVm, host_ip: String, port: String) -> anyhow::Result<()> {
+    let listen_port = port;
+    let listen_args = vec!["-l", &listen_port];
+    //Create a recv file in host, then ncat listen a port and re-direct to this file
+    let recv_file = File::create("/tmp/host_recv.txt")?;
+    Command::new("ncat")
+        .args(listen_args)
+        .stdout(Stdio::from(recv_file))
+        .spawn()
+        .expect("fail to spawn");
+
+    // create a random file in guest and get the md5sum value of this file
+    vm.exec_in_guest("mount -t tmpfs tmpfs /tmp")
+        .expect("fail to mount tmpfs in vm");
+
+    vm.exec_in_guest("dd if=/dev/random of=/tmp/guest_send.txt bs=1M count=10")
+        .expect("fail to generate a random file");
+
+    let md5_guest = vm
+        .exec_in_guest("md5sum /tmp/guest_send.txt | awk '{ print $1 }'")
+        .expect("fail to check md5sum")
+        .stdout;
+
+    // Transfer this file to host via virtio-net and calculate its md5sum value
+    vm.exec_in_guest(&format!(
+        "ncat {} {listen_port} < /tmp/guest_send.txt",
+        host_ip
+    ))
+    .expect("fail to send file");
+
+    let res = Command::new("md5sum")
+        .stdout(Stdio::piped())
+        .args(["/tmp/host_recv.txt"])
+        .output()?
+        .stdout;
+    let md5_host = String::from_utf8(res)?
+        .split_whitespace()
+        .next()
+        .unwrap()
+        .to_string();
+
+    assert_eq!(md5_guest.trim_end(), md5_host);
+    Ok(())
+}
+
+fn host_to_guest_ncat_test(vm: &mut TestVm, guest_ip: String, port: String) -> anyhow::Result<()> {
+    vm.exec_in_guest("mount -t tmpfs tmpfs /tmp")
+        .expect("fail to mount tmpfs in vm");
+
+    //Create a send file in host
+    Command::new("dd")
+        .args([
+            "if=/dev/random",
+            "of=/tmp/host_send.txt",
+            "bs=1M",
+            "count=10",
+        ])
+        .output()
+        .expect("fail to generate send file");
+    let res = Command::new("md5sum")
+        .stdout(Stdio::piped())
+        .args(["/tmp/host_send.txt"])
+        .output()?
+        .stdout;
+    let md5_host = String::from_utf8(res)?
+        .split_whitespace()
+        .next()
+        .unwrap()
+        .to_string();
+    let guest_listen_cmd = format!("ncat -l {} > /tmp/guest_recv.txt", port.clone());
+    let guest_cmd = vm.exec_in_guest_async(&guest_listen_cmd).unwrap();
+
+    retry_with_delay(
+        move || {
+            let send_file = File::open("/tmp/host_send.txt")?;
+            let out = Command::new("ncat")
+                .args([&guest_ip, &port])
+                .stdin(Stdio::from(send_file))
+                .output();
+            // if connection refused, it will still return Ok, then retry will exit.
+            if out.as_ref().is_ok_and(|x| {
+                String::from_utf8(x.stderr.clone()).unwrap() != "Ncat: Connection refused.\n"
+            }) {
+                out
+            } else {
+                Err(std::io::Error::new(
+                    ErrorKind::Other,
+                    "Ncat: Connection refused",
+                ))
+            }
+        },
+        NCAT_RETRIES,
+        NCAT_RETRY_DELAY,
+    )
+    .unwrap();
+
+    guest_cmd.wait_ok(vm).unwrap();
+    let md5_guest = vm
+        .exec_in_guest("md5sum /tmp/guest_recv.txt | awk '{ print $1 }'")
+        .expect("fail to check md5sum")
+        .stdout;
+    assert_eq!(md5_guest.trim_end(), md5_host);
+    Ok(())
+}
+
+fn test_ncat_guest_to_host(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    guest_to_host_ncat_test(&mut vm, host_ip.clone(), "1111".to_owned())?;
+    Ok(())
+}
+
+fn test_ncat_host_to_guest(
+    config: VmConfig,
+    host_ip: String,
+    host_net_name: String,
+    guest_ip: String,
+    mrg_rxbuf: bool,
+    vhost_user_mode: bool,
+) -> anyhow::Result<()> {
+    let host_ip_with_mask = format!("{}/24", host_ip);
+    let (_vu_device, mut vm) = create_guest_with_virtio_net_backend(
+        config,
+        host_ip_with_mask,
+        host_net_name.clone(),
+        mrg_rxbuf,
+        vhost_user_mode,
+    )
+    .expect("fail to create device and start vm");
+    let virtio_name = network_configure_in_guest(&mut vm, host_ip.clone(), guest_ip.clone())?;
+
+    assert_eq!(
+        mrg_rxbuf,
+        check_driver_negotiated_features_with_mrg_rxbuf(&mut vm, virtio_name)?
+    );
+    host_to_guest_ncat_test(&mut vm, guest_ip.clone(), "1234".to_owned())?;
+
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ncat_test_with_mrg_rxbuf_guest2host() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_ncat_guest_to_host(
+        vm_config,
+        "192.168.14.1".to_owned(),
+        "crosvm_tap4".to_owned(),
+        "192.168.14.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}
+
+#[test]
+fn vhost_user_net_ncat_test_with_mrg_rxbuf_host2guest() -> anyhow::Result<()> {
+    let vm_config = VmConfig::new();
+    test_ncat_host_to_guest(
+        vm_config,
+        "192.168.15.1".to_owned(),
+        "crosvm_tap5".to_owned(),
+        "192.168.15.2".to_owned(),
+        true,
+        true,
+    )?;
+    Ok(())
+}
diff --git a/profiles/benchmarks.profdata.xz b/profiles/benchmarks.profdata.xz
index b14de00..3f417b4 100644
--- a/profiles/benchmarks.profdata.xz
+++ b/profiles/benchmarks.profdata.xz
Binary files differ
diff --git a/rutabaga_gfx/kumquat/server/src/kumquat_gpu/mod.rs b/rutabaga_gfx/kumquat/server/src/kumquat_gpu/mod.rs
index 06798a3..2b47d66 100644
--- a/rutabaga_gfx/kumquat/server/src/kumquat_gpu/mod.rs
+++ b/rutabaga_gfx/kumquat/server/src/kumquat_gpu/mod.rs
@@ -5,7 +5,6 @@
 use std::collections::btree_map::Entry;
 use std::collections::BTreeMap as Map;
 use std::collections::BTreeSet as Set;
-use std::io::Cursor;
 use std::os::raw::c_void;
 use std::path::Path;
 use std::sync::Arc;
@@ -305,7 +304,7 @@
 
                     kumquat_gpu
                         .rutabaga
-                        .transfer_write(cmd.ctx_id, resource_id, transfer)?;
+                        .transfer_write(cmd.ctx_id, resource_id, transfer, None)?;
 
                     let mut event: RutabagaEvent = emulated_fence.try_into()?;
                     event.signal()?;