fix: resolve eight confirmed bugs from production bug scan#75
Merged
Conversation
Fixes BUG-5, BUG-6, BUG-7, BUG-8, BUG-9, BUG-10, BUG-13, BUG-14 from
internal-docs/bug_scan.md. BUG-4 was re-verified against current code and
found correct — no change needed.
Error classification (BUG-5, BUG-6): remove overly broad substrings from
isDaemonUnreachable ("no such file or directory", "file does not exist") and
isImageNotFoundMessage ("not found"). Both were firing on unrelated errors
such as missing credential helpers, routing errors, and certificate failures,
misdirecting users toward the wrong root cause. Daemon check now requires
daemon-specific phrasing; image-not-found check uses OCI spec terms
("name unknown", "name not known") instead of the bare "not found" substring.
Engine env var parity (BUG-7, BUG-8): DockerResolver now honours DOCKER_CONFIG
when resolving config.json and contexts/meta/. PodmanResolver now honours
PODMAN_CONNECTIONS_CONF and CONTAINERS_CONF before falling back to XDG defaults.
Both match the behaviour of the respective engine's own CLI.
Config discovery (BUG-9): config.Load() now walks up from CWD (max 50 levels)
looking for .layerx.yaml, then falls back to $XDG_CONFIG_HOME/layerx/config.yaml.
Adds --config PATH persistent flag to bypass the walk and load from an explicit
path. Fixes silent threshold substitution when layerx ci is invoked from a
monorepo subdirectory.
Archive auto-detection (BUG-10): isRegularFilePath switches from os.Stat to
os.Lstat. A symlink named like an image reference (e.g. "nginx:latest") is no
longer mistaken for an archive path on Linux/macOS.
Cache prune safety (BUG-13): normalizeDigest now requires exactly 64 lowercase
hex characters after the sha256: prefix. Arbitrary directory names in a shared
LAYERX_CACHE_DIR are no longer accepted as evictable cache entries.
File extraction symlink safety (BUG-14): production statFile changed from
os.Stat to os.Lstat so uniquePath treats a dangling symlink as present and
bumps to a .N suffix. atomicWriteFile now detects dangling symlinks via Lstat
when EvalSymlinks returns ErrNotExist and returns an error rather than silently
replacing the link with a regular file.
The initial fix commit (f935d85) tightened three classifiers but shipped with test-side regressions that surfaced on CI: isDaemonUnreachable — the substring guard removed 'no such file or directory' and 'file does not exist' entirely, breaking classifier tests that assert 'dial unix /var/run/docker.sock: connect: no such file or directory' and 'open //./pipe/docker_engine: file does not exist' are still recognised as daemon-unreachable (both are the real moby SDK dial-error format for a missing daemon socket). Restore both substrings but require a co-occurring socket or named-pipe token (.sock / /pipe/ / podman.sock) before firing. Unrelated filesystem errors (credential helpers, certificates) don't mention a socket path and remain excluded, so BUG-5's false-positive fix is preserved. isImageNotFoundMessage — the pushed commit dropped the bare 'not found' needle (BUG-6) but forgot to remove the pre-existing test case 'plain_not_found' that expected 'Error response from daemon: not found' to match. Delete that case and replace it with a regression guard that asserts a credential-helper error MUST NOT be classified as image-not-found. normalizeDigest — the pushed commit's 64-hex check tripped staticcheck QF1001 (De Morgan's law) on '!((c >= 0-9) || (c >= a-f))'. Rewrite the predicate using two named booleans (isDigit / isHex) so no reducible negation-of-disjunction remains for the linter to flag. The behaviour is identical. Test fixture migration — cache_test.go and analysis_test.go used short synthetic digests like 'sha256:abcdef' and 'sha256:driftguard' that fail the newly-strict normalizeDigest. Every short fixture is now 'sha256:' + strings.Repeat('<hex>', 64), preserving each test's intent while satisfying the canonical-form requirement. TestNormalizeDigest_RejectsUnsafe extended with length-not-64, uppercase-hex, and non-hex-ASCII cases so a future loosening of the validator fails a test rather than silently regressing BUG-13. CHANGELOG — 'isDaemonUnreachable' bullet reworded to reflect the guarded behaviour ('.sock/pipe context required') rather than the pushed commit's 'requires daemon-specific phrasing' phrasing that is no longer accurate.
ci: pin Go to 1.26.5 for GO-2026-5856 (CVE-2026-42505)
govulncheck flagged the stdlib TLS vulnerability GO-2026-5856 when setup-go with spec '1.26' resolved to go1.26.4 on the CI runner. Pinning to 1.26.5 lands the patched toolchain. go.mod minimum stays at 1.26.2 so users building from source with an older 1.26.x are not forced to upgrade — only CI is.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
isDaemonUnreachableandisImageNotFoundMessage: remove overly broad substrings ("no such file or directory","not found") that were misfiring on missing credential helpers, certificate errors, and routing failures, misdirecting users toward the wrong root causeDockerResolvernow honoursDOCKER_CONFIG;PodmanResolvernow honoursPODMAN_CONNECTIONS_CONFandCONTAINERS_CONF, matching the behaviour of the respective engine CLIsconfig.Load()walks up from CWD to find.layerx.yaml, with an XDG fallback; adds--config PATHpersistent flag for explicit paths (fixes silent threshold substitution in monorepos)os.Lstatso a symlink named like an image reference isn't mistaken for an archive on Linux/macOSnormalizeDigestrequires exactly 64 lowercase hex chars aftersha256:, preventinglayerx cache prunefrom deleting unrelated directories in a sharedLAYERX_CACHE_DIRxkey) no longer silently overwrites a dangling symlink;uniquePathusesos.Lstatto treat broken links as present, andatomicWriteFilereturns an error rather than replacing the linkBUG-4 (whiteout re-add) was re-verified against current code and found correct — no change needed.
Test plan
go build ./...andgo vet ./...pass (verified locally)go test ./...including engine resolver tests with injected env fakeslayerx --config /path/to/config.yaml ci IMAGEloads the explicit configlayerx ci IMAGEinvoked from a monorepo subdirectory picks up the root.layerx.yamlDOCKER_CONFIG=/tmp/alt layerx IMAGEreads context from the alternate directoryPODMAN_CONNECTIONS_CONF=/tmp/conns.json layerx --engine podman IMAGEroutes correctlylayerx cache prunewith a sharedLAYERX_CACHE_DIRdoes not delete non-hex-named sibling directories