1717package container
1818
1919import (
20+ "archive/tar"
21+ "bytes"
22+ "compress/gzip"
23+ "context"
24+ "encoding/json"
25+ "errors"
26+ "fmt"
27+ "io"
28+ "path/filepath"
2029 "testing"
2130
31+ digest "github.com/opencontainers/go-digest"
32+ imagespecversioned "github.com/opencontainers/image-spec/specs-go"
33+ imagespec "github.com/opencontainers/image-spec/specs-go/v1"
34+ "gotest.tools/v3/assert"
35+
36+ containerd "github.com/containerd/containerd/v2/client"
37+ "github.com/containerd/containerd/v2/core/content"
38+ "github.com/containerd/containerd/v2/core/images"
39+ "github.com/containerd/containerd/v2/defaults"
40+ "github.com/containerd/nerdctl/mod/tigron/expect"
2241 "github.com/containerd/nerdctl/mod/tigron/require"
2342 "github.com/containerd/nerdctl/mod/tigron/test"
43+ "github.com/containerd/nerdctl/mod/tigron/tig"
44+ "github.com/containerd/platforms"
45+ "github.com/containerd/stargz-snapshotter/estargz"
2446
47+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
2548 "github.com/containerd/nerdctl/v2/pkg/testutil"
2649 "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
50+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry"
2751)
2852
2953func TestRunStargz (t * testing.T ) {
@@ -41,3 +65,148 @@ func TestRunStargz(t *testing.T) {
4165
4266 testCase .Run (t )
4367}
68+
69+ // createInvalidESGZImage creates an eStargz image from the first layer of srcRef (an eStargz image) with an extra
70+ // file added to the layer, while keeping the original image config. As the diffID in the config (the one of srcRef)
71+ // doesn't match the modified layer, the resulting image has the same ChainID as srcRef but different contents and
72+ // TOC digest. The image is stored in containerd as newRef.
73+ func createInvalidESGZImage (ctx context.Context , t tig.T , client * containerd.Client , srcRef , newRef string ) {
74+ cs := client .ContentStore ()
75+ srcImg , err := client .GetImage (ctx , srcRef )
76+ assert .NilError (t , err )
77+ srcManifest , err := images .Manifest (ctx , cs , srcImg .Target (), platforms .Default ())
78+ assert .NilError (t , err )
79+ layer , err := content .ReadBlob (ctx , cs , srcManifest .Layers [0 ])
80+ assert .NilError (t , err )
81+
82+ var modified bytes.Buffer
83+ tw := tar .NewWriter (& modified )
84+ gz , err := gzip .NewReader (bytes .NewReader (layer ))
85+ assert .NilError (t , err )
86+ tr := tar .NewReader (gz )
87+ for {
88+ h , err := tr .Next ()
89+ if err == io .EOF {
90+ break
91+ }
92+ assert .NilError (t , err )
93+ assert .NilError (t , tw .WriteHeader (h ))
94+ _ , err = io .Copy (tw , tr )
95+ assert .NilError (t , err )
96+ }
97+ contents := "MODIFIED"
98+ assert .NilError (t , tw .WriteHeader (& tar.Header {Typeflag : tar .TypeReg , Name : "/modified.txt" , Size : int64 (len (contents ))}))
99+ _ , err = tw .Write ([]byte (contents ))
100+ assert .NilError (t , err )
101+ assert .NilError (t , tw .Close ())
102+
103+ modifiedData := modified .Bytes ()
104+ esgz , err := estargz .Build (io .NewSectionReader (bytes .NewReader (modifiedData ), 0 , int64 (len (modifiedData ))))
105+ assert .NilError (t , err )
106+ defer esgz .Close ()
107+ layerData , err := io .ReadAll (esgz )
108+ assert .NilError (t , err )
109+ layerDesc := imagespec.Descriptor {
110+ MediaType : imagespec .MediaTypeImageLayerGzip ,
111+ Digest : digest .FromBytes (layerData ),
112+ Size : int64 (len (layerData )),
113+ Annotations : map [string ]string {estargz .TOCJSONDigestAnnotation : esgz .TOCDigest ().String ()},
114+ }
115+ assert .NilError (t , content .WriteBlob (ctx , cs , "layer-invalid" , bytes .NewReader (layerData ), layerDesc ))
116+
117+ srcConfig , err := images .Config (ctx , cs , srcImg .Target (), platforms .Default ())
118+ assert .NilError (t , err )
119+ manifest := & imagespec.Manifest {
120+ Versioned : imagespecversioned.Versioned {SchemaVersion : 2 },
121+ MediaType : imagespec .MediaTypeImageManifest ,
122+ Config : srcConfig ,
123+ Layers : []imagespec.Descriptor {layerDesc },
124+ }
125+ manifestBlob , err := json .Marshal (manifest )
126+ assert .NilError (t , err )
127+ manifestDesc := imagespec.Descriptor {
128+ MediaType : imagespec .MediaTypeImageManifest ,
129+ Digest : digest .FromBytes (manifestBlob ),
130+ Size : int64 (len (manifestBlob )),
131+ }
132+ labels := map [string ]string {}
133+ for i , d := range []imagespec.Descriptor {layerDesc , srcConfig } {
134+ labels [fmt .Sprintf ("containerd.io/gc.ref.content.%d" , i )] = d .Digest .String ()
135+ }
136+ assert .NilError (t , content .WriteBlob (ctx , cs , "manifest-invalid" , bytes .NewReader (manifestBlob ), manifestDesc , content .WithLabels (labels )))
137+ _ , err = client .ImageService ().Create (ctx , images.Image {Name : newRef , Target : manifestDesc })
138+ assert .NilError (t , err )
139+ }
140+
141+ func newTestContainerdClient (helpers test.Helpers ) * containerd.Client {
142+ addr := defaults .DefaultAddress
143+ if rootlessutil .IsRootless () {
144+ stateDir , err := rootlessutil .RootlessKitStateDir ()
145+ assert .NilError (helpers .T (), err )
146+ childPid , err := rootlessutil .RootlessKitChildPid (stateDir )
147+ assert .NilError (helpers .T (), err )
148+ addr = filepath .Join ("/proc" , fmt .Sprintf ("%d" , childPid ), "root" , defaults .DefaultAddress )
149+ }
150+ client , err := containerd .New (addr , containerd .WithDefaultNamespace (string (helpers .Read (nerdtest .Namespace ))))
151+ assert .NilError (helpers .T (), err )
152+ return client
153+ }
154+
155+ func TestPullStargzInvalidChainID (t * testing.T ) {
156+ testCase := nerdtest .Setup ()
157+
158+ testCase .Require = require .All (
159+ nerdtest .Stargz ,
160+ nerdtest .Registry ,
161+ require .Amd64 ,
162+ require .Not (nerdtest .Docker ),
163+ )
164+
165+ var reg * registry.Server
166+
167+ testCase .Setup = func (data test.Data , helpers test.Helpers ) {
168+ reg = nerdtest .RegistryWithNoAuth (data , helpers , 0 , false )
169+ reg .Setup (data , helpers )
170+ base := fmt .Sprintf ("127.0.0.1:%d/alpine" , reg .Port )
171+ esgz := base + ":esgz"
172+ invalid := base + ":esgz-invalid"
173+
174+ // A valid eStargz image
175+ helpers .Ensure ("pull" , "--quiet" , testutil .AlpineImage )
176+ helpers .Ensure ("image" , "convert" , "--estargz" , "--oci" , testutil .AlpineImage , esgz )
177+ helpers .Ensure ("push" , esgz )
178+
179+ // An eStargz image whose layer doesn't match the diffID in the config
180+ client := newTestContainerdClient (helpers )
181+ defer client .Close ()
182+ createInvalidESGZImage (context .Background (), helpers .T (), client , esgz , invalid )
183+ helpers .Ensure ("push" , invalid )
184+
185+ // Remove local copies so that the following pulls fetch from the registry
186+ helpers .Ensure ("rmi" , "-f" , esgz , invalid )
187+
188+ data .Labels ().Set ("esgz" , esgz )
189+ data .Labels ().Set ("invalid" , invalid )
190+ }
191+
192+ testCase .Cleanup = func (data test.Data , helpers test.Helpers ) {
193+ if v := data .Labels ().Get ("esgz" ); v != "" {
194+ helpers .Anyhow ("rmi" , "-f" , v )
195+ }
196+ if v := data .Labels ().Get ("invalid" ); v != "" {
197+ helpers .Anyhow ("rmi" , "-f" , v )
198+ }
199+ if reg != nil {
200+ reg .Cleanup (data , helpers )
201+ }
202+ }
203+
204+ testCase .Command = func (data test.Data , helpers test.Helpers ) test.TestableCommand {
205+ helpers .Ensure ("--snapshotter=stargz" , "pull" , data .Labels ().Get ("invalid" ))
206+ return helpers .Command ("--snapshotter=stargz" , "pull" , data .Labels ().Get ("esgz" ))
207+ }
208+
209+ testCase .Expected = test .Expects (expect .ExitCodeGenericFail , []error {errors .New ("existing images contain invalid ChainIDs" )}, nil )
210+
211+ testCase .Run (t )
212+ }
0 commit comments