Privilege escalation to root in Lima QEMU guests via a world-writable agent socket (CVE-2026-53657)
An unprivileged user inside a Lima QEMU guest could reach the root-owned guest-agent socket and run commands as root in the VM. Fixed in Lima v2.1.3.
| Severity | High — CVSS 3.1: 8.2 (AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H) (per Lima) |
| Asset | lima-vm/lima — lima-guestagent Unix socket (QEMU driver) |
| Affected | Lima <= v2.1.2 |
| Fixed in | v2.1.3 (2026-06-19) |
| CVE | CVE-2026-53657 (GHSA-2j9v-p4xj-cjw2) |
TL;DR (for the non-technical reader)
Lima runs Linux virtual machines on your laptop — it is the engine behind a lot of local container and dev tooling on macOS and Linux. Inside each VM, Lima runs a small helper process called the guest agent. On the QEMU backend, that agent ran as root and left its control socket open to every user account in the VM. Any unprivileged user in the guest could talk to it and, through a feature meant for connection forwarding, borrow the agent’s root identity to run commands as root — inside that VM.
This is a privilege escalation within the guest, not a way out of the VM onto
your host. Lima fixed it in v2.1.3 by locking the socket down to a single
owner. It only ever affected the QEMU driver; the macOS default (the VZ driver)
was never exposed. If you run Lima with --vm-type=qemu, or on Linux, upgrade.
What’s vulnerable
The Lima guest agent (lima-guestagent) runs inside the VM and gives the
host a control channel for the things a VM-management tool needs: port
forwarding, time synchronisation, and a stream of guest events. The host talks to
it over a gRPC API.
On the QEMU driver, that API is exposed through a Unix domain socket inside the
guest, /run/lima-guestagent.sock. The agent runs as root — it needs root to
program iptables for port forwarding. The host reaches the socket over an SSH
local-forward, so in normal operation the socket only ever needs to be reachable
by Lima’s own machinery.
The macOS default driver, VZ (Apple Virtualization.framework), does not use this socket at all — it talks to the agent over a vsock port with host-CID filtering. That path was never affected.
The shape of the bug
Two design facts combined into a privilege-escalation primitive:
- The root-owned socket was created world-writable, so any local user in the guest could connect to it.
- The gRPC server behind it had no authentication — no TLS, no caller check. Anyone who could open the socket could call any RPC.
That alone is an exposure problem. What turns it into root execution is one of
the RPCs the agent offers: a Tunnel call that opens an outbound connection to a
caller-supplied address — and does so under the agent’s own (root) credentials,
with no allowlist of where it may connect. So an unprivileged caller can ask the
root agent to reach a destination that only root is supposed to reach, and then
ride that connection.
flowchart LR U["unprivileged guest user"]:::accent S["/run/lima-guestagent.sock
world-writable, root-owned"]:::accent G["guest-agent gRPC
no authentication"]:::n T["Tunnel RPC
dials any address as root"]:::n I["root-only local IPC
(reached on the agent's behalf)"]:::n R["command runs as root
in the guest"]:::alert U --> S --> G --> T --> I --> R classDef n fill:#1A1A1C,stroke:#2A2A2D,color:#EDEAE3 classDef accent fill:#0A0A0B,stroke:#FF4A1C,color:#EDEAE3 classDef alert fill:#0A0A0B,stroke:#E8342B,color:#EDEAE3
Vulnerable code path
The root cause is a single line. When the daemon creates the socket, it sets the mode to world-readable, world-writable:
// cmd/lima-guestagent/daemon_linux.go:141 (Lima v2.1.2)
if err := os.Chmod(socket, 0o777); err != nil {
return err
}
The daemon runs as root (it checks os.Geteuid() during startup), and the gRPC
server it stands up registers no authentication or authorization interceptors.
Mode 0o777 on a root-owned control socket means every account in the VM has a
direct, unauthenticated line to a root service. The
v2.1.2 source
shows the pre-fix daemon.
Why this pattern recurs
A privileged daemon that exposes a world-accessible IPC socket with no
authentication is a recurring footgun in container and VM tooling. The Docker
socket is the canonical example: “access to docker.sock is root” is by now
folklore, precisely because a permissive socket in front of a privileged service
collapses the boundary between the caller and the daemon.
The subtler half here is credential confusion. When a process connects to a
local IPC service, the service often identifies the peer by its kernel-reported
credentials (SO_PEERCRED). If a low-privileged client persuades a root daemon
to make the connection on its behalf, the downstream service sees the daemon’s
identity — root — not the original caller’s. A forwarding primitive that doesn’t
constrain its destinations turns “I can talk to a root process” into “I can act
as that root process toward anything it can reach.” That is the whole escalation,
in one sentence.
What we confirmed
We reported this through a private GitHub Security Advisory and verified the full
chain dynamically against Lima v2.1.2 (QEMU driver, Ubuntu 25.10 arm64): starting
from a freshly created, non-sudo guest account, we reached uid=0 command
execution inside the VM. We also confirmed that the same unauthenticated socket
left other guest-agent RPCs reachable by any local user — including the
time-synchronisation call (no bound on the requested clock adjustment) and the
event-stream interface (no per-caller limits) — all flowing from the same missing
access control on the socket.
The patch shipped today (v2.1.3). Because users are still upgrading, we are deliberately holding the step-by-step exploitation detail — the forwarding target, the proxy mechanics, and our proof-of-concept binaries stay private for now. If you maintain a fleet of Lima QEMU guests and need to validate exposure before or after upgrading, reach out and we will help privately.
Calibrated impact
What this is: a local privilege escalation. An unprivileged, non-sudo user who already has a foothold inside a Lima QEMU guest can become root inside that same guest. It needs that foothold first — a malicious or compromised process running as some ordinary user in the VM.
What this is not: it is not, on its own, a confirmed guest-to-host escape — we
did not demonstrate breaking out onto the macOS or Linux host, and we do not claim
it. It is not remotely exploitable (AV:L — the attacker is already local to the
guest). And it does not affect the VZ driver, which is the default on supported
macOS hosts.
Lima scored it High, CVSS 8.2 with Scope: Changed, reflecting that
crossing from an unprivileged account to root within the VM crosses a security
boundary that other components rely on. We quote the vendor’s rating as assigned.
Who should act
- Upgrade to Lima v2.1.3. This is the fix.
- If you cannot upgrade immediately: on supported macOS hosts, prefer the VZ driver (the default), which was never affected. On any driver, treat the inside of a QEMU guest as a single trust domain — do not run untrusted or multi-tenant workloads alongside trusted ones in the same guest, since the escalation only matters when an attacker already has an unprivileged account there.
The fix — what v2.1.3 changed
The fix landed in commit
8a45892
and shipped in v2.1.3. It tightens the socket from 0o777 to 0o600 and
hands ownership to the main user rather than leaving it as root:
// cmd/lima-guestagent/daemon_linux.go (post-fix, v2.1.3)
// The daemon runs as root (for iptables), but the host connects to the
// socket as the main user over an SSH local-forward. Hand the socket to
// that user so 0600 restricts access to the main user rather than root.
if socketOwner > 0 {
if err := os.Chown(socket, socketOwner, -1); err != nil {
return err
}
}
if err := os.Chmod(socket, 0o600); err != nil {
return err
}
A new --socket-owner daemon flag carries the UID; install_systemd_linux.go
wires it through, and the guest boot script
(25-guestagent-base.sh) passes --socket-owner "${LIMA_CIDATA_UID}" so the
socket is owned by the same user the host authenticates as.
Three things worth noting about the shape of the fix:
- Ownership, not just mode. Dropping to
0o600alone would have locked out the host, because the daemon runs as root but the host connects as the main user. Chowning the socket to that user is what lets0o600be both safe and functional — only the main user (and therefore only the host’s SSH-forward) can reach it. - It closes the exposure at the boundary. The patch fixes reachability — who can open the socket — rather than hardening each RPC behind it.
- The downstream RPCs were left as-is. Defense-in-depth we suggested — an
allowlist on the
Tunneldestination, authentication on the gRPC server, bounds on the time-sync adjustment, stream-rate limits — was not part of this release. Once the socket is no longer world-writable, those become defense-in-depth rather than the load-bearing control.
Before / after
Before — Lima <= v2.1.2:
flowchart LR A["any guest user"]:::accent B["socket mode 0o777
owner: root"]:::alert C["unauthenticated
root gRPC"]:::alert D["root execution
in guest"]:::alert A --> B --> C --> D classDef n fill:#1A1A1C,stroke:#2A2A2D,color:#EDEAE3 classDef accent fill:#0A0A0B,stroke:#FF4A1C,color:#EDEAE3 classDef alert fill:#0A0A0B,stroke:#E8342B,color:#EDEAE3
After — Lima v2.1.3:
flowchart LR H["host via SSH local-forward
(connects as main user)"]:::n O["other guest user"]:::accent S["socket mode 0o600
owner: main user"]:::n G["guest-agent gRPC"]:::n X["denied at the socket"]:::n H --> S --> G O -->|permission denied| X classDef n fill:#1A1A1C,stroke:#2A2A2D,color:#EDEAE3 classDef accent fill:#0A0A0B,stroke:#FF4A1C,color:#EDEAE3 classDef alert fill:#0A0A0B,stroke:#E8342B,color:#EDEAE3
Timeline
| Date (UTC) | Event |
|---|---|
| 2026-06-01 | Reported to Lima maintainers via GitHub Security Advisory |
| 2026-06-02 | Confirmed by maintainer |
| 2026-06-11 | CVE-2026-53657 assigned |
| 2026-06-16 | Fix committed (8a45892) |
| 2026-06-19 | Lima v2.1.3 released; advisory GHSA-2j9v-p4xj-cjw2 published |
| 2026-06-19 | This writeup published |
A root-owned, world-writable IPC socket is the whole bug — everything else is just what you can reach through it.
— Syntetisk research (@misop00p (aka @ansjdnakjdnajkd))