diff --git a/src/libraries/rust/stargate/crates/stargate/BUILD.bazel b/src/libraries/rust/stargate/crates/stargate/BUILD.bazel index 810482c6b..9f63927f6 100644 --- a/src/libraries/rust/stargate/crates/stargate/BUILD.bazel +++ b/src/libraries/rust/stargate/crates/stargate/BUILD.bazel @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +load("@rules_go//go:def.bzl", "go_test") load("@rules_shell//shell:sh_test.bzl", "sh_test") load("@stargate_crates//:defs.bzl", "aliases", "all_crate_deps") load("@rules_rust//cargo:defs.bzl", "cargo_build_script") @@ -134,14 +135,19 @@ sh_test( data = [":image_layer"], ) -# Composite-image guard. Inspect the assembled OCI layout so removing the -# router's extra layer fails this test. -sh_test( +# Composite-image guard. Inspect every platform manifest reachable from the +# assembled OCI index so removing the router's extra layer fails this test. +go_test( + name = "image_router_binary_unit_test", + srcs = ["image_router_binary_test.go"], +) + +go_test( name = "image_router_binary_test", - srcs = ["//src/libraries/rust/stargate/tools/ci:test-oci-image-contains-path.sh"], + srcs = ["image_router_binary_test.go"], args = [ - "$(location :image)", - "/usr/local/bin/stargate-k8s-router", + "-image-layout=$(location :image)", + "-image-path=/usr/local/bin/stargate-k8s-router", ], data = [":image"], ) diff --git a/src/libraries/rust/stargate/crates/stargate/image_router_binary_test.go b/src/libraries/rust/stargate/crates/stargate/image_router_binary_test.go new file mode 100644 index 000000000..a6f0a78ef --- /dev/null +++ b/src/libraries/rust/stargate/crates/stargate/image_router_binary_test.go @@ -0,0 +1,514 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package stargateimage_test + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "os" + pathpkg "path" + "path/filepath" + "strings" + "testing" +) + +var ( + imageLayout = flag.String("image-layout", "", "OCI image layout to inspect") + imagePath = flag.String("image-path", "", "absolute path required in every image manifest") +) + +type descriptor struct { + Digest string `json:"digest"` + MediaType string `json:"mediaType,omitempty"` + Size int `json:"size,omitempty"` +} + +type document struct { + Manifests json.RawMessage `json:"manifests,omitempty"` + Layers json.RawMessage `json:"layers,omitempty"` +} + +type layerPathState struct { + present bool + removesLower bool +} + +func TestStargateImageContainsRouter(t *testing.T) { + if *imageLayout == "" || *imagePath == "" { + t.Skip("Bazel supplies the assembled OCI image layout and required path") + } + + if err := imageContainsPath(resolveRunfile(*imageLayout), *imagePath); err != nil { + t.Fatal(err) + } +} + +func TestResolveRunfile(t *testing.T) { + const ( + workspace = "_main" + relative = "src/libraries/rust/stargate/crates/stargate/image_pre_transitioned" + ) + + runfilesDir := t.TempDir() + t.Setenv("TEST_SRCDIR", runfilesDir) + t.Setenv("TEST_WORKSPACE", workspace) + + want := filepath.Join(runfilesDir, workspace, relative) + if got := resolveRunfile(relative); got != want { + t.Fatalf("resolveRunfile(%q) = %q, want %q", relative, got, want) + } +} + +func resolveRunfile(path string) string { + if filepath.IsAbs(path) { + return path + } + if _, err := os.Stat(path); err == nil { + return path + } + + runfilesDir := os.Getenv("TEST_SRCDIR") + workspace := os.Getenv("TEST_WORKSPACE") + if runfilesDir != "" && workspace != "" { + return filepath.Join(runfilesDir, workspace, path) + } + return path +} + +func TestImageContainsPathUsesReachableManifests(t *testing.T) { + const target = "/usr/local/bin/stargate-k8s-router" + + t.Run("ignores unreferenced layer", func(t *testing.T) { + layout := newLayoutBuilder(t) + missing := layout.addManifest(layout.addLayer("usr/local/bin/stargate")) + layout.addLayer(strings.TrimPrefix(target, "/")) + layout.writeIndex(missing) + + if err := imageContainsPath(layout.root, target); err == nil { + t.Fatal("unreferenced target layer made the check pass") + } + }) + + t.Run("requires path in every nested manifest", func(t *testing.T) { + layout := newLayoutBuilder(t) + present := layout.addManifest(layout.addLayer( + strings.TrimPrefix(target, "/"), + "platform/amd64", + )) + missing := layout.addManifest(layout.addLayer( + "usr/local/bin/stargate", + "platform/arm64", + )) + nested := layout.addIndex(present, missing) + layout.writeIndex(nested) + + if err := imageContainsPath(layout.root, target); err == nil { + t.Fatal("one manifest missing the target made the check pass") + } + }) + + t.Run("accepts path in every nested manifest", func(t *testing.T) { + layout := newLayoutBuilder(t) + first := layout.addManifest(layout.addLayer( + strings.TrimPrefix(target, "/"), + "platform/amd64", + )) + second := layout.addManifest(layout.addLayer( + strings.TrimPrefix(target, "/"), + "platform/arm64", + )) + nested := layout.addIndex(first, second) + layout.writeIndex(nested) + + if err := imageContainsPath(layout.root, target); err != nil { + t.Fatal(err) + } + }) + + t.Run("accepts path in a gzip-compressed layer", func(t *testing.T) { + layout := newLayoutBuilder(t) + manifest := layout.addManifest(layout.addGzipLayer(strings.TrimPrefix(target, "/"))) + layout.writeIndex(manifest) + + if err := imageContainsPath(layout.root, target); err != nil { + t.Fatal(err) + } + }) + + t.Run("rejects target removed by a later file whiteout", func(t *testing.T) { + layout := newLayoutBuilder(t) + lower := layout.addLayer(strings.TrimPrefix(target, "/")) + upper := layout.addLayer("usr/local/bin/.wh.stargate-k8s-router") + manifest := layout.addManifest(lower, upper) + layout.writeIndex(manifest) + + if err := imageContainsPath(layout.root, target); err == nil { + t.Fatal("a target removed by a later file whiteout made the check pass") + } + }) + + t.Run("rejects target removed by a later opaque-directory whiteout", func(t *testing.T) { + layout := newLayoutBuilder(t) + lower := layout.addLayer(strings.TrimPrefix(target, "/")) + upper := layout.addLayer("usr/local/bin/.wh..wh..opq") + manifest := layout.addManifest(lower, upper) + layout.writeIndex(manifest) + + if err := imageContainsPath(layout.root, target); err == nil { + t.Fatal("a target removed by a later opaque-directory whiteout made the check pass") + } + }) + + t.Run("rejects target removed by a later root opaque whiteout", func(t *testing.T) { + layout := newLayoutBuilder(t) + lower := layout.addLayer(strings.TrimPrefix(target, "/")) + upper := layout.addLayer(".wh..wh..opq") + manifest := layout.addManifest(lower, upper) + layout.writeIndex(manifest) + + if err := imageContainsPath(layout.root, target); err == nil { + t.Fatal("a target removed by a later root opaque whiteout made the check pass") + } + }) +} + +func TestImageContainsPathVerifiesDescriptors(t *testing.T) { + const target = "/usr/local/bin/stargate-k8s-router" + + t.Run("rejects tampered manifest blob", func(t *testing.T) { + layout := newLayoutBuilder(t) + manifest := layout.addManifest(layout.addLayer(strings.TrimPrefix(target, "/"))) + layout.writeIndex(manifest) + layout.overwriteBlob(manifest, bytes.Repeat([]byte{' '}, manifest.Size)) + + err := imageContainsPath(layout.root, target) + if err == nil || !strings.Contains(err.Error(), "digest mismatch") { + t.Fatalf("tampered manifest error = %v, want digest mismatch", err) + } + }) + + t.Run("rejects tampered layer blob", func(t *testing.T) { + layout := newLayoutBuilder(t) + layer := layout.addLayer(strings.TrimPrefix(target, "/")) + manifest := layout.addManifest(layer) + layout.writeIndex(manifest) + layout.overwriteBlob(layer, bytes.Repeat([]byte{0}, layer.Size)) + + err := imageContainsPath(layout.root, target) + if err == nil || !strings.Contains(err.Error(), "digest mismatch") { + t.Fatalf("tampered layer error = %v, want digest mismatch", err) + } + }) + + t.Run("rejects descriptor size mismatch", func(t *testing.T) { + layout := newLayoutBuilder(t) + manifest := layout.addManifest(layout.addLayer(strings.TrimPrefix(target, "/"))) + manifest.Size++ + layout.writeIndex(manifest) + + err := imageContainsPath(layout.root, target) + if err == nil || !strings.Contains(err.Error(), "size mismatch") { + t.Fatalf("descriptor size error = %v, want size mismatch", err) + } + }) +} + +func imageContainsPath(layout, requiredPath string) error { + requiredPath = normalizeTarPath(requiredPath) + if requiredPath == "." || requiredPath == "" { + return fmt.Errorf("required image path is empty") + } + + indexBytes, err := os.ReadFile(filepath.Join(layout, "index.json")) + if err != nil { + return fmt.Errorf("read OCI index: %w", err) + } + + var index struct { + Manifests []descriptor `json:"manifests"` + } + if err := json.Unmarshal(indexBytes, &index); err != nil { + return fmt.Errorf("parse OCI index: %w", err) + } + if len(index.Manifests) == 0 { + return fmt.Errorf("OCI index does not reference an image manifest") + } + + visited := make(map[string]bool) + manifestCount := 0 + var walk func(descriptor) error + walk = func(desc descriptor) error { + if visited[desc.Digest] { + return nil + } + visited[desc.Digest] = true + + contents, err := readBlob(layout, desc) + if err != nil { + return err + } + var doc document + if err := json.Unmarshal(contents, &doc); err != nil { + return fmt.Errorf("parse reachable descriptor %s: %w", desc.Digest, err) + } + + switch { + case doc.Manifests != nil: + var manifests []descriptor + if err := json.Unmarshal(doc.Manifests, &manifests); err != nil { + return fmt.Errorf("parse manifests in %s: %w", desc.Digest, err) + } + for _, manifest := range manifests { + if err := walk(manifest); err != nil { + return err + } + } + return nil + case doc.Layers != nil: + manifestCount++ + var layers []descriptor + if err := json.Unmarshal(doc.Layers, &layers); err != nil { + return fmt.Errorf("parse layers in %s: %w", desc.Digest, err) + } + found := false + for _, layer := range layers { + state, err := inspectLayerPath(layout, layer, requiredPath) + if err != nil { + return fmt.Errorf("inspect layer %s from manifest %s: %w", layer.Digest, desc.Digest, err) + } + if state.removesLower { + found = false + } + if state.present { + found = true + } + } + if found { + return nil + } + return fmt.Errorf("missing /%s in OCI image manifest %s", requiredPath, desc.Digest) + default: + return fmt.Errorf("reachable descriptor %s is not an image index or manifest", desc.Digest) + } + } + + for _, desc := range index.Manifests { + if err := walk(desc); err != nil { + return err + } + } + if manifestCount == 0 { + return fmt.Errorf("OCI index does not reach an image manifest") + } + return nil +} + +func readBlob(layout string, desc descriptor) ([]byte, error) { + algorithm, encoded, ok := strings.Cut(desc.Digest, ":") + if !ok || algorithm != "sha256" || len(encoded) != sha256.Size*2 { + return nil, fmt.Errorf("unsupported OCI digest %q", desc.Digest) + } + if _, err := hex.DecodeString(encoded); err != nil { + return nil, fmt.Errorf("invalid OCI digest %q: %w", desc.Digest, err) + } + + contents, err := os.ReadFile(filepath.Join(layout, "blobs", algorithm, encoded)) + if err != nil { + return nil, fmt.Errorf("read referenced OCI blob %s: %w", desc.Digest, err) + } + if len(contents) != desc.Size { + return nil, fmt.Errorf("OCI blob %s size mismatch: got %d, want %d", desc.Digest, len(contents), desc.Size) + } + actual := sha256.Sum256(contents) + if hex.EncodeToString(actual[:]) != encoded { + return nil, fmt.Errorf("OCI blob %s digest mismatch", desc.Digest) + } + return contents, nil +} + +func inspectLayerPath(layout string, desc descriptor, requiredPath string) (layerPathState, error) { + contents, err := readBlob(layout, desc) + if err != nil { + return layerPathState{}, err + } + + buffered := bufio.NewReader(bytes.NewReader(contents)) + reader := io.Reader(buffered) + magic, err := buffered.Peek(2) + if err != nil && err != io.EOF { + return layerPathState{}, err + } + if len(magic) == 2 && magic[0] == 0x1f && magic[1] == 0x8b { + compressed, err := gzip.NewReader(buffered) + if err != nil { + return layerPathState{}, err + } + defer compressed.Close() + reader = compressed + } + + var state layerPathState + archive := tar.NewReader(reader) + for { + header, err := archive.Next() + if err == io.EOF { + return state, nil + } + if err != nil { + return layerPathState{}, err + } + entryPath := normalizeTarPath(header.Name) + if entryPath == requiredPath { + state.present = true + } + if whiteoutRemovesPath(entryPath, requiredPath) { + state.removesLower = true + } + } +} + +func whiteoutRemovesPath(entryPath, requiredPath string) bool { + directory, name := pathpkg.Split(entryPath) + directory = strings.TrimSuffix(directory, "/") + if name == ".wh..wh..opq" { + if directory == "" { + return requiredPath != "" + } + return requiredPath != directory && strings.HasPrefix(requiredPath, directory+"/") + } + if !strings.HasPrefix(name, ".wh.") { + return false + } + + removedPath := pathpkg.Join(directory, strings.TrimPrefix(name, ".wh.")) + return requiredPath == removedPath || strings.HasPrefix(requiredPath, removedPath+"/") +} + +func normalizeTarPath(value string) string { + return pathpkg.Clean(strings.TrimLeft(value, "/")) +} + +type layoutBuilder struct { + t *testing.T + root string +} + +func newLayoutBuilder(t *testing.T) *layoutBuilder { + t.Helper() + return &layoutBuilder{t: t, root: t.TempDir()} +} + +func (b *layoutBuilder) addBlob(contents []byte, mediaType string) descriptor { + b.t.Helper() + digest := sha256.Sum256(contents) + encoded := hex.EncodeToString(digest[:]) + path := filepath.Join(b.root, "blobs", "sha256", encoded) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + b.t.Fatal(err) + } + if err := os.WriteFile(path, contents, 0o644); err != nil { + b.t.Fatal(err) + } + return descriptor{ + Digest: "sha256:" + encoded, + MediaType: mediaType, + Size: len(contents), + } +} + +func (b *layoutBuilder) overwriteBlob(desc descriptor, contents []byte) { + b.t.Helper() + encoded := strings.TrimPrefix(desc.Digest, "sha256:") + if err := os.WriteFile(filepath.Join(b.root, "blobs", "sha256", encoded), contents, 0o644); err != nil { + b.t.Fatal(err) + } +} + +func (b *layoutBuilder) addLayer(paths ...string) descriptor { + b.t.Helper() + return b.addBlob(b.layerTar(paths...), "application/vnd.oci.image.layer.v1.tar") +} + +func (b *layoutBuilder) addGzipLayer(paths ...string) descriptor { + b.t.Helper() + var compressed bytes.Buffer + writer := gzip.NewWriter(&compressed) + if _, err := writer.Write(b.layerTar(paths...)); err != nil { + b.t.Fatal(err) + } + if err := writer.Close(); err != nil { + b.t.Fatal(err) + } + return b.addBlob(compressed.Bytes(), "application/vnd.oci.image.layer.v1.tar+gzip") +} + +func (b *layoutBuilder) layerTar(paths ...string) []byte { + b.t.Helper() + var contents bytes.Buffer + archive := tar.NewWriter(&contents) + for _, name := range paths { + body := []byte(name) + header := &tar.Header{Name: name, Mode: 0o755, Size: int64(len(body))} + if err := archive.WriteHeader(header); err != nil { + b.t.Fatal(err) + } + if _, err := archive.Write(body); err != nil { + b.t.Fatal(err) + } + } + if err := archive.Close(); err != nil { + b.t.Fatal(err) + } + return contents.Bytes() +} + +func (b *layoutBuilder) addManifest(layers ...descriptor) descriptor { + b.t.Helper() + return b.addJSON(document{Layers: mustJSON(b.t, layers)}, "application/vnd.oci.image.manifest.v1+json") +} + +func (b *layoutBuilder) addIndex(manifests ...descriptor) descriptor { + b.t.Helper() + return b.addJSON(document{Manifests: mustJSON(b.t, manifests)}, "application/vnd.oci.image.index.v1+json") +} + +func (b *layoutBuilder) addJSON(value any, mediaType string) descriptor { + b.t.Helper() + contents, err := json.Marshal(value) + if err != nil { + b.t.Fatal(err) + } + return b.addBlob(contents, mediaType) +} + +func (b *layoutBuilder) writeIndex(manifests ...descriptor) { + b.t.Helper() + contents, err := json.Marshal(struct { + SchemaVersion int `json:"schemaVersion"` + Manifests []descriptor `json:"manifests"` + }{SchemaVersion: 2, Manifests: manifests}) + if err != nil { + b.t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(b.root, "index.json"), contents, 0o644); err != nil { + b.t.Fatal(err) + } +} + +func mustJSON(t *testing.T, value any) json.RawMessage { + t.Helper() + contents, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + return contents +} diff --git a/src/libraries/rust/stargate/tools/ci/BUILD.bazel b/src/libraries/rust/stargate/tools/ci/BUILD.bazel index 92885fc84..8e3a1cf74 100644 --- a/src/libraries/rust/stargate/tools/ci/BUILD.bazel +++ b/src/libraries/rust/stargate/tools/ci/BUILD.bazel @@ -2,9 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 exports_files( - [ - "image_entrypoint_mode_test.sh", - "test-oci-image-contains-path.sh", - ], + ["image_entrypoint_mode_test.sh"], visibility = ["//visibility:public"], ) diff --git a/src/libraries/rust/stargate/tools/ci/test-oci-image-contains-path.sh b/src/libraries/rust/stargate/tools/ci/test-oci-image-contains-path.sh deleted file mode 100755 index c389a8607..000000000 --- a/src/libraries/rust/stargate/tools/ci/test-oci-image-contains-path.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -if [[ $# -ne 2 ]]; then - echo "usage: $0 " >&2 - exit 2 -fi - -image_layout="$1" -image_path="${2#/}" - -if [[ ! -f "${image_layout}/index.json" || ! -d "${image_layout}/blobs/sha256" ]]; then - echo "${image_layout} is not an OCI image layout" >&2 - exit 1 -fi - -while IFS= read -r -d '' blob; do - if entries="$(tar -tf "${blob}" 2>/dev/null)"; then - while IFS= read -r entry; do - entry="${entry#./}" - entry="${entry#/}" - if [[ "${entry}" == "${image_path}" ]]; then - exit 0 - fi - done <<< "${entries}" - fi -# rules_oci may symlink blob files to their source layers. Select those links -# directly without following symlinked directories outside the blob tree. -done < <(find "${image_layout}/blobs/sha256" \( -type f -o -type l \) -print0) - -echo "missing /${image_path} in ${image_layout}" >&2 -exit 1