Merge branch 'codex/prerelease-cleanup': pre-release cleanup and dependency pins

Fifteen reviewed commits: streaming-trailer trust fix and a single
replication-trust evaluator, Object Lock versioning invariant, resident-only
CORS lookup with fail-closed startup and load-failure states, dead-code
removal, compatibility baseline without exported symbols, upstream minio-go
with Console 43f8447fd and mcli 20260901, advisory ledger SN-2026-006..010,
contributor records, verification scripts with a valid root password, and a
deterministic dynamic-timeout test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvgysXDmhPBBimCReYtA8q
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-02 21:34:30 +08:00
44 changed files with 419 additions and 9744 deletions
+1 -4
View File
@@ -5,7 +5,7 @@ linters:
- durationcheck - durationcheck
- forcetypeassert - forcetypeassert
- gocritic - gocritic
- gomodguard - gomodguard_v2
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
@@ -59,9 +59,6 @@ formatters:
exclusions: exclusions:
generated: lax generated: lax
paths: paths:
# gofumpt v0.11.0 and Go 1.27's gofmt disagree on the indentation of
# multiple composite literals returned from a single statement.
- internal/ioutil/wait_pipe\.go$
- third_party$ - third_party$
- builtin$ - builtin$
- examples$ - examples$
+4
View File
@@ -44,6 +44,10 @@ Before opening a pull request:
- Add or update tests for changed behavior. - Add or update tests for changed behavior.
- Run `make verifiers`. - Run `make verifiers`.
- If `make rebrand-guard` reports a changed compatibility set, review the
listed identifiers; when the change is intended, refresh the baseline with
`go run ./buildscripts/rebrand-guard --write` and commit
`buildscripts/rebrand-guard/compat-baseline.json`.
- Run the smallest relevant package tests, then `make test` when practical. - Run the smallest relevant package tests, then `make test` when practical.
- Run `make build` and confirm the generated executable is `silo`. - Run `make build` and confirm the generated executable is `silo`.
- Explain any preserved `MINIO_*`, `minio_*`, `x-minio-*`, `/minio/*`, - Explain any preserved `MINIO_*`, `minio_*`, `x-minio-*`, `/minio/*`,
+3 -1
View File
@@ -23,6 +23,9 @@ Contributors whose changes are merged into `main`.
| [@mfredenhagen](https://github.com/mfredenhagen) | Bumped `go.opentelemetry.io` to address CVE-2026-39883 | [#19](https://github.com/pgsty/silo/pull/19) | [`1869bd3`](https://github.com/pgsty/silo/commit/1869bd30b) | | [@mfredenhagen](https://github.com/mfredenhagen) | Bumped `go.opentelemetry.io` to address CVE-2026-39883 | [#19](https://github.com/pgsty/silo/pull/19) | [`1869bd3`](https://github.com/pgsty/silo/commit/1869bd30b) |
| [@pinginfo](https://github.com/pinginfo) | Implemented `Flush` on `trackingResponseWriter`, repairing bucket notification streaming | [#34](https://github.com/pgsty/silo/pull/34) | [`65795ee`](https://github.com/pgsty/silo/commit/65795ee1f) | | [@pinginfo](https://github.com/pinginfo) | Implemented `Flush` on `trackingResponseWriter`, repairing bucket notification streaming | [#34](https://github.com/pgsty/silo/pull/34) | [`65795ee`](https://github.com/pgsty/silo/commit/65795ee1f) |
| [@waterkip](https://github.com/waterkip) | Repointed documentation links from the upstream domain to the Silo portal | [#41](https://github.com/pgsty/silo/pull/41) | [`d495d30`](https://github.com/pgsty/silo/commit/d495d30d5) | | [@waterkip](https://github.com/waterkip) | Repointed documentation links from the upstream domain to the Silo portal | [#41](https://github.com/pgsty/silo/pull/41) | [`d495d30`](https://github.com/pgsty/silo/commit/d495d30d5) |
| [@Dansyuqri](https://github.com/Dansyuqri) | Added `ChecksumType` to the `CompleteMultipartUpload` response | [#57](https://github.com/pgsty/silo/pull/57) | [`d014a12`](https://github.com/pgsty/silo/commit/d014a12cf) |
| [@ycjlin](https://github.com/ycjlin) | `ListObjects` returns `NoSuchBucket` for a prefix on a missing bucket | [#37](https://github.com/pgsty/silo/pull/37) | [`e9c5340`](https://github.com/pgsty/silo/commit/e9c5340be) |
| [@h5vx](https://github.com/h5vx) | Implemented per-bucket CORS: stored configuration, S3 handlers, and request enforcement | [#71](https://github.com/pgsty/silo/pull/71) | [`e4e3007`](https://github.com/pgsty/silo/commit/e4e3007da) |
## Proposed changes ## Proposed changes
@@ -32,7 +35,6 @@ differently.
| Contributor | Change | Pull request | Status | | Contributor | Change | Pull request | Status |
| :-- | :-- | :-- | :-- | | :-- | :-- | :-- | :-- |
| [@magicxor](https://github.com/magicxor) | `DELETE` precondition checks for the `If-Match` header | [#12](https://github.com/pgsty/silo/pull/12) | Open, queued for review | | [@magicxor](https://github.com/magicxor) | `DELETE` precondition checks for the `If-Match` header | [#12](https://github.com/pgsty/silo/pull/12) | Open, queued for review |
| [@ycjlin](https://github.com/ycjlin) | `ListObjects` should return `NoSuchBucket` for a prefix on a missing bucket | [#37](https://github.com/pgsty/silo/pull/37) | Open, queued for review |
| [@davinkevin](https://github.com/davinkevin) | Distroless-based Docker image variant | [#21](https://github.com/pgsty/silo/pull/21) | Superseded by the distroless variant shipped in RELEASE.2026-08-06, which the PR anticipated by four months | | [@davinkevin](https://github.com/davinkevin) | Distroless-based Docker image variant | [#21](https://github.com/pgsty/silo/pull/21) | Superseded by the distroless variant shipped in RELEASE.2026-08-06, which the PR anticipated by four months |
| [@lem21h](https://github.com/lem21h) | Assorted fixes and improvements | [#36](https://github.com/pgsty/silo/pull/36) | Closed | | [@lem21h](https://github.com/lem21h) | Assorted fixes and improvements | [#36](https://github.com/pgsty/silo/pull/36) | Closed |
| [@sulin37392](https://github.com/sulin37392) | Dependency updates against the fork | [#8](https://github.com/pgsty/silo/pull/8) | Closed | | [@sulin37392](https://github.com/sulin37392) | Dependency updates against the fork | [#8](https://github.com/pgsty/silo/pull/8) | Closed |
+60 -13
View File
@@ -17599,20 +17599,67 @@ For more information on this, and how to apply and follow the GNU AGPL, see
Bundled NOTICE file: Bundled NOTICE file:
This file is part of Console Server SILO Console
============
This program is free software: you can redistribute it and/or modify This product includes software developed at MinIO, Inc. (https://min.io/):
it under the terms of the GNU Affero General Public License as published by MinIO Console, Copyright (c) 2015-2026 MinIO, Inc.
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, SILO Console (this distribution, published from https://github.com/pgsty/silo-console
but WITHOUT ANY WARRANTY; without even the implied warranty of and shipped as `silo-console`) is a community-maintained fork of MinIO Console.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the The code was carried forward through two earlier community maintenance lines
GNU Affero General Public License for more details. before this one:
You should have received a copy of the GNU Affero General Public License - Alevsk/console https://github.com/Alevsk/console
along with this program. If not, see <http://www.gnu.org/licenses/>. - georgmangold/console https://github.com/georgmangold/console
Console portions Copyright (c) Georg Mangold and contributors
Copyright in inherited code remains with MinIO, Inc. and the respective
contributors. Modifications authored for SILO by PGSTY are
Copyright (c) 2025-2026 PGSTY (Ruohang Feng) and the SILO contributors; other
modifications remain the copyright of their respective authors. All existing
copyright, license and attribution notices are kept intact.
SILO and SILO Console are independent community projects and are not
affiliated with, endorsed by, or sponsored by MinIO, Inc. MinIO(R) is a
registered trademark of MinIO, Inc. Amazon S3 is a trademark of Amazon.com,
Inc. or its affiliates; references to S3 describe protocol compatibility only.
License
-------
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program (the LICENSE file next to this notice). If not, see
<https://www.gnu.org/licenses/>.
Corresponding source
--------------------
Under section 13 of the AGPL, every user who interacts with this program over a
network is offered its corresponding source. A release build reports the exact
source revision it was built from: `console version` prints it, the HTTP server
serves it in the page metadata used by the License, Login and anonymous pages,
and the container image carries it in the `io.pgsty.silo-console.source`
label. A build that cannot prove its exact revision says so instead of
guessing; operators of such builds must publish their modified source and may
point users at it with CONSOLE_CORRESPONDING_SOURCE_URL.
Third-party notices
-------------------
The licenses and notices of every third-party component linked into the binary
or bundled into the web application are collected in the CREDITS file next to
this notice (`console credits`, or /legal/CREDITS on a running server).
================================================================ ================================================================
@@ -21707,8 +21754,8 @@ https://github.com/minio/md5-simd
================================================================ ================================================================
github.com/minio/minio-go/v7 (replaced by github.com/pgsty/silo-go/v7) github.com/minio/minio-go/v7
https://github.com/pgsty/silo-go/v7 https://github.com/minio/minio-go/v7
---------------------------------------------------------------- ----------------------------------------------------------------
Apache License Apache License
+3 -3
View File
@@ -6,9 +6,9 @@ ENV GOPATH=/go
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
ARG MC_REPO=pgsty/mc ARG MC_REPO=pgsty/mc
ARG MC_VERSION=RELEASE.2026-08-06T00-00-00Z ARG MC_VERSION=RELEASE.2026-09-01T00-00-00Z
ARG MC_AMD64_SHA256=4b488bd30af54ad4214e5b654746677c79cd93dc6cad4be3aa2d09dbb48370ff ARG MC_AMD64_SHA256=6387cbeebb17c4bd52b447ee332ba22777e129034befa6598308c3aa04f02d09
ARG MC_ARM64_SHA256=83f6fedb16ed9c1e8efa8aea6776203dff132bc474214543d5c0767ed2066c2f ARG MC_ARM64_SHA256=5fc434c7e416bb4787e92306a8165d55284aac639a29744160b2a198fdd7573a
RUN apk add -U --no-cache \ RUN apk add -U --no-cache \
ca-certificates \ ca-certificates \
+2
View File
@@ -114,6 +114,8 @@ Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md); e
<p> <p>
<a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a> <a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a>
<a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a> <a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a>
<a href="https://github.com/h5vx"><img src="https://github.com/h5vx.png?size=64" width="44" alt="h5vx" title="@h5vx"></a>
<a href="https://github.com/Dansyuqri"><img src="https://github.com/Dansyuqri.png?size=64" width="44" alt="Dansyuqri" title="@Dansyuqri"></a>
<a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a> <a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a>
<a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a> <a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a>
<a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a> <a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a>
+2
View File
@@ -114,6 +114,8 @@ S3 API、`MINIO_*` 环境变量、`minio_*` 指标、`x-minio-*` 头、`/minio/*
<p> <p>
<a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a> <a href="https://github.com/magicxor"><img src="https://github.com/magicxor.png?size=64" width="44" alt="magicxor" title="@magicxor"></a>
<a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a> <a href="https://github.com/ycjlin"><img src="https://github.com/ycjlin.png?size=64" width="44" alt="ycjlin" title="@ycjlin"></a>
<a href="https://github.com/h5vx"><img src="https://github.com/h5vx.png?size=64" width="44" alt="h5vx" title="@h5vx"></a>
<a href="https://github.com/Dansyuqri"><img src="https://github.com/Dansyuqri.png?size=64" width="44" alt="Dansyuqri" title="@Dansyuqri"></a>
<a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a> <a href="https://github.com/davinkevin"><img src="https://github.com/davinkevin.png?size=64" width="44" alt="davinkevin" title="@davinkevin"></a>
<a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a> <a href="https://github.com/lem21h"><img src="https://github.com/lem21h.png?size=64" width="44" alt="lem21h" title="@lem21h"></a>
<a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a> <a href="https://github.com/sulin37392"><img src="https://github.com/sulin37392.png?size=64" width="44" alt="sulin37392" title="@sulin37392"></a>
+2 -2
View File
@@ -22,8 +22,8 @@ function start_silo_4drive() {
start_port=$1 start_port=$1
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/" export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
export MINIO_CI_CD=1 export MINIO_CI_CD=1
+1 -1
View File
@@ -40,7 +40,7 @@ if [ -n "${MCLI_BIN:-}" ]; then
exit 0 exit 0
fi fi
release=${MCLI_RELEASE:-RELEASE.2026-08-06T00-00-00Z} release=${MCLI_RELEASE:-RELEASE.2026-09-01T00-00-00Z}
version_hyphen=${release#RELEASE.} version_hyphen=${release#RELEASE.}
package_version=$(printf '%s\n' "${version_hyphen}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/') package_version=$(printf '%s\n' "${version_hyphen}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')
if [ "${package_version}" = "${version_hyphen}" ]; then if [ "${package_version}" = "${version_hyphen}" ]; then
+3 -3
View File
@@ -45,8 +45,8 @@ function start_silo_10drive() {
start_port=$1 start_port=$1
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/" export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
export MINIO_CI_CD=1 export MINIO_CI_CD=1
@@ -71,7 +71,7 @@ function start_silo_10drive() {
"${PWD}/mc" mb --with-versioning silo/bucket "${PWD}/mc" mb --with-versioning silo/bucket
export AWS_ACCESS_KEY_ID=silo export AWS_ACCESS_KEY_ID=silo
export AWS_SECRET_ACCESS_KEY=silo123 export AWS_SECRET_ACCESS_KEY=silo1234
aws --endpoint-url http://localhost:"$start_port" s3api create-multipart-upload --bucket bucket --key obj-1 >upload-id.json aws --endpoint-url http://localhost:"$start_port" s3api create-multipart-upload --bucket bucket --key obj-1 >upload-id.json
uploadId=$(jq -r '.UploadId' upload-id.json) uploadId=$(jq -r '.UploadId' upload-id.json)
File diff suppressed because it is too large Load Diff
+10 -86
View File
@@ -30,7 +30,7 @@ import (
"strings" "strings"
) )
const manifestVersion = 3 const manifestVersion = 4
var ( var (
minioImportRE = regexp.MustCompile(`github\.com/minio/[A-Za-z0-9_./-]+`) minioImportRE = regexp.MustCompile(`github\.com/minio/[A-Za-z0-9_./-]+`)
@@ -55,7 +55,6 @@ type manifest struct {
GridRoutes []string `json:"grid_routes"` GridRoutes []string `json:"grid_routes"`
StorageMarkers []string `json:"storage_markers"` StorageMarkers []string `json:"storage_markers"`
PolicyValues []string `json:"policy_values"` PolicyValues []string `json:"policy_values"`
ExportedSymbols []string `json:"exported_symbols"`
BrandAllowlist []string `json:"brand_allowlist"` BrandAllowlist []string `json:"brand_allowlist"`
} }
@@ -110,7 +109,6 @@ func collect(repo string) (manifest, error) {
"grid": {}, "grid": {},
"storage": {}, "storage": {},
"policy": {}, "policy": {},
"exported": {},
"brand": {}, "brand": {},
} }
modulePath := "" modulePath := ""
@@ -163,17 +161,17 @@ func collect(repo string) (manifest, error) {
sets["imports"][value] = struct{}{} sets["imports"][value] = struct{}{}
} }
} }
collectStringMatches(sets["routes"], routeRE, file)
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
if rel == "internal/grid/manager.go" {
collectStringMatches(sets["grid"], routeRE, file)
}
if !strings.HasSuffix(rel, "_test.go") { if !strings.HasSuffix(rel, "_test.go") {
collectExported(sets["exported"], filepath.ToSlash(filepath.Dir(rel)), file) // Test files hold request paths for fixtures, not served routes.
collectStringMatches(sets["routes"], routeRE, file)
if strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/") { if strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/") {
collectBrandStrings(sets["brand"], rel, file) collectBrandStrings(sets["brand"], rel, file)
} }
} }
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
if rel == "internal/grid/manager.go" {
collectStringMatches(sets["grid"], routeRE, file)
}
} }
} }
// This was a shell-local PID variable in the generated inspect script, // This was a shell-local PID variable in the generated inspect script,
@@ -195,7 +193,6 @@ func collect(repo string) (manifest, error) {
GridRoutes: sorted(sets["grid"]), GridRoutes: sorted(sets["grid"]),
StorageMarkers: sorted(sets["storage"]), StorageMarkers: sorted(sets["storage"]),
PolicyValues: sorted(sets["policy"]), PolicyValues: sorted(sets["policy"]),
ExportedSymbols: sorted(sets["exported"]),
BrandAllowlist: sorted(sets["brand"]), BrandAllowlist: sorted(sets["brand"]),
}, nil }, nil
} }
@@ -262,7 +259,7 @@ func collectStringMatches(dst map[string]struct{}, re *regexp.Regexp, file *ast.
} }
func trackedFiles(repo string) ([]string, error) { func trackedFiles(repo string) ([]string, error) {
cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "--others", "--exclude-standard", "-z") cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "-z")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("git ls-files: %w", err) return nil, fmt.Errorf("git ls-files: %w", err)
@@ -286,78 +283,6 @@ func addMatches(dst map[string]struct{}, re *regexp.Regexp, text string, lower b
} }
} }
func collectExported(dst map[string]struct{}, dir string, file *ast.File) {
prefix := dir + ":" + file.Name.Name + ":"
for _, decl := range file.Decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
if !ast.IsExported(decl.Name.Name) {
continue
}
if decl.Recv == nil {
dst[prefix+"func:"+decl.Name.Name] = struct{}{}
continue
}
receiver := receiverName(decl.Recv.List[0].Type)
dst[prefix+"method:"+receiver+"."+decl.Name.Name] = struct{}{}
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
if !ast.IsExported(spec.Name.Name) {
continue
}
dst[prefix+"type:"+spec.Name.Name] = struct{}{}
collectExportedFields(dst, prefix, spec.Name.Name, spec.Type)
case *ast.ValueSpec:
kind := strings.ToLower(decl.Tok.String())
for _, name := range spec.Names {
if ast.IsExported(name.Name) {
dst[prefix+kind+":"+name.Name] = struct{}{}
}
}
}
}
}
}
}
func collectExportedFields(dst map[string]struct{}, prefix, typeName string, expr ast.Expr) {
var fields *ast.FieldList
switch typed := expr.(type) {
case *ast.StructType:
fields = typed.Fields
case *ast.InterfaceType:
fields = typed.Methods
default:
return
}
for _, field := range fields.List {
for _, name := range field.Names {
if ast.IsExported(name.Name) {
dst[prefix+"field:"+typeName+"."+name.Name] = struct{}{}
}
}
}
}
func receiverName(expr ast.Expr) string {
switch expr := expr.(type) {
case *ast.Ident:
return expr.Name
case *ast.StarExpr:
return receiverName(expr.X)
case *ast.IndexExpr:
return receiverName(expr.X)
case *ast.IndexListExpr:
return receiverName(expr.X)
case *ast.SelectorExpr:
return receiverName(expr.X) + "." + expr.Sel.Name
default:
return fmt.Sprintf("%T", expr)
}
}
func sorted(set map[string]struct{}) []string { func sorted(set map[string]struct{}) []string {
values := make([]string, 0, len(set)) values := make([]string, 0, len(set))
for value := range set { for value := range set {
@@ -409,7 +334,6 @@ func compare(want, got manifest) error {
{"grid_routes", want.GridRoutes, got.GridRoutes}, {"grid_routes", want.GridRoutes, got.GridRoutes},
{"storage_markers", want.StorageMarkers, got.StorageMarkers}, {"storage_markers", want.StorageMarkers, got.StorageMarkers},
{"policy_values", want.PolicyValues, got.PolicyValues}, {"policy_values", want.PolicyValues, got.PolicyValues},
{"exported_symbols", want.ExportedSymbols, got.ExportedSymbols},
{"brand_allowlist", want.BrandAllowlist, got.BrandAllowlist}, {"brand_allowlist", want.BrandAllowlist, got.BrandAllowlist},
} }
for _, check := range checks { for _, check := range checks {
@@ -454,10 +378,10 @@ func setDiff(want, got []string) (missing, added []string) {
} }
func printSummary(value manifest) { func printSummary(value manifest) {
fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d exported=%d brand=%d sha256=%s\n", fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d brand=%d sha256=%s\n",
len(value.MinioImports), len(value.Environment), len(value.Metrics), len(value.Headers), len(value.MinioImports), len(value.Environment), len(value.Metrics), len(value.Headers),
len(value.Routes), len(value.RouteRoots), len(value.GridRoutes), len(value.StorageMarkers), len(value.PolicyValues), len(value.Routes), len(value.RouteRoots), len(value.GridRoutes), len(value.StorageMarkers), len(value.PolicyValues),
len(value.ExportedSymbols), len(value.BrandAllowlist), manifestDigest(value)) len(value.BrandAllowlist), manifestDigest(value))
} }
func manifestDigest(value manifest) string { func manifestDigest(value manifest) string {
+2 -2
View File
@@ -18,8 +18,8 @@ function start_silo_5drive() {
start_port=$1 start_port=$1
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/" export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
export MINIO_CI_CD=1 export MINIO_CI_CD=1
+5 -5
View File
@@ -28,8 +28,8 @@ function verify_rewrite() {
start_port=$1 start_port=$1
export MINIO_ACCESS_KEY=silo export MINIO_ACCESS_KEY=silo
export MINIO_SECRET_KEY=silo123 export MINIO_SECRET_KEY=silo1234
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/" export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
export MINIO_CI_CD=1 export MINIO_CI_CD=1
@@ -87,7 +87,7 @@ function verify_rewrite() {
-debug \ -debug \
-versions \ -versions \
-access-key silo \ -access-key silo \
-secret-key silo123 \ -secret-key silo1234 \
-endpoint "http://127.0.0.1:${start_port}/" 2>&1 | grep INTACT; then -endpoint "http://127.0.0.1:${start_port}/" 2>&1 | grep INTACT; then
echo "server1 log:" echo "server1 log:"
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
@@ -105,14 +105,14 @@ function verify_rewrite() {
exit 1 exit 1
fi fi
go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "silo" "silo123" go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "silo" "silo1234"
sleep 1 sleep 1
if ! ./s3-check-md5 \ if ! ./s3-check-md5 \
-debug \ -debug \
-versions \ -versions \
-access-key silo \ -access-key silo \
-secret-key silo123 \ -secret-key silo1234 \
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then -endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
echo "server1 log:" echo "server1 log:"
cat "${WORK_DIR}/server1.log" cat "${WORK_DIR}/server1.log"
+2 -2
View File
@@ -74,8 +74,8 @@ function test_silo_with_timeout() {
start_port=$1 start_port=$1
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MC_HOST_silo="http://silo:silo123@127.0.0.1:${start_port}/" export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
export MINIO_CI_CD=1 export MINIO_CI_CD=1
mkdir ${WORK_DIR} mkdir ${WORK_DIR}
+4 -4
View File
@@ -15,10 +15,10 @@ WORK_DIR="$PWD/.verify-$RANDOM"
export MINT_MODE=core export MINT_MODE=core
export MINT_DATA_DIR="$WORK_DIR/data" export MINT_DATA_DIR="$WORK_DIR/data"
export SERVER_ENDPOINT="127.0.0.1:9000" export SERVER_ENDPOINT="127.0.0.1:9000"
export MC_HOST_verify="http://silo:silo123@${SERVER_ENDPOINT}/" export MC_HOST_verify="http://silo:silo1234@${SERVER_ENDPOINT}/"
export MC_HOST_verify_ipv6="http://silo:silo123@[::1]:9000/" export MC_HOST_verify_ipv6="http://silo:silo1234@[::1]:9000/"
export ACCESS_KEY="silo" export ACCESS_KEY="silo"
export SECRET_KEY="silo123" export SECRET_KEY="silo1234"
export ENABLE_HTTPS=0 export ENABLE_HTTPS=0
export GO111MODULE=on export GO111MODULE=on
export GOGC=25 export GOGC=25
@@ -225,7 +225,7 @@ function __init__() {
shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
## version is purposefully set to '3' for minio to migrate configuration file ## version is purposefully set to '3' for minio to migrate configuration file
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json" echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
"$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \ "$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \
https://raw.githubusercontent.com/pgsty/mc/4c4dcc4b55baf238cd0c81030d77945b3828f157/functional-tests.sh \ https://raw.githubusercontent.com/pgsty/mc/4c4dcc4b55baf238cd0c81030d77945b3828f157/functional-tests.sh \
@@ -15,7 +15,7 @@ SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
function start_silo_3_node() { function start_silo_3_node() {
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MINIO_ERASURE_SET_DRIVE_COUNT=6 export MINIO_ERASURE_SET_DRIVE_COUNT=6
export MINIO_CI_CD=1 export MINIO_CI_CD=1
@@ -37,7 +37,7 @@ function start_silo_3_node() {
pid3=$! pid3=$!
disown $pid3 disown $pid3
export MC_HOST_mysilo="http://silo:silo123@127.0.0.1:$((start_port + 1))" export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
timeout 15m /tmp/mc ready mysilo || fail timeout 15m /tmp/mc ready mysilo || fail
@@ -116,7 +116,7 @@ function __init__() {
mkdir -p "$SILO_CONFIG_DIR" mkdir -p "$SILO_CONFIG_DIR"
## version is purposefully set to '3' for minio to migrate configuration file ## version is purposefully set to '3' for minio to migrate configuration file
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json" echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
if [ ! -f /tmp/mc ]; then if [ ! -f /tmp/mc ]; then
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc "$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
@@ -17,7 +17,7 @@ function start_silo() {
start_port=$1 start_port=$1
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
unset MINIO_CI_CD unset MINIO_CI_CD
unset CI unset CI
+3 -3
View File
@@ -20,7 +20,7 @@ function start_silo_3_node() {
done done
export MINIO_ROOT_USER=silo export MINIO_ROOT_USER=silo
export MINIO_ROOT_PASSWORD=silo123 export MINIO_ROOT_PASSWORD=silo1234
export MINIO_ERASURE_SET_DRIVE_COUNT=6 export MINIO_ERASURE_SET_DRIVE_COUNT=6
export MINIO_CI_CD=1 export MINIO_CI_CD=1
@@ -46,7 +46,7 @@ function start_silo_3_node() {
pid3=$! pid3=$!
disown $pid3 disown $pid3
export MC_HOST_mysilo="http://silo:silo123@127.0.0.1:$((start_port + 1))" export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
timeout 15m /tmp/mc ready mysilo || fail timeout 15m /tmp/mc ready mysilo || fail
[ ${first_time} -eq 0 ] && upload_objects [ ${first_time} -eq 0 ] && upload_objects
@@ -117,7 +117,7 @@ function __init__() {
mkdir -p "$SILO_CONFIG_DIR" mkdir -p "$SILO_CONFIG_DIR"
## version is purposefully set to '3' for minio to migrate configuration file ## version is purposefully set to '3' for minio to migrate configuration file
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo123"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json" echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
if [ ! -f /tmp/mc ]; then if [ ! -f /tmp/mc ]; then
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc "$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
+10 -12
View File
@@ -461,15 +461,15 @@ func registerAPIRouter(router *mux.Router) {
router.Methods(http.MethodPut). router.Methods(http.MethodPut).
HandlerFunc(s3APIMiddleware(api.PutBucketACLHandler)). HandlerFunc(s3APIMiddleware(api.PutBucketACLHandler)).
Queries("acl", "") Queries("acl", "")
// GetBucketCors - this is a dummy call. // GetBucketCors
router.Methods(http.MethodGet). router.Methods(http.MethodGet).
HandlerFunc(s3APIMiddleware(api.GetBucketCorsHandler)). HandlerFunc(s3APIMiddleware(api.GetBucketCorsHandler)).
Queries("cors", "") Queries("cors", "")
// PutBucketCors - this is a dummy call. // PutBucketCors
router.Methods(http.MethodPut). router.Methods(http.MethodPut).
HandlerFunc(s3APIMiddleware(api.PutBucketCorsHandler)). HandlerFunc(s3APIMiddleware(api.PutBucketCorsHandler)).
Queries("cors", "") Queries("cors", "")
// DeleteBucketCors - this is a dummy call. // DeleteBucketCors
router.Methods(http.MethodDelete). router.Methods(http.MethodDelete).
HandlerFunc(s3APIMiddleware(api.DeleteBucketCorsHandler)). HandlerFunc(s3APIMiddleware(api.DeleteBucketCorsHandler)).
Queries("cors", "") Queries("cors", "")
@@ -787,15 +787,13 @@ func corsHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Origin") != "" { if r.Header.Get("Origin") != "" {
if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil { if bucket, _ := request2BucketObjectName(r); bucket != "" && globalBucketMetadataSys != nil {
// Resident-only lookup: this runs pre-auth for every // Resident-only lookup: this runs before authentication with a
// Origin-bearing request using a client-supplied path segment as // client-supplied path segment as the bucket name, so it must
// the bucket name. It must never load or cache metadata for // never load or cache metadata. While startup loading is still
// arbitrary names (see GetResidentCorsConfig). GetResidentCorsConfig // running, for a real bucket whose metadata failed to load, and
// is the single decision point: it returns errInvalidArgument for // for a bucket whose stored CORS document failed to parse, the
// the internal .minio.sys namespace (fail closed), a config for a // request gets no CORS headers; any other non-resident name falls
// resident bucket, errBucketMetadataNotInitialized for a real but // back to the global policy below.
// unloaded bucket (fail closed), and errConfigNotFound otherwise
// (fall back to the global policy below).
cfg, _, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket) cfg, _, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket)
if err == nil && cfg != nil { if err == nil && cfg != nil {
if applyBucketCors(w, r, cfg) { if applyBucketCors(w, r, cfg) {
+5 -32
View File
@@ -261,6 +261,11 @@ func TestBucketCorsMetadataErrorFailsClosed(t *testing.T) {
oldMetadataSys := globalBucketMetadataSys oldMetadataSys := globalBucketMetadataSys
setObjectLayer(nil) setObjectLayer(nil)
globalBucketMetadataSys = NewBucketMetadataSys() globalBucketMetadataSys = NewBucketMetadataSys()
// A resident bucket whose stored CORS document does not parse must not be
// answered with the global policy: it has a configuration we cannot honor.
meta := newBucketMetadata("cors-metadata-error")
meta.corsConfigErr = fmt.Errorf("invalid bucket CORS configuration")
globalBucketMetadataSys.Set("cors-metadata-error", meta)
defer func() { defer func() {
setObjectLayer(oldObjectAPI) setObjectLayer(oldObjectAPI)
globalBucketMetadataSys = oldMetadataSys globalBucketMetadataSys = oldMetadataSys
@@ -706,35 +711,3 @@ func testBucketCorsLoadFailedBucketFailsClosed(obj ObjectLayer, _ string, _ stri
t.Fatalf("load-failed CORS lookup performed %d synchronous bucket metadata reads", got) t.Fatalf("load-failed CORS lookup performed %d synchronous bucket metadata reads", got)
} }
} }
// TestBucketCorsInternalBucketFailsClosed guards P2: an Origin-bearing request
// whose first path segment is the reserved .minio.sys namespace must preserve
// GetConfig's errInvalidArgument semantics and fail closed, not fall back to
// the permissive global CORS policy.
func TestBucketCorsInternalBucketFailsClosed(t *testing.T) {
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
t: t,
objAPITest: testBucketCorsInternalBucketFailsClosed,
endpoints: []string{"GetBucketCors"},
})
}
func testBucketCorsInternalBucketFailsClosed(_ ObjectLayer, _ string, _ string, _ http.Handler, _ auth.Credentials, t *testing.T) {
restoreInit := markBucketMetadataInitialized(t)
defer restoreInit()
wrapped := corsHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, getGetObjectURL("", minioMetaBucket, "object"), nil)
req.Header.Set("Origin", "https://app.example.com")
wrapped.ServeHTTP(rec, req)
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("internal bucket fell back to global allow-origin %q", got)
}
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != "" {
t.Fatalf("internal bucket fell back to global credentials %q", got)
}
}
+5 -5
View File
@@ -85,7 +85,7 @@ func testPeerBucketCorsReplicationOrdering(_ ObjectLayer, _ string, bucket strin
if !meta.CorsConfigUpdatedAt.Equal(putAt) { if !meta.CorsConfigUpdatedAt.Equal(putAt) {
t.Fatalf("peer PUT timestamp = %v, want source time %v", meta.CorsConfigUpdatedAt, putAt) t.Fatalf("peer PUT timestamp = %v, want source time %v", meta.CorsConfigUpdatedAt, putAt)
} }
cfg, cfgAt, err := globalBucketMetadataSys.GetCorsConfig(bucket) cfg, cfgAt, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket)
if err != nil { if err != nil {
t.Fatalf("peer PUT stored raw XML but no parsed config: %v", err) t.Fatalf("peer PUT stored raw XML but no parsed config: %v", err)
} }
@@ -165,10 +165,10 @@ func TestSiteReplicationMetaInfoPreservesCorsTombstone(t *testing.T) {
func testSiteReplicationMetaInfoPreservesCorsTombstone(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) { func testSiteReplicationMetaInfoPreservesCorsTombstone(obj ObjectLayer, _ string, bucket string, _ http.Handler, _ auth.Credentials, t *testing.T) {
ctx := t.Context() ctx := t.Context()
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketCorsConfig, []byte(testSiteReplicationCORSDoc)); err != nil { if _, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, []byte(testSiteReplicationCORSDoc)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
deleteAt, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketCorsConfig) deleteAt, err := updateLocalBucketCORSMetadata(ctx, obj, bucket, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -918,8 +918,8 @@ func testLegacyInvalidCorsMetadataCanBeDeleted(obj ObjectLayer, _ string, bucket
t.Fatalf("legacy CORS state = (%#v, %v), want fail-closed parse error", loaded.corsConfig, loaded.corsConfigErr) t.Fatalf("legacy CORS state = (%#v, %v), want fail-closed parse error", loaded.corsConfig, loaded.corsConfigErr)
} }
globalBucketMetadataSys.Set(bucket, loaded) globalBucketMetadataSys.Set(bucket, loaded)
if _, gotAt, err := globalBucketMetadataSys.GetCorsConfig(bucket); err == nil || !gotAt.Equal(legacyAt) { if _, gotAt, err := globalBucketMetadataSys.GetResidentCorsConfig(bucket); err == nil || !gotAt.Equal(legacyAt) {
t.Fatalf("GetCorsConfig = timestamp %v, error %v; want legacy timestamp and error", gotAt, err) t.Fatalf("GetResidentCorsConfig = timestamp %v, error %v; want legacy timestamp and error", gotAt, err)
} }
if _, gotAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket); err == nil || !gotAt.Equal(legacyAt) { if _, gotAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket); err == nil || !gotAt.Equal(legacyAt) {
t.Fatalf("GetCorsConfigXML = timestamp %v, error %v; want legacy timestamp and error", gotAt, err) t.Fatalf("GetCorsConfigXML = timestamp %v, error %v; want legacy timestamp and error", gotAt, err)
-4
View File
@@ -509,10 +509,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
reqInfo.ObjectName = object.ObjectName reqInfo.ObjectName = object.ObjectName
reqInfo.VersionID = object.VersionID reqInfo.VersionID = object.VersionID
if apiErrCode := authorizeRequest(ctx, r, deleteObjectAction(object.VersionID)); apiErrCode != ErrNone { if apiErrCode := authorizeRequest(ctx, r, deleteObjectAction(object.VersionID)); apiErrCode != ErrNone {
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
return
}
apiErr := errorCodes.ToAPIErr(apiErrCode) apiErr := errorCodes.ToAPIErr(apiErrCode)
deleteResults[index].errInfo = DeleteError{ deleteResults[index].errInfo = DeleteError{
Code: apiErr.Code, Code: apiErr.Code,
+34 -78
View File
@@ -51,16 +51,19 @@ type BucketMetadataSys struct {
initialized bool initialized bool
group *singleflight.Group group *singleflight.Group
metadataMap map[string]BucketMetadata metadataMap map[string]BucketMetadata
// loadFailed tracks real buckets whose metadata could not be loaded at // loadFailed records real buckets whose metadata could not be loaded at
// startup (concurrentLoad) or during a refresh. Such buckets are NOT // startup or during a refresh. They are absent from metadataMap even though
// resident in metadataMap even though the subsystem is Initialized, so a // the subsystem is initialized, and without this bit a resident-only lookup
// plain map miss cannot distinguish "not a bucket" from "known bucket whose // could not tell them apart from a name that is not a bucket at all. The
// config we could not read". Callers that must fail closed for a real but // set is bounded by the number of failed loads and empty in normal operation.
// unreadable bucket (e.g. per-bucket CORS) consult this set. It is bounded
// by the number of load failures and is empty in normal operation.
loadFailed map[string]struct{} loadFailed map[string]struct{}
} }
// noteLoadFailure and clearLoadFailure maintain loadFailed; both expect the
// caller to hold sys.Lock.
func (sys *BucketMetadataSys) noteLoadFailure(bucket string) { sys.loadFailed[bucket] = struct{}{} }
func (sys *BucketMetadataSys) clearLoadFailure(bucket string) { delete(sys.loadFailed, bucket) }
// Count returns number of bucket metadata map entries. // Count returns number of bucket metadata map entries.
func (sys *BucketMetadataSys) Count() int { func (sys *BucketMetadataSys) Count() int {
sys.RLock() sys.RLock()
@@ -75,7 +78,7 @@ func (sys *BucketMetadataSys) Remove(buckets ...string) {
for _, bucket := range buckets { for _, bucket := range buckets {
sys.group.Forget(bucket) sys.group.Forget(bucket)
delete(sys.metadataMap, bucket) delete(sys.metadataMap, bucket)
delete(sys.loadFailed, bucket) sys.clearLoadFailure(bucket)
globalBucketMonitor.DeleteBucket(bucket) globalBucketMonitor.DeleteBucket(bucket)
} }
sys.Unlock() sys.Unlock()
@@ -95,7 +98,7 @@ func (sys *BucketMetadataSys) RemoveStaleBuckets(diskBuckets set.StringSet) {
} }
for bucket := range sys.loadFailed { for bucket := range sys.loadFailed {
if !diskBuckets.Contains(bucket) { if !diskBuckets.Contains(bucket) {
delete(sys.loadFailed, bucket) sys.clearLoadFailure(bucket)
} }
} }
} }
@@ -109,7 +112,7 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
if !isMinioMetaBucketName(bucket) { if !isMinioMetaBucketName(bucket) {
sys.Lock() sys.Lock()
sys.metadataMap[bucket] = meta sys.metadataMap[bucket] = meta
delete(sys.loadFailed, bucket) sys.clearLoadFailure(bucket)
sys.Unlock() sys.Unlock()
} }
} }
@@ -163,9 +166,6 @@ func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string,
case bucketTaggingConfig: case bucketTaggingConfig:
meta.TaggingConfigXML = configData meta.TaggingConfigXML = configData
meta.TaggingConfigUpdatedAt = updatedAt meta.TaggingConfigUpdatedAt = updatedAt
case bucketCorsConfig:
meta.CorsConfigXML = configData
meta.CorsConfigUpdatedAt = updatedAt
case bucketQuotaConfigFile: case bucketQuotaConfigFile:
meta.QuotaConfigJSON = configData meta.QuotaConfigJSON = configData
meta.QuotaConfigUpdatedAt = updatedAt meta.QuotaConfigUpdatedAt = updatedAt
@@ -415,51 +415,17 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC
return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil
} }
// GetCorsConfig returns the CORS configuration for the given bucket. // GetResidentCorsConfig returns the CORS configuration of a bucket whose
// The returned object must not be modified. // metadata is already resident in memory. It runs before authentication for
func (sys *BucketMetadataSys) GetCorsConfig(bucket string) (*cors.Config, time.Time, error) { // every Origin-bearing request with a client-supplied path segment, so it
meta, _, err := sys.GetConfig(GlobalContext, bucket) // never loads or caches metadata. A non-resident name gets no CORS answer
if err != nil { // (errBucketMetadataNotInitialized) while startup loading is still running,
return nil, time.Time{}, err // and afterwards when it is a real bucket whose metadata failed to load: a
} // presigned URL is authenticated on its own, so the bucket's CORS document is
if meta.corsConfigErr != nil { // the only origin boundary a browser enforces for it. Any other non-resident
return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr // name reports errConfigNotFound and the caller applies the global CORS
} // policy exactly as releases without per-bucket CORS did.
if meta.corsConfig == nil {
return nil, time.Time{}, errConfigNotFound
}
return meta.corsConfig, meta.CorsConfigUpdatedAt, nil
}
// GetResidentCorsConfig returns the CORS configuration for the given bucket
// using only bucket metadata that is already resident in memory. Unlike
// GetCorsConfig it never loads metadata from disk and never caches a new
// entry.
//
// The per-request CORS middleware runs before authentication, for every
// Origin-bearing request, using the validated first path segment as the bucket
// name.
// Routing that through GetCorsConfig (which loads and caches) let an
// unauthenticated client grow metadataMap without bound and trigger an
// erasure metadata probe for every distinct, attacker-controlled,
// non-existent name it sent with an Origin header (e.g. /minio/... , /api/... ,
// or random buckets). Every bucket that can carry a CORS document is made
// resident when the document is written (Set) and when metadata is loaded at
// startup (Init/concurrentLoad), so a resident-only read is complete for real
// buckets while costing only an in-memory map lookup for everything else.
//
// While bucket metadata is still loading (not yet Initialized) a non-resident
// bucket returns errBucketMetadataNotInitialized so the caller fails closed
// rather than answering with the permissive global policy for a bucket whose
// restrictive CORS document may simply not be loaded yet.
func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config, time.Time, error) { func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config, time.Time, error) {
if isMinioMetaBucketName(bucket) {
// Preserve GetConfig's semantics for the internal namespace: this is
// not a real bucket, and returning a non-errConfigNotFound error makes
// the CORS middleware fail closed rather than answer for .minio.sys
// with the permissive global policy.
return nil, time.Time{}, errInvalidArgument
}
if isReservedOrInvalidBucket(bucket, true) { if isReservedOrInvalidBucket(bucket, true) {
return nil, time.Time{}, errConfigNotFound return nil, time.Time{}, errConfigNotFound
} }
@@ -468,7 +434,12 @@ func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config
_, failed := sys.loadFailed[bucket] _, failed := sys.loadFailed[bucket]
initialized := sys.initialized initialized := sys.initialized
sys.RUnlock() sys.RUnlock()
if ok { if !ok {
if !initialized || failed {
return nil, time.Time{}, errBucketMetadataNotInitialized
}
return nil, time.Time{}, errConfigNotFound
}
if meta.corsConfigErr != nil { if meta.corsConfigErr != nil {
return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr return nil, meta.CorsConfigUpdatedAt, meta.corsConfigErr
} }
@@ -477,18 +448,6 @@ func (sys *BucketMetadataSys) GetResidentCorsConfig(bucket string) (*cors.Config
} }
return meta.corsConfig, meta.CorsConfigUpdatedAt, nil return meta.corsConfig, meta.CorsConfigUpdatedAt, nil
} }
// Not resident. Two cases must not be conflated:
// - metadata is still loading (!initialized), or this is a real bucket
// whose metadata failed to load: we cannot rule out a restrictive CORS
// config, so fail closed rather than answer with the global policy.
// - a fully initialized subsystem with no record of the name: it is not a
// bucket that can carry CORS, so fall back to the global policy without
// loading or caching metadata for an arbitrary, client-supplied name.
if !initialized || failed {
return nil, time.Time{}, errBucketMetadataNotInitialized
}
return nil, time.Time{}, errConfigNotFound
}
// GetCorsConfigXML returns the raw stored CORS configuration XML for the // GetCorsConfigXML returns the raw stored CORS configuration XML for the
// given bucket, preserving the document exactly as it was PUT (including // given bucket, preserving the document exactly as it was PUT (including
@@ -636,6 +595,7 @@ func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (met
} }
sys.Lock() sys.Lock()
sys.metadataMap[bucket] = meta sys.metadataMap[bucket] = meta
sys.clearLoadFailure(bucket)
sys.Unlock() sys.Unlock()
return meta, true, nil return meta, true, nil
@@ -687,13 +647,10 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []stri
sys.Lock() sys.Lock()
for i, meta := range bucketMetas { for i, meta := range bucketMetas {
if errs[i] != nil { if errs[i] != nil {
// Real bucket whose metadata could not be loaded: record it so sys.noteLoadFailure(buckets[i])
// consumers that must fail closed (per-bucket CORS) can tell it
// apart from a name that is not a bucket at all.
sys.loadFailed[buckets[i]] = struct{}{}
continue continue
} }
delete(sys.loadFailed, buckets[i]) sys.clearLoadFailure(buckets[i])
sys.metadataMap[buckets[i]] = meta sys.metadataMap[buckets[i]] = meta
} }
sys.Unlock() sys.Unlock()
@@ -744,7 +701,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
if err != nil { if err != nil {
internalLogIf(ctx, err, logger.WarningKind) internalLogIf(ctx, err, logger.WarningKind)
sys.Lock() sys.Lock()
sys.loadFailed[bucket] = struct{}{} sys.noteLoadFailure(bucket)
sys.Unlock() sys.Unlock()
wait() // wait to proceed to next entry. wait() // wait to proceed to next entry.
continue continue
@@ -756,8 +713,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
updated = true updated = true
sys.metadataMap[bucket] = meta sys.metadataMap[bucket] = meta
} }
// A successful (re)load clears any earlier load failure. sys.clearLoadFailure(bucket)
delete(sys.loadFailed, bucket)
sys.Unlock() sys.Unlock()
if updated { if updated {
+9 -7
View File
@@ -377,13 +377,6 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa
b.corsConfig = nil b.corsConfig = nil
} }
if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
if versioningErr != nil || !config.Enabled() {
b.VersioningConfigXML = enabledBucketVersioningConfig
}
}
if len(b.ObjectLockConfigXML) != 0 { if len(b.ObjectLockConfigXML) != 0 {
b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML)) b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML))
if err != nil { if err != nil {
@@ -392,6 +385,15 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa
} else { } else {
b.objectLockConfig = nil b.objectLockConfig = nil
} }
if b.objectLockConfig != nil {
// Object Lock requires every object to be versioned. Whatever the lock
// document contains, a suspended or prefix-excluded versioning document
// is replaced by plain Enabled versioning; Save persists the result.
config, versioningErr := versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
if versioningErr != nil || !config.Enabled() || config.PrefixesExcluded() {
b.VersioningConfigXML = enabledBucketVersioningConfig
}
}
if len(b.VersioningConfigXML) != 0 { if len(b.VersioningConfigXML) != 0 {
b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML)) b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML))
+6 -4
View File
@@ -180,12 +180,14 @@ func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() fl
func TestDynamicTimeoutAdjustExponential(t *testing.T) { func TestDynamicTimeoutAdjustExponential(t *testing.T) {
timeout := newDynamicTimeout(time.Minute, time.Second) timeout := newDynamicTimeout(time.Minute, time.Second)
rand.Seed(0) // A private source keeps the sample independent of other tests that use
// the global generator concurrently.
rng := rand.New(rand.NewSource(0))
initial := timeout.Timeout() initial := timeout.Timeout()
for range 10 { for range 10 {
testDynamicTimeoutAdjust(t, timeout, rand.ExpFloat64) testDynamicTimeoutAdjust(t, timeout, rng.ExpFloat64)
} }
adjusted := timeout.Timeout() adjusted := timeout.Timeout()
@@ -197,13 +199,13 @@ func TestDynamicTimeoutAdjustExponential(t *testing.T) {
func TestDynamicTimeoutAdjustNormalized(t *testing.T) { func TestDynamicTimeoutAdjustNormalized(t *testing.T) {
timeout := newDynamicTimeout(time.Minute, time.Second) timeout := newDynamicTimeout(time.Minute, time.Second)
rand.Seed(0) rng := rand.New(rand.NewSource(0))
initial := timeout.Timeout() initial := timeout.Timeout()
for range 10 { for range 10 {
testDynamicTimeoutAdjust(t, timeout, func() float64 { testDynamicTimeoutAdjust(t, timeout, func() float64 {
return 1.0 + rand.NormFloat64() return 1.0 + rng.NormFloat64()
}) })
} }
-23
View File
@@ -355,29 +355,6 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
} }
} }
// checkSSECCopySourceKey authenticates the SSE-C copy source key against the
// sealed object key held in metadata. This keeps the diverted rotation safe on
// its own and remains defense in depth when the read path also authenticates
// zero-byte objects. Mirrors the errors rotateKey reports.
func checkSSECCopySourceKey(h http.Header, metadata map[string]string, bucket, object string, newKey []byte) error {
oldKey, err := ParseSSECopyCustomerRequest(h, metadata)
if err != nil {
return err
}
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
if err != nil {
return err
}
var objectKey crypto.ObjectKey
if err := objectKey.Unseal(oldKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
return errInvalidSSEParameters
}
return crypto.ErrInvalidCustomerKey
}
return nil
}
func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) { func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) {
var sealedKey crypto.SealedKey var sealedKey crypto.SealedKey
switch kind { switch kind {
+2 -2
View File
@@ -911,7 +911,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
meta.SetCreatedAt(opts.CreatedAt) meta.SetCreatedAt(opts.CreatedAt)
} }
if opts.LockEnabled { if opts.LockEnabled {
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, true); err != nil {
return err return err
} }
if len(meta.ObjectLockConfigXML) == 0 { if len(meta.ObjectLockConfigXML) == 0 {
@@ -920,7 +920,7 @@ func (z *erasureServerPools) MakeBucket(ctx context.Context, bucket string, opts
} }
} }
if opts.VersioningEnabled { if opts.VersioningEnabled {
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, opts.LockEnabled); err != nil {
return err return err
} }
} }
+9 -41
View File
@@ -1308,18 +1308,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidStorageClass), r.URL)
return return
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, dstBucket, dstObject, policy.ReplicateObjectAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
replicaTrusted := trustedReplication && rawReplica
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
} }
@@ -1522,17 +1515,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
canRotateKeyInPlace := !srcInfo.Legacy && canRotateKeyInPlace := !srcInfo.Legacy &&
!copyRewritesObjectData(srcInfo.metadataOnly, copySrcOpts, dstOpts) !copyRewritesObjectData(srcInfo.metadataOnly, copySrcOpts, dstOpts)
// The rotation shortcut authenticates the source key by unsealing it. The
// re-encrypting fallback authenticates it only through the source decryptor,
// which GetObjectNInfo skips for a zero byte object, so check it here before
// the destination is written under the new key.
if cpSrcDstSame && sseCopyC && sseC && !chStorageClass && !canRotateKeyInPlace {
if err := checkSSECCopySourceKey(r.Header, srcInfo.UserDefined, srcBucket, srcObject, newKey); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
// If src == dst and either // If src == dst and either
// - the object is encrypted using SSE-C and two different SSE-C keys are present // - the object is encrypted using SSE-C and two different SSE-C keys are present
// - the object is encrypted using SSE-S3 and the SSE-S3 header is present // - the object is encrypted using SSE-S3 and the SSE-S3 header is present
@@ -2073,14 +2055,9 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
} }
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
if _, ok := r.Header[xhttp.MinIOSourceReplicationCheck]; ok { if _, ok := r.Header[xhttp.MinIOSourceReplicationCheck]; ok {
@@ -2093,8 +2070,6 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
replicaTrusted := trustedReplication && rawReplica
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
} }
@@ -2832,17 +2807,11 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return return
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, replica, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateDeleteAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateDeleteAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
var s3Error APIErrorCode var s3Error APIErrorCode
if trustedReplication { if trustedReplication {
s3Error = authorizeReplicationDelete(ctx, r) s3Error = authorizeReplicationDelete(ctx, r)
@@ -2858,7 +2827,6 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationPermissionCheckError), r.URL)
return return
} }
replica := trustedReplication && rawReplica
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replica) ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replica)
} }
+10 -29
View File
@@ -171,18 +171,11 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return return
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
replicaTrusted := trustedReplication && rawReplica
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted) ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
} }
@@ -870,17 +863,11 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return return
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, _, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
storedReplica := mi.UserDefined[xhttp.AmzBucketReplicationStatus] == replication.Replica.String() storedReplica := mi.UserDefined[xhttp.AmzBucketReplicationStatus] == replication.Replica.String()
replicaTrusted := trustedReplication && storedReplica replicaTrusted := trustedReplication && storedReplica
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
@@ -1110,19 +1097,13 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
return return
} }
rawReplica := hasReplicaStatus(r.Header) trustedReplication, replicaTrusted, trustErr := evaluateReplicationTrust(ctx, r, bucket, object, policy.ReplicateObjectAction)
markerExact := hasReplicationMarker(r.Header) if trustErr != ErrNone {
replicationPermitted := false writeErrorResponse(ctx, w, errorCodes.ToAPIErr(trustErr), r.URL)
if rawReplica || markerExact {
replicationPermitted = replicationPermissionAllowed(ctx, r, bucket, object, policy.ReplicateObjectAction)
}
if rawReplica && !replicationPermitted {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return return
} }
trustedReplication := markerExact && replicationPermitted
if hasReplicationRequestHeaders(r.Header) { if hasReplicationRequestHeaders(r.Header) {
ctx, r = applyReplicationTrust(ctx, r, trustedReplication, trustedReplication && rawReplica) ctx, r = applyReplicationTrust(ctx, r, trustedReplication, replicaTrusted)
} }
// Get upload id. // Get upload id.
+22
View File
@@ -70,6 +70,25 @@ func replicationPermissionAllowed(ctx context.Context, r *http.Request, bucket,
return authorizeRequest(ctx, r, action) == ErrNone return authorizeRequest(ctx, r, action) == ErrNone
} }
// evaluateReplicationTrust decides whether a request may carry replication
// semantics for the given action. A request that declares itself a replica
// without holding the replication permission is rejected. trusted reports that
// the exact marker came from a permitted principal; replica additionally
// requires the request to declare REPLICA status.
func evaluateReplicationTrust(ctx context.Context, r *http.Request, bucket, object string, action policy.Action) (trusted, replica bool, s3Err APIErrorCode) {
rawReplica := hasReplicaStatus(r.Header)
markerExact := hasReplicationMarker(r.Header)
permitted := false
if rawReplica || markerExact {
permitted = replicationPermissionAllowed(ctx, r, bucket, object, action)
}
if rawReplica && !permitted {
return false, false, ErrAccessDenied
}
trusted = markerExact && permitted
return trusted, trusted && rawReplica, ErrNone
}
// replicationRequestHeaders are internal request controls. They are removed // replicationRequestHeaders are internal request controls. They are removed
// only after signature verification when a request has not earned replication // only after signature verification when a request has not earned replication
// trust. Public S3/SSE/checksum headers, proxy loop guards, and replication // trust. Public S3/SSE/checksum headers, proxy loop guards, and replication
@@ -110,6 +129,9 @@ func hasReplicationRequestHeaders(h http.Header) bool {
func cloneRequestWithoutReplicationHeaders(ctx context.Context, r *http.Request) *http.Request { func cloneRequestWithoutReplicationHeaders(ctx context.Context, r *http.Request) *http.Request {
clone := r.Clone(ctx) clone := r.Clone(ctx)
stripReplicationRequestHeaders(clone.Header) stripReplicationRequestHeaders(clone.Header)
// A streaming body reader built from the original request fills r.Trailer
// as the body is consumed; the checksum reader must observe that same map.
clone.Trailer = r.Trailer
return clone return clone
} }
+54
View File
@@ -830,3 +830,57 @@ func testAPISSECMultipartReplicationTrust(obj ObjectLayer, instanceType, bucketN
t.Fatal("replicated SSE-C multipart object did not decrypt to source plaintext") t.Fatal("replicated SSE-C multipart object did not decrypt to source plaintext")
} }
} }
// TestAPIStreamingTrailerWithUntrustedReplicationHeaders verifies that a
// request which does not earn replication trust is still processed as an
// ordinary upload. The streaming body reader fills the original request's
// trailer while the handler continues with a header-stripped clone, so the
// trailing checksum must remain visible through that clone.
func TestAPIStreamingTrailerWithUntrustedReplicationHeaders(t *testing.T) {
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIStreamingTrailerWithUntrustedReplicationHeaders})
}
func testAPIStreamingTrailerWithUntrustedReplicationHeaders(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, _ auth.Credentials, t *testing.T) {
putOnly := newObjectAttributesAuthzUser(t, instanceType, bucketName, `"s3:PutObject"`)
payload := bytes.Repeat([]byte("trailer probe "), 4096)
send := func(targetURL string) *httptest.ResponseRecorder {
req, err := newStreamingUnsignedTrailerRequest(http.MethodPut, targetURL, payload, UTCNow())
if err != nil {
t.Fatal(err)
}
req.Header.Set(xhttp.MinIOSourceReplicationRequest, "true")
req.Header.Set(xhttp.MinIOSourceETag, "forged-etag")
if err := signRequestV4(req, putOnly.AccessKey, putOnly.SecretKey); err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
apiRouter.ServeHTTP(rec, req)
return rec
}
if rec := send(getPutObjectURL("", bucketName, "trailer-object")); rec.Code != http.StatusOK {
t.Fatalf("%s: PutObject status %d: %s", instanceType, rec.Code, rec.Body.String())
}
info, err := obj.GetObjectInfo(t.Context(), bucketName, "trailer-object", ObjectOptions{})
if err != nil {
t.Fatal(err)
}
if info.Size != int64(len(payload)) || info.ETag == "forged-etag" {
t.Fatalf("%s: stored size %d etag %q, want %d bytes with a computed etag", instanceType, info.Size, info.ETag, len(payload))
}
upload, err := obj.NewMultipartUpload(t.Context(), bucketName, "trailer-multipart", ObjectOptions{})
if err != nil {
t.Fatal(err)
}
if rec := send(getPutObjectPartURL("", bucketName, "trailer-multipart", upload.UploadID, "1")); rec.Code != http.StatusOK {
t.Fatalf("%s: PutObjectPart status %d: %s", instanceType, rec.Code, rec.Body.String())
}
parts, err := obj.ListObjectParts(t.Context(), bucketName, "trailer-multipart", upload.UploadID, 0, 10, ObjectOptions{})
if err != nil {
t.Fatal(err)
}
if len(parts.Parts) != 1 || parts.Parts[0].Size != int64(len(payload)) {
t.Fatalf("%s: uploaded parts %+v, want one part of %d bytes", instanceType, parts.Parts, len(payload))
}
}
+54 -7
View File
@@ -39,7 +39,9 @@ func testPeerBucketAdoptionPreservesLockAndVersioningConfigs(_ ObjectLayer, inst
_ http.Handler, _ auth.Credentials, t *testing.T, _ http.Handler, _ auth.Credentials, t *testing.T,
) { ) {
objectLockXML := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`) objectLockXML := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`)
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`) // A locked bucket carries plain Enabled versioning; adoption must keep the
// existing document and its timestamp rather than rewrite them.
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`)
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil { if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, objectLockXML); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -77,15 +79,15 @@ func TestPeerBucketAdoptionBootstrapsMissingConfigs(t *testing.T) {
}) })
} }
func TestPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(t *testing.T) { func TestPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(t *testing.T) {
defer DetectTestLeak(t)() defer DetectTestLeak(t)()
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
t: t, t: t,
objAPITest: testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock, objAPITest: testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock,
}) })
} }
func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string, func testPeerBucketAdoptionNormalizesVersioningWhenEnablingLock(_ ObjectLayer, instanceType, bucketName string,
_ http.Handler, _ auth.Credentials, t *testing.T, _ http.Handler, _ auth.Credentials, t *testing.T,
) { ) {
versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`) versioningXML := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`)
@@ -106,8 +108,8 @@ func testPeerBucketAdoptionPreservesCustomVersioningWhenEnablingLock(_ ObjectLay
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(after.VersioningConfigXML, before.VersioningConfigXML) || !after.VersioningConfigUpdatedAt.Equal(before.VersioningConfigUpdatedAt) { if !bytes.Equal(after.VersioningConfigXML, enabledBucketVersioningConfig) || !after.VersioningConfigUpdatedAt.After(before.VersioningConfigUpdatedAt) {
t.Fatalf("%s: custom versioning changed while enabling Object Lock", instanceType) t.Fatalf("%s: prefix-excluded versioning survived enabling Object Lock: %q", instanceType, after.VersioningConfigXML)
} }
if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) { if !bytes.Equal(after.ObjectLockConfigXML, enabledBucketObjectLockConfig) {
t.Fatalf("%s: Object Lock was not bootstrapped", instanceType) t.Fatalf("%s: Object Lock was not bootstrapped", instanceType)
@@ -152,7 +154,7 @@ func TestEnablePeerBucketVersioningRepairsInvalidConfig(t *testing.T) {
meta := newBucketMetadata("bucket") meta := newBucketMetadata("bucket")
meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC) meta.Created = time.Date(2026, time.August, 29, 8, 0, 0, 0, time.UTC)
meta.VersioningConfigXML = []byte(`<VersioningConfiguration>`) meta.VersioningConfigXML = []byte(`<VersioningConfiguration>`)
if err := enablePeerBucketVersioning(&meta); err != nil { if err := enablePeerBucketVersioning(&meta, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() { if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) || meta.VersioningConfigUpdatedAt.IsZero() {
@@ -188,3 +190,48 @@ func testPeerBucketAdoptionBootstrapsMissingConfigs(_ ObjectLayer, instanceType,
after.ObjectLockConfigUpdatedAt, after.VersioningConfigUpdatedAt, before.Created) after.ObjectLockConfigUpdatedAt, after.VersioningConfigUpdatedAt, before.Created)
} }
} }
// TestLockedBucketNormalizesVersioningOnSave covers the metadata boundary
// itself: whatever writer stores a suspended or prefix-excluded versioning
// document on a bucket that carries an Object Lock configuration, including
// one with a default retention rule, Save replaces it with plain Enabled
// versioning.
func TestLockedBucketNormalizesVersioningOnSave(t *testing.T) {
defer DetectTestLeak(t)()
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
t: t,
objAPITest: testLockedBucketNormalizesVersioningOnSave,
makeBucketOptions: MakeBucketOptions{LockEnabled: true},
})
}
func testLockedBucketNormalizesVersioningOnSave(_ ObjectLayer, instanceType, bucketName string,
_ http.Handler, _ auth.Credentials, t *testing.T,
) {
lockWithRule := []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`)
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, objectLockConfig, lockWithRule); err != nil {
t.Fatal(err)
}
for name, versioningXML := range map[string][]byte{
"prefix-excluded": []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status><ExcludeFolders>true</ExcludeFolders><ExcludedPrefixes><Prefix>temporary/</Prefix></ExcludedPrefixes></VersioningConfiguration>`),
"suspended": []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Suspended</Status></VersioningConfiguration>`),
} {
if _, err := globalBucketMetadataSys.Update(t.Context(), bucketName, bucketVersioningConfig, versioningXML); err != nil {
t.Fatal(err)
}
meta, err := globalBucketMetadataSys.Get(bucketName)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) {
t.Fatalf("%s/%s: locked bucket kept versioning %q", instanceType, name, meta.VersioningConfigXML)
}
reloaded, err := loadBucketMetadata(t.Context(), newObjectLayerFn(), bucketName)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(reloaded.VersioningConfigXML, enabledBucketVersioningConfig) {
t.Fatalf("%s/%s: locked bucket persisted versioning %q", instanceType, name, reloaded.VersioningConfigXML)
}
}
}
+4 -2
View File
@@ -159,8 +159,10 @@ func testPeerBucketObjectLockMetadataWithoutLockEnabled(_ ObjectLayer, instanceT
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if meta.objectLockConfig == nil || len(meta.VersioningConfigXML) != 0 { // A lock configuration implies versioning: the bucket was created without
t.Fatalf("%s: unlocked bucket metadata = objectLock:%v versioning:%q", instanceType, meta.objectLockConfig, meta.VersioningConfigXML) // lock, so receiving the configuration turns plain Enabled versioning on.
if meta.objectLockConfig == nil || !bytes.Equal(meta.VersioningConfigXML, enabledBucketVersioningConfig) {
t.Fatalf("%s: bucket metadata = objectLock:%v versioning:%q", instanceType, meta.objectLockConfig, meta.VersioningConfigXML)
} }
} }
@@ -69,12 +69,14 @@ func testSiteReplicationStatusAccountsPerSiteAndSurvivesMalformedConfig(obj Obje
bucketSSEConfig: sseXML, bucketSSEConfig: sseXML,
bucketQuotaConfigFile: quotaJSON, bucketQuotaConfigFile: quotaJSON,
bucketPolicyConfig: policyJSON, bucketPolicyConfig: policyJSON,
bucketCorsConfig: []byte(testSiteReplicationCORSDoc),
} { } {
if _, err := globalBucketMetadataSys.Update(ctx, localBucket, configFile, data); err != nil { if _, err := globalBucketMetadataSys.Update(ctx, localBucket, configFile, data); err != nil {
t.Fatalf("%s: update %s: %v", instanceType, configFile, err) t.Fatalf("%s: update %s: %v", instanceType, configFile, err)
} }
} }
if _, err := updateLocalBucketCORSMetadata(ctx, obj, localBucket, []byte(testSiteReplicationCORSDoc)); err != nil {
t.Fatalf("%s: update %s: %v", instanceType, bucketCorsConfig, err)
}
encode := func(data []byte) *string { encode := func(data []byte) *string {
encoded := base64.StdEncoding.EncodeToString(data) encoded := base64.StdEncoding.EncodeToString(data)
+7 -3
View File
@@ -889,7 +889,11 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string
return errors.Unwrap(cerr) return errors.Unwrap(cerr)
} }
func enablePeerBucketVersioning(meta *BucketMetadata) error { // enablePeerBucketVersioning turns versioning on for a bucket that is being
// created or adopted. With lockEnabled, Object Lock requires every object to
// be versioned: the S3 API rejects suspended or prefix-excluded versioning on
// a locked bucket, so such a configuration is replaced rather than preserved.
func enablePeerBucketVersioning(meta *BucketMetadata, lockEnabled bool) error {
if len(meta.VersioningConfigXML) == 0 { if len(meta.VersioningConfigXML) == 0 {
meta.VersioningConfigXML = enabledBucketVersioningConfig meta.VersioningConfigXML = enabledBucketVersioningConfig
if meta.VersioningConfigUpdatedAt.IsZero() { if meta.VersioningConfigUpdatedAt.IsZero() {
@@ -898,7 +902,7 @@ func enablePeerBucketVersioning(meta *BucketMetadata) error {
return nil return nil
} }
config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML)) config, err := versioning.ParseConfig(bytes.NewReader(meta.VersioningConfigXML))
if err != nil { if err != nil || (lockEnabled && (config.Suspended() || config.PrefixesExcluded())) {
meta.VersioningConfigXML = enabledBucketVersioningConfig meta.VersioningConfigXML = enabledBucketVersioningConfig
meta.VersioningConfigUpdatedAt = UTCNow() meta.VersioningConfigUpdatedAt = UTCNow()
return nil return nil
@@ -943,7 +947,7 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con
} }
meta.SetCreatedAt(opts.CreatedAt) meta.SetCreatedAt(opts.CreatedAt)
if err = enablePeerBucketVersioning(&meta); err != nil { if err = enablePeerBucketVersioning(&meta, opts.LockEnabled || len(meta.ObjectLockConfigXML) != 0); err != nil {
return err return err
} }
if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 { if opts.LockEnabled && len(meta.ObjectLockConfigXML) == 0 {
+10 -4
View File
@@ -1,6 +1,6 @@
# pgsty/minio Security Advisories # pgsty/silo Security Advisories
This document summarizes fork-specific security fixes and closely related upgrade-impacting security notes in `pgsty/minio`. It is intentionally narrower than a full changelog and focuses on release-impacting security behavior. This document summarizes fork-specific security fixes and closely related upgrade-impacting security notes in `pgsty/silo`. It is intentionally narrower than a full changelog and focuses on release-impacting security behavior.
Entries carry a CVE identifier where one exists. Where none does, they carry a fork-local `SN-<year>-<sequence>` identifier so that a finding without a CVE can still be referenced stably from release notes, commits and issues. An `SN-` identifier is **not** a CVE and is not registered in any vulnerability database; it is deliberately not written in CVE form so that scanners do not mistake it for one. Upstream `minio/minio` is archived, so for findings in inherited code there is no upstream maintainer to coordinate a CVE assignment with. `SN-2026-001` is the streaming-flush regression in `trackingResponseWriter`, which is a reliability defect rather than a security one and is tracked in the release notes rather than here. Entries carry a CVE identifier where one exists. Where none does, they carry a fork-local `SN-<year>-<sequence>` identifier so that a finding without a CVE can still be referenced stably from release notes, commits and issues. An `SN-` identifier is **not** a CVE and is not registered in any vulnerability database; it is deliberately not written in CVE form so that scanners do not mistake it for one. Upstream `minio/minio` is archived, so for findings in inherited code there is no upstream maintainer to coordinate a CVE assignment with. `SN-2026-001` is the streaming-flush regression in `trackingResponseWriter`, which is a reliability defect rather than a security one and is tracked in the release notes rather than here.
@@ -24,10 +24,15 @@ The first Silo community release was cut from upstream history that already cont
| [CVE-2026-40344](https://github.com/advisories/GHSA-9c4q-hq6p-c237) | `efb6e5b00` | Snowball auto-extract authentication | Yes | Verifies request authentication before tar extraction in Snowball unsigned-trailer flows | Upgrade if you use `PutObjectExtract` or Snowball uploads. | | [CVE-2026-40344](https://github.com/advisories/GHSA-9c4q-hq6p-c237) | `efb6e5b00` | Snowball auto-extract authentication | Yes | Verifies request authentication before tar extraction in Snowball unsigned-trailer flows | Upgrade if you use `PutObjectExtract` or Snowball uploads. |
| [CVE-2026-42600](https://github.com/advisories/GHSA-xh8f-g2qw-gcm7) | `73ac52472` | Internode `ReadMultiple` storage-REST endpoint | Yes (cluster-root JWT required) | Removes the unused endpoint that allowed path traversal outside configured drive roots | Upgrade distributed-erasure deployments. Single-node deployments do not register this route. | | [CVE-2026-42600](https://github.com/advisories/GHSA-xh8f-g2qw-gcm7) | `73ac52472` | Internode `ReadMultiple` storage-REST endpoint | Yes (cluster-root JWT required) | Removes the unused endpoint that allowed path traversal outside configured drive roots | Upgrade distributed-erasure deployments. Single-node deployments do not register this route. |
| `SN-2026-002` | `ca7baa670` and follow-ups | Internode storage-REST and Grid RPC payloads | Yes (cluster-root / internode JWT required) | Completes CVE-2026-42600. Its fix removed one endpoint that exercised the gap; the gap itself -- request bodies and grid frames never reaching the validity middleware, and no containment in the storage layer -- remained across three further protocol surfaces. Closes path traversal on both the volume and path axes (including the peer-S3 bucket RPCs, which bypass the storage-REST wrapper entirely), an unrecoverable divide-by-zero that killed a node per RPC frame, metadata that reported truncated shards as intact, and three allocations sized from caller-declared values. | Upgrade distributed-erasure deployments. Single-node deployments register none of these routes. No S3 API behaviour changes; object keys containing `.` or `..` path segments were already refused at the S3 boundary. | | `SN-2026-002` | `ca7baa670` and follow-ups | Internode storage-REST and Grid RPC payloads | Yes (cluster-root / internode JWT required) | Completes CVE-2026-42600. Its fix removed one endpoint that exercised the gap; the gap itself -- request bodies and grid frames never reaching the validity middleware, and no containment in the storage layer -- remained across three further protocol surfaces. Closes path traversal on both the volume and path axes (including the peer-S3 bucket RPCs, which bypass the storage-REST wrapper entirely), an unrecoverable divide-by-zero that killed a node per RPC frame, metadata that reported truncated shards as intact, and three allocations sized from caller-declared values. | Upgrade distributed-erasure deployments. Single-node deployments register none of these routes. No S3 API behaviour changes; object keys containing `.` or `..` path segments were already refused at the S3 boundary. |
| `SN-2026-003` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`2f55347f7`](https://github.com/pgsty/minio/commit/2f55347f78352aed8e08866d370c9426c73362cf) | S3/IAM bucket-policy condition values | Yes (policy-dependent) | Prevents raw request entries that spell condition-key names from shadowing or synthesizing internal condition values; confines `s3:signatureAge` to verified SigV4 presigned requests; separates query-only list fields from header-backed `x-amz-*` fields; and stops client request tags from impersonating stored existing-object tags. | The compatible query form remains for storage class and upload tagging on handlers that consume it; an explicitly present header wins, including an empty header. The historical `X-Amz-Tagging` Header mapping remains a client-supplied `RequestObjectTag` source, so use request-tag conditions only on operations that consume tags. Header-only `x-amz-*` policy keys no longer accept query substitutes. `aws:SourceIp` was left following the existing forwarding-header trust model; that model is addressed separately in the next row. See [Condition value sources and precedence](https://silo.pgsty.com/administration/identity-access-management/policy-based-access-control/#condition-value-sources). | | `SN-2026-003` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`2f55347f7`](https://github.com/pgsty/silo/commit/2f55347f78352aed8e08866d370c9426c73362cf) | S3/IAM bucket-policy condition values | Yes (policy-dependent) | Prevents raw request entries that spell condition-key names from shadowing or synthesizing internal condition values; confines `s3:signatureAge` to verified SigV4 presigned requests; separates query-only list fields from header-backed `x-amz-*` fields; and stops client request tags from impersonating stored existing-object tags. | The compatible query form remains for storage class and upload tagging on handlers that consume it; an explicitly present header wins, including an empty header. The historical `X-Amz-Tagging` Header mapping remains a client-supplied `RequestObjectTag` source, so use request-tag conditions only on operations that consume tags. Header-only `x-amz-*` policy keys no longer accept query substitutes. `aws:SourceIp` was left following the existing forwarding-header trust model; that model is addressed separately in the next row. See [Condition value sources and precedence](https://silo.pgsty.com/administration/identity-access-management/policy-based-access-control/#condition-value-sources). |
| Not a vulnerability | `fe6dc4780` | Client source address (`aws:SourceIp`, audit `remotehost`, event notification `Host`) | N/A -- opt-in hardening | Adds an enforceable forwarded-header trust boundary, `MINIO_API_TRUSTED_PROXIES`. Set to a list of addresses or CIDR blocks, forwarded headers are believed only from those peers and forwarding chains are read right-to-left past listed hops -- which also stops the client-supplied left-most entry that an appending proxy (the stock nginx `$proxy_add_x_forwarded_for` recipe, or HAProxy's added second header line) leaves in place. Set to `none`, no forwarded header is believed at all. This is the guarantee `_MINIO_API_XFF_HEADER=off` never provided: it suppresses `X-Forwarded-For` alone, so `X-Real-IP` and RFC 7239 `Forwarded` remain one-line substitutions for anyone that setting was meant to stop. | **No behaviour change for any existing deployment**, so there is nothing to do on upgrade unless you want the new boundary. Not assigned a CVE: the default matches upstream, and upstream's own position (maintainer response in [discussion #17878](https://github.com/minio/minio/discussions/17878), Aug 2023) is that IP-based restrictions are impractical without reliable source-IP visibility. The gap being closed is that this was never written anywhere an operator would find it -- an `IpAddress` condition is accepted and behaves as though it works. **If you use `IpAddress` or `NotIpAddress` conditions, note that they were not enforceable before this change**, including behind a reverse proxy whose `X-Forwarded-For` recipe appends rather than overwrites. If you do not, the change affects only the accuracy of client addresses in logs. The new variable is opt-in and inert when unset; `_MINIO_API_XFF_HEADER` keeps its exact upstream semantics, and upstream's `TestXFFDisabled` is retained unmodified as the proof. An `IpAddress` condition remains unenforceable by default against a client with direct network access to the API port -- that is the condition the allowlist exists to fix, not a regression introduced here. When enabling the allowlist: it must name proxies, not the subnet they sit in, because entries are skipped while walking the chain, so a range that also covers clients lets those clients forge. Multi-node deployments must include their own node addresses, since MinIO forwards some requests between nodes and a client can force a hop through the `ListObjectsV2` continuation token; prefer the allowlist over `none` on a cluster for that reason. Loopback is always trusted as a peer so FTP and SFTP keep attributing their sessions. A malformed value stops startup, as does one that names no proxy at all (`","`) or one whose `env://` remote could not be read -- `env.Get` discards that error and yields an empty string, which would otherwise read as unset. Whitespace-only remains equivalent to unset. The policy is read after `MINIO_CONFIG_ENV_FILE` is loaded so environment-file deployments are covered; `_MINIO_API_XFF_HEADER` deliberately keeps upstream's earlier read timing, where a value written into an environment file is ignored. `MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES` now shares the same list parser, but is behaviourally untouched: the extraction is pure code motion, verified identical to the previous implementation across every combination of 37 allowlist values and 21 peer addresses. See [Client source address trust](source-address-trust.md). | | Not a vulnerability | `fe6dc4780` | Client source address (`aws:SourceIp`, audit `remotehost`, event notification `Host`) | N/A -- opt-in hardening | Adds an enforceable forwarded-header trust boundary, `MINIO_API_TRUSTED_PROXIES`. Set to a list of addresses or CIDR blocks, forwarded headers are believed only from those peers and forwarding chains are read right-to-left past listed hops -- which also stops the client-supplied left-most entry that an appending proxy (the stock nginx `$proxy_add_x_forwarded_for` recipe, or HAProxy's added second header line) leaves in place. Set to `none`, no forwarded header is believed at all. This is the guarantee `_MINIO_API_XFF_HEADER=off` never provided: it suppresses `X-Forwarded-For` alone, so `X-Real-IP` and RFC 7239 `Forwarded` remain one-line substitutions for anyone that setting was meant to stop. | **No behaviour change for any existing deployment**, so there is nothing to do on upgrade unless you want the new boundary. Not assigned a CVE: the default matches upstream, and upstream's own position (maintainer response in [discussion #17878](https://github.com/minio/minio/discussions/17878), Aug 2023) is that IP-based restrictions are impractical without reliable source-IP visibility. The gap being closed is that this was never written anywhere an operator would find it -- an `IpAddress` condition is accepted and behaves as though it works. **If you use `IpAddress` or `NotIpAddress` conditions, note that they were not enforceable before this change**, including behind a reverse proxy whose `X-Forwarded-For` recipe appends rather than overwrites. If you do not, the change affects only the accuracy of client addresses in logs. The new variable is opt-in and inert when unset; `_MINIO_API_XFF_HEADER` keeps its exact upstream semantics, and upstream's `TestXFFDisabled` is retained unmodified as the proof. An `IpAddress` condition remains unenforceable by default against a client with direct network access to the API port -- that is the condition the allowlist exists to fix, not a regression introduced here. When enabling the allowlist: it must name proxies, not the subnet they sit in, because entries are skipped while walking the chain, so a range that also covers clients lets those clients forge. Multi-node deployments must include their own node addresses, since MinIO forwards some requests between nodes and a client can force a hop through the `ListObjectsV2` continuation token; prefer the allowlist over `none` on a cluster for that reason. Loopback is always trusted as a peer so FTP and SFTP keep attributing their sessions. A malformed value stops startup, as does one that names no proxy at all (`","`) or one whose `env://` remote could not be read -- `env.Get` discards that error and yields an empty string, which would otherwise read as unset. Whitespace-only remains equivalent to unset. The policy is read after `MINIO_CONFIG_ENV_FILE` is loaded so environment-file deployments are covered; `_MINIO_API_XFF_HEADER` deliberately keeps upstream's earlier read timing, where a value written into an environment file is ignored. `MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES` now shares the same list parser, but is behaviourally untouched: the extraction is pure code motion, verified identical to the previous implementation across every combination of 37 allowlist values and 21 peer addresses. See [Client source address trust](source-address-trust.md). |
| `SN-2026-004` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`97b7d2804`](https://github.com/pgsty/minio/commit/97b7d28040d109061c0a46a4c01bfc7800a97cc1) | IAM policy evaluation of bucket-level actions | Yes (policy-dependent) | Withholds twelve sensitive bucket-level writes from an object-only resource pattern. The IAM matcher appended a trailing slash for bucket-level requests (empty object name), so a resource of `arn:aws:s3:::bucket/*` matched `"bucket/"` and authorized bucket-level actions it was never meant to reach -- upstream [minio/minio#20449](https://github.com/minio/minio/issues/20449). The bucket-policy evaluation path never had the slash and was already reference-correct. | **This is an authorization tightening; read this row before upgrading if you write your own bucket-scoped policies.** Withheld from `bucket/*` on `Allow` statements only: `PutBucketPolicy`, `DeleteBucketPolicy`, `PutBucketObjectLockConfiguration`, `PutBucketVersioning`, `PutReplicationConfiguration`, `PutBucketLifecycle`, `DeleteBucket`, `ForceDeleteBucket`, `PutBucketCors`, `DeleteBucketCors`, `PutBucketQOS`, `PutInventoryConfiguration`. Membership was decided by one question -- does reaching this action give the caller something its object-scoped grant does not already give it? -- because the bug only fires when the statement already grants the bucket action, which in practice means `s3:*`, so the affected principal already holds full object CRUD. Only actions that hand out access to others, defeat a protection aimed at write-holders, act under server credentials, outlive the grant, or destroy the bucket entity qualify. **Deliberately not withheld, and asserted by test so re-adding one is a deliberate act**: `ListBucket`, `GetBucketLocation` and the read/list family, `PutBucketTagging`, `PutBucketEncryption`, `PutBucketNotification`, and `CreateBucket` -- so `mc ls`, SDK session setup and ordinary tenant self-service keep working through `bucket/*`. Breaking those is what got upstream's own full fix reverted. **What to change**: add the bare bucket ARN (`arn:aws:s3:::bucket`) alongside `arn:aws:s3:::bucket/*` in any statement that legitimately grants one of the twelve. Built-in canned policies are unaffected (all use `Resource: "*"`). `Deny` statements are untouched, so no bucket lock is ever weakened, and `NotResource` exclusions keep their full reach. The hardening is monotone by construction rather than by argument: the protected path requires **both** the bare and the historical `"bucket/"` form to match, an intersection with the historical decision -- without that, a fixed-width wildcard such as `mybucke?` would match `"mybucket"` while never having matched `"mybucket/"`, and the hardening would have granted a write the buggy matcher refused. `MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH=on` restores the historical behaviour in full; it is read once at startup. Still deferred to a migration-gated release: the read/list family, a startup audit naming affected policies, and a self-explaining denial log. | | `SN-2026-004` | [`silo-pkg v3.11.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.11.0) and [`97b7d2804`](https://github.com/pgsty/silo/commit/97b7d28040d109061c0a46a4c01bfc7800a97cc1) | IAM policy evaluation of bucket-level actions | Yes (policy-dependent) | Withholds twelve sensitive bucket-level writes from an object-only resource pattern. The IAM matcher appended a trailing slash for bucket-level requests (empty object name), so a resource of `arn:aws:s3:::bucket/*` matched `"bucket/"` and authorized bucket-level actions it was never meant to reach -- upstream [minio/minio#20449](https://github.com/minio/minio/issues/20449). The bucket-policy evaluation path never had the slash and was already reference-correct. | **This is an authorization tightening; read this row before upgrading if you write your own bucket-scoped policies.** Withheld from `bucket/*` on `Allow` statements only: `PutBucketPolicy`, `DeleteBucketPolicy`, `PutBucketObjectLockConfiguration`, `PutBucketVersioning`, `PutReplicationConfiguration`, `PutBucketLifecycle`, `DeleteBucket`, `ForceDeleteBucket`, `PutBucketCors`, `DeleteBucketCors`, `PutBucketQOS`, `PutInventoryConfiguration`. Membership was decided by one question -- does reaching this action give the caller something its object-scoped grant does not already give it? -- because the bug only fires when the statement already grants the bucket action, which in practice means `s3:*`, so the affected principal already holds full object CRUD. Only actions that hand out access to others, defeat a protection aimed at write-holders, act under server credentials, outlive the grant, or destroy the bucket entity qualify. **Deliberately not withheld, and asserted by test so re-adding one is a deliberate act**: `ListBucket`, `GetBucketLocation` and the read/list family, `PutBucketTagging`, `PutBucketEncryption`, `PutBucketNotification`, and `CreateBucket` -- so `mc ls`, SDK session setup and ordinary tenant self-service keep working through `bucket/*`. Breaking those is what got upstream's own full fix reverted. **What to change**: add the bare bucket ARN (`arn:aws:s3:::bucket`) alongside `arn:aws:s3:::bucket/*` in any statement that legitimately grants one of the twelve. Built-in canned policies are unaffected (all use `Resource: "*"`). `Deny` statements are untouched, so no bucket lock is ever weakened, and `NotResource` exclusions keep their full reach. The hardening is monotone by construction rather than by argument: the protected path requires **both** the bare and the historical `"bucket/"` form to match, an intersection with the historical decision -- without that, a fixed-width wildcard such as `mybucke?` would match `"mybucket"` while never having matched `"mybucket/"`, and the hardening would have granted a write the buggy matcher refused. `MINIO_API_LEGACY_BUCKET_RESOURCE_MATCH=on` restores the historical behaviour in full; it is read once at startup. Still deferred to a migration-gated release: the read/list family, a startup audit naming affected policies, and a self-explaining denial log. |
| `SN-2026-005` | [`silo-pkg v3.12.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.12.0) and [`eee05a17c`](https://github.com/pgsty/silo/commit/eee05a17c34a07cebb27220d12697be74c8bd617) | IAM named-policy and service-account policy writes | No direct remote exploit; policy-dependent | Rejects S3, S3 Tables, and KMS ARN namespace prefixes that name no resource, including their historical `*arn:...` serialization, in both `Resource` and `NotResource`. A resource-matching `Deny` using such a prefix could silently fail to deny, while an `Allow` with the prefix in `NotResource` could match far more broadly than intended. The guard applies when creating named policies and when creating or updating service-account session policies. | **This is an authorization tightening for new and updated policies.** Existing policies keep loading, matching, importing, and replicating with unchanged runtime behavior, but a policy containing one of these prefixes cannot be submitted unchanged; replace it with the intended concrete resource, or use an explicit wildcard such as `arn:aws:s3:::*` only when all resources are intended. Enabling the strict path also rejects an admin statement that combines `Resource` with `NotResource`, and rejects non-S3 resources on bucket-scoped admin actions. Here “bare ARN prefix” means a namespace with no resource after it (`arn:aws:s3:::`); it is distinct from the valid “bare bucket ARN” in `SN-2026-004` (`arn:aws:s3:::bucket`). IAM import, site-replication receive paths, stored-policy loading, and STS inline policies remain on the permissive compatibility path in this release. | | `SN-2026-005` | [`silo-pkg v3.12.0`](https://github.com/pgsty/silo-pkg/releases/tag/v3.12.0) and [`eee05a17c`](https://github.com/pgsty/silo/commit/eee05a17c34a07cebb27220d12697be74c8bd617) | IAM named-policy and service-account policy writes | No direct remote exploit; policy-dependent | Rejects S3, S3 Tables, and KMS ARN namespace prefixes that name no resource, including their historical `*arn:...` serialization, in both `Resource` and `NotResource`. A resource-matching `Deny` using such a prefix could silently fail to deny, while an `Allow` with the prefix in `NotResource` could match far more broadly than intended. The guard applies when creating named policies and when creating or updating service-account session policies. | **This is an authorization tightening for new and updated policies.** Existing policies keep loading, matching, importing, and replicating with unchanged runtime behavior, but a policy containing one of these prefixes cannot be submitted unchanged; replace it with the intended concrete resource, or use an explicit wildcard such as `arn:aws:s3:::*` only when all resources are intended. Enabling the strict path also rejects an admin statement that combines `Resource` with `NotResource`, and rejects non-S3 resources on bucket-scoped admin actions. Here “bare ARN prefix” means a namespace with no resource after it (`arn:aws:s3:::`); it is distinct from the valid “bare bucket ARN” in `SN-2026-004` (`arn:aws:s3:::bucket`). IAM import, site-replication receive paths, stored-policy loading, and STS inline policies remain on the permissive compatibility path in this release. |
| `SN-2026-006` | [`b73581b05`](https://github.com/pgsty/silo/commit/b73581b05) and [`c4fd97d0b`](https://github.com/pgsty/silo/commit/c4fd97d0b) ([#82](https://github.com/pgsty/silo/issues/82)) | SSE-C reads of zero-byte objects (`GetObject`, `HeadObject`, `CopyObject` source, `GetObjectAttributes`) | Yes; requires read access to the object | Zero-byte SSE-C objects never unsealed the customer-provided key, so a wrong key was accepted with `200` instead of `403`, and a copy or new version could be created under a key of the caller's choosing without knowing the current one. | Wrong keys now fail with `403 AccessDenied` as on AWS; correct keys behave as before and no client change is needed. Inherited from upstream; every earlier release is affected. |
| `SN-2026-007` | [`474cd5801`](https://github.com/pgsty/silo/commit/474cd5801), [`74c97d005`](https://github.com/pgsty/silo/commit/74c97d005), [`21870fa2e`](https://github.com/pgsty/silo/commit/21870fa2e) ([#84](https://github.com/pgsty/silo/issues/84)) | `GetObjectAttributes` on SSE-C objects | Yes; requires read access to the object | Attributes of SSE-C objects were returned without authenticating the customer key, and a bare `X-Minio-Source-Replication-Request` header skipped the check entirely. | A wrong key returns `403`, a replication marker without the key returns `400`; replication peers holding `s3:ReplicateObject` are unaffected. Inherited from upstream. |
| `SN-2026-008` | [PR #101](https://github.com/pgsty/silo/pull/101) ([`938603458`](https://github.com/pgsty/silo/commit/938603458) through [`04b097fd9`](https://github.com/pgsty/silo/commit/04b097fd9)) | Internal replication request headers such as `X-Minio-Source-Etag`, `X-Minio-Source-Mtime`, `X-Minio-Source-Replication-Request`, the replication SSE key headers, and `X-Amz-Bucket-Replication-Status` on object reads, writes, multipart uploads, deletes, Snowball extraction, and bucket events | Yes; any authenticated principal that can read or write the object | Completes CVE-2026-34204. The server still trusted these internal headers on presence in most handlers: any client could preserve arbitrary ETags and modification times, read SSE-C ciphertext without the key, inject replication checksums and Object Lock timestamps, suppress bucket notifications, and route deletes as replication deletes. | Replication semantics now require the exact marker value together with `s3:ReplicateObject` or `s3:ReplicateDelete`; other requests have these headers removed after signature verification and are processed as ordinary requests. Site replication service accounts and bucket-replication targets that already hold the replication permissions are unaffected. Inherited from upstream. |
| `SN-2026-009` | [`58735ee38`](https://github.com/pgsty/silo/commit/58735ee38) and [`229fe2b3c`](https://github.com/pgsty/silo/commit/229fe2b3c) ([PR #73](https://github.com/pgsty/silo/pull/73)) | Admin `SetUserStatus` and `SetGroupStatus` | Yes; authenticated admin API | Status changes were authorized against `admin:EnableUser` / `admin:EnableGroup` regardless of the requested status, so a principal allowed only to enable could also disable, and vice versa. | Enable and disable now require the action matching the target status. Policies that grant only one of the pair lose the other operation; `admin:*` and the built-in `consoleAdmin` policy are unaffected. Inherited from upstream. |
| `SN-2026-010` | [PR #104](https://github.com/pgsty/silo/pull/104) ([`75a6734e4`](https://github.com/pgsty/silo/commit/75a6734e4) through [`d2d47a41f`](https://github.com/pgsty/silo/commit/d2d47a41f), [#58](https://github.com/pgsty/silo/issues/58)) | `DeleteObject` and `DeleteObjects` with an explicit `versionId` | Yes; authenticated S3 API | Explicit version deletes were authorized as `s3:DeleteObject` with only a deny check on `s3:DeleteObjectVersion`, diverging from AWS. | Explicit version deletes now require `s3:DeleteObjectVersion`, as on AWS. **Two policy effects:** principals granted only `s3:DeleteObject` can no longer delete specific versions, and a policy that relied on `Deny s3:DeleteObject` to block permanent deletes must also deny `s3:DeleteObjectVersion`, because `Allow s3:*` now permits explicit version deletes. Replication targets keep the `s3:ReplicateDelete` contract. Inherited from upstream. |
## Dependency security updates ## Dependency security updates
@@ -36,6 +41,7 @@ The first Silo community release was cut from upstream history that already cont
| `CVE-2026-34986` | `68e0ba997` | Upgrades `go-jose` to `v4.1.4`. | | `CVE-2026-34986` | `68e0ba997` | Upgrades `go-jose` to `v4.1.4`. |
| `CVE-2026-39883` | `1869bd30b`, `e4fa06394` | Updates OpenTelemetry dependencies. | | `CVE-2026-39883` | `1869bd30b`, `e4fa06394` | Updates OpenTelemetry dependencies. |
| Upstream Go security fixes | [Go 1.26.5](https://go.dev/doc/devel/release#go1.26.5) | Bumps the required toolchain to Go 1.26.5, which includes security fixes to `crypto/tls` and `os`. | | Upstream Go security fixes | [Go 1.26.5](https://go.dev/doc/devel/release#go1.26.5) | Bumps the required toolchain to Go 1.26.5, which includes security fixes to `crypto/tls` and `os`. |
| Toolchain and dependency refresh | [Go 1.27.0](https://go.dev/doc/devel/release#go1.27) via [`43f4bb7ed`](https://github.com/pgsty/silo/commit/43f4bb7ed), [`edc8be6ed`](https://github.com/pgsty/silo/commit/edc8be6ed), [`4d6e1ea8e`](https://github.com/pgsty/silo/commit/4d6e1ea8e) | Moves the toolchain to Go 1.27.0 and refreshes the dependency stack (etcd client v3.7.1, `jwx` v3.0.13, `klauspost/compress` v1.19.2). The pre-release cleanup then returns to upstream `minio-go` (v7.3.1 pre-release) and retires the `silo-go` fork; `govulncheck` reports no reachable vulnerability on the release candidate. |
| [GO-2026-6061](https://pkg.go.dev/vuln/GO-2026-6061) / [GHSA-hrxh-6v49-42gf](https://github.com/advisories/GHSA-hrxh-6v49-42gf) | gRPC `v1.82.1` | Updates gRPC to the first fixed version for vulnerabilities in the xDS RBAC authorization engine and HTTP/2 transport server. | | [GO-2026-6061](https://pkg.go.dev/vuln/GO-2026-6061) / [GHSA-hrxh-6v49-42gf](https://github.com/advisories/GHSA-hrxh-6v49-42gf) | gRPC `v1.82.1` | Updates gRPC to the first fixed version for vulnerabilities in the xDS RBAC authorization engine and HTTP/2 transport server. |
| [GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970) / `CVE-2026-56852` | `x/text` `v0.39.0` | Updates `x/text` to the first fixed version for an infinite loop on invalid input. | | [GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970) / `CVE-2026-56852` | `x/text` `v0.39.0` | Updates `x/text` to the first fixed version for an infinite loop on invalid input. |
+7 -96
View File
@@ -2,13 +2,13 @@
## Status ## Status
- Issue: [pgsty/silo#75](https://github.com/pgsty/silo/issues/75) - Issue: [pgsty/silo#75](https://github.com/pgsty/silo/issues/75), closed; follow-ups
- Baseline: `e4e3007da6d7d1198a6a050e34f84566d40a9654` [#77](https://github.com/pgsty/silo/issues/77) and [#102](https://github.com/pgsty/silo/issues/102)
- Working branch: `codex/issue-75-cors-hardening` - Merged: [PR #80](https://github.com/pgsty/silo/pull/80) implemented this register
- Decision: CORS-specific deterministic last-writer-wins register, described below (2026-08-29); [PR #101](https://github.com/pgsty/silo/pull/101) restricted the
- Implementation state: B2 is commit `724f8703d`; the final B2+B3 integration is signed commit `0eebc928f` on the PR #80 branch and has passed combined local acceptance pre-authentication lookup to resident metadata; [PR #103](https://github.com/pgsty/silo/pull/103)
- Release state: PR #80 remains open; nothing is merged, tagged, packaged, published as an image, or deployed replaced the CORS-specific lock with the shared `metadata.lock`
- Final design/implementation review: the B2 implementation was GO; the combined B2+B3 Opus 5 Max review found one test-build conflict and one legacy-metadata load risk, both corrected before combined testing - Release state: on `main`, not yet in a tagged release as of 2026-09-02
This document defines the replication state, ordering, persistence, status, This document defines the replication state, ordering, persistence, status,
healing, concurrency, compatibility, and test contract for per-bucket CORS. healing, concurrency, compatibility, and test contract for per-bucket CORS.
@@ -501,92 +501,3 @@ The required test matrix is:
| Restart | cache removal/disk reload preserves tombstone or live state and status timestamp | | Restart | cache removal/disk reload preserves tombstone or live state and status timestamp |
| Legacy repair | a lenient historical document loads fail-closed without hiding other metadata and can be deleted or replaced | | Legacy repair | a lenient historical document loads fail-closed without hiding other metadata and can be deleted or replaced |
| Full seam | signed admin dispatch -> peer apply -> real status collection -> local heal -> cache reload -> remote heal dispatch | | Full seam | signed admin dispatch -> peer apply -> real status collection -> local heal -> cache reload -> remote heal dispatch |
## Local Verification Record
The committed B2 implementation passed:
- the supplied adversarial base64 and same-payload/newer-timestamp tests;
- focused CORS normal tests;
- focused CORS race tests;
- `go test ./internal/bucket/cors` and its race run;
- `go test ./cmd -count=1`;
- `go vet ./...`;
- `go build ./...`;
- repository-configured golangci-lint v2.13.1 with zero issues;
- gofmt and `git diff --check`;
- a signed admin dispatch -> apply -> status -> heal -> cache reload test.
After integrating B3 and resolving overlap, the frozen combination passed:
- focused strict-parser, validation, middleware, replication, namespace,
legacy-repair, and race tests;
- CI-tagged `go test ./...`, full vet/build, module verification, pinned lint,
and rebrand/compatibility checks;
- a real local two-site deployment with two nodes per site, including
bidirectional replacement, a site missing DELETE while offline, restart
heal, and a second restart preserving the tombstone; and
- raw SigV4 wire probes that reject a lowercase method and trailing XML root,
accept a 255-code-point Unicode ID, and replicate the accepted config.
The public EN/ZH design records pass a warning-fatal Hugo build, rendered link
checking, and local browser QA. These results are acceptance evidence, not a
release, deployment, tag, or production claim.
The repository `make lint` bootstrap could not download its private copy of
golangci-lint because the network returned HTTP status 000. The same exact
v2.13.1 binary already installed locally was used with the Makefile's build
tags, timeout, and configuration and reported zero issues.
## Independent Review Record
Four read-only local Claude Code reviews used canonical model
`claude-opus-5` at `max` effort.
The first review rejected the pre-fix candidate and identified the unsafe
CreatedAt-based baseline, missing deterministic tie-break, missing atomic join,
non-monotonic local barrier, timestamp-blind status, and initial-sync tombstone
gap. The selected C-prime model incorporated the valid findings while rejecting
the suggestion to rewrite normal source timestamps.
The second review found no P0. Its `GO WITH FIXES` findings were peer semantic
validation, the legacy/default admin mutation path bypassing the then-current
CORS lock and join, CreatedAt-floor observability, and missing tests for
invalid XML, lineage, and concurrent local transitions. Those required changes
and tests are now in the working tree.
The final review examined this design and the exact dirty diff, independently
reran build, vet, lint, normal tests, and race tests, and found no P0 or P1.
Its verdict was `GO WITH FIXES`: the implementation was explicitly judged GO,
while five design-document statements required correction. It also suggested
an optional status hardening so semantically invalid canonical payloads are
not selected and retransmitted. The hardening and all mandatory documentation
corrections are incorporated in the current tree. The final selected solution
is therefore the C-prime register and invariants recorded in this document.
The fourth review examined the resolved B2+B3 combination. It confirmed that
the C-prime register, strict wire parser, MaxAge presence, wildcard credentials,
Origin-null marker, rejected-preflight `Vary`, checksum classification, and
peer validation can coexist. It found a conflict-resolution test helper typo
and the risk that strict parsing could make all bucket metadata unavailable for
a document accepted by a lenient development build. The helper was corrected;
metadata loading now stashes a CORS-specific error, fails browser behavior
closed, rejects new invalid saves, and allows a valid CORS PUT/DELETE repair.
## Release Gates
An implementation-level GO means only that the local CORS state machine and
tests satisfy this document. It does not authorize a release.
Before closing issue #75 or publishing a server artifact:
1. commit the exact reviewed implementation and design with DCO sign-off;
2. push a focused branch and run remote PR CI;
3. merge and confirm main CI on the merge commit;
4. finish public EN/ZH upgrade, fallback, and downgrade documentation in
`silo.pgsty.com`;
5. run a real two-site process test for PUT, DELETE, simultaneous conflict,
offline peer restart, status, and heal;
6. verify no release tag or image contains an intermediate candidate; and
7. treat package, image, SBOM, signature, canary, and production verification
as separate gates.
+11 -12
View File
@@ -2,22 +2,21 @@ module github.com/minio/minio
go 1.27.0 go 1.27.0
// Use PGSTY's maintained Silo Go SDK while preserving upstream import paths. // Use Pigsty's SILO Console while preserving upstream import paths. The
// Keep the required version on a real upstream tag because replace directives // pseudo-version pins the last commit of the v2.3.0 line before Console moved
// are ignored when this module is consumed as a dependency. // to the github.com/pgsty/silo-pkg/v3 module path: it carries the v2.3.0
replace github.com/minio/minio-go/v7 => github.com/pgsty/silo-go/v7 v7.3.1 // security fixes and still consumes silo-pkg through the replacement below.
replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38
// Use Pigsty's SILO Console v2.2.1 release while preserving upstream import paths.
// The pseudo-version pins v2.2.1's commit because the compatible module path has no /v2 suffix.
replace github.com/minio/console => github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf
// Use Pigsty's maintained mc fork for Console's embedded client code. // Use Pigsty's maintained mc fork for Console's embedded client code.
replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1 replace github.com/minio/mc => github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1
// Use Pigsty's maintained SILO package fork while preserving upstream import paths. // Use Pigsty's maintained SILO package fork while preserving upstream import paths.
// This retains the LDAP TLS fix tracked in https://github.com/pgsty/silo/issues/15. // This retains the LDAP TLS fix tracked in https://github.com/pgsty/silo/issues/15,
// v3.12.2 retains the minio/minio#20449 bucket-write boundary hardening, // the minio/minio#20449 bucket-write boundary hardening, bare ARN rejection on
// rejects bare ARN prefixes on strict policy-write paths, and selects Silo Go v7.3.1. // strict policy-write paths. It is the last silo-pkg commit
// that declares the github.com/minio/pkg/v3 module path; v3.13.0 moved to
// github.com/pgsty/silo-pkg/v3 and cannot be selected through this replace.
replace github.com/minio/pkg/v3 => github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7 replace github.com/minio/pkg/v3 => github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7
// v22.7.0 does not compile on NetBSD because its unix implementation uses // v22.7.0 does not compile on NetBSD because its unix implementation uses
@@ -80,7 +79,7 @@ require (
github.com/minio/kms-go/kes v0.3.1 github.com/minio/kms-go/kes v0.3.1
github.com/minio/kms-go/kms v0.6.0 github.com/minio/kms-go/kms v0.6.0
github.com/minio/madmin-go/v3 v3.0.110 github.com/minio/madmin-go/v3 v3.0.110
github.com/minio/minio-go/v7 v7.3.0 github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe
github.com/minio/mux v1.9.2 github.com/minio/mux v1.9.2
github.com/minio/pkg/v3 v3.6.1 github.com/minio/pkg/v3 v3.6.1
github.com/minio/selfupdate v0.6.0 github.com/minio/selfupdate v0.6.0
+4 -4
View File
@@ -474,6 +474,8 @@ github.com/minio/madmin-go/v3 v3.0.110 h1:FIYekj7YPc430ffpXFWiUtyut3qBt/unIAcDzJ
github.com/minio/madmin-go/v3 v3.0.110/go.mod h1:WOe2kYmYl1OIlY2DSRHVQ8j1v4OItARQ6jGyQqcCud8= github.com/minio/madmin-go/v3 v3.0.110/go.mod h1:WOe2kYmYl1OIlY2DSRHVQ8j1v4OItARQ6jGyQqcCud8=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe h1:By2FKNSOUGLOeb0x4D7xJMHr8x/X1ZW8PG780SpKUwQ=
github.com/minio/minio-go/v7 v7.3.1-0.20260828014306-0e78d3f18efe/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
github.com/minio/mux v1.9.2 h1:dQchne49BUBgOlxIHjx5wVe1gl5VXF2sxd4YCXkikTw= github.com/minio/mux v1.9.2 h1:dQchne49BUBgOlxIHjx5wVe1gl5VXF2sxd4YCXkikTw=
github.com/minio/mux v1.9.2/go.mod h1:OuHAsZsux+e562bcO2P3Zv/P0LMo6fPQ310SmoyG7mQ= github.com/minio/mux v1.9.2/go.mod h1:OuHAsZsux+e562bcO2P3Zv/P0LMo6fPQ310SmoyG7mQ=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
@@ -543,10 +545,8 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwp
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1 h1:CotSAPr11rZy3VK46j5rJQYQ2LHT2mjrd9vf0Ak12bA= github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1 h1:CotSAPr11rZy3VK46j5rJQYQ2LHT2mjrd9vf0Ak12bA=
github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1/go.mod h1:pwkfUxTikOkKc+yvEWqFg7CAPZDj5Yl0YHFcZoPKmU8= github.com/pgsty/mc v0.0.0-20260829103737-5ed037ef4ec1/go.mod h1:pwkfUxTikOkKc+yvEWqFg7CAPZDj5Yl0YHFcZoPKmU8=
github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf h1:BwUlE3Qr5+/JWkdNYnpSFin7RW1Uiaf7ddGlR87Y9YQ= github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38 h1:PObL76LBUBKq+y9TPAmc8wlKD8C/BNyrXGjfUrWtUvk=
github.com/pgsty/silo-console v0.0.0-20260829111139-e07ef01ab8bf/go.mod h1:Zqpx0h9NB5WzXI+OE/VVFcrXD6fKtSqqfCurmtxyKPo= github.com/pgsty/silo-console v0.0.0-20260901090952-43f8447fda38/go.mod h1:NHgq4XTLPa+Zt9lAIouHzgiFMULcyYjC3VwcMSuZf8U=
github.com/pgsty/silo-go/v7 v7.3.1 h1:CchXB5hdv1KGUCGBzR5Tz8hSE+3P9sGJFPY+ku8tC04=
github.com/pgsty/silo-go/v7 v7.3.1/go.mod h1:3uUcXVLE1xLBTy+A/js/hHKkvcYQ/zMbj8rbtNQavxE=
github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7 h1:YSCnSqNjU2ddU84dA8ybJXOoWYx8VDu2qAzXMVweisc= github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7 h1:YSCnSqNjU2ddU84dA8ybJXOoWYx8VDu2qAzXMVweisc=
github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7/go.mod h1:hSvfIz9FWTvEZqTsGEWQ8P/gD81pDee9W4j5oIvHZMY= github.com/pgsty/silo-pkg/v3 v3.12.3-0.20260829103855-748c94bf8ab7/go.mod h1:hSvfIz9FWTvEZqTsGEWQ8P/gD81pDee9W4j5oIvHZMY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
-6
View File
@@ -290,12 +290,6 @@ func (r Rule) matchAllowedOrigin(origin string) (string, bool) {
return "", false return "", false
} }
// HasAllowedOrigin reports whether the rule allows the given origin.
func (r Rule) HasAllowedOrigin(origin string) bool {
_, ok := r.matchAllowedOrigin(origin)
return ok
}
// HasAllowedMethod reports whether the rule allows the given HTTP method. // HasAllowedMethod reports whether the rule allows the given HTTP method.
func (r Rule) HasAllowedMethod(method string) bool { func (r Rule) HasAllowedMethod(method string) bool {
for _, m := range r.AllowedMethods { for _, m := range r.AllowedMethods {
-5
View File
@@ -72,11 +72,6 @@ const (
EnvMySQLFormat = "MINIO_NOTIFY_MYSQL_FORMAT" EnvMySQLFormat = "MINIO_NOTIFY_MYSQL_FORMAT"
EnvMySQLDSNString = "MINIO_NOTIFY_MYSQL_DSN_STRING" EnvMySQLDSNString = "MINIO_NOTIFY_MYSQL_DSN_STRING"
EnvMySQLTable = "MINIO_NOTIFY_MYSQL_TABLE" EnvMySQLTable = "MINIO_NOTIFY_MYSQL_TABLE"
EnvMySQLHost = "MINIO_NOTIFY_MYSQL_HOST"
EnvMySQLPort = "MINIO_NOTIFY_MYSQL_PORT"
EnvMySQLUsername = "MINIO_NOTIFY_MYSQL_USERNAME"
EnvMySQLPassword = "MINIO_NOTIFY_MYSQL_PASSWORD"
EnvMySQLDatabase = "MINIO_NOTIFY_MYSQL_DATABASE"
EnvMySQLQueueLimit = "MINIO_NOTIFY_MYSQL_QUEUE_LIMIT" EnvMySQLQueueLimit = "MINIO_NOTIFY_MYSQL_QUEUE_LIMIT"
EnvMySQLQueueDir = "MINIO_NOTIFY_MYSQL_QUEUE_DIR" EnvMySQLQueueDir = "MINIO_NOTIFY_MYSQL_QUEUE_DIR"
EnvMySQLMaxOpenConnections = "MINIO_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS" EnvMySQLMaxOpenConnections = "MINIO_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS"
-5
View File
@@ -69,11 +69,6 @@ const (
EnvPostgresFormat = "MINIO_NOTIFY_POSTGRES_FORMAT" EnvPostgresFormat = "MINIO_NOTIFY_POSTGRES_FORMAT"
EnvPostgresConnectionString = "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING" EnvPostgresConnectionString = "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING"
EnvPostgresTable = "MINIO_NOTIFY_POSTGRES_TABLE" EnvPostgresTable = "MINIO_NOTIFY_POSTGRES_TABLE"
EnvPostgresHost = "MINIO_NOTIFY_POSTGRES_HOST"
EnvPostgresPort = "MINIO_NOTIFY_POSTGRES_PORT"
EnvPostgresUsername = "MINIO_NOTIFY_POSTGRES_USERNAME"
EnvPostgresPassword = "MINIO_NOTIFY_POSTGRES_PASSWORD"
EnvPostgresDatabase = "MINIO_NOTIFY_POSTGRES_DATABASE"
EnvPostgresQueueDir = "MINIO_NOTIFY_POSTGRES_QUEUE_DIR" EnvPostgresQueueDir = "MINIO_NOTIFY_POSTGRES_QUEUE_DIR"
EnvPostgresQueueLimit = "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT" EnvPostgresQueueLimit = "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT"
EnvPostgresMaxOpenConnections = "MINIO_NOTIFY_POSTGRES_MAX_OPEN_CONNECTIONS" EnvPostgresMaxOpenConnections = "MINIO_NOTIFY_POSTGRES_MAX_OPEN_CONNECTIONS"
+3 -7
View File
@@ -57,11 +57,7 @@ func WaitPipe() (*PipeReader, *PipeWriter) {
r, w := io.Pipe() r, w := io.Pipe()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
return &PipeReader{ pr := &PipeReader{PipeReader: r, wait: wg.Wait}
PipeReader: r, pw := &PipeWriter{PipeWriter: w, done: wg.Done}
wait: wg.Wait, return pr, pw
}, &PipeWriter{
PipeWriter: w,
done: wg.Done,
}
} }