..

Cross-Tenant Session Kill in BoxCompute Sandboxes

Disclosure: This advisory was generated by using AI assistance for better clarity

Overview

BoxCompute is a cloud service that gives AI agents their own computers: full Linux VMs (“sandboxes”) that stay on, drivable through a chat agent or the API, with live previews and a browser, VMs hosted in Cyberjaya, Malaysia, with more Southeast Asia regions on the way.

Under the hood each tenant VM talks to the virtualization host over virtio (AF_VSOCK) sockets. During an evaluation period, I found that the sandbox-to-host vsock channel can be attacked from a neighboring tenant’s VM: a guest that shares the virtualization host with a victim can forge vsock packets that get delivered into the victim’s live host-side sessions, killing them.

The impact is availability - cross-tenant denial of service against in-flight work, including API-issued commands. It is not a data exposure issue: the sessions involved are TLS-wrapped end to end, which is exactly why the primitive is a kill, not a read or an inject.

Notes

The defect is in the receiving kernel’s vsock lookup (the host node’s, in the platform’s case - Linux 7.0 class, with older branches behaviorally equivalent), so the review was done in two stages:

  • a local QEMU lab with two guests and a forged-packet kernel module, where the delivery path could be instrumented end to end; and
  • a live proof on the platform itself, using only two sandboxes I owned - one as the “victim”, one as the “attacker” - with co-location confirmed before firing, single-shot tests, and teardown after each run.

No other tenant’s sandbox was touched at any point. The single-shot evidence below is from the live run; controls were fired on four neighboring ports to rule out coincidence.

Vulnerabilities

1. vsock bound-table fallback delivers foreign packets into established sessions

AF_VSOCK delivery on the receiving side is a two-table lookup. When a packet arrives from the transport, the kernel first tries the connected table, keyed by the full 4-tuple (source cid:port + destination cid:port). On a miss it falls back to the bound table, keyed by destination only:

// net/vmw_vsock/virtio_transport_common.c - virtio_transport_recv_pkt()

    /* The socket must be in connected or bound table
     * otherwise send reset back
     */
    sk = vsock_find_connected_socket_net(&src, &dst, net);
    if (!sk) {
        sk = vsock_find_bound_socket_net(&dst, net);   // <-- destination only!
        if (!sk) {
            (void)virtio_transport_reset_no_sock(t, skb, net);
            goto free_pkt;
        }
    }

The fallback exists for listening and connecting sockets - but sockets stay in the bound table after connect() succeeds, so it can also return a non-listening socket. And the bound-table matcher only ever looks at the local address:

// net/vmw_vsock/af_vsock.c - __vsock_find_bound_socket_net()

    list_for_each_entry(vsk, vsock_bound_sockets(addr), bound_table) {
        struct sock *sk = sk_vsock(vsk);

        if (vsock_addr_equals_addr(addr, &vsk->local_addr) && ...
            return sk;      // matches (dst_cid, dst_port) - src is never checked

Nothing in this path compares the packet’s source against the peer the socket actually connected to. The upstream commit that fixed this (dee44f41f206, “vsock/virtio: validate packet source for connected sockets”) describes the same hole from the reporter’s angle: a RESPONSE from an unrelated source can complete a victim socket’s handshake, and subsequent data from that source keeps flowing through the destination-only fallback.

Put together, the full kill path from a neighboring guest looks like this:

attacker guest (root, kernel module)
    forge virtqueue packet: OP_RW, src=(attacker_cid, any_port), dst=(2, P_victim)
        ↓ virtqueue
    vhost_vsock: src_cid == guest_cid ✓ (only check) → pass
        ↓
    host vsock core: connected-lookup miss (foreign src)
        ↓  [BUG] dst-only bound-table fallback
    match: victim tenant's ESTABLISHED platform socket on (2, P_victim)
        ↓
    13 attacker bytes enter the victim session's stream
        ↓
    host TLS record layer: garbage record → fatal unexpected_message alert
        ↓
    victim sandbox's platform control channel: torn down / wedged 60s+

Aiming the shot: co-location and port discovery, entirely from inside the VM

Before any packet could be fired, two things had to be established: that the two sandboxes shared a virtualization host, and the port of a live host↔victim session. Both fall out of one observation made purely from guest userspace - no platform API, no control-plane access, nothing beyond the rented VM itself.

The host dials into every sandbox on a known vsock port for routine polling (health, file stat, configure). A guest that simply listens there and calls accept() sees each inbound connection’s peer address - and the peer port is a host-side auto-bind port. Running that listener on both sandboxes at the same time makes the co-location test self-evident:

SB1 accepted dials from host ports:  ...728, 730, 738, 748
SB2 accepted dials from host ports:  ...743, 744, 751, 760

Both streams interleave on one sequential counter with the same ~2 s cadence - same node, proven. That counter is the node’s single port allocator: every host-initiated vsock connection for every tenant on the node draws its port from it. Observed advancing ~370–650 per 2 s, so live session ports sit inside a ~600-wide moving window a guest can track in real time just by watching its own dials. Probing a dead port costs an RST with no penalty and no alarm - at the time of the report the platform had no vsock-level telemetry on its nodes at all.

So the entire targeting step is: listen, accept, read the peer port, diff two streams for co-location, track the window. The single-shot kill below was aimed with exactly this - and the shared counter is also why the proof was single-shot by necessity: a sprayed port would land in some other tenant’s channel.

Here is what the forged send looks like from the attacking guest (kernel module, one sendmsg on a raw vsock socket):

forge_bc: single port=2716070736 ret=13

ret=13 is the byte count returned by the forged sendmsg - the 13-byte marker payload was accepted and delivered. On the victim side, the op that was mid-flight when the shot landed never completed: it wedged in the TLS layer for ~60 seconds and then died:

[+0000.12] TARGET port=2716070736 op=configure - holding 25s
[+0085.16] TARGET port=2716070736 REPLY-FAIL age=85.0
[+0085.17] DEATH   port=2716070736 age=85.0 SSLError

Four control sessions on neighbouring ports (…744, …754, …762, …770), held through the same window and not fired at, each completed their ops cleanly

  • the kill is specific to the port carrying the live session.

2. Blast radius: the platform’s persistent exec channel dies with it

The interesting part is what the platform rides on these sessions. Reverse engineering the guest-side daemons (a stripped Go supervisor binary) showed that every sandbox keeps one persistent, boot-established TLS connection to the host that carries the start_process / wait_process / read_process_output operations - i.e. the API’s “run this command” path. Short-lived dials on the same transport carry health, stat and log polling.

When a forged packet kills a session of that family, the tenant-visible symptom is their API commands failing wholesale (gateway 524s) until the platform re-establishes the channel. So the primitive is not “reset one connection” - it is “take the victim’s command-execution path down” from a VM the attacker rents on the same host.

Co-location is the only precondition: the attacker needs to be on the same virtualization host as the victim, which the platform’s bin-packing makes routine. The forged source tuple is fully controlled by the attacker; the victim does not need to initiate anything.

3. Not just this platform: any host with untrusted virtio-vsock guests

The root cause is not BoxCompute’s - it shipped in the kernel when virtio vsock itself did: commit 06a8fc78367d (“VSOCK: Introduce virtio_vsock_common.ko”, 2016). Every kernel from that point until dee44f41f206 delivers packets to connected sockets through a destination-only fallback with no source validation. That is a decade of releases, and every stable branch at the time of our report was affected.

The upstream fix itself proves the primitive is not guest-module-only. Its commit message describes an unprivileged, capability-empty repro under different UIDs on the same host: the attacker finds the target tuple via AF_VSOCK sock_diag, and a forged RESPONSE moves the victim socket from SYN_SENT to ESTABLISHED - after which further forged RW packets keep arriving through the same fallback. Same bug, different (and easier) entry.

So the exposure set is: any QEMU/KVM deployment using vhost-vsock with guests that are not fully trusted - sandbox platforms obviously, but also Kata Containers-style isolation, Firecracker with vsock devices, CI runners that execute untrusted code in VMs. If your host-initiated vsock sessions carry anything the guest should not be able to kill, check whether your kernel carries the fix before checking anything else.

Fix

The upstream fix adds a source validation helper, called after lock_sock() in the virtio receive path - a non-listening socket now only accepts packets whose source matches its stored peer:

// net/vmw_vsock/af_vsock.c - added by dee44f41f206

bool vsock_check_source(const struct vsock_sock *vsk,
            const struct vsock_transport *transport,
            const struct sockaddr_vm *src)
{
    if (vsk->transport != transport)
        return false;

    if (src->svm_port != vsk->remote_addr.svm_port)
        return false;

    if (src->svm_cid == vsk->remote_addr.svm_cid)
        return true;

    return transport->get_local_cid() == VMADDR_CID_LOCAL;
}

BoxCompute deployed the fix after our report - the source-tuple check is now enforced on their nodes, and the forged-packet delivery path is closed. Platform-side hardening worth keeping regardless of kernel version: randomizing the host auto-bind port allocator (the port space was previously guessable, which is what made live session ports cheap to find), and alerting on the RST/timeout differential that a forged-packet kill produces on the host side. Credit to the dev team for the fast turnaround.

Timeline

  • 2026-08-26: Daehyeon Ko lands the upstream fix dee44f41f206 (“vsock/virtio: validate packet source for connected sockets”), including an unprivileged same-host repro - independent of, and prior to, our work; full credit for the kernel-side discovery and fix
  • 2026-10-11: independent discovery during the review; local QEMU lab reproduces the delivery path (two guests, forged-packet module, kernel 7.0)
  • 2026-10-12: live single-shot proof on BoxCompute (own sandboxes only, co-location confirmed, four clean controls)
  • 2026-10-12: reported to the developer with full technical record and a replication script
  • 2026-10-14: fix confirmed deployed; public release authorized

Appendix: reproducing locally

The full campaign kit is not published with this post, but the core of the bug reproduces in a two-VM lab with no access to any cloud platform:

  • Guests: two QEMU instances with vhost-vsock-pci devices (guest-cid=10, guest-cid=11), an unpatched kernel (we used a 7.0 tinyconfig build; any pre-dee44f41f206 kernel works), and serial consoles for visibility:

    qemu-system-x86_64 -kernel bzImage -initrd initramfs-A.gz \
        -append "console=ttyS0 panic=-1" -m 512 -smp 1 \
        -device vhost-vsock-pci,guest-cid=10 \
        -nographic -no-reboot -monitor none
    # second instance: guest-cid=11, initramfs-B.gz
    
  • Host: a small listener standing in for the platform’s session endpoint - bind AF_VSOCK CID 2, accept, and watch what arrives.
  • Attacker guest: the forge driver - a kernel module that emits a virtqueue OP_RW packet with a chosen source tuple toward (2, <listener port>). On an unpatched host kernel the bytes are delivered into the established listener socket; on a fixed kernel they are not.
  • What to observe: on the victim connection, garbage bytes mid-stream (TLS listeners die with a fatal unexpected_message alert, as on the platform); on a patched host, the forged packet is dropped and nothing changes.