From: Narpat Mali <[email protected]>

A flaw was found in the `/v2/_catalog` endpoint in distribution/distribution,
which accepts a parameter to control the maximum number of records returned
(query string: `n`). This vulnerability allows a malicious user to submit an
unreasonably large value for `n,` causing the allocation of a massive string
array, possibly causing a denial of service through excessive use of memory.

References:
https://github.com/distribution/distribution/security/advisories/GHSA-hqxw-f8mx-cpmw
https://github.com/distribution/distribution/commit/521ea3d973cb0c7089ebbcdd4ccadc34be941f54

Signed-off-by: Narpat Mali <[email protected]>
---
 .../docker-distribution_git.bb                |   1 +
 ...ix-runaway-allocation-on-v2-_catalog.patch | 669 ++++++++++++++++++
 2 files changed, 670 insertions(+)
 create mode 100644 
recipes-containers/docker-distribution/files/0001-Fix-runaway-allocation-on-v2-_catalog.patch

diff --git a/recipes-containers/docker-distribution/docker-distribution_git.bb 
b/recipes-containers/docker-distribution/docker-distribution_git.bb
index 859b27b8..8c7fefe3 100644
--- a/recipes-containers/docker-distribution/docker-distribution_git.bb
+++ b/recipes-containers/docker-distribution/docker-distribution_git.bb
@@ -7,6 +7,7 @@ SRCREV_distribution= "b5ca020cfbe998e5af3457fda087444cf5116496"
 SRC_URI = 
"git://github.com/docker/distribution.git;branch=release/2.8;name=distribution;destsuffix=git/src/github.com/docker/distribution;protocol=https
 \
            file://docker-registry.service \
            file://0001-build-use-to-use-cross-go-compiler.patch \
+           file://0001-Fix-runaway-allocation-on-v2-_catalog.patch \
           "
 
 PACKAGES =+ "docker-registry"
diff --git 
a/recipes-containers/docker-distribution/files/0001-Fix-runaway-allocation-on-v2-_catalog.patch
 
b/recipes-containers/docker-distribution/files/0001-Fix-runaway-allocation-on-v2-_catalog.patch
new file mode 100644
index 00000000..69da7054
--- /dev/null
+++ 
b/recipes-containers/docker-distribution/files/0001-Fix-runaway-allocation-on-v2-_catalog.patch
@@ -0,0 +1,669 @@
+From 521ea3d973cb0c7089ebbcdd4ccadc34be941f54 Mon Sep 17 00:00:00 2001
+From: "Jose D. Gomez R" <[email protected]>
+Date: Mon, 24 Apr 2023 18:52:27 +0200
+Subject: [PATCH] Fix runaway allocation on /v2/_catalog
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Introduced a Catalog entry in the configuration struct. With it,
+it's possible to control the maximum amount of entries returned
+by /v2/catalog (`GetCatalog` in registry/handlers/catalog.go).
+
+It's set to a default value of 1000.
+
+`GetCatalog` returns 100 entries by default if no `n` is
+provided. When provided it will be validated to be between `0`
+and `MaxEntries` defined in Configuration. When `n` is outside
+the aforementioned boundary, ErrorCodePaginationNumberInvalid is
+returned.
+
+`GetCatalog` now handles `n=0` gracefully with an empty response
+as well.
+
+Signed-off-by: José D. Gómez R. <[email protected]>
+Co-authored-by: Cory Snider <[email protected]>
+
+CVE: CVE-2023-2253
+
+Upstream-Status: Backport 
[https://github.com/distribution/distribution/commit/521ea3d973cb0c7089ebbcdd4ccadc34be941f54]
+
+Signed-off-by: Narpat Mali <[email protected]>
+---
+ configuration/configuration.go      |  18 +-
+ configuration/configuration_test.go |   4 +
+ registry/api/v2/descriptors.go      |  17 ++
+ registry/api/v2/errors.go           |   9 +
+ registry/handlers/api_test.go       | 316 +++++++++++++++++++++++++---
+ registry/handlers/catalog.go        |  54 +++--
+ 6 files changed, 376 insertions(+), 42 deletions(-)
+
+diff --git a/configuration/configuration.go b/configuration/configuration.go
+index dd315485..1e696613 100644
+--- a/configuration/configuration.go
++++ b/configuration/configuration.go
+@@ -193,7 +193,8 @@ type Configuration struct {
+               } `yaml:"pool,omitempty"`
+       } `yaml:"redis,omitempty"`
+
+-      Health Health `yaml:"health,omitempty"`
++      Health  Health  `yaml:"health,omitempty"`
++      Catalog Catalog `yaml:"catalog,omitempty"`
+
+       Proxy Proxy `yaml:"proxy,omitempty"`
+
+@@ -244,6 +245,16 @@ type Configuration struct {
+       } `yaml:"policy,omitempty"`
+ }
+
++// Catalog is composed of MaxEntries.
++// Catalog endpoint (/v2/_catalog) configuration, it provides the 
configuration
++// options to control the maximum number of entries returned by the catalog 
endpoint.
++type Catalog struct {
++      // Max number of entries returned by the catalog endpoint. Requesting n 
entries
++      // to the catalog endpoint will return at most MaxEntries entries.
++      // An empty or a negative value will set a default of 1000 maximum 
entries by default.
++      MaxEntries int `yaml:"maxentries,omitempty"`
++}
++
+ // LogHook is composed of hook Level and Type.
+ // After hooks configuration, it can execute the next handling automatically,
+ // when defined levels of log message emitted.
+@@ -670,6 +681,11 @@ func Parse(rd io.Reader) (*Configuration, error) {
+                                       if v0_1.Loglevel != Loglevel("") {
+                                               v0_1.Loglevel = Loglevel("")
+                                       }
++
++                                      if v0_1.Catalog.MaxEntries <= 0 {
++                                              v0_1.Catalog.MaxEntries = 1000
++                                      }
++
+                                       if v0_1.Storage.Type() == "" {
+                                               return nil, errors.New("no 
storage configuration provided")
+                                       }
+diff --git a/configuration/configuration_test.go 
b/configuration/configuration_test.go
+index 0d6136e1..48cc9980 100644
+--- a/configuration/configuration_test.go
++++ b/configuration/configuration_test.go
+@@ -71,6 +71,9 @@ var configStruct = Configuration{
+                       },
+               },
+       },
++      Catalog: Catalog{
++              MaxEntries: 1000,
++      },
+       HTTP: struct {
+               Addr         string        `yaml:"addr,omitempty"`
+               Net          string        `yaml:"net,omitempty"`
+@@ -524,6 +527,7 @@ func copyConfig(config Configuration) *Configuration {
+       configCopy.Version = MajorMinorVersion(config.Version.Major(), 
config.Version.Minor())
+       configCopy.Loglevel = config.Loglevel
+       configCopy.Log = config.Log
++      configCopy.Catalog = config.Catalog
+       configCopy.Log.Fields = make(map[string]interface{}, 
len(config.Log.Fields))
+       for k, v := range config.Log.Fields {
+               configCopy.Log.Fields[k] = v
+diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go
+index a9616c58..c3bf90f7 100644
+--- a/registry/api/v2/descriptors.go
++++ b/registry/api/v2/descriptors.go
+@@ -134,6 +134,19 @@ var (
+               },
+       }
+
++      invalidPaginationResponseDescriptor = ResponseDescriptor{
++              Name:        "Invalid pagination number",
++              Description: "The received parameter n was invalid in some way, 
as described by the error code. The client should resolve the issue and retry 
the request.",
++              StatusCode:  http.StatusBadRequest,
++              Body: BodyDescriptor{
++                      ContentType: "application/json",
++                      Format:      errorsBody,
++              },
++              ErrorCodes: []errcode.ErrorCode{
++                      ErrorCodePaginationNumberInvalid,
++              },
++      }
++
+       repositoryNotFoundResponseDescriptor = ResponseDescriptor{
+               Name:        "No Such Repository Error",
+               StatusCode:  http.StatusNotFound,
+@@ -490,6 +503,7 @@ var routeDescriptors = []RouteDescriptor{
+                                                       },
+                                               },
+                                               Failures: []ResponseDescriptor{
++                                                      
invalidPaginationResponseDescriptor,
+                                                       
unauthorizedResponseDescriptor,
+                                                       
repositoryNotFoundResponseDescriptor,
+                                                       
deniedResponseDescriptor,
+@@ -1578,6 +1592,9 @@ var routeDescriptors = []RouteDescriptor{
+                                                               },
+                                                       },
+                                               },
++                                              Failures: []ResponseDescriptor{
++                                                      
invalidPaginationResponseDescriptor,
++                                              },
+                                       },
+                               },
+                       },
+diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go
+index 97d6923a..87e9f3c1 100644
+--- a/registry/api/v2/errors.go
++++ b/registry/api/v2/errors.go
+@@ -133,4 +133,13 @@ var (
+               longer proceed.`,
+               HTTPStatusCode: http.StatusNotFound,
+       })
++
++      ErrorCodePaginationNumberInvalid = errcode.Register(errGroup, 
errcode.ErrorDescriptor{
++              Value:   "PAGINATION_NUMBER_INVALID",
++              Message: "invalid number of results requested",
++              Description: `Returned when the "n" parameter (number of results
++              to return) is not an integer, "n" is negative or "n" is bigger 
than
++              the maximum allowed.`,
++              HTTPStatusCode: http.StatusBadRequest,
++      })
+ )
+diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go
+index 2d3edc74..bf037d45 100644
+--- a/registry/handlers/api_test.go
++++ b/registry/handlers/api_test.go
+@@ -81,21 +81,23 @@ func TestCheckAPI(t *testing.T) {
+
+ // TestCatalogAPI tests the /v2/_catalog endpoint
+ func TestCatalogAPI(t *testing.T) {
+-      chunkLen := 2
+       env := newTestEnv(t, false)
+       defer env.Shutdown()
+
+-      values := url.Values{
+-              "last": []string{""},
+-              "n":    []string{strconv.Itoa(chunkLen)}}
++      maxEntries := env.config.Catalog.MaxEntries
++      allCatalog := []string{
++              "foo/aaaa", "foo/bbbb", "foo/cccc", "foo/dddd", "foo/eeee", 
"foo/ffff",
++      }
+
+-      catalogURL, err := env.builder.BuildCatalogURL(values)
++      chunkLen := maxEntries - 1
++
++      catalogURL, err := env.builder.BuildCatalogURL()
+       if err != nil {
+               t.Fatalf("unexpected error building catalog url: %v", err)
+       }
+
+       // -----------------------------------
+-      // try to get an empty catalog
++      // Case No. 1: Empty catalog
+       resp, err := http.Get(catalogURL)
+       if err != nil {
+               t.Fatalf("unexpected error issuing request: %v", err)
+@@ -113,23 +115,22 @@ func TestCatalogAPI(t *testing.T) {
+               t.Fatalf("error decoding fetched manifest: %v", err)
+       }
+
+-      // we haven't pushed anything to the registry yet
++      // No images pushed = no image returned
+       if len(ctlg.Repositories) != 0 {
+-              t.Fatalf("repositories has unexpected values")
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", 0, len(ctlg.Repositories))
+       }
+
++      // No pagination should be returned
+       if resp.Header.Get("Link") != "" {
+               t.Fatalf("repositories has more data when none expected")
+       }
+
+-      // -----------------------------------
+-      // push something to the registry and try again
+-      images := []string{"foo/aaaa", "foo/bbbb", "foo/cccc"}
+-
+-      for _, image := range images {
++      for _, image := range allCatalog {
+               createRepository(env, t, image, "sometag")
+       }
+
++      // -----------------------------------
++      // Case No. 2: Catalog populated & n is not provided nil (n internally 
will be min(100, maxEntries))
+       resp, err = http.Get(catalogURL)
+       if err != nil {
+               t.Fatalf("unexpected error issuing request: %v", err)
+@@ -143,27 +144,60 @@ func TestCatalogAPI(t *testing.T) {
+               t.Fatalf("error decoding fetched manifest: %v", err)
+       }
+
+-      if len(ctlg.Repositories) != chunkLen {
+-              t.Fatalf("repositories has unexpected values")
++      // it must match max entries
++      if len(ctlg.Repositories) != maxEntries {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", maxEntries, len(ctlg.Repositories))
+       }
+
+-      for _, image := range images[:chunkLen] {
++      // it must return the first maxEntries entries from the catalog
++      for _, image := range allCatalog[:maxEntries] {
+               if !contains(ctlg.Repositories, image) {
+                       t.Fatalf("didn't find our repository '%s' in the 
catalog", image)
+               }
+       }
+
++      // fail if there's no pagination
+       link := resp.Header.Get("Link")
+       if link == "" {
+               t.Fatalf("repositories has less data than expected")
+       }
++      // -----------------------------------
++      // Case No. 2.1: Second page (n internally will be min(100, maxEntries))
++
++      // build pagination link
++      values := checkLink(t, link, maxEntries, 
ctlg.Repositories[len(ctlg.Repositories)-1])
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
+
+-      newValues := checkLink(t, link, chunkLen, 
ctlg.Repositories[len(ctlg.Repositories)-1])
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      expectedRemainder := len(allCatalog) - maxEntries
++      if len(ctlg.Repositories) != expectedRemainder {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", expectedRemainder, len(ctlg.Repositories))
++      }
+
+       // -----------------------------------
+-      // get the last chunk of data
++      // Case No. 3: request n = maxentries
++      values = url.Values{
++              "last": []string{""},
++              "n":    []string{strconv.Itoa(maxEntries)},
++      }
+
+-      catalogURL, err = env.builder.BuildCatalogURL(newValues)
++      catalogURL, err = env.builder.BuildCatalogURL(values)
+       if err != nil {
+               t.Fatalf("unexpected error building catalog url: %v", err)
+       }
+@@ -181,18 +215,239 @@ func TestCatalogAPI(t *testing.T) {
+               t.Fatalf("error decoding fetched manifest: %v", err)
+       }
+
+-      if len(ctlg.Repositories) != 1 {
+-              t.Fatalf("repositories has unexpected values")
++      if len(ctlg.Repositories) != maxEntries {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", maxEntries, len(ctlg.Repositories))
+       }
+
+-      lastImage := images[len(images)-1]
+-      if !contains(ctlg.Repositories, lastImage) {
+-              t.Fatalf("didn't find our repository '%s' in the catalog", 
lastImage)
++      // fail if there's no pagination
++      link = resp.Header.Get("Link")
++      if link == "" {
++              t.Fatalf("repositories has less data than expected")
++      }
++
++      // -----------------------------------
++      // Case No. 3.1: Second (last) page
++
++      // build pagination link
++      values = checkLink(t, link, maxEntries, 
ctlg.Repositories[len(ctlg.Repositories)-1])
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
+       }
+
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      expectedRemainder = len(allCatalog) - maxEntries
++      if len(ctlg.Repositories) != expectedRemainder {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", expectedRemainder, len(ctlg.Repositories))
++      }
++
++      // -----------------------------------
++      // Case No. 4: request n < maxentries
++      values = url.Values{
++              "n": []string{strconv.Itoa(chunkLen)},
++      }
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      // returns the requested amount
++      if len(ctlg.Repositories) != chunkLen {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", expectedRemainder, len(ctlg.Repositories))
++      }
++
++      // fail if there's no pagination
+       link = resp.Header.Get("Link")
+-      if link != "" {
+-              t.Fatalf("catalog has unexpected data")
++      if link == "" {
++              t.Fatalf("repositories has less data than expected")
++      }
++
++      // -----------------------------------
++      // Case No. 4.1: request n < maxentries (second page)
++
++      // build pagination link
++      values = checkLink(t, link, chunkLen, 
ctlg.Repositories[len(ctlg.Repositories)-1])
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      expectedRemainder = len(allCatalog) - chunkLen
++      if len(ctlg.Repositories) != expectedRemainder {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", expectedRemainder, len(ctlg.Repositories))
++      }
++
++      // -----------------------------------
++      // Case No. 5: request n > maxentries | return err: 
ErrorCodePaginationNumberInvalid
++      values = url.Values{
++              "n": []string{strconv.Itoa(maxEntries + 10)},
++      }
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, 
http.StatusBadRequest)
++      checkBodyHasErrorCodes(t, "invalid number of results requested", resp, 
v2.ErrorCodePaginationNumberInvalid)
++
++      // -----------------------------------
++      // Case No. 6: request n > maxentries but <= total catalog | return 
err: ErrorCodePaginationNumberInvalid
++      values = url.Values{
++              "n": []string{strconv.Itoa(len(allCatalog))},
++      }
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, 
http.StatusBadRequest)
++      checkBodyHasErrorCodes(t, "invalid number of results requested", resp, 
v2.ErrorCodePaginationNumberInvalid)
++
++      // -----------------------------------
++      // Case No. 7: n = 0 | n is set to max(0, min(defaultEntries, 
maxEntries))
++      values = url.Values{
++              "n": []string{"0"},
++      }
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      // it must be empty
++      if len(ctlg.Repositories) != 0 {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", 0, len(ctlg.Repositories))
++      }
++
++      // -----------------------------------
++      // Case No. 8: n = -1 | n is set to max(0, min(defaultEntries, 
maxEntries))
++      values = url.Values{
++              "n": []string{"-1"},
++      }
++
++      catalogURL, err = env.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      // it must match max entries
++      if len(ctlg.Repositories) != maxEntries {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", expectedRemainder, len(ctlg.Repositories))
++      }
++
++      // -----------------------------------
++      // Case No. 9: n = 5, max = 5, total catalog = 4
++      values = url.Values{
++              "n": []string{strconv.Itoa(maxEntries)},
++      }
++
++      envWithLessImages := newTestEnv(t, false)
++      for _, image := range allCatalog[0:(maxEntries - 1)] {
++              createRepository(envWithLessImages, t, image, "sometag")
++      }
++
++      catalogURL, err = envWithLessImages.builder.BuildCatalogURL(values)
++      if err != nil {
++              t.Fatalf("unexpected error building catalog url: %v", err)
++      }
++
++      resp, err = http.Get(catalogURL)
++      if err != nil {
++              t.Fatalf("unexpected error issuing request: %v", err)
++      }
++      defer resp.Body.Close()
++
++      checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
++
++      dec = json.NewDecoder(resp.Body)
++      if err = dec.Decode(&ctlg); err != nil {
++              t.Fatalf("error decoding fetched manifest: %v", err)
++      }
++
++      // it must match max entries
++      if len(ctlg.Repositories) != maxEntries-1 {
++              t.Fatalf("repositories returned unexpected entries (expected: 
%d, returned: %d)", maxEntries-1, len(ctlg.Repositories))
+       }
+ }
+
+@@ -207,7 +462,7 @@ func checkLink(t *testing.T, urlStr string, numEntries 
int, last string) url.Val
+       urlValues := linkURL.Query()
+
+       if urlValues.Get("n") != strconv.Itoa(numEntries) {
+-              t.Fatalf("Catalog link entry size is incorrect")
++              t.Fatalf("Catalog link entry size is incorrect (expected: %v, 
returned: %v)", urlValues.Get("n"), strconv.Itoa(numEntries))
+       }
+
+       if urlValues.Get("last") != last {
+@@ -2023,6 +2278,9 @@ func newTestEnvMirror(t *testing.T, deleteEnabled bool) 
*testEnv {
+               Proxy: configuration.Proxy{
+                       RemoteURL: "http://example.com";,
+               },
++              Catalog: configuration.Catalog{
++                      MaxEntries: 5,
++              },
+       }
+       config.Compatibility.Schema1.Enabled = true
+
+@@ -2039,6 +2297,9 @@ func newTestEnv(t *testing.T, deleteEnabled bool) 
*testEnv {
+                               "enabled": false,
+                       }},
+               },
++              Catalog: configuration.Catalog{
++                      MaxEntries: 5,
++              },
+       }
+
+       config.Compatibility.Schema1.Enabled = true
+@@ -2291,7 +2552,6 @@ func checkResponse(t *testing.T, msg string, resp 
*http.Response, expectedStatus
+       if resp.StatusCode != expectedStatus {
+               t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, 
expectedStatus)
+               maybeDumpResponse(t, resp)
+-
+               t.FailNow()
+       }
+
+diff --git a/registry/handlers/catalog.go b/registry/handlers/catalog.go
+index eca98468..83ec0a9c 100644
+--- a/registry/handlers/catalog.go
++++ b/registry/handlers/catalog.go
+@@ -9,11 +9,13 @@ import (
+       "strconv"
+
+       "github.com/docker/distribution/registry/api/errcode"
++      v2 "github.com/docker/distribution/registry/api/v2"
+       "github.com/docker/distribution/registry/storage/driver"
++
+       "github.com/gorilla/handlers"
+ )
+
+-const maximumReturnedEntries = 100
++const defaultReturnedEntries = 100
+
+ func catalogDispatcher(ctx *Context, r *http.Request) http.Handler {
+       catalogHandler := &catalogHandler{
+@@ -38,29 +40,55 @@ func (ch *catalogHandler) GetCatalog(w 
http.ResponseWriter, r *http.Request) {
+
+       q := r.URL.Query()
+       lastEntry := q.Get("last")
+-      maxEntries, err := strconv.Atoi(q.Get("n"))
+-      if err != nil || maxEntries < 0 {
+-              maxEntries = maximumReturnedEntries
++
++      entries := defaultReturnedEntries
++      maximumConfiguredEntries := ch.App.Config.Catalog.MaxEntries
++
++      // parse n, if n unparseable, or negative assign it to 
defaultReturnedEntries
++      if n := q.Get("n"); n != "" {
++              parsedMax, err := strconv.Atoi(n)
++              if err == nil {
++                      if parsedMax > maximumConfiguredEntries {
++                              ch.Errors = append(ch.Errors, 
v2.ErrorCodePaginationNumberInvalid.WithDetail(map[string]int{"n": parsedMax}))
++                              return
++                      } else if parsedMax >= 0 {
++                              entries = parsedMax
++                      }
++              }
+       }
+
+-      repos := make([]string, maxEntries)
++      // then enforce entries to be between 0 & maximumConfiguredEntries
++      // max(0, min(entries, maximumConfiguredEntries))
++      if entries < 0 || entries > maximumConfiguredEntries {
++              entries = maximumConfiguredEntries
++      }
+
+-      filled, err := ch.App.registry.Repositories(ch.Context, repos, 
lastEntry)
+-      _, pathNotFound := err.(driver.PathNotFoundError)
++      repos := make([]string, entries)
++      filled := 0
+
+-      if err == io.EOF || pathNotFound {
++      // entries is guaranteed to be >= 0 and < maximumConfiguredEntries
++      if entries == 0 {
+               moreEntries = false
+-      } else if err != nil {
+-              ch.Errors = append(ch.Errors, 
errcode.ErrorCodeUnknown.WithDetail(err))
+-              return
++      } else {
++              returnedRepositories, err := 
ch.App.registry.Repositories(ch.Context, repos, lastEntry)
++              if err != nil {
++                      _, pathNotFound := err.(driver.PathNotFoundError)
++                      if err != io.EOF && !pathNotFound {
++                              ch.Errors = append(ch.Errors, 
errcode.ErrorCodeUnknown.WithDetail(err))
++                              return
++                      }
++                      // err is either io.EOF or not PathNotFoundError
++                      moreEntries = false
++              }
++              filled = returnedRepositories
+       }
+
+       w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+       // Add a link header if there are more entries to retrieve
+       if moreEntries {
+-              lastEntry = repos[len(repos)-1]
+-              urlStr, err := createLinkEntry(r.URL.String(), maxEntries, 
lastEntry)
++              lastEntry = repos[filled-1]
++              urlStr, err := createLinkEntry(r.URL.String(), entries, 
lastEntry)
+               if err != nil {
+                       ch.Errors = append(ch.Errors, 
errcode.ErrorCodeUnknown.WithDetail(err))
+                       return
+--
+2.40.0
-- 
2.40.0

-=-=-=-=-=-=-=-=-=-=-=-
Links: You receive all messages sent to this group.
View/Reply Online (#8132): 
https://lists.yoctoproject.org/g/meta-virtualization/message/8132
Mute This Topic: https://lists.yoctoproject.org/mt/100348523/21656
Group Owner: [email protected]
Unsubscribe: https://lists.yoctoproject.org/g/meta-virtualization/unsub 
[[email protected]]
-=-=-=-=-=-=-=-=-=-=-=-

Reply via email to