Skip to content

Conversation

@kzantow
Copy link
Contributor

@kzantow kzantow commented Jul 28, 2025

This PR fixes an issue where reading a lazyBoundedReadCloser is unable to seek due to being closed but not reset properly. In other words: .Seek(0, io.SeekStart) attempts to call d.openFile(), but since there is a non-nil reference to a reader, it does not reopen the file but fails to seek and read from it afterwards instead of actually reopening it for reading.

@kzantow kzantow requested a review from a team July 28, 2025 16:24
@github-actions
Copy link

github-actions bot commented Jul 28, 2025

Benchmark Test Results

Benchmark results from the latest changes vs base branch
make .tool/task
make[1]: Entering directory '/home/runner/work/stereoscope/stereoscope'
make[1]: Leaving directory '/home/runner/work/stereoscope/stereoscope'
.tool/task show-benchstat
?   	github.com/anchore/stereoscope	[no test files]
?   	github.com/anchore/stereoscope/examples	[no test files]
PASS
ok  	github.com/anchore/stereoscope/internal	0.003s
?   	github.com/anchore/stereoscope/internal/bus	[no test files]
PASS
ok  	github.com/anchore/stereoscope/internal/containerd	0.007s
PASS
ok  	github.com/anchore/stereoscope/internal/docker	0.004s
?   	github.com/anchore/stereoscope/internal/log	[no test files]
PASS
ok  	github.com/anchore/stereoscope/internal/podman	0.004s
?   	github.com/anchore/stereoscope/pkg/event	[no test files]
?   	github.com/anchore/stereoscope/pkg/event/parsers	[no test files]
goos: linux
goarch: amd64
pkg: github.com/anchore/stereoscope/pkg/file
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkTarIndex-4   	   35785	     33484 ns/op	    5698 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35372	     33546 ns/op	    5697 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35496	     33552 ns/op	    5699 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35643	     34368 ns/op	    5700 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35816	     33535 ns/op	    5700 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35618	     33482 ns/op	    5699 B/op	      93 allocs/op
BenchmarkTarIndex-4   	   35970	     33271 ns/op	    5697 B/op	      93 allocs/op
PASS
ok  	github.com/anchore/stereoscope/pkg/file	10.790s
PASS
ok  	github.com/anchore/stereoscope/pkg/filetree	0.005s
?   	github.com/anchore/stereoscope/pkg/filetree/filenode	[no test files]
PASS
ok  	github.com/anchore/stereoscope/pkg/image	0.005s
PASS
ok  	github.com/anchore/stereoscope/pkg/image/containerd	0.007s
PASS
ok  	github.com/anchore/stereoscope/pkg/image/docker	0.005s
PASS
ok  	github.com/anchore/stereoscope/pkg/image/oci	0.005s
PASS
ok  	github.com/anchore/stereoscope/pkg/image/oci/credhelpers	0.005s
?   	github.com/anchore/stereoscope/pkg/image/podman	[no test files]
PASS
ok  	github.com/anchore/stereoscope/pkg/image/sif	0.004s
?   	github.com/anchore/stereoscope/pkg/imagetest	[no test files]
PASS
ok  	github.com/anchore/stereoscope/pkg/tree	0.003s
PASS
ok  	github.com/anchore/stereoscope/pkg/tree/node	0.003s
goos: linux
goarch: amd64
pkg: github.com/anchore/stereoscope/test/integration
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1126	   1037761 ns/op	  275462 B/op	    2239 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1135	   1039448 ns/op	  274946 B/op	    2239 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1135	   1037307 ns/op	  274898 B/op	    2239 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1150	   1105510 ns/op	  274586 B/op	    2238 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	     991	   1045270 ns/op	  274590 B/op	    2238 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1150	   1053026 ns/op	  274584 B/op	    2238 allocs/op
BenchmarkSimpleImage_GetImage/docker-archive-4 	    1131	   1022264 ns/op	  274359 B/op	    2238 allocs/op
--- FAIL: BenchmarkSimpleImage_GetImage/podman
    fixture_image_simple_test.go:175: could not get fixture image: unable to detect input for 'stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7', errs: podman not available: no host address
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 345B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load build context
#3 transferring context: 209B done
#3 DONE 0.0s

#4 [2/3] ADD file-2.txt /somefile-2.txt
#4 CACHED

#5 [1/3] ADD file-1.txt /somefile-1.txt
#5 CACHED

#6 [3/3] ADD target /
#6 CACHED

#7 exporting to image
#7 exporting layers done
#7 writing image sha256:90940e90cb0649cc630ec4eed4163570d136b724cb91709770b60e2c57e856e3 done
#7 naming to docker.io/library/stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7 done
#7 naming to docker.io/library/stereoscope-fixture-image-simple:latest done
#7 DONE 0.0s
ctr: failed to dial "/run/containerd/containerd.sock": connection error: desc = "transport: error while dialing: dial unix /run/containerd/containerd.sock: connect: permission denied"
--- FAIL: BenchmarkSimpleImage_GetImage
    image_fixtures.go:193: using existing image tar: 'test-fixtures/cache/stereoscope-fixture-image-simple-04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7.tar' (size: 22528, modified: 2025-07-28 16:41:36.06462854 +0000 UTC, mode: -rw-r--r--)
    image_fixtures.go:241: Build docker image: name="stereoscope-fixture-image-simple" tag="04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"
    image_fixtures.go:291: saveImage running: docker image save stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7
    image_fixtures.go:286: 
        	Error Trace:	/home/runner/work/stereoscope/stereoscope/pkg/imagetest/image_fixtures.go:286
        	            				/home/runner/work/stereoscope/stereoscope/pkg/imagetest/image_fixtures.go:162
        	            				/home/runner/work/stereoscope/stereoscope/pkg/imagetest/image_fixtures.go:152
        	            				/home/runner/work/stereoscope/stereoscope/pkg/imagetest/image_fixtures.go:33
        	            				/home/runner/work/stereoscope/stereoscope/test/integration/fixture_image_simple_test.go:163
        	Error:      	Received unexpected error:
        	            	exit status 1
        	Test:       	BenchmarkSimpleImage_GetImage
        	Messages:   	could not import docker image to containerd (shell out)
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61881	     19308 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61520	     19409 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61904	     19357 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61315	     19288 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61945	     19243 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   62026	     19319 ns/op	    2568 B/op	      18 allocs/op
BenchmarkSimpleImage_FetchSquashedContents/docker-archive-4         	   61981	     19337 ns/op	    2568 B/op	      18 allocs/op
--- FAIL: BenchmarkSimpleImage_FetchSquashedContents
    image_fixtures.go:193: using existing image tar: 'test-fixtures/cache/stereoscope-fixture-image-simple-04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7.tar' (size: 22528, modified: 2025-07-28 16:41:36.06462854 +0000 UTC, mode: -rw-r--r--)
    image_fixtures.go:75: error getting fixture image: 'podman' 'image-simple' with request 'podman:stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7': unable to detect input for 'stereoscope-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7', errs: podman not available: no host address
FAIL
exit status 1
FAIL	github.com/anchore/stereoscope/test/integration	19.517s
?   	github.com/anchore/stereoscope/test/integration/test-fixtures/registry	[no test files]
FAIL
goos: linux
goarch: amd64
pkg: github.com/anchore/stereoscope/pkg/file
cpu: AMD EPYC 7763 64-Core Processor                
ctr: 
           │ .tmp/benchmark-79f60cf.txt │
           │           sec/op           │
TarIndex-4                  33.54µ ± 2%

           │ .tmp/benchmark-79f60cf.txt │
           │            B/op            │
TarIndex-4                 5.565Ki ± 0%

           │ .tmp/benchmark-79f60cf.txt │
           │         allocs/op          │
TarIndex-4                   93.00 ± 0%

pkg: github.com/anchore/stereoscope/test/integration
                                      │ .tmp/benchmark-79f60cf.txt │
                                      │           sec/op           │
SimpleImage_GetImage/docker-archive-4                  1.039m ± 6%

                                      │ .tmp/benchmark-79f60cf.txt │
                                      │            B/op            │
SimpleImage_GetImage/docker-archive-4                 268.2Ki ± 0%

                                      │ .tmp/benchmark-79f60cf.txt │
                                      │         allocs/op          │
SimpleImage_GetImage/docker-archive-4                  2.238k ± 0%

ctr: failed to dial "/run/containerd/containerd.sock": connection error: desc = "transport: error while dialing: dial unix /run/containerd/containerd.sock: connect: permission denied"
                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │           sec/op           │
SimpleImage_FetchSquashedContents/docker-archive-4                  19.32µ ± 0%

                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │            B/op            │
SimpleImage_FetchSquashedContents/docker-archive-4                 2.508Ki ± 0%

                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │         allocs/op          │
SimpleImage_FetchSquashedContents/docker-archive-4                   18.00 ± 0%
goos: linux
goarch: amd64
pkg: github.com/anchore/stereoscope/pkg/file
cpu: AMD EPYC 7763 64-Core Processor                
ctr: 
           │ .tmp/benchmark-79f60cf.txt │
           │           sec/op           │
TarIndex-4                  33.54µ ± 2%

           │ .tmp/benchmark-79f60cf.txt │
           │            B/op            │
TarIndex-4                 5.565Ki ± 0%

           │ .tmp/benchmark-79f60cf.txt │
           │         allocs/op          │
TarIndex-4                   93.00 ± 0%

pkg: github.com/anchore/stereoscope/test/integration
                                      │ .tmp/benchmark-79f60cf.txt │
                                      │           sec/op           │
SimpleImage_GetImage/docker-archive-4                  1.039m ± 6%

                                      │ .tmp/benchmark-79f60cf.txt │
                                      │            B/op            │
SimpleImage_GetImage/docker-archive-4                 268.2Ki ± 0%

                                      │ .tmp/benchmark-79f60cf.txt │
                                      │         allocs/op          │
SimpleImage_GetImage/docker-archive-4                  2.238k ± 0%

ctr: failed to dial "/run/containerd/containerd.sock": connection error: desc = "transport: error while dialing: dial unix /run/containerd/containerd.sock: connect: permission denied"
                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │           sec/op           │
SimpleImage_FetchSquashedContents/docker-archive-4                  19.32µ ± 0%

                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │            B/op            │
SimpleImage_FetchSquashedContents/docker-archive-4                 2.508Ki ± 0%

                                                   │ .tmp/benchmark-79f60cf.txt │
                                                   │         allocs/op          │
SimpleImage_FetchSquashedContents/docker-archive-4                   18.00 ± 0%

@kzantow kzantow force-pushed the fix/seekable-readers branch from 7493937 to 0b61951 Compare July 28, 2025 16:40
if err != nil && errors.Is(err, io.EOF) {
// we've reached the end of the file, force a release of the file descriptor. If the file has already been
// closed, ignore the error.
if closeErr := d.file.Close(); !errors.Is(closeErr, os.ErrClosed) {
Copy link
Contributor

@spiffcs spiffcs Jul 28, 2025

Choose a reason for hiding this comment

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

Review note for self: d.Close() and d.file.Close() are different. d.Close() does not hoist from d.file.Close(). It's using: https://github.com/anchore/stereoscope/blob/main/pkg/file/lazy_bounded_read_closer.go#L53-L66

d.Close() calls d.file.Close() while also resetting d.file and d.reader to nil
closeErr cannot be os.ErrClosed given this err type is already accounted for by d.Close() and ignored.

@kzantow what was the original issue where you saw this behave weird when it wasn't reset?

I just want to know how this manifested in the wild so I can understand the change before 🟢

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is evident in syft main branch, JDK binary detection against an image. For example:

go run ./cmd/syft ibmjava:8-jre@sha256:3588cd1cc9b8646fe03b3b15210e69b1b520f1321f8518b69c0e7013d702fd23 | grep '^java '

... returns no results. The reason for this is the java binary is read multiple times, calling .Seek here but stereoscope maintains a reference to the closed file, preventing further .Seek calls from opening it again, whereas d.Close sets this to nil, resulting in the next d.openFile() call to actually open the file again for reading. I don't think the error returned from Close should be returned, because this only occurs when EOF is returned and if EOF is not returned to the original caller, there are cases it may not proceed or retry and get in an infinite loop, etc..

Tests are passing in syft because they are using a directory source, not an image source. 😢

With this change, you should see:

go run ./cmd/syft ibmjava:8-jre@sha256:3588cd1cc9b8646fe03b3b15210e69b1b520f1321f8518b69c0e7013d702fd23 | grep '^java '

...
java                 1.8.0-_2025_04_14_02_37-b00              binary                        

Copy link
Contributor

@spiffcs spiffcs Jul 29, 2025

Choose a reason for hiding this comment

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

Awesome - was able to reproduce on my end with the new change. TY for the walk through.

I don't think the error returned from Close should be returned, because this only occurs when EOF is returned and if EOF is not returned to the original caller, there are cases it may not proceed or retry and get in an infinite loop, etc..

Hard agree on the above change is 🟢 for me.

// we've reached the end of the file, release of the file descriptor. continue to return EOF
// IMPORTANT: call d.Close to reset internal state
if closeErr := d.Close(); closeErr != nil {
log.Tracef("unable to close: %v: %v", d.path, closeErr)
Copy link
Contributor Author

@kzantow kzantow Jul 28, 2025

Choose a reason for hiding this comment

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

I'm not entirely sure the best behavior here, can certainly be convinced to change this... but this is only hit if we received EOF during the Read call, which will be returned to the caller and we're managing open file handles behind the scenes, so while an error closing here can indicate a problem, there's not much that a caller can do about this if it fails and they are already receiving an EOF, so should stop further reading anyway unless they Seek, at which time we need to be able to reopen the file and a serious problem like the file being removed will cause an error then, more appropriately, etc.

@kzantow kzantow merged commit 30b639c into main Jul 29, 2025
7 checks passed
@kzantow kzantow deleted the fix/seekable-readers branch July 29, 2025 13:13
@kzantow kzantow added the bug Something isn't working label Jul 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants