Skip to content

cso: Docker socket admission fails on macOS 27 (Bun realpathSync EOPNOTSUPP on sockets) #3248

Closed

Description

@ivanlion

Summary

On macOS 27, gstack-cso doctor / provision-images report ISOLATION_FAILED: Pinned local Docker socket is unavailable even when docker info works (seen with Colima; Docker Desktop is affected the same way). All scanners then show unavailable.

Root cause

lib/cso/docker.ts canonicalizes the socket with fs.realpathSync(requestedSocket) (and the registry socket at the registrySocket mount). Under Bun 1.4.2/1.4.3 on macOS 27, realpathSync throws EOPNOTSUPP for any Unix socket, while lstat/stat succeed. Node is not affected. Upstream Bun report: oven-sh/bun#45752

Repro

import { dockerEndpoint } from './lib/cso/docker';
await dockerEndpoint(tmpHome, { HOME: tmpHome, DOCKER_HOST: 'unix:///path/to/colima/docker.sock' });
// -> CsoError ISOLATION_FAILED 'Pinned local Docker socket is unavailable'

Local fix that works

Resolve the leaf symlink chain by hand and canonicalize only the parent directory, keeping every existing check (isSocket, dev/inode pinning, symlink refusal for the registry socket):

function realSocketPath(path: string): string {
  let current = path;
  for (let hops = 0; hops < 40; hops++) {
    const parent = fs.realpathSync(dirname(current));
    if (!fs.lstatSync(current).isSymbolicLink()) return join(parent, basename(current));
    current = resolve(parent, fs.readlinkSync(current));
  }
  throw new CsoError('ISOLATION_FAILED', 'Docker socket symlink chain is too deep');
}

Used at both realpathSync call sites on sockets. With it, doctor reports local-docker-isolation: ready, and all 6 qualified scanner images provision. A regression test (socket reached through a symlinked dir + symlinked leaf, and a dangling symlink still refused) fails without the change and passes with it. The rest of the non-integration cso-* suite has the same failure set with and without the change.

gstack 1.91.67.0, Bun 1.4.2, macOS 27.0.1 arm64, Colima 0.10.3, Docker 29.

Activity

  1. ivanlion commented on Oct 17, 2026

    @ivanlion
    Author

    Related, found while getting the full audit to run once the socket fix was in: Semgrep never completes on a large repo, and the real cause is hidden behind a cleanup error.

    Symptom: scanner:semgrep ends not_assessed with ISOLATION_FAILED: Exact reproduction cleanup failed; verification evidence was withheld and the watchdog remains responsible. No containers are left behind.

    What actually happens: I added a temporary log of the error swallowed in DockerGroup.cleanup(). It is DEADLINE: Docker operation exceeded the reproduction deadline, thrown from cleanup()'s own docker ps/rm calls.

    1. scanners.ts runs Semgrep with --jobs=1, and scanner-executor.ts caps each scanner at min(300, remaining) seconds.
    2. On a repo of about 6,000 files (TS/TSX, 55 MiB), single-job Semgrep needs more than 300 s, so the scan hits the deadline. Each attempt failed at about 5 min 02 s, with both 4 and 8 VM CPUs.
    3. DockerGroup.docker() computes its timeout from the same this.deadline, so every cleanup command gets about 1 ms and times out.
    4. cleanup() catches that and rethrows the generic cleanup message, so the scan timeout is never reported.

    Reference point: the same pinned image and ruleset (/policy/catalog/semgrep/rules.yml), with the same flags except --jobs=8, run offline (--network none --read-only --cap-drop ALL), finishes in 3 min 25 s over 6,046 files.

    Suggestions:

    • Give cleanup its own short budget, separate from the scan deadline, so teardown can still run after a scan timeout.
    • Report the scan DEADLINE as the gap, not the derived cleanup failure.
    • Consider --jobs derived from the admitted CPU limit, or a larger per-scanner cap for Semgrep.

    Same environment as above: gstack 1.91.67.0, macOS 27.0.1 arm64, Colima 0.10.3, Docker 29.

  2. capy-ai commented on Oct 19, 2026

    @capy-ai
    Contributor

    The socket fix itself (realpathSocket() in lib/cso/docker.ts) shipped in 1.91.96.0 (#3164), after the 1.91.67.0 you were running. It resolves the leaf by hand on EOPNOTSUPP and keeps every socket check. Running gstack-upgrade should clear the ISOLATION_FAILED: Pinned local Docker socket is unavailable from doctor.

    #3303 adds an endpoint-level regression test: dockerEndpoint() is called with realpathSync refusing every socket, through a symlinked directory and a symlinked leaf like Colima's, and the endpoint is pinned to the socket's identity. Negative controls show that non-sockets, dangling or looping links, .. paths and TCP are still refused.

    Your Semgrep finding is a separate bug: on a large repo the 300 s per-scanner cap is reported as a derived cleanup failure, and cleanup shares the scan's deadline. It isn't in #3303. Thanks for tracing it to DockerGroup.cleanup(). I've passed it on to be tracked separately.

  3. capy-ai commented on Oct 19, 2026

    @capy-ai
    Contributor

    Fixed in #3303, released as v1.91.152.0 (404ddf0). Thanks for the detailed report and root-cause analysis. If anything still fails after you upgrade, reopen this with the gstack-cso output. — GStack Minder

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions