exec.CommandContext is the right call, but context.Background() never
expires, so no deadline is actually enforced. All five gpg call sites
route through runGPG and inherit this: gpgSign, gpgExportPublicKey, gpgGetKeyFingerprint, gpgExtractPubKeyFingerprint, gpgVerify.
This is a real hang risk, not a theoretical one: gpg blocks indefinitely
waiting on a passphrase prompt, a stalled gpg-agent, or entropy. In a
non-interactive context — CI, a cron job, a server embedding this library —
that is an unbounded hang with no way out.
It is also the most likely cause of the intermittent test-suite timeout
described in the script/test issue: the gpg tests generate real RSA-2048
keys, and a slow keygen has nothing to bound it.
Definition of done
Every gpg subprocess call runs under a context with a real deadline.
The timeout is a named constant, not a magic number, and is documented as
to why that value was chosen.
Callers that already have a context.Context in scope propagate it
instead of manufacturing a fresh background context, so cancellation from
above works.
When the deadline fires, the returned error clearly says the gpg
invocation timed out and which operation it was — not a bare context deadline exceeded.
Test coverage: a test that a gpg call which exceeds its deadline returns
the timeout error rather than hanging. Use a stub or a deliberately
slow fake binary; do not add a multi-second sleep to the suite.
make check passes. TODO.md updated in the same commit.
Implementation requirements
Do not paper over this by raising the go test timeout. The production
code path is what is unbounded.
Check whether killing the context actually kills gpg. CommandContext
sends os.Interrupt/Kill to the direct child only; if gpg has spawned
or delegated to a gpg-agent, the child may not die. If a process group
kill is needed, do it properly and explain in a comment.
There is an existing //nolint on one of these exec sites referring to a
"non-cancellable signing exec". If signing genuinely cannot be cancelled,
that claim needs to be justified in a comment or withdrawn — it should not
survive as an unexplained suppression.
Preserve current behavior on the success path exactly; this is a
robustness fix, not a redesign. Replacing gpg with pure-Go crypto is a
separate, owner-gated decision and is out of scope here.
Commit title must end with (closes #62).
## Context
`mfer/gpg.go:52-53` runs every gpg invocation as:
```go
exec.CommandContext(context.Background(), "gpg", fullArgs...)
```
`exec.CommandContext` is the right call, but `context.Background()` never
expires, so **no deadline is actually enforced**. All five gpg call sites
route through `runGPG` and inherit this: `gpgSign`, `gpgExportPublicKey`,
`gpgGetKeyFingerprint`, `gpgExtractPubKeyFingerprint`, `gpgVerify`.
This is a real hang risk, not a theoretical one: `gpg` blocks indefinitely
waiting on a passphrase prompt, a stalled `gpg-agent`, or entropy. In a
non-interactive context — CI, a cron job, a server embedding this library —
that is an unbounded hang with no way out.
It is also the most likely cause of the intermittent test-suite timeout
described in the `script/test` issue: the gpg tests generate real RSA-2048
keys, and a slow keygen has nothing to bound it.
## Definition of done
- Every gpg subprocess call runs under a context with a real deadline.
- The timeout is a named constant, not a magic number, and is documented as
to why that value was chosen.
- Callers that already have a `context.Context` in scope propagate it
instead of manufacturing a fresh background context, so cancellation from
above works.
- When the deadline fires, the returned error clearly says the gpg
invocation timed out and which operation it was — not a bare
`context deadline exceeded`.
- Test coverage: a test that a gpg call which exceeds its deadline returns
the timeout error rather than hanging. Use a stub or a deliberately
slow fake binary; do not add a multi-second sleep to the suite.
- `make check` passes. `TODO.md` updated in the same commit.
## Implementation requirements
- Do not paper over this by raising the `go test` timeout. The production
code path is what is unbounded.
- Check whether killing the context actually kills `gpg`. `CommandContext`
sends `os.Interrupt`/`Kill` to the direct child only; if `gpg` has spawned
or delegated to a `gpg-agent`, the child may not die. If a process group
kill is needed, do it properly and explain in a comment.
- There is an existing `//nolint` on one of these exec sites referring to a
"non-cancellable signing exec". If signing genuinely cannot be cancelled,
that claim needs to be justified in a comment or withdrawn — it should not
survive as an unexplained suppression.
- Preserve current behavior on the success path exactly; this is a
robustness fix, not a redesign. Replacing gpg with pure-Go crypto is a
separate, owner-gated decision and is out of scope here.
- Commit title must end with ` (closes #62)`.
clawbot
added this to the 1.0.0 milestone 2026-08-09 03:38:22 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
mfer/gpg.go:52-53runs every gpg invocation as:exec.CommandContextis the right call, butcontext.Background()neverexpires, so no deadline is actually enforced. All five gpg call sites
route through
runGPGand inherit this:gpgSign,gpgExportPublicKey,gpgGetKeyFingerprint,gpgExtractPubKeyFingerprint,gpgVerify.This is a real hang risk, not a theoretical one:
gpgblocks indefinitelywaiting on a passphrase prompt, a stalled
gpg-agent, or entropy. In anon-interactive context — CI, a cron job, a server embedding this library —
that is an unbounded hang with no way out.
It is also the most likely cause of the intermittent test-suite timeout
described in the
script/testissue: the gpg tests generate real RSA-2048keys, and a slow keygen has nothing to bound it.
Definition of done
to why that value was chosen.
context.Contextin scope propagate itinstead of manufacturing a fresh background context, so cancellation from
above works.
invocation timed out and which operation it was — not a bare
context deadline exceeded.the timeout error rather than hanging. Use a stub or a deliberately
slow fake binary; do not add a multi-second sleep to the suite.
make checkpasses.TODO.mdupdated in the same commit.Implementation requirements
go testtimeout. The productioncode path is what is unbounded.
gpg.CommandContextsends
os.Interrupt/Killto the direct child only; ifgpghas spawnedor delegated to a
gpg-agent, the child may not die. If a process groupkill is needed, do it properly and explain in a comment.
//nolinton one of these exec sites referring to a"non-cancellable signing exec". If signing genuinely cannot be cancelled,
that claim needs to be justified in a comment or withdrawn — it should not
survive as an unexplained suppression.
robustness fix, not a redesign. Replacing gpg with pure-Go crypto is a
separate, owner-gated decision and is out of scope here.
(closes #62).