Skip to content

Various bug fixes + reliability improvements on the Framer fork - #24

Open
ErikBooijFR wants to merge 16 commits into
gmr:mainfrom
framer:feat/upstream-fixes
Open

ErikBooijFR wants to merge 16 commits into
gmr:mainfrom
framer:feat/upstream-fixes

Conversation

@ErikBooijFR

@ErikBooijFR ErikBooijFR commented Aug 11, 2026 •

Copy link
Copy Markdown

See #23

These are the changes we've implemented on our fork, after an automated audit of the codebase. Each individual commit, contains reasoning/explanation on the what and why.

If there are any you wish to opt-out of, feel free to let me know, and I'll drop them. Same if you'd prefer I split them into separate PRs (in that case, please let me know how you'd like them split).

Summary by CodeRabbit

  • New Features

    • Wrapped commands now receive their flags correctly.
    • SSM-provided environment variables take precedence over inherited variables.
    • Supports pristine command execution without inheriting the parent environment.
    • Signals are forwarded more reliably to running commands.
  • Bug Fixes

    • Command failures now return consistent exit codes, including usage errors, missing commands, and signal termination.
    • Environment-variable name collisions are resolved deterministically.
  • Documentation

    • Added documentation covering command and env-aws-params exit codes.

ErikBooijFR and others added 14 commits August 7, 2026 15:56
The forwarding goroutine did a single blocking channel read, forwarded
one signal, and exited. signal.Notify stays registered for the life of
the process, so every later SIGTERM/SIGINT was still intercepted by the
runtime but never read: it neither reached the child nor terminated the
wrapper. A second Ctrl-C, or a supervisor escalating its shutdown
request, was silently swallowed until SIGKILL.

Drain the channel in a loop instead, and grow the buffer from 1 to 32:
signal.Notify sends without blocking and drops signals when the buffer
is full, so bursts could otherwise be lost while a forward is in
flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kill(-getpid()) targets the process group whose ID equals the wrapper's
own PID. That group only exists when the wrapper happens to be a group
leader, e.g. PID 1 in a container or a job started by an interactive
shell. Launched from a shell script or a supervisor, the kill failed
with ESRCH and the child never received the signal at all. Where the
group did exist, terminal-generated signals arrived twice (once from
the kernel to the foreground group, once from us), and each forward
re-signalled the wrapper itself. The log reported -cmd.Process.Pid, a
group that never existed, while the code signalled a different one.

Use cmd.Process.Signal to address exactly the process we started; it
behaves identically in every launch context and delivers exactly once.
Propagating further down the tree is the child's responsibility, the
same contract as tini's default behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
signal.Notify ran after cmd.Start, so a signal arriving between the two
killed the wrapper via default disposition and orphaned the
just-started child. Supervisors that start and almost immediately stop
a service (crash loops, instant rollbacks) can hit this window.

Register interception before starting the child. Signals arriving
before the forwarding goroutine is up queue in the buffered channel and
are delivered once the child runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only SIGHUP, SIGINT, SIGTERM and SIGQUIT were forwarded. Any other
signal, e.g. SIGUSR1 to rotate logs or SIGWINCH on resize, killed the
wrapper through its default disposition and orphaned the child. An
entrypoint wrapper should be transparent: whatever an operator sends to
the visible PID must reach the application behind it.

Subscribe to every catchable signal and forward, with two exceptions:
SIGCHLD is addressed to the wrapper about its own child, and SIGURG is
used continuously by the Go runtime for goroutine preemption.

Demote the per-forward log line to debug; with SIGWINCH forwarded, a
terminal resize would otherwise spam info-level logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A signal-killed child has no exit code, so ExitCode() returns -1 and
os.Exit truncated it to 255, collapsing SIGTERM, SIGKILL and SIGSEGV
into one indistinguishable value.

Translate to the shell convention of 128+N: 143 for SIGTERM, 137 for
SIGKILL, the code supervisors and container tooling already interpret
(137 is the well-known OOM-kill signature). Voluntary exits still pass
through unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
urfave/cli v3 parses flags interspersed with positional arguments, so
flags belonging to the wrapped command were claimed by the wrapper:
"env-aws-params --prefix /x bash -c set" failed with "flag provided but
not defined: -c" unless callers inserted "--" by hand.

Set StopOnNthArg to 1: parsing stops at the first positional argument,
and everything after the command passes through verbatim, matching how
env(1), sudo and docker run treat their command tails. Wrapper flags
must consequently appear before the command; trailing flags now belong
to the child.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrapper-side failures exited with 1 or 2 (validation), 255 (Parameter
Store errors, via -1) and 128 (spawn failures). All of these collide
with codes real children use, or with the 128+N signal range, so
callers could not tell "the app failed" from "the plumbing failed".

Adopt the shell and Docker convention: 125 for wrapper-side errors,
126 for a command that was found but could not be started, 127 for a
command that was not found. exec.ErrNotFound covers failed PATH
lookups; os.ErrNotExist covers explicit paths. A child may still exit
with these codes itself; that ambiguity is inherent to the convention.

validateArgs now returns only an error since both validation failures
map to 125. Document the whole contract in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The --sanitize, --strip and --upcase transforms are lossy: db-host and
db_host both become DB_HOST. Both entries were emitted, and the
effective winner was picked by os/exec's keep-last deduplication over
a slice sorted as whole "KEY=value" strings, i.e. by lexical order of
the values. Rotating a value could silently flip which parameter won.

Iterate parameters in sorted-name order into a map keyed by the final
env key: exactly one entry per key is emitted and the parameter whose
name sorts last wins, independent of values. Collisions log a warning
naming both parameters, the contested key and the winner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds that skip the -X ldflag, e.g. a plain go build or go install,
printed an empty string for --version. The linker flag overwrites the
initial value, so stamped release builds are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
govulncheck reports the Encrypted Client Hello privacy leak in
crypto/tls (GO-2026-5856) as reachable from this binary through the SSM
client's TLS calls. The fix ships with the toolchain rather than a
module, so the floor is expressed as a toolchain directive: any Go
since 1.21 fetches 1.26.5 automatically, and GOTOOLCHAIN=local fails
loudly instead of silently producing a vulnerable binary. CI floats on
1.26.x and picks the patch up by itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
contents: write applied to the whole workflow, handing a repo-write
token to the test and build jobs. Those run on every push and pull
request and execute the most third-party code, yet only need to read
the repository. Default the workflow token to contents: read and grant
write solely to the release job, which creates GitHub Releases. A
compromised action or dependency in the hot path can then no longer
push commits, move tags or forge releases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A version tag is a mutable pointer: whoever controls, or compromises,
softprops/action-gh-release can repoint v3, and every workflow using it
executes the new code on its next run. This is exactly how the
tj-actions/changed-files compromise spread (CVE-2025-30066). The action
is third-party, runs in the only job holding contents: write, and
uploads the release binaries users download directly, so a hijacked
version could replace them.

Pin to the commit v3.0.2 resolves to; the trailing comment keeps the
version readable and lets Dependabot propose bumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI runs go test -race ./... while make test ran a bare go test, so
data races surfaced only in CI. Align the Makefile with CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ErikBooijFR, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b2367513-752b-4290-b779-a9e1f588037c

📥 Commits

Reviewing files that changed from the base of the PR and between 6af0aee and 6f356a8.

📒 Files selected for processing (2)
  • README.md
  • go.mod

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24d963c0-5aab-43a9-aa85-cd9d779e2bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 14806ea and 6af0aee.

📒 Files selected for processing (1)
  • main.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • main.go

📝 Walkthrough

Walkthrough

The CLI now merges SSM and inherited environment variables deterministically, passes wrapped-command arguments correctly, preserves execution exit codes, and forwards signals to the child. Tests use the race detector across all packages. Release permissions and action references are more restricted.

Changes

Runtime behavior

Layer / File(s) Summary
Deterministic environment construction
data.go, data_test.go
Adds MergeEnvVars. SSM values override inherited values. BuildEnvVars sorts parameters, resolves transformed-key collisions deterministically, and logs warnings.
Command execution and exit handling
main.go, runner.go, main_test.go, README.md
Stops CLI parsing at the wrapped command. Validation errors return status 125. Missing commands return 127, other execution failures return 126, and signal termination returns 128 + N. Signals are forwarded to the child.
Verification and release controls
Makefile, go.mod, .github/workflows/ci.yml
Runs race-enabled tests across all packages, selects Go 1.26.5, scopes release write permission, and pins the release action commit.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant BuildEnvVars
  participant MergeEnvVars
  participant RunCommand
  participant WrappedCommand
  CLI->>BuildEnvVars: Build SSM environment entries
  CLI->>MergeEnvVars: Merge inherited and SSM environments
  MergeEnvVars-->>CLI: Return SSM-preferred environment
  CLI->>RunCommand: Start wrapped command
  RunCommand->>WrappedCommand: Execute with environment and arguments
  WrappedCommand-->>RunCommand: Return status or signal
  RunCommand-->>CLI: Return mapped exit status
Loading

Poem

I’m a rabbit with a tidy nest,
SSM vars now sort out best.
Signals hop from shell to shell,
Exit codes land where they should dwell.
Tests race safely—carrots swell!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the broad bug fixes and reliability changes in the pull request, although it does not identify the specific CLI and environment changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go.mod`:
- Line 5: Set the go.mod minimum Go version to 1.26.5 using the go directive
rather than relying on the toolchain directive, and update both CI job
Go-version entries to 1.26.5. Ensure all three configuration points consistently
enforce the required crypto/tls security baseline.

In `@main.go`:
- Around line 46-47: Configure the CLI application's OnUsageError handler
alongside the existing validateArgs handling so parser errors return cli.Exit
with errorPrefix(err) and status 125 instead of reaching log.Fatal. Update
main.go accordingly; README.md lines 147-153 document the expected behavior and
require no direct change.
🪄 Autofix

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 Plus

Run ID: d229ca6e-8b0b-42af-9e99-4d40664e7762

📥 Commits

Reviewing files that changed from the base of the PR and between 62284e0 and 14806ea.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • Makefile
  • README.md
  • data.go
  • data_test.go
  • go.mod
  • main.go
  • main_test.go
  • runner.go

Comment thread go.mod Outdated
Comment thread main.go
ErikBooijFR and others added 2 commits August 11, 2026 10:13
README documents exit code 125 for invalid usage, and validateArgs
errors honor that, but a malformed flag never reaches the action:
cli.Command.Run returns the parse error to main, where log.Fatal
exits with status 1, breaking the documented contract. Set
OnUsageError to route parse errors through the same
cli.Exit(..., 125) path as other usage errors.

This also reports the error once, in the ERROR: prefix style, instead
of twice: previously cli printed "Incorrect Usage" plus a full help
dump and logrus then repeated the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The intent of requiring 1.26.5 (the crypto/tls fix for GO-2026-5856)
was expressed as go 1.26 plus a toolchain directive, but the toolchain
directive is only a suggestion: it is honored under the default
GOTOOLCHAIN=auto and silently ignored under GOTOOLCHAIN=local, where a
1.26.0 toolchain would build a binary without the fix. The go
directive is the mechanism that actually enforces a floor - an older
toolchain either auto-upgrades or fails loudly.

Raise the go directive to 1.26.5 and drop the now-redundant toolchain
line. The module is not importable as a library, so the stricter
directive burdens no dependents. CI stays on go-version: '1.26', which
setup-go resolves to the latest 1.26.x patch, so release builds keep
picking up future security fixes without manual bumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ErikBooijFR

Copy link
Copy Markdown
Author

Hey @gmr, just checking in if there's something I can do to move this forward 🙏

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