Skip to content

fix: identify earth binaries via Go build info, not magic strings - #920

Draft
kmannislands wants to merge 1 commit into
mainfrom
fix/is-earth-binary
Draft

kmannislands wants to merge 1 commit into
mainfrom
fix/is-earth-binary

Conversation

@kmannislands

Copy link
Copy Markdown

The problem

IsEarthlyBinary is a safety guard in front of a destructive operation. symlinkEarthlyToEarth (cmd/earth/subcmd/bootstrap_cmds.go) runs when the binary is invoked as earthly, looks for a sibling file named earth, and if the check passes it does os.Remove(earthPath) before symlinking. A false positive deletes a user's file.

The check grepped the target file's raw bytes for docs.earthly.dev, api.earthly.dev and Earthfile. That is unsound in the one direction that matters:

  • Those first two strings appear nowhere else in the Go source. A current binary contains them only because this very function's own string literals get compiled into it — it passed by accident, and any refactor touching those literals would have silently broken bootstrap.
  • Byte-presence is weak evidence. Any file that happens to contain those three substrings would be accepted and then deleted.

The fix

Use debug/buildinfo.ReadFile, which reads a Go binary's module metadata without executing it, and match info.Main.Path against the EarthBuild module path or the legacy upstream github.com/earthly/earthly (pre-fork binaries are exactly the ones bootstrap wants to replace).

Fails closed: any error — missing file, directory, truncated or non-Go file, permission denied — reports false. A false negative just skips the symlink.

Renamed to IsEarthBinary per the AGENTS.md naming convention; there was exactly one call site. Dropped the now-unused bytes and buildcontext imports. No new module dependencies.

Behavior change worth noting

Binaries built outside module mode, or with build info stripped, now report false where the old grep might have returned true. That is the safe direction, but it means a very old pre-modules earthly binary will no longer be auto-replaced.

Tests

Table-driven, including the regression the old code got wrong — a non-Go file stuffed with the legacy magic strings must report false.

--- PASS: TestIsEarthBinary (0.00s)
    --- PASS: TestIsEarthBinary/empty_file (0.00s)
    --- PASS: TestIsEarthBinary/directory (0.00s)
    --- PASS: TestIsEarthBinary/non-Go_file_containing_the_legacy_magic_strings (0.00s)
    --- PASS: TestIsEarthBinary/nonexistent_path (0.00s)
    --- PASS: TestIsEarthBinary/real_binary_built_from_this_module (4.21s)
PASS

go build ./..., go test ./cmd/... ./buildkitd/..., gofmt -l and go vet all clean.

🤖 Generated with Claude Code

IsEarthlyBinary guarded a destructive path: symlinkEarthlyToEarth removes
the sibling "earth" file and replaces it with a symlink whenever this
returns true, so a false positive deletes a user's file.

The old check grepped the target's raw bytes for "docs.earthly.dev",
"api.earthly.dev" and buildcontext.Earthfile. That was unsound. The first
two strings appear nowhere else in the source tree - a real earth binary
contained them only because this function's own literals were compiled
into it, so it passed by accident. Worse, byte presence is weak evidence
in the direction that matters: any file that happens to contain those
substrings was treated as an earth binary and deleted.

Replace it with a real identity check using debug/buildinfo, which reads
the Go module metadata embedded in a binary without executing it. Accept
the binary only when its main module path is github.com/EarthBuild/earthbuild
or the legacy upstream github.com/earthly/earthly (genuine pre-fork earthly
binaries are exactly the ones bootstrap wants to replace). Every error
from ReadFile - not a Go binary, directory, truncated, unreadable - fails
closed to false.

Renamed to IsEarthBinary per the earth/EarthBuild naming convention and
updated the sole call site. The buildcontext import is no longer needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kmannislands
kmannislands requested a review from a team as a code owner September 11, 2026 18:09
@kmannislands
kmannislands requested review from janishorsts and removed request for a team September 11, 2026 18:09
@kmannislands
kmannislands marked this pull request as draft September 11, 2026 18:13
@kmannislands kmannislands self-assigned this Sep 11, 2026

import (
"bytes"
"debug/buildinfo"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

is it a problem to import this in a production build? no idea.

@kmannislands

Copy link
Copy Markdown
Author

Tested against real binaries — the old check was never functional

Ran the old heuristic and the new one side by side against every earth/earthly binary available, including a genuine pre-fork upstream binary installed from Homebrew (earthly 0.8.16, since uninstalled):

!! old=false new=true   homebrew earthly 0.8.16      github.com/earthly/earthly v0.8.16
!! old=false new=true   nix earthbuild 0.8.18        github.com/EarthBuild/earthbuild (devel)
   old=false new=false  shell wrappers (x5)          not a Go binary
   old=false new=false  /bin/ls, /usr/bin/git        not a Go executable

Both module paths resolve correctly, including the legacy github.com/earthly/earthly branch.

The old check returns false on every real binary

Not just on EarthBuild builds — on genuine upstream earthly too:

docs.earthly.dev   present
api.earthly.dev    ABSENT    <- old check bails here
Earthfile          present

api.earthly.dev is not present in any shipped earthly binary, on either side of the fork. This was not broken by the rename and is not a fork artifact: the string appears nowhere in the source except this function's own literal, so the only binaries that could ever satisfy it were ones that happened to embed it incidentally.

That means symlinkEarthlyToEarth has been silently no-op'ing on every real binary it was ever pointed at, for the entire history of the feature. It bails before os.Remove and reports nothing, which is why it went unnoticed.

So this PR is less "hardening" than "bug fix restoring a feature that never worked". The silver lining is that the deprecation symlink also never mis-fired destructively.

Reviewer note: this reactivates a destructive path

Once this merges, symlinkEarthlyToEarth starts actually running — removing a sibling earth file and symlinking over it — on installs where it has been inert until now. The guard is correct, so it only fires on real earth/earthly binaries, but it is a behavior change in the destructive direction and is worth a deliberate look rather than being waved through as a refactor.

Worth confirming the intended behavior for an install where earth and earthly are both real files from the same package, and for read-only install roots (a Nix store path, for instance, where the os.Remove will fail and surface an error where previously there was silence).

@github-actions

Copy link
Copy Markdown

➖ Are we earthbuild yet?

No change in "earthly" occurrences

📈 Overall Progress

Branch Total Count
main 2815
This PR 2815
Difference +0

Keep up the great work migrating from Earthly to Earthbuild! 🚀

💡 Tips for finding more occurrences

Run locally to see detailed breakdown:

./.github/scripts/count-earthly.sh

Note that the goal is not to reach 0.
There is anticipated to be at least some occurrences of earthly in the source code due to backwards compatibility with config files and language constructs.

@janishorsts

Copy link
Copy Markdown
Collaborator

What issue are we trying to resolve?

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.

2 participants