Skip to content

Stricter download verification for agent shims in SSH - #6844

Open
rclarey wants to merge 2 commits into
mainfrom
ssh-agent-shim-pinning
Open

rclarey wants to merge 2 commits into
mainfrom
ssh-agent-shim-pinning

Conversation

@rclarey

@rclarey rclarey commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Pin exact version for uv and node
  • Pin SHA for Unity Gateway CLI
  • Pin and validate SHA256 checksums for uv and node rather than downloading the checksum each time

Why

Supply chain security

Tests

Added unit tests

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: adbd2c3

Run: 36123283896

Env ✅​pass 🙈​skip Time
✅​ aws linux 276 15 5:14
✅​ aws windows 278 13 3:53
✅​ azure linux 275 15 5:25
✅​ azure windows 277 13 3:59
✅​ gcp windows 278 13 3:30
Top 5 slowest tests (at least 2 minutes):
duration env testname
4:01 aws linux TestAccept
3:57 azure linux TestAccept
3:57 azure windows TestAccept
3:51 aws windows TestAccept
3:29 gcp windows TestAccept

@anton-107 anton-107 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for three issues (details inline):

  1. TestEnsureBinary fails on Windows CI, because GNU tar treats C: as a remote host.
  2. When uv is already on PATH, the installed ucode never gets onto PATH, so every launch fails.
  3. ensureToolchain swallows setup-lock errors.

The pins themselves check out: all four SHA-256 values match upstream (uv 0.12.18 .sha256 files, Node v24.21.0 SHASUMS256.txt), and 7f408803… is the commit behind the annotated v0.1.0 tag.

return buf.Bytes()
}

func TestEnsureBinary(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails on Windows CI (task test-exp-ssh (windows), run 36123224218). Every subtest that extracts an archive errors with tar: Cannot connect to C: resolve failed.

ensureBinary shells out to the system tar with an absolute temp path (agentshim.go:439). CI steps use shell: bash, so on Windows that's Git's GNU tar, which parses C:\... as host:path (a remote archive). The shim only ever runs on the Linux driver, so either skip this test on Windows or avoid handing tar a drive-letter path.

Comment on lines 364 to 371
if _, err := exec.LookPath("uv"); err != nil {
cmdio.LogString(ctx, "Installing uv...")
if err := runShell(ctx, "curl -LsSf https://astral.sh/uv/install.sh | sh"); err != nil {
uvPath, err := ensureBinary(ctx, home, uvArchiveSpec(runtime.GOARCH))
if err != nil {
return fmt.Errorf("failed to install uv: %w", err)
}
prependPath(ctx, uvPath, uvToolBinDirAbsolute)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If uv is already on PATH (shipped by the image or installed by the user), this branch is skipped, so uvToolBinDirAbsolute never gets on PATH. But ucode is always installed into it (line 376), so launchAgent fails with "the Unity Gateway CLI was not found on PATH after setup". The failure is permanent: on every later launch toolchainReady() is false, uv tool install finds ucode already installed, and the launch fails again.

Before this PR, ~/.local/bin (uv's default tool bin dir) was prepended on every launch, so this case worked.

Even when uv is missing, PATH is now only rebuilt through these install branches. So every relaunch takes the setup lock and prints "Installing uv..." / "Installing npm..." without downloading anything. I'd prepend deps/uv, uvToolBinDir and deps/node/bin unconditionally in bootstrapAndLaunchAgent, as before.

Repro (fails on this PR's head with exec: "ucode": executable file not found in $PATH):

func TestUvOnPathUcodeReachable(t *testing.T) {
	home, fakeBin := t.TempDir(), t.TempDir()
	writeScript := func(name, body string) {
		require.NoError(t, os.WriteFile(filepath.Join(fakeBin, name), []byte("#!/bin/sh\n"+body+"\n"), 0o755))
	}
	// fake uv: `uv tool install` drops ucode into $UV_TOOL_BIN_DIR, as real uv does
	writeScript("uv", `mkdir -p "$UV_TOOL_BIN_DIR" && printf '#!/bin/sh\n' > "$UV_TOOL_BIN_DIR/ucode" && chmod +x "$UV_TOOL_BIN_DIR/ucode"`)
	writeScript("npm", "exit 0")
	t.Setenv("PATH", fakeBin+":/usr/bin:/bin")

	require.NoError(t, ensureToolchain(cmdio.MockDiscard(t.Context()), home))
	_, err := exec.LookPath("ucode")
	assert.NoError(t, err)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good catch, I was testing the no uv case heavily since I always tested with uv installed before and missed this 🤦

unlock, err := acquireSetupLock(ctx, home)
if err != nil {
return err
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be return err. Swallowing the error means a cancelled context (Ctrl-C while waiting on a concurrent setup), a failed MkdirAll, or a permission error on the lock file skips installation silently. The user then gets the misleading "the Unity Gateway CLI was not found on PATH after setup" from launchAgent instead of the real cause. This change also looks unrelated to the pinning work.

Repro (on this PR's head, ensureToolchain returns nil):

func TestSetupLockErrorSurfaces(t *testing.T) {
	home := t.TempDir()
	t.Setenv("PATH", "/usr/bin:/bin")
	lock := filepath.Join(home, agentRootDir, setupLockName)
	require.NoError(t, os.MkdirAll(filepath.Dir(lock), 0o755))
	require.NoError(t, os.WriteFile(lock, nil, 0o644)) // held by a concurrent client

	ctx, cancel := context.WithTimeout(cmdio.MockDiscard(t.Context()), 1500*time.Millisecond)
	defer cancel()
	assert.Error(t, ensureToolchain(ctx, home))
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is just bad refactoring 🤦 Had this function returning ([]string, error) before and I must have removed the wrong return

@rugpanov

Copy link
Copy Markdown
Contributor

A couple of non-blocking suggestions on top of @anton-107's review:

Three separable changes here: the constant renames, the ensureNode→ensureBinary refactor, and the actual pinning/curl|sh removal. Consider separating.

The pinned values have no bump path: four checksums + two versions + two URLs frozen in source, no tooling (cf. bump-sdk/bump-tf), and Node no longer auto-tracks LTS. Worth a generator or at least a documented bump procedure so they don't rot.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants