Skip to content

feat: arch mismatch detection#37

Merged
moshloop merged 10 commits intomainfrom
feat/arch-mismatch-detection
Mar 20, 2026
Merged

feat: arch mismatch detection#37
moshloop merged 10 commits intomainfrom
feat/arch-mismatch-detection

Conversation

@moshloop
Copy link
Member

@moshloop moshloop commented Mar 19, 2026

Summary by CodeRabbit

  • New Features

    • Post-install version verification now fails with a distinct exit code on mismatches
    • Added a registry entry for Smallstep CLI
  • Bug Fixes

    • Reject incompatible-architecture assets instead of falling back silently
    • Better diagnostics for missing or mismatched system libraries (macOS/Linux)
    • Installer forces reinstall when on-disk binary arch doesn't match requested arch
  • Improvements

    • More precise version parsing for jq and ClickHouse
    • Version-command errors now include stderr in messages
  • Tests

    • Added/updated tests covering dylib diagnostics, asset filtering and ClickHouse version parsing

@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a VersionMismatchError type and CLI exit code 2 path, enforces post-install version verification, tightens architecture-aware asset/install logic, introduces cross-platform dynamic-library diagnostics, adds pipeline logging helpers, updates defaults/registry loading, and includes tests and small refactors.

Changes

Cohort / File(s) Summary
CLI & Exports
cmd/deps/main.go, deps.go
CLI main checks for *installer.VersionMismatchError and exits with code 2; deps.go re-exports VersionMismatchError.
Installer Errors
pkg/installer/errors.go
Adds exported VersionMismatchError{Tool, Expected, Got} with Error() implementation.
Installer Behavior
pkg/installer/installer.go
Arch-aware reinstall decision (archMatches), unconditional archive post-processing, post-install version verification calling versionpkg.GetInstalledVersionWithMode, returns VersionMismatchError on unacceptable versions; minor refactors.
Pipeline dylib diagnostics
pkg/pipeline/fix_dylibs.go, pkg/pipeline/fix_dylibs_darwin.go, pkg/pipeline/fix_dylibs_linux.go, pkg/pipeline/fix_dylibs_other.go, pkg/pipeline/fix_dylibs_test.go
New dylib utilities: DetectBinaryArch, DiagnoseLibraryIssues, platform-specific detection (otool/ldd), search heuristics, and tests.
Asset filtering & tests
pkg/manager/asset_filter.go, pkg/manager/asset_filter_test.go
filterByArch now detects other-arch markers and returns an error instead of falling back to all assets; added hasOtherArchMarkers; tests updated to expect errors for wrong-arch assets.
Version package & tests
pkg/version/helpers.go, pkg/version/check.go, pkg/version/resolver.go, pkg/version/clickhouse_test.go
Renamed compareVersionsCompareVersions (exported); improved stderr handling and error returns in version detection; replaced builder patterns with fmt.Fprintf; added ClickHouse tests.
Registry & defaults
pkg/config/registry.go, pkg/config/defaults.yaml, pkg/config/config_ginkgo_test.go
GetGlobalRegistry() lazily re-initializes via initGlobalRegistry() when nil; updated defaults.yaml (jq regex, clickhouse expr/regex and regex, added registry.step for smallstep/cli, whitespace fix); added test verifying defaults contain powershell and step.
Pipeline logging & small tweaks
pkg/pipeline/context.go, pkg/pipeline/cel.go
Added PipelineContext.LogTrace and LogWarn; deleteFiles logging now includes provided context.
Formatting/IO refactors
pkg/manager/github/github_tags.go, pkg/manager/recommendations.go, pkg/version/resolver.go
Replaced WriteString(fmt.Sprintf(...)) with fmt.Fprintf(&builder, ...) to write formatted strings directly into builders.
Runtime & tests
pkg/runtime/detector.go, pkg/runtime/runtime_test.go
Runtime detector now errors when global registry is nil and checks registry membership; increased timeout in a runtime test.
CI
.github/workflows/test.yml
Added go clean -cache step before tests in unit and full test matrix.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Main
    participant Installer as Installer
    participant VerCmd as Version Command
    participant Comparator as CompareVersions
    participant Diagnoser as DiagnoseLibraryIssues

    CLI->>Installer: request install
    Installer->>Installer: extract/copy binary and post-processing
    Installer->>VerCmd: run package.VersionCommand
    alt version command succeeds
        VerCmd-->>Installer: version output
        Installer->>Comparator: Compare installed vs expected
        alt OK or Newer
            Comparator-->>Installer: OK
            Installer-->>CLI: success
        else mismatch
            Comparator-->>Installer: MISMATCH
            Installer->>Diagnoser: DiagnoseLibraryIssues(binary)
            Diagnoser-->>Installer: diagnostics
            Installer-->>CLI: return VersionMismatchError
            CLI->>CLI: detect VersionMismatchError -> os.Exit(2)
        end
    else version command fails
        VerCmd-->>Installer: error
        Installer-->>CLI: propagate error -> os.Exit(1)
    end
Loading

Possibly related PRs

  • refactor(github): migrate from GraphQL to REST API #32 — modifies installation finalization and version-resolution logic in pkg/installer/installer.go; strongly related to the finalize/version handling changes.
  • feat: add stern #30 — also touches pkg/installer/installer.go and installer platform/version logic; related due to overlapping installer behavior modifications.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'arch mismatch detection' accurately captures the main objective of the PR, which introduces architecture compatibility validation across multiple components including installer verification, asset filtering, and dylib diagnostics.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/arch-mismatch-detection
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/arch-mismatch-detection
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
pkg/manager/asset_filter_test.go (1)

308-323: Inconsistent indentation in test cases.

The new test cases have inconsistent indentation compared to the surrounding code. The struct fields should be indented with tabs to match the other test cases in this file.

🔧 Proposed fix for indentation
 		{
 			name: "should not return wrong-arch asset as universal (aarch64 when amd64 requested)",
-		assets: []string{
-			"postgrest-v14.6-macos-aarch64.tar.xz",
-		},
-		arch:    "amd64",
-		wantErr: true,
-	},
-	{
-		name: "should not return wrong-arch asset as universal (x86_64 when arm64 requested)",
-		assets: []string{
-			"tool-macos-x86_64.tar.gz",
-		},
-		arch:    "arm64",
-		wantErr: true,
-	},
-	{
-		name: "should prefer arm64 over arm when both exist",
+			assets: []string{
+				"postgrest-v14.6-macos-aarch64.tar.xz",
+			},
+			arch:    "amd64",
+			wantErr: true,
+		},
+		{
+			name: "should not return wrong-arch asset as universal (x86_64 when arm64 requested)",
+			assets: []string{
+				"tool-macos-x86_64.tar.gz",
+			},
+			arch:    "arm64",
+			wantErr: true,
+		},
+		{
+			name: "should prefer arm64 over arm when both exist",
 			assets: []string{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/manager/asset_filter_test.go` around lines 308 - 323, The two new test
cases named "should not return wrong-arch asset as universal (aarch64 when amd64
requested)" and "should not return wrong-arch asset as universal (x86_64 when
arm64 requested)" have inconsistent spacing for struct fields; update those test
case entries so the struct fields (name, assets, arch, wantErr) use the same tab
indentation style as the surrounding tests (align fields with tabs, not spaces)
in the test table (look for the table of test cases and the assets/arch/wantErr
fields in the same block).
pkg/version/clickhouse_test.go (1)

11-11: Extract duplicated version expression into a shared constant.

The same long expression is duplicated in both tests; centralizing it will reduce drift risk when the expression changes.

♻️ Suggested refactor
+const clickhouseVersionExpr = `(tag.contains("-stable") || tag.contains("-lts")) ? (tag.substring(1).split("-")[0].split(".").size() >= 4 ? tag.substring(1).split("-")[0].split(".")[0] + "." + tag.substring(1).split("-")[0].split(".")[1] + "." + tag.substring(1).split("-")[0].split(".")[2] + "+" + tag.substring(1).split("-")[0].split(".")[3] : tag.substring(1).split("-")[0]) : ""`
+
 func TestClickHouseVersionExpr(t *testing.T) {
-	expr := `(tag.contains("-stable") || tag.contains("-lts")) ? (tag.substring(1).split("-")[0].split(".").size() >= 4 ? tag.substring(1).split("-")[0].split(".")[0] + "." + tag.substring(1).split("-")[0].split(".")[1] + "." + tag.substring(1).split("-")[0].split(".")[2] + "+" + tag.substring(1).split("-")[0].split(".")[3] : tag.substring(1).split("-")[0]) : ""`
+	expr := clickhouseVersionExpr
 ...
 func TestClickHousePartialVersionConstraint(t *testing.T) {
-	expr := `(tag.contains("-stable") || tag.contains("-lts")) ? (tag.substring(1).split("-")[0].split(".").size() >= 4 ? tag.substring(1).split("-")[0].split(".")[0] + "." + tag.substring(1).split("-")[0].split(".")[1] + "." + tag.substring(1).split("-")[0].split(".")[2] + "+" + tag.substring(1).split("-")[0].split(".")[3] : tag.substring(1).split("-")[0]) : ""`
+	expr := clickhouseVersionExpr

Also applies to: 60-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/version/clickhouse_test.go` at line 11, The long ClickHouse tag parsing
expression assigned to expr in clickhouse_test.go is duplicated across tests;
extract it into a shared constant (e.g., const clickhouseTagExpr) and replace
both occurrences with that constant to avoid drift—declare the constant at
package scope in the same test file (or a new test_helpers_test.go in the
pkg/version package) and update the tests that reference expr to use
clickhouseTagExpr instead.
pkg/pipeline/fix_dylibs_darwin.go (1)

55-68: Universal binaries may report multiple architectures.

The file command on a universal (fat) binary returns both architectures (e.g., "Mach-O universal binary with 2 architectures: [x86_64:...] [arm64:...]"). The current loop returns the first match (arm64), which may not reflect the architecture actually used at runtime.

Consider parsing more carefully or using lipo -archs for accurate detection on universal binaries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/pipeline/fix_dylibs_darwin.go` around lines 55 - 68, The detectArch
function currently inspects `file` output and returns the first matching
architecture, which is incorrect for universal binaries; change detectArch to
accurately detect the runtime/desired arch by invoking `lipo -archs <path>` (or
parsing its output) instead of or in addition to `file`, and return a single,
explicit architecture string (e.g., "arm64" or "x86_64"); update any callers
that expect detectArch to possibly return multiple entries to handle a single
definitive arch.
pkg/installer/installer.go (1)

839-841: Consider using runtime.GOARCH normalization for consistency.

The cross-platform check compares ArchOverride directly against runtime.GOARCH. However, ArchOverride might use aliases like x86_64 while runtime.GOARCH returns amd64. Consider using archMatches for consistency.

♻️ Proposed fix
 	isCrossPlatform := (i.options.OSOverride != "" && i.options.OSOverride != runtime.GOOS) ||
-		(i.options.ArchOverride != "" && i.options.ArchOverride != runtime.GOARCH)
+		(i.options.ArchOverride != "" && !archMatches(i.options.ArchOverride, runtime.GOARCH))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/installer/installer.go` around lines 839 - 841, The isCrossPlatform check
currently compares i.options.ArchOverride directly to runtime.GOARCH which can
mismatch due to aliasing; update the logic to use the existing archMatches
helper (or equivalent) when comparing i.options.ArchOverride to runtime.GOARCH
so aliases like "x86_64" map to "amd64" correctly—i.e., replace the direct
ArchOverride == runtime.GOARCH comparison in the isCrossPlatform expression with
a call to archMatches(i.options.ArchOverride, runtime.GOARCH) (keeping the
OSOverride comparison as-is) so isCrossPlatform reliably detects cross-platform
requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/manager/asset_filter.go`:
- Around line 208-230: The hasOtherArchMarkers function uses an incomplete amd64
alias set and compares requestedArch without normalizing, causing false
negatives and positives; update hasOtherArchMarkers to reuse/sync with
getArchAliases (or the canonical alias map) adding missing aliases like "386",
"64bit", and "64-bit" for amd64, and normalize the incoming requestedArch to its
canonical key (e.g., map "x86_64"/"x64" -> "amd64") before the archKey ==
requestedArch check so the function skips only the requested architecture's
aliases when scanning asset.Name.

In `@pkg/pipeline/fix_dylibs_linux.go`:
- Around line 11-15: In detectBrokenDylibs, stop invoking ldd on possibly
untrusted binaries; replace the exec.Command("ldd", binaryPath) call with a
static ELF metadata inspection (e.g., run readelf -d or objdump -p on binaryPath
and parse lines with "NEEDED") to extract shared-library dependencies, then feed
those NEEDED entries into the existing DylibRef parsing/validation logic; ensure
errors from readelf/objdump are returned with context and that parsing handles
trimmed tokens and optional versioned sonames.

---

Nitpick comments:
In `@pkg/installer/installer.go`:
- Around line 839-841: The isCrossPlatform check currently compares
i.options.ArchOverride directly to runtime.GOARCH which can mismatch due to
aliasing; update the logic to use the existing archMatches helper (or
equivalent) when comparing i.options.ArchOverride to runtime.GOARCH so aliases
like "x86_64" map to "amd64" correctly—i.e., replace the direct ArchOverride ==
runtime.GOARCH comparison in the isCrossPlatform expression with a call to
archMatches(i.options.ArchOverride, runtime.GOARCH) (keeping the OSOverride
comparison as-is) so isCrossPlatform reliably detects cross-platform requests.

In `@pkg/manager/asset_filter_test.go`:
- Around line 308-323: The two new test cases named "should not return
wrong-arch asset as universal (aarch64 when amd64 requested)" and "should not
return wrong-arch asset as universal (x86_64 when arm64 requested)" have
inconsistent spacing for struct fields; update those test case entries so the
struct fields (name, assets, arch, wantErr) use the same tab indentation style
as the surrounding tests (align fields with tabs, not spaces) in the test table
(look for the table of test cases and the assets/arch/wantErr fields in the same
block).

In `@pkg/pipeline/fix_dylibs_darwin.go`:
- Around line 55-68: The detectArch function currently inspects `file` output
and returns the first matching architecture, which is incorrect for universal
binaries; change detectArch to accurately detect the runtime/desired arch by
invoking `lipo -archs <path>` (or parsing its output) instead of or in addition
to `file`, and return a single, explicit architecture string (e.g., "arm64" or
"x86_64"); update any callers that expect detectArch to possibly return multiple
entries to handle a single definitive arch.

In `@pkg/version/clickhouse_test.go`:
- Line 11: The long ClickHouse tag parsing expression assigned to expr in
clickhouse_test.go is duplicated across tests; extract it into a shared constant
(e.g., const clickhouseTagExpr) and replace both occurrences with that constant
to avoid drift—declare the constant at package scope in the same test file (or a
new test_helpers_test.go in the pkg/version package) and update the tests that
reference expr to use clickhouseTagExpr instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 550bc371-7af8-457c-9f2e-423cf3c3e6c2

📥 Commits

Reviewing files that changed from the base of the PR and between 43a96e6 and 4bcd11b.

📒 Files selected for processing (20)
  • cmd/deps/main.go
  • deps.go
  • pkg/config/defaults.yaml
  • pkg/installer/errors.go
  • pkg/installer/installer.go
  • pkg/manager/asset_filter.go
  • pkg/manager/asset_filter_test.go
  • pkg/manager/github/github_tags.go
  • pkg/manager/recommendations.go
  • pkg/pipeline/cel.go
  • pkg/pipeline/context.go
  • pkg/pipeline/fix_dylibs.go
  • pkg/pipeline/fix_dylibs_darwin.go
  • pkg/pipeline/fix_dylibs_linux.go
  • pkg/pipeline/fix_dylibs_other.go
  • pkg/pipeline/fix_dylibs_test.go
  • pkg/version/check.go
  • pkg/version/clickhouse_test.go
  • pkg/version/helpers.go
  • pkg/version/resolver.go

Comment on lines +208 to +230
// hasOtherArchMarkers returns true if any asset contains an arch alias
// belonging to a different architecture than the requested one.
func hasOtherArchMarkers(assets []AssetInfo, requestedArch string) bool {
allArchAliases := map[string][]string{
"amd64": {"amd64", "x86_64", "x64", "x86-64", "x86", "i386", "i686"},
"arm64": {"arm64", "aarch64"},
"arm": {"armv7", "armv7l"},
}
for _, asset := range assets {
nameLower := strings.ToLower(asset.Name)
for archKey, aliases := range allArchAliases {
if archKey == requestedArch {
continue
}
for _, alias := range aliases {
if strings.Contains(nameLower, alias) {
return true
}
}
}
}
return false
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check call sites of filterByArch to see if arch is already normalized
rg -n -B5 -A2 'filterByArch\(' --type=go

Repository: flanksource/deps

Length of output: 1459


🏁 Script executed:

# First, let's read the filterByArch function and see how it calls hasOtherArchMarkers
sed -n '168,210p' pkg/manager/asset_filter.go

Repository: flanksource/deps

Length of output: 1346


🏁 Script executed:

# Get the getArchAliases function to compare alias definitions
sed -n '144,165p' pkg/manager/asset_filter.go

Repository: flanksource/deps

Length of output: 560


🏁 Script executed:

# Check if there are test cases for hasOtherArchMarkers
rg -n 'hasOtherArchMarkers' --type=go pkg/manager/

Repository: flanksource/deps

Length of output: 342


🏁 Script executed:

# Read the actual hasOtherArchMarkers function at lines 208-230
sed -n '208,230p' pkg/manager/asset_filter.go

Repository: flanksource/deps

Length of output: 755


🏁 Script executed:

# Check test coverage for hasOtherArchMarkers with different arch forms
rg -n -A10 'hasOtherArchMarkers' pkg/manager/asset_filter_test.go

Repository: flanksource/deps

Length of output: 42


Alias list inconsistency and missing arch normalization in hasOtherArchMarkers.

The function has two issues:

  1. Incomplete amd64 alias list: Missing "386", "64bit", and "64-bit" compared to getArchAliases. This causes false negatives — assets with these markers won't be flagged as wrong architecture.

  2. Non-canonical arch handling: The requestedArch parameter is not normalized (e.g., "x86_64" vs "amd64"). If a non-canonical form is passed, archKey == requestedArch won't match the canonical key, causing all aliases (including the requested architecture's own) to be checked and flagged as "other" markers — producing false positives.

Synchronize the alias lists and normalize requestedArch to the canonical form before comparison.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/manager/asset_filter.go` around lines 208 - 230, The hasOtherArchMarkers
function uses an incomplete amd64 alias set and compares requestedArch without
normalizing, causing false negatives and positives; update hasOtherArchMarkers
to reuse/sync with getArchAliases (or the canonical alias map) adding missing
aliases like "386", "64bit", and "64-bit" for amd64, and normalize the incoming
requestedArch to its canonical key (e.g., map "x86_64"/"x64" -> "amd64") before
the archKey == requestedArch check so the function skips only the requested
architecture's aliases when scanning asset.Name.

Comment on lines +11 to +15
func detectBrokenDylibs(binaryPath, _ string) ([]DylibRef, error) {
out, err := exec.Command("ldd", binaryPath).Output()
if err != nil {
return nil, fmt.Errorf("ldd failed: %w", err)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Is running ldd on untrusted executables considered unsafe on Linux, and what safer alternatives do glibc/binutils documentation recommend (e.g., readelf/objdump)?

💡 Result:

Yes, running ldd on untrusted executables is considered unsafe on Linux. The official ldd(1) man page warns: "Be aware that in some circumstances (e.g., where the program specifies an ELF interpreter other than ld-linux.so), some versions of ldd may attempt to obtain the dependency information by directly executing the program... Thus, you should never employ ldd on an untrusted executable." This can lead to arbitrary code execution if the binary has a malicious ELF interpreter. Safer alternatives recommended in the ldd man page and various technical sources include static analysis tools from binutils: readelf -d or objdump -p, which parse the ELF dynamic section without executing anything. For example: - readelf -d binary | grep 'NEEDED' - objdump -p binary | grep NEEDED These display the shared library dependencies (NEEDED entries) directly from the file structure. Binutils documentation for readelf and objdump confirms they inspect ELF files without execution, independent of BFD for readelf. Glibc's SECURITY.md notes: "The ldd tool is not expected to be used with untrusted executables," reinforcing the advice without specifying alternatives, but aligning with man page recommendations.

Citations:


🏁 Script executed:

# First, let's read the entire fix_dylibs_linux.go file to understand the full context
cat -n pkg/pipeline/fix_dylibs_linux.go

Repository: flanksource/deps

Length of output: 1331


🏁 Script executed:

# Check if there's any binary validation or trust checks before ldd is called
rg -n "detectBrokenDylibs" --type go

Repository: flanksource/deps

Length of output: 467


🏁 Script executed:

# Look for any context about how binaries arrive in this pipeline
rg -n "fix_dylibs\|binaryPath" --type go -B 3 -A 3

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Let's examine fix_dylibs.go to understand the pipeline context
head -100 pkg/pipeline/fix_dylibs.go | cat -n

Repository: flanksource/deps

Length of output: 3651


🏁 Script executed:

# Get context around line 79 where detectBrokenDylibs is called
sed -n '60,100p' pkg/pipeline/fix_dylibs.go | cat -n

Repository: flanksource/deps

Length of output: 1665


🏁 Script executed:

# Look for how binaryPath is sourced (downloaded, local, etc.)
rg -n "binaryPath\s*=" pkg/pipeline/fix_dylibs.go -B 2 -A 2

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Find where DiagnoseLibraryIssues is called from
rg -n "DiagnoseLibraryIssues" --type go -B 2 -A 2

Repository: flanksource/deps

Length of output: 1197


🏁 Script executed:

# Check if there's any download/validation logic in the codebase
rg -n "download\|untrusted\|verify\|signature" --type go -i | head -20

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Look for the main entry points and how binaries arrive in this pipeline
fd "main\.go|cmd" --type f | xargs rg -l "DiagnoseLibraryIssues\|fix_dylibs" 2>/dev/null || echo "No results"

Repository: flanksource/deps

Length of output: 70


🏁 Script executed:

# Get more context from installer.go around the DiagnoseLibraryIssues call
sed -n '820,880p' pkg/installer/installer.go | cat -n

Repository: flanksource/deps

Length of output: 3284


🏁 Script executed:

# Check if there's any signature verification or trust checks before binaries are executed
rg -n "verify\|signature\|checksum\|hash" pkg/installer/installer.go -i -B 2 -A 2 | head -40

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Check for any TODOs, FIXMEs, or security-related comments about ldd
rg -n "ldd\|TODO\|FIXME\|security\|untrust" --type go -i

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Check if there's any binary signature verification before installation
rg -n "signature\|gpg\|pgp\|verify.*binary" --type go -i

Repository: flanksource/deps

Length of output: 42


🏁 Script executed:

# Look at the full installer flow to understand if binaries are verified before reaching this point
head -50 pkg/installer/installer.go | cat -n

Repository: flanksource/deps

Length of output: 2109


Replace ldd with static ELF inspection to avoid executing untrusted binaries.

The ldd man page explicitly warns: "you should never employ ldd on an untrusted executable," as it may execute code from the target binary's interpreter on certain systems. Since this tool processes downloaded binaries, use readelf -d or objdump -p instead to extract NEEDED entries without execution.

At Line 12, replace:

out, err := exec.Command("ldd", binaryPath).Output()

With static inspection via readelf -d binary | grep NEEDED or equivalent parsing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/pipeline/fix_dylibs_linux.go` around lines 11 - 15, In
detectBrokenDylibs, stop invoking ldd on possibly untrusted binaries; replace
the exec.Command("ldd", binaryPath) call with a static ELF metadata inspection
(e.g., run readelf -d or objdump -p on binaryPath and parse lines with "NEEDED")
to extract shared-library dependencies, then feed those NEEDED entries into the
existing DylibRef parsing/validation logic; ensure errors from readelf/objdump
are returned with context and that parsing handles trimmed tokens and optional
versioned sonames.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/config/registry.go (1)

64-68: ⚠️ Potential issue | 🟡 Minor

Stale comment contradicts new lazy re-initialization behavior.

The comment states the registry "will not reload until the program is restarted," but with the lazy re-initialization added to GetGlobalRegistry(), the registry will now be reloaded on the next call after ResetGlobalRegistry().

📝 Proposed fix
 // ResetGlobalRegistry resets the global registry (useful for testing)
-// Note: This will not reload the registry until the program is restarted
-// since init functions only run once per program execution
+// Note: The registry will be re-initialized on the next call to GetGlobalRegistry()
 func ResetGlobalRegistry() {
 	globalRegistry = nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 64 - 68, Update the stale comment on
ResetGlobalRegistry to reflect the new lazy re-initialization behavior: explain
that ResetGlobalRegistry sets globalRegistry to nil and that GetGlobalRegistry()
will recreate/reload the registry on the next call (i.e., lazy
re-initialization), while preserving the note about init functions only running
once per execution where relevant; reference ResetGlobalRegistry and
GetGlobalRegistry by name so the maintainer can locate and update the comment
accordingly.
🧹 Nitpick comments (1)
pkg/config/registry.go (1)

35-37: Potential data race in lazy re-initialization.

If multiple goroutines call GetGlobalRegistry() concurrently when globalRegistry is nil, both may read nil and call initGlobalRegistry(), causing a data race on the globalRegistry pointer. Since this primarily affects testing scenarios (after ResetGlobalRegistry()), consider using sync.Once for thread-safe initialization if concurrent access is expected.

♻️ Thread-safe lazy initialization using sync.Once
 package config
 
 import (
+	"sync"
+
 	"github.com/flanksource/deps/pkg/types"
 )
 
 var globalRegistry *types.DepsConfig
+var registryOnce sync.Once
 
 func init() {
-	initGlobalRegistry()
+	registryOnce.Do(initGlobalRegistry)
 }
 func GetGlobalRegistry() *types.DepsConfig {
 	if globalRegistry == nil {
-		initGlobalRegistry()
+		registryOnce.Do(initGlobalRegistry)
 	}
 	return globalRegistry
 }

Note: If ResetGlobalRegistry() needs to allow re-initialization, you'd need to reset the sync.Once as well (which requires a different pattern, such as using sync.Mutex instead).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 35 - 37, GetGlobalRegistry currently
checks if globalRegistry == nil and calls initGlobalRegistry(), which can race
when multiple goroutines observe nil; change the lazy initialization to be
thread-safe by protecting initialization with a sync.Once (call
once.Do(initGlobalRegistry) inside GetGlobalRegistry) or, if ResetGlobalRegistry
must allow re-initialization, replace the pattern with a sync.Mutex to serialize
access in GetGlobalRegistry, ResetGlobalRegistry and initGlobalRegistry so only
one goroutine can initialize/reset the globalRegistry at a time; update
references to globalRegistry, GetGlobalRegistry, initGlobalRegistry and
ResetGlobalRegistry accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/config/registry.go`:
- Around line 64-68: Update the stale comment on ResetGlobalRegistry to reflect
the new lazy re-initialization behavior: explain that ResetGlobalRegistry sets
globalRegistry to nil and that GetGlobalRegistry() will recreate/reload the
registry on the next call (i.e., lazy re-initialization), while preserving the
note about init functions only running once per execution where relevant;
reference ResetGlobalRegistry and GetGlobalRegistry by name so the maintainer
can locate and update the comment accordingly.

---

Nitpick comments:
In `@pkg/config/registry.go`:
- Around line 35-37: GetGlobalRegistry currently checks if globalRegistry == nil
and calls initGlobalRegistry(), which can race when multiple goroutines observe
nil; change the lazy initialization to be thread-safe by protecting
initialization with a sync.Once (call once.Do(initGlobalRegistry) inside
GetGlobalRegistry) or, if ResetGlobalRegistry must allow re-initialization,
replace the pattern with a sync.Mutex to serialize access in GetGlobalRegistry,
ResetGlobalRegistry and initGlobalRegistry so only one goroutine can
initialize/reset the globalRegistry at a time; update references to
globalRegistry, GetGlobalRegistry, initGlobalRegistry and ResetGlobalRegistry
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42a960d7-0346-4306-94a6-5f580ec9c8ef

📥 Commits

Reviewing files that changed from the base of the PR and between 255826c and b839e46.

📒 Files selected for processing (3)
  • pkg/config/registry.go
  • pkg/runtime/detector.go
  • pkg/runtime/runtime_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/runtime/runtime_test.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/config/registry.go (1)

34-34: Gate success diagnostics behind debug/verbose mode.

Line 34 unconditionally writes a success message to stderr on initialization. This can create unnecessary noise in CLI pipelines and brittle log expectations. The codebase uses verbose/debug flags to gate diagnostic output (see cmd/update.go, cmd/lock.go); this informational message should follow the same pattern, either using logger.Debugf() or conditionally gated output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` at line 34, The unconditional stderr diagnostic in
initGlobalRegistry (the fmt.Fprintf writing "deps: initGlobalRegistry
completed...") should be gated by the existing verbose/debug logging system:
replace the direct os.Stderr write with a call to the project's debug logger
(e.g., logger.Debugf) or wrap it behind the same verbose flag check used by
cmd/update.go and cmd/lock.go, so the message about len(globalRegistry.Registry)
only emits when verbose/debug is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/registry.go`:
- Around line 37-39: Comments around GetGlobalRegistry and ResetGlobalRegistry
are inconsistent: update the reset-related comments to reflect the new lazy
re-initialization behavior (GetGlobalRegistry now re-initializes the global
registry when it's nil) so they no longer claim a process restart is required;
specifically edit the comment block for GetGlobalRegistry and any comments on
ResetGlobalRegistry to state that ResetGlobalRegistry sets the global registry
to nil and that GetGlobalRegistry will reload defaults + user config on next
access, referencing the functions GetGlobalRegistry and ResetGlobalRegistry to
make the behavior explicit.
- Around line 40-42: globalRegistry is accessed concurrently without
synchronization (seen in GetGlobalRegistry and other functions like
ResetGlobalRegistry/SetGlobalRegistry), causing races and duplicate
initialization; fix by adding a package-level synchronization primitive (e.g., a
sync.RWMutex named registryMu or a sync.Once for one-time init) and guard all
reads/writes to globalRegistry: use registryMu.RLock/RUnlock for reads in
GetGlobalRegistry, registryMu.Lock/Unlock for writes in SetGlobalRegistry and
ResetGlobalRegistry, and ensure initGlobalRegistry is called under the write
lock or protected by sync.Once to prevent double initialization.

---

Nitpick comments:
In `@pkg/config/registry.go`:
- Line 34: The unconditional stderr diagnostic in initGlobalRegistry (the
fmt.Fprintf writing "deps: initGlobalRegistry completed...") should be gated by
the existing verbose/debug logging system: replace the direct os.Stderr write
with a call to the project's debug logger (e.g., logger.Debugf) or wrap it
behind the same verbose flag check used by cmd/update.go and cmd/lock.go, so the
message about len(globalRegistry.Registry) only emits when verbose/debug is
enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 390eb5bf-9e01-419c-997a-c4ec5cc8158e

📥 Commits

Reviewing files that changed from the base of the PR and between b839e46 and 2426442.

📒 Files selected for processing (2)
  • pkg/config/registry.go
  • pkg/runtime/detector.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/runtime/detector.go

Comment on lines +40 to +42
if globalRegistry == nil {
initGlobalRegistry()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unsynchronized shared-state access patterns in this file.
rg -n -C2 '\bglobalRegistry\b|sync\.(Mutex|RWMutex|Once)|atomic\.' pkg/config/registry.go

Repository: flanksource/deps

Length of output: 1021


🏁 Script executed:

cat -n pkg/config/registry.go | head -100

Repository: flanksource/deps

Length of output: 2734


Add synchronization to prevent concurrent access race conditions on globalRegistry.

Lines 40-42 (and other accesses in ResetGlobalRegistry/SetGlobalRegistry) read and write globalRegistry without any locking. Concurrent callers can race—for example, multiple threads calling GetGlobalRegistry() simultaneously with nil value will both invoke initGlobalRegistry(), triggering duplicate initialization and potential data races on the global state.

Suggested fix (mutex-guarded access)
 import (
 	"fmt"
 	"os"
+	"sync"

 	"github.com/flanksource/deps/pkg/types"
 )

 var globalRegistry *types.DepsConfig
+var globalRegistryMu sync.Mutex

 func init() {
+	globalRegistryMu.Lock()
+	defer globalRegistryMu.Unlock()
 	initGlobalRegistry()
 }
@@
 func GetGlobalRegistry() *types.DepsConfig {
+	globalRegistryMu.Lock()
+	defer globalRegistryMu.Unlock()
 	if globalRegistry == nil {
 		initGlobalRegistry()
 	}
 	return globalRegistry
 }
@@
 func ResetGlobalRegistry() {
+	globalRegistryMu.Lock()
+	defer globalRegistryMu.Unlock()
 	globalRegistry = nil
 }
@@
 func SetGlobalRegistry(config *types.DepsConfig) {
+	globalRegistryMu.Lock()
+	defer globalRegistryMu.Unlock()
 	globalRegistry = config
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 40 - 42, globalRegistry is accessed
concurrently without synchronization (seen in GetGlobalRegistry and other
functions like ResetGlobalRegistry/SetGlobalRegistry), causing races and
duplicate initialization; fix by adding a package-level synchronization
primitive (e.g., a sync.RWMutex named registryMu or a sync.Once for one-time
init) and guard all reads/writes to globalRegistry: use registryMu.RLock/RUnlock
for reads in GetGlobalRegistry, registryMu.Lock/Unlock for writes in
SetGlobalRegistry and ResetGlobalRegistry, and ensure initGlobalRegistry is
called under the write lock or protected by sync.Once to prevent double
initialization.

@moshloop moshloop force-pushed the feat/arch-mismatch-detection branch from 6aa7f88 to c76c3e8 Compare March 20, 2026 11:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/config/config_ginkgo_test.go (1)

15-15: Prefer intent-based assertion over a hardcoded registry count.

Line 15 (> 50) is brittle for legitimate defaults cleanup. A non-empty assertion plus required key checks (Lines 16-17) is more stable.

Proposed test tweak
-			Expect(len(config.Registry)).To(BeNumerically(">", 50))
+			Expect(config.Registry).ToNot(BeEmpty())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/config_ginkgo_test.go` at line 15, Replace the brittle numeric
assertion on the registry length with an intent-based non-empty check: instead
of Expect(len(config.Registry)).To(BeNumerically(">", 50)) assert that
config.Registry is not empty (e.g., Expect(config.Registry).ToNot(BeEmpty()) or
Expect(len(config.Registry)).To(BeNumerically(">", 0))) and keep the subsequent
required key checks (the assertions referencing specific keys on
config.Registry) intact; update the test in config_ginkgo_test.go around the
Expect on config.Registry accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/config_ginkgo_test.go`:
- Around line 13-17: The embedded defaults.yaml contains duplicate mapping keys
for the "step" registry entry which causes YAML unmarshal to fail before
LoadDefaultConfig() can be asserted in pkg/config/config_ginkgo_test.go; open
pkg/config/defaults.yaml, locate the two "step" mapping entries (around the
reported duplicate lines) and merge them into a single "step" registry entry
preserving all unique fields (or remove the redundant one), ensuring there are
no duplicate keys so LoadDefaultConfig() can parse the defaults and the test
assertions on config.Registry (including
Expect(config.Registry).To(HaveKey("step"))) succeed.

---

Nitpick comments:
In `@pkg/config/config_ginkgo_test.go`:
- Line 15: Replace the brittle numeric assertion on the registry length with an
intent-based non-empty check: instead of
Expect(len(config.Registry)).To(BeNumerically(">", 50)) assert that
config.Registry is not empty (e.g., Expect(config.Registry).ToNot(BeEmpty()) or
Expect(len(config.Registry)).To(BeNumerically(">", 0))) and keep the subsequent
required key checks (the assertions referencing specific keys on
config.Registry) intact; update the test in config_ginkgo_test.go around the
Expect on config.Registry accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12235cdd-c78b-477d-9825-03407fffb4f6

📥 Commits

Reviewing files that changed from the base of the PR and between 2426442 and c76c3e8.

📒 Files selected for processing (5)
  • .github/workflows/test.yml
  • pkg/config/config_ginkgo_test.go
  • pkg/config/registry.go
  • pkg/runtime/detector.go
  • pkg/runtime/runtime_test.go
✅ Files skipped from review due to trivial changes (2)
  • .github/workflows/test.yml
  • pkg/runtime/runtime_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/runtime/detector.go
  • pkg/config/registry.go

…stall

Exit with code 2 on VersionMismatchError to distinguish from other failures. Detects when downloaded binary architecture doesn't match requested --arch override and skips cached installation. Validates installed version matches expected and provides diagnostic suggestions for library/architecture issues.

BREAKING CHANGE: detect and reject architecture mismatches during install
- Add //go:build darwin constraint to fix_dylibs_test.go
- Use fmt.Fprintf instead of WriteString(fmt.Sprintf) (staticcheck QF1012)
- Skip version verification for cross-platform and alias installs
- Use full bin-dir path for version checks
- Fix jq version_regex to capture patch version
- Simplify clickhouse version_expr and capture 4-part version
- Use context param in deleteFiles error message
For directory-mode packages with symlinks, binPath resolves to a
symlink to a binary file. Passing directory mode to version check
causes it to treat the symlink as a working directory, which fails.
- Extract init() body into initGlobalRegistry() for reuse
- Make GetGlobalRegistry() lazily re-initialize when nil
- Add nil guard and registry-size diagnostic in installRuntime
- Add LoadDefaultConfig test to catch yaml parsing errors early
- Increase PowerShell test timeout to 120s for CI install
@moshloop moshloop force-pushed the feat/arch-mismatch-detection branch from 731d2e1 to 8003a8f Compare March 20, 2026 11:44
- Add prefix matching to CompareVersions so "26.2.5.45" matches
  "26.2.5.45-stable" (clickhouse-style versions)
- Use CheckBinaryVersion in finalizeInstallation to properly resolve
  binary paths for directory-mode packages with symlinks
- Fix resolveBinaryPath to skip shell-wrapped commands (bash -c)
  which must run in directory mode
- Fix svu version_regex to match GitVersion output format
- Fix powershell/uv version_command: remove binary name prefix since
  the binary path is already resolved
- Fix double-backslash regexes in single-quoted YAML for activemq,
  activemq-artemis, and tomee
… version transformation

Add version_fallback field to Package that transforms the resolved version
before URL templating. This allows kube-apiserver on darwin/windows to use
envtest minor-only versions (e.g., 1.35.0) while linux uses the exact
patch version (e.g., 1.35.3) from kubernetes/kubernetes tags.
@moshloop moshloop enabled auto-merge (squash) March 20, 2026 13:35
Refactor GetInstalledVersionWithMode to return raw command output
alongside the parsed version. This enables verify_expr CEL expressions
to match against the full output when standard semver comparison fails.

Adds verify_expr to openjdk8 config to accept 1.8.0/8.0.x outputs
that don't match the jdk8u482-b08 tag format.
… files

The build job already uploads binaries via svenstaro/upload-release-action.
The release job just needs to update the release with generated notes
instead of trying to create a new release with files from a non-existent
release/ directory.
@moshloop moshloop disabled auto-merge March 20, 2026 14:25
@moshloop moshloop merged commit 05a43a6 into main Mar 20, 2026
12 of 14 checks passed
@moshloop moshloop deleted the feat/arch-mismatch-detection branch March 20, 2026 14:25
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.

1 participant