Skip to content

Commit 48f7b34

Browse files
committed
Update a cancelableReader importing gpg keys
Signed-off-by: Qi Wang <[email protected]>
1 parent 22d50c5 commit 48f7b34

File tree

6 files changed

+105
-14
lines changed

6 files changed

+105
-14
lines changed

image/signature/mechanism.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package signature
44

55
import (
66
"bytes"
7+
"context"
78
"errors"
89
"fmt"
910
"io"
@@ -78,7 +79,7 @@ func NewGPGSigningMechanism() (SigningMechanism, error) {
7879
// of these keys.
7980
// The caller must call .Close() on the returned SigningMechanism.
8081
func NewEphemeralGPGSigningMechanism(blob []byte) (SigningMechanism, []string, error) {
81-
return newEphemeralGPGSigningMechanism([][]byte{blob})
82+
return newEphemeralGPGSigningMechanism(context.Background(), [][]byte{blob})
8283
}
8384

8485
// gpgUntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,

image/signature/mechanism_gpgme_only.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
package signature
44

55
import (
6+
"bytes"
7+
"context"
8+
"io"
69
"os"
710

811
"github.com/proglottis/gpgme"
@@ -12,7 +15,7 @@ import (
1215
// recognizes _only_ public keys from the supplied blobs, and returns the identities
1316
// of these keys.
1417
// The caller must call .Close() on the returned SigningMechanism.
15-
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
18+
func newEphemeralGPGSigningMechanism(ctx context.Context, blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
1619
dir, err := os.MkdirTemp("", "containers-ephemeral-gpg-")
1720
if err != nil {
1821
return nil, nil, err
@@ -23,34 +26,61 @@ func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassph
2326
os.RemoveAll(dir)
2427
}
2528
}()
26-
ctx, err := newGPGMEContext(dir)
29+
gpgmeCtx, err := newGPGMEContext(dir)
2730
if err != nil {
2831
return nil, nil, err
2932
}
3033
keyIdentities := []string{}
3134
for _, blob := range blobs {
32-
ki, err := importKeysFromBytes(ctx, blob)
35+
ki, err := importKeysFromBytes(ctx, gpgmeCtx, blob)
3336
if err != nil {
3437
return nil, nil, err
3538
}
3639
keyIdentities = append(keyIdentities, ki...)
3740
}
3841

39-
mech := newGPGMESigningMechanism(ctx, dir)
42+
mech := newGPGMESigningMechanism(gpgmeCtx, dir)
4043
removeDir = false
4144
return mech, keyIdentities, nil
4245
}
4346

47+
// cancelableReader wraps an io.Reader and checks context cancellation on each Read call.
48+
type cancelableReader struct {
49+
ctx context.Context
50+
reader io.Reader
51+
}
52+
53+
func (r *cancelableReader) Read(p []byte) (int, error) {
54+
// Check if context is cancelled before each read
55+
if err := r.ctx.Err(); err != nil {
56+
return 0, err
57+
}
58+
n, err := r.reader.Read(p)
59+
// Check again after read in case cancellation happened during the read
60+
if err == nil && r.ctx.Err() != nil {
61+
return n, r.ctx.Err()
62+
}
63+
return n, err
64+
}
65+
4466
// importKeysFromBytes imports public keys from the supplied blob and returns their identities.
4567
// The blob is assumed to have an appropriate format (the caller is expected to know which one).
4668
// NOTE: This may modify long-term state (e.g. key storage in a directory underlying the mechanism);
4769
// but we do not make this public, it can only be used through newEphemeralGPGSigningMechanism.
48-
func importKeysFromBytes(ctx *gpgme.Context, blob []byte) ([]string, error) {
49-
inputData, err := gpgme.NewDataBytes(blob)
70+
// The context can be used to cancel the operation; if cancelled, the reader will return an error
71+
// which may allow the GPGME operation to abort (though there's no guarantee the C library will
72+
// respect this immediately).
73+
func importKeysFromBytes(ctx context.Context, gpgmeCtx *gpgme.Context, blob []byte) ([]string, error) {
74+
// Create a cancelable reader that checks context on each Read call
75+
reader := &cancelableReader{
76+
ctx: ctx,
77+
reader: bytes.NewReader(blob),
78+
}
79+
inputData, err := gpgme.NewDataReader(reader)
5080
if err != nil {
5181
return nil, err
5282
}
53-
res, err := ctx.Import(inputData)
83+
res, err := gpgmeCtx.Import(inputData)
5484
if err != nil {
5585
return nil, err
5686
}

image/signature/mechanism_openpgp.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package signature
44

55
import (
66
"bytes"
7+
"context"
78
"errors"
89
"fmt"
910
"io"
@@ -63,7 +64,9 @@ func newGPGSigningMechanismInDirectory(optionalDir string) (signingMechanismWith
6364
// recognizes _only_ public keys from the supplied blob, and returns the identities
6465
// of these keys.
6566
// The caller must call .Close() on the returned SigningMechanism.
66-
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
67+
// The context parameter is accepted for API consistency but is not used by this implementation.
68+
func newEphemeralGPGSigningMechanism(ctx context.Context, blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
69+
_ = ctx // Context not used in this implementation
6770
m := &openpgpSigningMechanism{
6871
keyring: openpgp.EntityList{},
6972
}

image/signature/mechanism_sequoia.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package signature
44

55
import (
6+
"context"
7+
68
"go.podman.io/image/v5/signature/internal/sequoia"
79
)
810

@@ -18,7 +20,9 @@ type sequoiaEphemeralSigningMechanism struct {
1820
// recognizes _only_ public keys from the supplied blobs, and returns the identities
1921
// of these keys.
2022
// The caller must call .Close() on the returned SigningMechanism.
21-
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
23+
// The context parameter is accepted for API consistency but is not used by this implementation.
24+
func newEphemeralGPGSigningMechanism(ctx context.Context, blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
25+
_ = ctx // Context not used in this implementation
2226
if err := sequoia.Init(); err != nil {
2327
return nil, nil, err // Coverage: This is impractical to test in-process, with the static go_sequoia_dlhandle.
2428
}

image/signature/mechanism_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package signature
44

55
import (
66
"bytes"
7+
"context"
78
"os"
89
"path/filepath"
910
"slices"
@@ -175,7 +176,7 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) {
175176
require.NoError(t, err)
176177
keyBlob2, err := os.ReadFile("./fixtures/public-key-2.gpg")
177178
require.NoError(t, err)
178-
mech, keyIdentities, err = newEphemeralGPGSigningMechanism([][]byte{keyBlob1, keyBlob2})
179+
mech, keyIdentities, err = newEphemeralGPGSigningMechanism(context.Background(), [][]byte{keyBlob1, keyBlob2})
179180
require.NoError(t, err)
180181
defer mech.Close()
181182
assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprintWithPassphrase}, keyIdentities)

image/signature/policy_eval_signedby.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"time"
910

1011
digest "github.com/opencontainers/go-digest"
1112
"go.podman.io/image/v5/internal/multierr"
1213
"go.podman.io/image/v5/internal/private"
1314
"go.podman.io/image/v5/manifest"
1415
)
1516

17+
const importKeyTimeOut = 60 * time.Second
18+
1619
func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) {
1720
switch pr.KeyType {
1821
case SBKeyTypeGPGKeys:
@@ -40,9 +43,58 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva
4043
}
4144

4245
// FIXME: move this to per-context initialization
43-
mech, trustedIdentities, err := newEphemeralGPGSigningMechanism(data)
44-
if err != nil {
45-
return sarRejected, nil, err
46+
// Import the keys with a 60s timeout to avoid hanging indefinitely. see issues.redhat.com/browse/OCPBUGS-57893
47+
// Create a context with timeout that can be used to cancel the operation via the cancelable reader
48+
importCtx, cancel := context.WithTimeout(ctx, importKeyTimeOut)
49+
defer cancel()
50+
51+
// Use a goroutine with timeout to handle cases where GPGME hangs in C library calls
52+
// that don't read from the data source (where the cancelable reader can't help).
53+
// The cancelable reader provides faster cancellation when GPGME is reading, but we still
54+
// need the timeout goroutine to detect hangs and clean up resources.
55+
type result struct {
56+
mech signingMechanismWithPassphrase
57+
keys []string
58+
err error
59+
}
60+
// Use a buffered channel so the goroutine can always send without blocking
61+
done := make(chan result, 1)
62+
63+
go func() {
64+
mech, keys, err := newEphemeralGPGSigningMechanism(importCtx, data)
65+
done <- result{mech, keys, err}
66+
}()
67+
68+
var mech signingMechanismWithPassphrase
69+
var trustedIdentities []string
70+
71+
select {
72+
case <-importCtx.Done():
73+
// Timeout occurred. The goroutine is still running and may complete later.
74+
// Spawn a cleanup goroutine to handle the result when it arrives and clean up
75+
// any resources that were created, preventing leaks.
76+
go func() {
77+
// Wait for the operation to complete (with a reasonable upper bound
78+
// to avoid waiting forever if something goes very wrong)
79+
select {
80+
case r := <-done:
81+
// The operation completed. If a mechanism was created, close it
82+
// to free file descriptors and other resources.
83+
if r.mech != nil {
84+
r.mech.Close()
85+
}
86+
case <-time.After(5 * time.Minute):
87+
// If it still hasn't completed after 5 minutes, give up on cleanup.
88+
// The goroutine will continue running, but at least we've tried.
89+
}
90+
}()
91+
return sarRejected, nil, fmt.Errorf("GPG/OpenPGP key import timed out after %s", importKeyTimeOut)
92+
case r := <-done:
93+
mech = r.mech
94+
trustedIdentities = r.keys
95+
if r.err != nil {
96+
return sarRejected, nil, r.err
97+
}
4698
}
4799
defer mech.Close()
48100
if len(trustedIdentities) == 0 {

0 commit comments

Comments
 (0)