Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Fixed
body: Reject unsupported manylinux policy versions and architecture floors in portable Python binding artifacts.
107 changes: 91 additions & 16 deletions internal/portabletool/record_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,33 +788,108 @@ func portableToolCatalogWheelTagComponentV1(value string) bool {
}

func portableToolCatalogWheelPlatformCompatibleV1(tag, platform string) bool {
_, err := ProjectWheelPlatformForTargetV1(tag, platform)
return err == nil
}

// WheelPlatformProjectionV1 is the single portable-tool wheel platform
// policy projection. It deliberately contains only the policy facts needed
// by record validation and provider runtime guards; pip remains the runtime
// compatibility authority.
type WheelPlatformProjectionV1 struct {
Kind string
Architecture string
MinimumGlibcMajor string
MinimumGlibcMinor string
}

const (
WheelPlatformAnyV1 = "any"
WheelPlatformLinuxV1 = "linux"
WheelPlatformManylinuxV1 = "manylinux"
)

// ProjectWheelPlatformV1 validates and projects one canonical wheel platform
// tag. Unsupported policy aliases, numeric versions, architectures, and
// floors are rejected so all consumers share this one fail-closed table.
func ProjectWheelPlatformV1(tag string) (WheelPlatformProjectionV1, error) {
if tag == "any" {
return true
return WheelPlatformProjectionV1{Kind: WheelPlatformAnyV1}, nil
}
architecture := ""
switch platform {
case "linux/amd64":
if strings.HasSuffix(tag, "_x86_64") {
architecture = "x86_64"
case "linux/arm64":
} else if strings.HasSuffix(tag, "_aarch64") {
architecture = "aarch64"
} else {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q has an unsupported architecture", tag)
}
policy := strings.TrimSuffix(tag, "_"+architecture)
projection := WheelPlatformProjectionV1{Architecture: architecture}
switch policy {
case "linux":
projection.Kind = WheelPlatformLinuxV1
case "manylinux1":
if architecture != "x86_64" {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is unsupported on %s", tag, architecture)
}
projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "5"
case "manylinux2010":
if architecture != "x86_64" {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is unsupported on %s", tag, architecture)
}
projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "12"
case "manylinux2014":
projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, "2", "17"
default:
return false
version, found := strings.CutPrefix(policy, "manylinux_")
if !found {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q uses an unsupported policy", tag)
}
parts := strings.Split(version, "_")
if len(parts) != 2 || validatePortableToolCatalogDecimalV1("manylinux major", parts[0], false) != nil || validatePortableToolCatalogDecimalV1("manylinux minor", parts[1], false) != nil {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q has an invalid manylinux version", tag)
}
if parts[0] != "2" {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q uses unsupported manylinux major %q", tag, parts[0])
}
minimum := uint64(5)
if architecture == "aarch64" {
minimum = 17
}
minor, _ := strconv.ParseUint(parts[1], 10, 63)
if minor < minimum {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q is below the %s architecture floor", tag, architecture)
}
projection.Kind, projection.MinimumGlibcMajor, projection.MinimumGlibcMinor = WheelPlatformManylinuxV1, parts[0], parts[1]
}
if !strings.HasSuffix(tag, "_"+architecture) {
return false
return projection, nil
}

// ProjectWheelPlatformForTargetV1 additionally proves that a projected
// architecture belongs to the selected Linux target. The any policy is
// architecture independent.
func ProjectWheelPlatformForTargetV1(tag, target string) (WheelPlatformProjectionV1, error) {
projection, err := ProjectWheelPlatformV1(tag)
if err != nil {
return WheelPlatformProjectionV1{}, err
}
policy := strings.TrimSuffix(tag, "_"+architecture)
if policy == "linux" || policy == "manylinux2014" {
return true
architecture := ""
switch target {
case "linux/amd64":
architecture = "x86_64"
case "linux/arm64":
architecture = "aarch64"
default:
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel target %q is unsupported", target)
}
if policy == "manylinux1" || policy == "manylinux2010" {
return architecture == "x86_64"
if projection.Kind == WheelPlatformAnyV1 {
return projection, nil
}
if version, found := strings.CutPrefix(policy, "manylinux_"); found {
parts := strings.Split(version, "_")
return len(parts) == 2 && validatePortableToolCatalogDecimalV1("manylinux major", parts[0], false) == nil && validatePortableToolCatalogDecimalV1("manylinux minor", parts[1], false) == nil
if projection.Architecture != architecture {
return WheelPlatformProjectionV1{}, fmt.Errorf("wheel platform %q does not match target %q", tag, target)
}
return false
return projection, nil
}

func portableToolCatalogStringsEqualV1(left, right []string) bool {
Expand Down
85 changes: 85 additions & 0 deletions internal/portabletool/record_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,91 @@ func TestValidateRecordEnvelopeV1RejectsShortPayloadID(t *testing.T) {
}
}

func TestProjectWheelPlatformV1UsesOneBoundedManylinuxPolicy(t *testing.T) {
t.Parallel()
accepted := []struct {
tag, kind, architecture, major, minor string
}{
{"any", portabletool.WheelPlatformAnyV1, "", "", ""},
{"linux_x86_64", portabletool.WheelPlatformLinuxV1, "x86_64", "", ""},
{"linux_aarch64", portabletool.WheelPlatformLinuxV1, "aarch64", "", ""},
{"manylinux1_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "5"},
{"manylinux2010_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "12"},
{"manylinux2014_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "17"},
{"manylinux2014_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "17"},
{"manylinux_2_5_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "5"},
{"manylinux_2_6_x86_64", portabletool.WheelPlatformManylinuxV1, "x86_64", "2", "6"},
{"manylinux_2_17_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "17"},
{"manylinux_2_40_aarch64", portabletool.WheelPlatformManylinuxV1, "aarch64", "2", "40"},
}
for _, test := range accepted {
test := test
t.Run(test.tag, func(t *testing.T) {
t.Parallel()
projection, err := portabletool.ProjectWheelPlatformV1(test.tag)
if err != nil {
t.Fatal(err)
}
if projection.Kind != test.kind || projection.Architecture != test.architecture || projection.MinimumGlibcMajor != test.major || projection.MinimumGlibcMinor != test.minor {
t.Fatalf("projection = %#v", projection)
}
})
}
for _, tag := range []string{
"manylinux1_aarch64", "manylinux2010_aarch64",
"manylinux_2_4_x86_64", "manylinux_2_16_aarch64",
"manylinux_1_17_aarch64", "manylinux_3_17_aarch64",
"manylinux_02_17_aarch64", "manylinux_2_017_aarch64",
"manylinux_x_17_aarch64", "musllinux_1_2_x86_64", "linux_ppc64le",
} {
if _, err := portabletool.ProjectWheelPlatformV1(tag); err == nil {
t.Errorf("unsupported platform %q was accepted", tag)
}
}
if _, err := portabletool.ProjectWheelPlatformForTargetV1("linux_x86_64", "linux/arm64"); err == nil {
t.Fatal("x86_64 wheel platform was accepted for arm64 target")
}
direct, err := portabletool.ProjectWheelPlatformV1("manylinux_2_17_aarch64")
if err != nil {
t.Fatal(err)
}
targeted, err := portabletool.ProjectWheelPlatformForTargetV1("manylinux_2_17_aarch64", "linux/arm64")
if err != nil {
t.Fatalf("matching manylinux platform was rejected: %v", err)
}
if targeted != direct {
t.Fatalf("target projection %#v differs from direct projection %#v", targeted, direct)
}
if projection, err := portabletool.ProjectWheelPlatformForTargetV1("any", "linux/amd64"); err != nil || projection.Kind != portabletool.WheelPlatformAnyV1 {
t.Fatalf("any platform did not project for a supported target: %#v, %v", projection, err)
}
if _, err := portabletool.ProjectWheelPlatformForTargetV1("any", "darwin/amd64"); err == nil {
t.Fatal("any wheel platform was accepted for an unsupported target")
}
}

func TestValidateRecordEnvelopeV1UsesWheelPlatformProjection(t *testing.T) {
t.Parallel()
value := readDefinitionObjectV1(t, "playwright/releases/1.61.0/bindings/python/linux-amd64.json")
value["filename"] = "playwright-1.61.0-py3-none-manylinux_2_5_x86_64.whl"
value["tags"] = []any{"py3-none-manylinux_2_5_x86_64"}
if err := portabletool.ValidateRecordEnvelopeV1(canonical.Envelope{
Schema: portabletool.BindingArtifactSchemaV1,
Value: value,
}); err != nil {
t.Fatalf("supported projected platform was rejected: %v", err)
}

value["filename"] = "playwright-1.61.0-py3-none-manylinux_3_17_x86_64.whl"
value["tags"] = []any{"py3-none-manylinux_3_17_x86_64"}
if err := portabletool.ValidateRecordEnvelopeV1(canonical.Envelope{
Schema: portabletool.BindingArtifactSchemaV1,
Value: value,
}); err == nil {
t.Fatal("record validation accepted an unsupported manylinux policy major")
}
}

func TestToolcatalogCompatibilityAliasPreservesCanonicalIdentity(t *testing.T) {
t.Parallel()
shared := &portabletool.BindingContractV1{
Expand Down
Loading