VM backend

Check D7 · execution family · automatic · one of the 11 Docker Desktop checks

The question this check asks: does this extension run a long-lived service beside the Docker engine, and what has it asked that service be allowed to do? Read from what the image declares — metadata.json and the compose file it names — with the image never pulled, the service never started and its code never read.

Why it matters

Docker's architecture page describes the shape: "Usually, the backend is made of one container that runs within the Docker Desktop VM." The vm section of metadata.json "defines a backend service that runs inside the Desktop VM. It must define either an image or a compose.yaml file". That container is long-running — "the long running components of an extension", in SensePost's phrase — and hidden: Docker's settings documentation says that "By default, containers created by extensions are hidden from the list of containers in the Docker Desktop Dashboard and the Docker CLI." SensePost put it from the other side: a service VM "does not show up when you run docker ps".

SensePost's post — Leon Jacobs, Orange Cyberdefense's ethical-hacking team, 30 May 2023, tested on Docker Desktop 4.19.0 on macOS — is the one piece of published research on this surface, and it is about the compose file: "Service 'VM's' being the long running components of an extension which run as a container can have more privileges than you would be comfortable to give by adding extra port/volume/privilege labels to the extensions docker-compose.yml file." The post's own compose file mounts /var/run/docker.sock:/host-docker.sock. An extension could also omit the ui section altogether and "have a service VM running without it being visible in Docker Desktop". The conclusion: "Be extra careful with Docker Desktop Extensions given that it is significantly easier for a malicious extension to run commands, access files and more when compared to a traditional container." None of this is an incident — no malicious Docker Desktop extension has been publicly reported as of 18 September 2026 — and most of it is the marketplace's ordinary shape, which is why the check is graded rather than absolute.

How RepoGates scores it

Both files come out of the registry without a pull, the way D6 gets metadata.json: a registry pull token, the OCI index, the linux/amd64 manifest, then the config blob and the twelve smallest layers in parallel, each at most 512 KB. The SDK's build leaves the compose file in its own layer of 270 to 500 bytes; RepoGates reads its name from vm.composefile and matches it among the layers it opened. No image is ever pulled. Three rows read the result, and every one is graded on whether the extension is listed and established — in Docker's Marketplace index (D5) with a Hub repository at least 90 days old (dde_established_days).

dde-vm-backend — a vm section exists. The row says which kind, "a compose file" or "an image". INFO, 0 points, when listed and established ("more than half the listed marketplace does; noted"); MEDIUM, 5 points, otherwise ("on an image that is unlisted or new"):

"D7; a backend beside the Docker socket — more than half the listed marketplace (note); MED on an unlisted or new image"

dde-vm-privileged:<what> — one row per privilege shape in the compose file, up to four, in a fixed order: docker-socket (/var/run/docker.sock or //./pipe/docker_engine), privileged (privileged: true), host-network (network_mode: host), host-pid (pid: host), cap-add, devices, and host-path — a bind of an absolute host path, not a named volume; the socket is its own row and a /dev/ mapping is devices. INFO, 0 points, when listed and established; HIGH, 14 points each, otherwise:

"D7; the backend mounts the Docker socket, joins the host network, runs privileged, adds capabilities or binds host paths — listed extensions do (akita mounts the socket, tailscale takes the host network); HIGH on an unlisted or new image"

dde-vm-compose-unread — the compose file was named but not found among the layers read. Always a note, so that an unknown is reported as unknown:

"D7; the compose file could not be read — what the backend mounts is unknown, stated"

The compose file is read with regular expressions, not a YAML parser, on purpose: the shapes are line shapes, the backend carries no YAML dependency, and a parser that raised on an unusual file would turn a signal into a skip. An image backend has no compose file and produces the first row alone. When metadata.json itself was not among the twelve smallest layers, D6 and D7 are reported as not checked, never passed. There is no signature check: none of the twelve images sampled carries a cosign signature, so there is nothing to verify.

The verdict engine is the one behind all 11 checks: any CRITICAL finding is a BLOCK, any HIGH or MEDIUM finding is a REVIEW, and a report with nothing above INFO is a PASS. The score is 100 minus every fired penalty, floored at zero — green at 80 and above, amber from 50 to 79, red below 50 — so a new image with a socket-mounting backend reads REVIEW at 81 on D7 alone, and 66 once D5's 15 for being unlisted lands. The preflight's answer before docker extension install runs is the profile's: personal-default maps MEDIUM and HIGH to warn, an ask; org-strict names dde-vm-privileged and maps it to warn too — an ask, not a deny, because a dozen listed extensions hold the socket, and the deny for an unlisted image comes from D5. D7 feeds the Open in an editor, Install and run and Hand to an AI agent dimensions, each taking the worst of its checks and reading PASS only when every one ran.

False positives we know about

Measured 18 September 2026 against all 50 listed extensions: 50 PASS, 0 REVIEW, 0 BLOCK; 21 declare host binaries, 29 a VM backend, 12 of those mount the Docker socket; 39 come from a publisher with no Hub badge; 33 have not been pushed in a year. The privilege rows across the 29: docker-socket 12, host-network 4, cap-add 3, devices 3, privileged 1, host-path 1. Tailscale's backend takes the host network, adds capabilities and maps devices — NET_ADMIN and /dev/net/tun in the twelve-image sample — three rows, all notes; akita's mounts the socket. Every one of the 50 is listed and at least 853 days old, so every row is a note. Weighted as findings regardless of state, D7 alone would have turned 29 of the 50 into REVIEW, and Tailscale's three privilege rows would have put it at 53, amber.

Two more, from how the file is read: the socket pattern matches anywhere in the text, so a commented-out mount or a second, never-started service produces the row; and rows stop at four in the fixed order above. On this frozen marketplace D7 is a note-taker; the same declaration on an image off the index — which, with Marketplace submissions paused since 16 June 2026, means sideloaded or private — or whose Hub repository is under 90 days old, is MEDIUM and HIGH, and a new extension with an honest socket mount pays that for its first 90 days. Stated, not hidden.

What this check does not cover

What the backend does. The image is not pulled, the service is not started, its code is not read, and there is no signature to check. What it acquires after starting. The compose file describes the first container; a service holding the socket — the row's own text is "full control of the engine" — can start a second with any privilege it likes, and D7 sees only the declaration. What is running on your machine. D7 never looks; Docker hides extension containers from docker ps by default, and the ui-less extension SensePost described is still read here, because its vm section is declared. More than four privilege rows, in the order stated above.

RepoGates assesses a Docker Desktop extension on request — through the API, the MCP server (platform="dde") and the preflight for docker extension install. It does not see an extension installed from Docker Desktop's own Extensions Marketplace tab, which is the app's own fetch; the board inside Docker Desktop is not built, and cannot be — its marketplace is a native window no browser extension sees. The image is never pulled and its code is not read: a PASS means the provenance and the declared install surface are clean, never that the backend or the host binaries were inspected.

Check it yourself

RepoGates reads both files from the registry's smallest layers. Without that code the honest route is to pull the image — which RepoGates never does — and copy the two files out of a container that is created but never started:

docker pull OWNER/NAME
id=$(docker create OWNER/NAME)
docker cp "$id":/metadata.json ./metadata.json
f=$(jq -r '.vm.composefile // empty' metadata.json)
[ -n "$f" ] && docker cp "$id":/"$f" ./compose.yaml
docker rm "$id" >/dev/null
jq '.vm' metadata.json
grep -nE 'docker\.sock|docker_engine|privileged: *true|network_mode: *"?host|pid: *"?host|cap_add:|devices:|^\s*- */' compose.yaml

The grep is the check, near enough: every line it prints is a row D7 would show. After an install, docker extension ls shows a HOST column — whether the extension runs a backend container or has deployed binaries to the host. Three worked references: tailscale/docker-extension — host network, capabilities and devices, three notes; docker/disk-usage-extension — no vm section, no rows; library/nginx, which is no extension — no metadata.json to read, D7 not checked, its REVIEW 64 provenance. RepoGates itself: GET /v1/dde/score/OWNER/NAME with a personal API token (Authorization: Bearer rgp1.…), check_repo("OWNER", "NAME", platform="dde") on the MCP server, or docker extension install OWNER/NAME typed in Claude Code with the plugin installed, where the preflight answers before the line runs.

Questions

Why is a backend that mounts the Docker socket only a note? Because it is what the marketplace looks like. In the 18 September 2026 measurement of all 50 listed extensions, 29 declare a backend and 12 of those mount the Docker socket; 4 take the host network, 3 add capabilities, 3 map devices, 1 runs privileged, 1 binds a host path. Weighted as findings regardless, D7 alone would have turned 29 of the 50 into REVIEW. So on a listed extension with a Hub repository 90 days or older the rows are notes, and on an unlisted or new image the same shapes are MEDIUM at 5 for the backend and HIGH at 14 per privilege.

Why is the compose file read with regular expressions rather than parsed? Because the seven shapes are line shapes — a docker.sock or docker_engine mention, privileged: true, network_mode: host, pid: host, a cap_add: or devices: key, a bound absolute host path — and a YAML parser that raised on an unusual file would turn a signal into a skip. The cost is stated: the socket pattern matches anywhere in the text, so a commented-out mount or a second, unused service counts too. On the 50 listed extensions no compose file that was named went unread.

Can RepoGates see the backend running on my machine? No. D7 reads what the image declares — the vm section of metadata.json and the compose file it names — never what is running. Docker's own settings documentation says containers created by extensions are hidden from the container list in the Dashboard and the CLI by default. The image is never pulled and the backend's code is not read; a PASS means the declaration was ordinary for a listed, established extension, not that the service was inspected.

Related checks

D6 Host binaries — the other half of the install surface, from the same metadata.json · D5 Marketplace listing — the switch that turns these notes into findings · C10 Devcontainer auto-execution — the GitHub roster's container that starts because you opened something, the same class · V7 Declares code execution — the VS Code listing's declared surface, the nearest analogue.

Add RepoGates to Chrome Pricing

Numbers on this page: the 18 September 2026 measurement of all 50 extensions in Docker's Marketplace index, the twelve-image registry sample read the same day, and library/nginx as a control; Docker's own Extensions SDK documentation (architecture, metadata and settings pages); SensePost, 30 May 2023; weights from the product's scoring table.