mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
fix: drop the 32 MiB FIPS entropy buffer from release binaries (#2667)
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
96fdb6f779
commit
d8f281ba97
@@ -99,6 +99,9 @@ jobs:
|
||||
version: v2.18.3
|
||||
args: build --clean
|
||||
|
||||
- name: Verify Go overlay
|
||||
run: go run ./tools/go-overlay verify dist/windows_exporter-amd64.exe dist/windows_exporter-arm64.exe
|
||||
|
||||
- name: Stage build artifacts
|
||||
run: |
|
||||
mkdir -p output
|
||||
|
||||
@@ -23,3 +23,4 @@ local/
|
||||
!/.idea/copyright/windows_exporter.xml
|
||||
!/.idea/vcs.xml
|
||||
!/.idea/go.imports.xml
|
||||
/.go-overlay/
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# Removes the 32 MiB FIPS 140-3 entropy buffer that Go 1.26+ links into
|
||||
# every binary. See tools/go-overlay/main.go.
|
||||
- go run ./tools/go-overlay generate .go-overlay
|
||||
|
||||
builds:
|
||||
- id: windows_exporter
|
||||
main: ./cmd/windows_exporter
|
||||
@@ -16,6 +22,7 @@ builds:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
- -overlay=.go-overlay/overlay.json
|
||||
tags:
|
||||
- trimpath
|
||||
ldflags:
|
||||
|
||||
@@ -65,6 +65,13 @@ go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 run ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Release builds apply a `go build -overlay` from
|
||||
[`tools/go-overlay`](tools/go-overlay/main.go) that removes the 32 MiB FIPS
|
||||
140-3 entropy buffer Go links into every binary (golang/go#81956). It is pinned
|
||||
to the checksum of the patched standard library file, so a Go update that
|
||||
changes that file fails the build job until the overlay is refreshed or, once Go
|
||||
fixes the issue, removed. Plain `go build` and `make build` do not apply it.
|
||||
|
||||
For collector or concurrency changes, also run the affected packages natively
|
||||
on Windows with `go test -race -count=1 -timeout=10m <packages>`. Add regression
|
||||
tests for behavior changes and benchmarks for performance improvements. Check
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Patched copy of $GOROOT/src/crypto/internal/fips140/drbg/entropy_fips140.go
|
||||
// from Go 1.27.2, applied by tools/go-overlay. The only change is that the
|
||||
// 32 MiB entropy scratch buffer is allocated on first use instead of being a
|
||||
// package-level variable. On Windows, the package-level variable is part of
|
||||
// the writable .data section of the executable image, which is charged as
|
||||
// commit for every process even if FIPS 140-3 mode is never enabled.
|
||||
// See https://github.com/golang/go/issues/81956.
|
||||
//
|
||||
//go:build !wasm
|
||||
|
||||
package drbg
|
||||
|
||||
import (
|
||||
entropy "crypto/internal/entropy/v1.0.0"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// scratchMemory returns a scratch buffer that is accessed between samples by the
|
||||
// entropy source to expose it to memory access timings.
|
||||
//
|
||||
// It is allocated once and shared between Seed calls to avoid the cost of
|
||||
// zeroing a new allocation every time. The entropy source accesses it using
|
||||
// atomics (and doesn't care about its contents).
|
||||
var scratchMemory = sync.OnceValue(func() *entropy.ScratchBuffer {
|
||||
return new(entropy.ScratchBuffer)
|
||||
})
|
||||
|
||||
func getEntropy() *[SeedSize]byte {
|
||||
var retries int
|
||||
seed, err := entropy.Seed(scratchMemory())
|
||||
for err != nil {
|
||||
// The CPU jitter-based SP 800-90B entropy source has a non-negligible
|
||||
// chance of failing the startup health tests.
|
||||
//
|
||||
// Each time it does, it enters a permanent failure state, and we
|
||||
// restart it anew. This is not expected to happen more than a few times
|
||||
// in a row.
|
||||
if retries++; retries > 100 {
|
||||
panic("fips140/drbg: failed to obtain initial entropy")
|
||||
}
|
||||
seed, err = entropy.Seed(scratchMemory())
|
||||
}
|
||||
return &seed
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Command go-overlay prepares the `go build -overlay` file used for release
|
||||
// builds and verifies its effect on the built executables.
|
||||
//
|
||||
// Go 1.26 added a 32 MiB package-level scratch buffer to the FIPS 140-3
|
||||
// entropy source (crypto/internal/fips140/drbg.memory). On Windows it is part
|
||||
// of the writable data section of the executable image, which costs 32 MiB of
|
||||
// commit charge per process even when FIPS 140-3 mode is disabled. The overlay
|
||||
// replaces that file with a copy that allocates the buffer on first use.
|
||||
// See https://github.com/golang/go/issues/81956.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./tools/go-overlay generate <dir>
|
||||
// go run ./tools/go-overlay verify <executable>...
|
||||
//
|
||||
// generate writes overlay.json and the patched source file to dir. Pass
|
||||
// -overlay=<dir>/overlay.json to go build. verify fails if an executable still
|
||||
// contains the package-level buffer.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"debug/pe"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// targetFile is the Go standard library file replaced by the overlay.
|
||||
targetFile = "src/crypto/internal/fips140/drbg/entropy_fips140.go"
|
||||
|
||||
// targetSHA256 is the checksum of targetFile in the Go version the
|
||||
// overlay was written for. A Go update that changes the file must fail
|
||||
// the build, so the overlay is re-checked or dropped once Go fixes
|
||||
// golang/go#81956.
|
||||
targetSHA256 = "bd3c834a29e31c93e56d81da54d4a874561814a86550de1456c8d6d40a9c5fda"
|
||||
|
||||
// bufferSymbol is the package-level buffer removed by the overlay.
|
||||
bufferSymbol = "crypto/internal/fips140/drbg.memory"
|
||||
)
|
||||
|
||||
//go:embed entropy_fips140.go.overlay
|
||||
var patchedSource []byte
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "go-overlay:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errors.New("usage: go-overlay generate <dir> | verify <executable> [<executable> ...]")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "generate":
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: go-overlay generate <dir>")
|
||||
}
|
||||
|
||||
return generate(context.Background(), args[1])
|
||||
case "verify":
|
||||
for _, path := range args[1:] {
|
||||
if err := verify(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func generate(ctx context.Context, dir string) error {
|
||||
goroot, err := goEnv(ctx, "GOROOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := filepath.Join(goroot, filepath.FromSlash(targetFile))
|
||||
|
||||
original, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", target, err)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(original)
|
||||
if got := hex.EncodeToString(sum[:]); got != targetSHA256 {
|
||||
return fmt.Errorf("%s has checksum %s, expected %s: the Go toolchain changed this file, "+
|
||||
"so update tools/go-overlay/entropy_fips140.go.overlay from the new version "+
|
||||
"or remove the overlay if golang/go#81956 is fixed", target, got, targetSHA256)
|
||||
}
|
||||
|
||||
// The go command rejects overlays for files in the module cache, which is
|
||||
// where toolchains downloaded through GOTOOLCHAIN are stored.
|
||||
gomodcache, err := goEnv(ctx, "GOMODCACHE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gomodcache != "" && isWithin(goroot, gomodcache) {
|
||||
return fmt.Errorf("GOROOT %s is inside GOMODCACHE %s, where go build does not allow overlays; "+
|
||||
"install the Go version from go.mod instead of using a downloaded toolchain", goroot, gomodcache)
|
||||
}
|
||||
|
||||
dir, err = filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:gosec // The directory is chosen by the caller of this build tool.
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patched := filepath.Join(dir, "entropy_fips140.go")
|
||||
//nolint:gosec // Build input, not a secret.
|
||||
if err := os.WriteFile(patched, patchedSource, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlay, err := json.MarshalIndent(map[string]map[string]string{
|
||||
"Replace": {target: patched},
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(dir, "overlay.json"), append(overlay, '\n'), 0o644) //nolint:gosec // Build input, not a secret.
|
||||
}
|
||||
|
||||
func verify(path string) error {
|
||||
f, err := pe.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if len(f.Symbols) == 0 {
|
||||
return fmt.Errorf("%s has no symbol table, cannot verify the overlay", path)
|
||||
}
|
||||
|
||||
for _, sym := range f.Symbols {
|
||||
if sym.Name == bufferSymbol {
|
||||
return fmt.Errorf("%s contains %s: the go-overlay was not applied", path, bufferSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func goEnv(ctx context.Context, name string) (string, error) {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "env", name)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go env %s: %w: %s", name, err, stderr.String())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func isWithin(path, dir string) bool {
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
|
||||
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
||||
}
|
||||
Reference in New Issue
Block a user