From 86ee8c25a139b515d07d9321d179f4ada9bc9ad1 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 12 May 2026 01:24:47 -0500 Subject: [PATCH 1/2] fix(inject): chmod binary before mv to prevent entrypoint race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On BusyBox (Alpine), `command -v` returns true for non-executable files, unlike bash. This creates a race window in the inject flow: 1. inject_binary() writes temp file, mv's it to /usr/local/bin/devsy 2. Entrypoint's `command -v /usr/local/bin/devsy` returns true (file exists) 3. Entrypoint runs `exec /usr/local/bin/devsy agent container daemon` 4. exec fails because chmod +x hasn't run yet (it was in install_agent) 5. Container crashes (PID 1 died), docker exec gets EOF, injection retries Fix: chmod +x the temp file BEFORE mv'ing it to the final path in both inject_binary() and download_binary(). This makes the operation atomic from the entrypoint's perspective — when the file appears at the final path via mv, it is already executable. The redundant post-install chmod in install_agent() is removed. Also tighten the Go-side error handling in Inject() to not propagate exec EOF errors during the binary injection path (wasExecuted=false), since exec EOF is expected when the entrypoint detects the binary and replaces PID 1. --- pkg/inject/inject.go | 2 +- pkg/inject/inject.sh | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go index 7d1cb9c75..d9cc87991 100644 --- a/pkg/inject/inject.go +++ b/pkg/inject/inject.go @@ -145,7 +145,7 @@ func Inject(opts InjectOptions) (bool, error) { // prefer result error if result.err != nil { return result.wasExecuted, result.err - } else if err != nil { + } else if err != nil && result.wasExecuted { return result.wasExecuted, err } else if result.wasExecuted || opts.ScriptParams.Command == "" { return result.wasExecuted, nil diff --git a/pkg/inject/inject.sh b/pkg/inject/inject.sh index c888b3dd1..8326efe14 100644 --- a/pkg/inject/inject.sh +++ b/pkg/inject/inject.sh @@ -47,6 +47,14 @@ inject_binary() { return 1 fi + if [ "$CHMOD_PATH" = "true" ]; then + $sh_c "chmod +x \"$temp_file\"" || { + >&2 echo "Error: Failed to make $temp_file executable" + $sh_c "rm -f \"$temp_file\"" 2>/dev/null || true + return 1 + } + fi + if ! $sh_c "mv \"$temp_file\" \"$INSTALL_PATH\""; then >&2 echo "Error: Failed to move binary to $INSTALL_PATH" $sh_c "rm -f \"$temp_file\"" 2>/dev/null || true @@ -97,6 +105,14 @@ download_binary() { return 1 fi + if [ "$CHMOD_PATH" = "true" ]; then + $sh_c "chmod +x \"$temp_file\"" || { + >&2 echo "Error: Failed to make $temp_file executable" + $sh_c "rm -f \"$temp_file\"" 2>/dev/null || true + return 1 + } + fi + if ! $sh_c "mv \"$temp_file\" \"$INSTALL_PATH\""; then >&2 echo "Error: Failed to move downloaded binary to $INSTALL_PATH" $sh_c "rm -f \"$temp_file\"" 2>/dev/null || true @@ -147,13 +163,6 @@ install_agent() { } fi fi - - if [ "$CHMOD_PATH" = "true" ]; then - $sh_c "chmod +x \"$INSTALL_PATH\"" || { - >&2 echo "Error: Failed to make $INSTALL_PATH executable" - exit 1 - } - fi } execute_command() { From 48850928db0555f683dbab1957034a2efb0473a0 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 12 May 2026 01:33:45 -0500 Subject: [PATCH 2/2] refactor(inject): use early returns for clearer control flow Separates the inject result handling into distinct early-return blocks with a comment explaining why exec EOF is ignored during binary injection. --- pkg/inject/inject.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go index d9cc87991..43575ef0a 100644 --- a/pkg/inject/inject.go +++ b/pkg/inject/inject.go @@ -142,12 +142,19 @@ func Inject(opts InjectOptions) (bool, error) { } log.Debugf("injection: payload delivered elapsed=%s", time.Since(start)) - // prefer result error if result.err != nil { return result.wasExecuted, result.err - } else if err != nil && result.wasExecuted { + } + + // Exec EOF during binary injection is expected: the container entrypoint + // may exec the daemon (killing the docker exec process) before inject.sh + // prints its final response. Only surface exec errors when a command was + // actually executed through the script. + if err != nil && result.wasExecuted { return result.wasExecuted, err - } else if result.wasExecuted || opts.ScriptParams.Command == "" { + } + + if result.wasExecuted || opts.ScriptParams.Command == "" { return result.wasExecuted, nil }