Repository: project-copacetic/copacetic (go)

Issue:
[BUG] panic: runtime error: invalid memory address or nil pointer dereference in patch.patchWithContext during patch application
### Version of copa

v0.10.0

### Expected Behavior

`copa patch` should successfully apply the necessary OS package patches based on the provided vulnerability report (`-r os_vulns.json`) and create the output patched image tarball (`-o patched-image.tar`) without crashing or encountering runtime errors.



### Actual Behavior

The `copa patch` command crashes during execution with a Go runtime panic after successfully identifying the patches to apply from the report file. The specific error logged is:

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x2b7f88a]

goroutine 13 [running]:
github.com/project-copacetic/copacetic/pkg/patch.patchWithContext.func2()
	/home/runner/work/copacetic/copacetic/pkg/patch/patch.go:299 +0x38a
golang.org/x/sync/errgroup.(*Group).Go.func1()
	/home/runner/go/pkg/mod/golang.org/x/sync@v0.11.0/errgroup/errgroup.go:78 +0x50
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 8
	/home/runner/go/pkg/mod/golang.org/x/sync@v0.11.0/errgroup/errgroup.go:75 +0x93
```
This occurred after the following debug message was logged, indicating it had processed the report entry for perl-base:
```
time="2025-04-30T20:30:52Z" level=debug msg="updates to apply: &{{{ubuntu 22.04} {amd64}} [{perl-base 5.34.0-3ubuntu1.3 5.34.0-3ubuntu1.4 CVE-2024-56406}]}"
```

### Steps To Reproduce

1. Set up a **GitLab CI (v16.7.0 or similar) job** using the **Kubernetes executor**, which runs the job inside a Kubernetes Pod. Configure the job with a `docker:dind` service where `copa` can run and interact with a Docker buildx builder. (See attached [gitlab-ci.yml](https://github.com/user-attachments/files/19984367/gitlab-ci.txt) for an example configuration).
2. Have the target container image available. The image that triggered the bug was `localhost/your-actual-image:tag`. This image is built upon the base `docker.io/eclipse-temurin:21-jre-jammy`.
3. Create a vulnerability report file (e.g.,`os_vulns.json`) that requires at least the `perl-base` package to be updated from version `5.34.0-3ubuntu1.3` to `5.34.0-3ubuntu1.4` (related to CVE-2024-56406). (See attached [os_vulns.json](https://github.com/user-attachments/files/19984402/os_vulns.json) for the exact report used).
4. Execute the copa patch command targeting the image, using the report file, specifying a tarball output, pointing to the buildx builder, and enabling debug mode. Example command:
```

copa patch \
  --debug \
  --addr "buildx://copa-builder" \
  -i "my-image:tag" \
  -r os_vulns.json \
  -o patched-image.tar
```
5. Observe the command output for the panic message. (See attached [job-logs.txt](https://github.com/user-attachments/files/19984381/job-logs.txt) for the full execution log showing the panic).


### Are you willing to submit PRs to contribute to this bug fix?

- [ ] Yes, I am willing to implement it.

Pull request description:
fix: guard against error buildkit solve
<!--
Please include the type of change in the PR title as "<type>: <summary>"
Valid <type> include: build, ci, docs, feat, fix, perf, refactor, test
See https://github.com/project-copacetic/copacetic/blob/master/CONTRIBUTING.md#pull-requests) for guidelines.
-->

Add nil pointer dereference guard to buildkit solve response
Add regression test

Closes #1036 


Patch:
diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go
index 43de853a..a1eea019 100644
--- a/pkg/patch/patch.go
+++ b/pkg/patch/patch.go
@@ -41,6 +41,11 @@ const (
 	defaultTag      = "latest"
 )
 
+// for testing.
+var (
+	bkNewClient = buildkit.NewClient
+)
+
 // Patch command applies package updates to an OCI image given a vulnerability report.
 func Patch(
 	ctx context.Context, timeout time.Duration,
@@ -135,7 +140,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, rep
 		log.Debugf("updates to apply: %v", updates)
 	}
 
-	bkClient, err := buildkit.NewClient(ctx, bkOpts)
+	bkClient, err := bkNewClient(ctx, bkOpts)
 	if err != nil {
 		return err
 	}
@@ -313,7 +318,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, rep
 		}, buildChannel)
 
 		// Currently can only validate updates if updating via scanner
-		if reportFile != "" && validatedManifest != nil {
+		if err == nil && solveResponse != nil && reportFile != "" && validatedManifest != nil {
 			digest := solveResponse.ExporterResponse[exptypes.ExporterImageDigestKey]
 			nameDigestOrTag := getRepoNameWithDigest(patchedImageName, digest)
 			// vex document must contain at least one statement


Test patch:
diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go
index 4242993b..0731843e 100644
--- a/pkg/patch/patch_test.go
+++ b/pkg/patch/patch_test.go
@@ -4,11 +4,16 @@ import (
 	"context"
 	"errors"
 	"os"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/distribution/reference"
+	"github.com/project-copacetic/copacetic/pkg/buildkit"
 	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
+
+	buildkitclient "github.com/moby/buildkit/client"
 )
 
 func TestRemoveIfNotDebug(t *testing.T) {
@@ -493,3 +498,30 @@ func TestResolvePatchedTag(t *testing.T) {
 		})
 	}
 }
+
+func init() {
+	bkNewClient = func(ctx context.Context, _ buildkit.Opts) (*buildkitclient.Client, error) {
+		// a path that certainly does not have a BuildKit daemon listening.
+		return buildkitclient.New(ctx, "unix:///tmp/nowhere.sock")
+	}
+}
+
+func TestPatch_BuildReturnsNilResponse(t *testing.T) {
+	err := Patch(
+		context.Background(),
+		30*time.Second,
+		"alpine:3.19", "", "", "", "", "", "", "", "", "",
+		false, true,
+		buildkit.Opts{},
+	)
+
+	if err == nil {
+		t.Fatalf("expected error from Build(), got nil")
+	}
+
+	if !strings.Contains(err.Error(), "dial unix /tmp/nowhere.sock: connect: no such file or directory") {
+		t.Fatalf("unexpected error from Build(): %v", err)
+	}
+
+	t.Logf("Patch returned error as expected (and did not panic): %v", err)
+}