Files
buildkit/executor/proxyca_linux_test.go
Tonis Tiigi 3f864e412f executor: reject proxy CA bundle devices
Read proxy CA bundles through RegularInRoot before accessing their contents.
Keep temporary writes and replacement confined to a retained root handle.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 914ed29f24515cf7dbfeff1c169538123945437b)
2026-09-30 15:12:28 +02:00

222 lines
7.1 KiB
Go

//go:build linux
package executor
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/moby/buildkit/util/openfile"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
func TestInjectProxyCACleanupPreservesContainerChanges(t *testing.T) {
rootfs := t.TempDir()
root, err := os.OpenRoot(rootfs)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, root.Close())
})
const bundle = "etc/ssl/certs/ca-certificates.crt"
require.NoError(t, root.MkdirAll(filepath.Dir(bundle), 0o755))
original := []byte("original bundle\n")
require.NoError(t, root.WriteFile(bundle, original, 0o644))
caPEM := testCertPEM(t)
cleanup, err := InjectProxyCA(rootfs, caPEM)
require.NoError(t, err)
dt, err := root.ReadFile(bundle)
require.NoError(t, err)
require.Contains(t, string(dt), string(caPEM))
require.NoError(t, root.WriteFile(bundle, append(dt, []byte("container change\n")...), 0o644))
require.NoError(t, cleanup())
dt, err = root.ReadFile(bundle)
require.NoError(t, err)
require.NotContains(t, string(dt), string(caPEM))
require.Contains(t, string(dt), string(original))
require.Contains(t, string(dt), "container change\n")
}
func TestInjectProxyCACleanupRestoresBundleExactly(t *testing.T) {
for name, original := range map[string][]byte{
"empty": {},
"without trailing newline": []byte("original bundle"),
"with trailing newline": []byte("original bundle\n"),
} {
t.Run(name, func(t *testing.T) {
rootfs := t.TempDir()
root, err := os.OpenRoot(rootfs)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, root.Close())
})
const bundle = "etc/ssl/certs/ca-certificates.crt"
require.NoError(t, root.MkdirAll(filepath.Dir(bundle), 0o755))
require.NoError(t, root.WriteFile(bundle, original, 0o644))
cleanup, err := InjectProxyCA(rootfs, testCertPEM(t))
require.NoError(t, err)
require.NoError(t, cleanup())
dt, err := root.ReadFile(bundle)
require.NoError(t, err)
require.Equal(t, original, dt)
})
}
}
func TestInjectProxyCACleanupHandlesRetargetedSymlinks(t *testing.T) {
t.Run("external directory symlink", func(t *testing.T) {
rootfs := t.TempDir()
const testCABundle = "etc/ssl/certs/ca-certificates.crt"
bundle := filepath.Join(rootfs, testCABundle)
certsDir := filepath.Join(rootfs, "etc/ssl/certs")
require.NoError(t, os.MkdirAll(certsDir, 0o755))
require.NoError(t, os.WriteFile(bundle, []byte("original bundle\n"), 0o644))
caPEM := testCertPEM(t)
cleanup, err := InjectProxyCA(rootfs, caPEM)
require.NoError(t, err)
external := t.TempDir()
externalRoot, err := os.OpenRoot(external)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, externalRoot.Close())
})
const externalBundle = "ca-certificates.crt"
injected, err := os.ReadFile(bundle)
require.NoError(t, err)
require.Contains(t, string(injected), string(caPEM))
require.NoError(t, externalRoot.WriteFile(externalBundle, injected, 0o644))
require.NoError(t, os.RemoveAll(certsDir))
require.NoError(t, os.Symlink(external, certsDir))
require.NoError(t, cleanup())
after, err := externalRoot.ReadFile(externalBundle)
require.NoError(t, err)
require.Equal(t, string(injected), string(after))
require.Contains(t, string(after), string(caPEM))
})
t.Run("retargeted bundle symlink", func(t *testing.T) {
rootfs := t.TempDir()
const testCABundle = "etc/ssl/certs/ca-certificates.crt"
bundle := filepath.Join(rootfs, testCABundle)
require.NoError(t, os.MkdirAll(filepath.Dir(bundle), 0o755))
originalTarget := filepath.Join(rootfs, "original/ca-bundle.crt")
original := []byte("original bundle\n")
require.NoError(t, os.MkdirAll(filepath.Dir(originalTarget), 0o755))
require.NoError(t, os.WriteFile(originalTarget, original, 0o644))
retargetedTarget := filepath.Join(rootfs, "retargeted/ca-bundle.crt")
retargeted := []byte("retargeted bundle\n")
require.NoError(t, os.MkdirAll(filepath.Dir(retargetedTarget), 0o755))
require.NoError(t, os.WriteFile(retargetedTarget, retargeted, 0o644))
require.NoError(t, os.Symlink("/original/ca-bundle.crt", bundle))
caPEM := testCertPEM(t)
cleanup, err := InjectProxyCA(rootfs, caPEM)
require.NoError(t, err)
injected, err := os.ReadFile(originalTarget)
require.NoError(t, err)
require.Contains(t, string(injected), string(caPEM))
require.NoError(t, os.Remove(bundle))
require.NoError(t, os.Symlink("/retargeted/ca-bundle.crt", bundle))
require.NoError(t, cleanup())
afterOriginal, err := os.ReadFile(originalTarget)
require.NoError(t, err)
require.NotContains(t, string(afterOriginal), string(caPEM))
require.Contains(t, string(afterOriginal), string(original))
afterRetargeted, err := os.ReadFile(retargetedTarget)
require.NoError(t, err)
require.Equal(t, string(retargeted), string(afterRetargeted))
})
}
func TestInjectProxyCACleanupDoesNotBlockOnFIFO(t *testing.T) {
rootfs := t.TempDir()
const bundle = "etc/ssl/certs/ca-certificates.crt"
bundlePath := filepath.Join(rootfs, bundle)
require.NoError(t, os.MkdirAll(filepath.Dir(bundlePath), 0o755))
require.NoError(t, os.WriteFile(bundlePath, []byte("original bundle\n"), 0o644))
caPEM := testCertPEM(t)
cleanup, err := InjectProxyCA(rootfs, caPEM)
require.NoError(t, err)
// Simulate a malicious RUN step replacing the bundle with a FIFO that has
// no writer. A blocking open would hang cleanup indefinitely.
require.NoError(t, os.Remove(bundlePath))
require.NoError(t, syscall.Mkfifo(bundlePath, 0o644))
done := make(chan error, 1)
go func() {
done <- cleanup()
}()
select {
case err := <-done:
require.ErrorIs(t, err, openfile.ErrNotRegular)
case <-time.After(3 * time.Second):
t.Fatal("cleanup blocked on FIFO bundle")
}
}
func TestInjectProxyCACleanupRejectsDeviceNode(t *testing.T) {
rootfs := t.TempDir()
const bundle = "etc/ssl/certs/ca-certificates.crt"
bundlePath := filepath.Join(rootfs, bundle)
require.NoError(t, os.MkdirAll(filepath.Dir(bundlePath), 0o755))
require.NoError(t, os.WriteFile(bundlePath, []byte("original bundle\n"), 0o644))
cleanup, err := InjectProxyCA(rootfs, testCertPEM(t))
require.NoError(t, err)
require.NoError(t, os.Remove(bundlePath))
// Use /dev/null so the test remains harmless if the safety check regresses.
if err := unix.Mknod(bundlePath, unix.S_IFCHR|0o600, int(unix.Mkdev(1, 3))); err != nil {
t.Skipf("cannot create device node: %v", err)
}
err = cleanup()
require.ErrorIs(t, err, openfile.ErrNotRegular)
}
func testCertPEM(t *testing.T) []byte {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test buildkit proxy"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
require.NoError(t, err)
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}