fs: reject special files for ref reads

Ref and rootfs helpers may read paths supplied by frontends or build outputs.
Reject non-regular files before reading them into memory so device and FIFO
paths cannot feed no-EOF streams into parsers or bundle injection logic.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-06-22 16:08:42 -07:00
parent c17738edb7
commit 9cdf642943
5 changed files with 128 additions and 21 deletions

View File

@@ -41,6 +41,14 @@ func ReadFile(ctx context.Context, root string, req ReadRequest) ([]byte, error)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, errors.WithStack(err)
}
if !info.Mode().IsRegular() {
return nil, errors.Errorf("%s is not a regular file", req.Filename)
}
var rdr io.Reader = f
if req.Range != nil {
rdr = io.NewSectionReader(f, int64(req.Range.Offset), int64(req.Range.Length))

View File

@@ -7,6 +7,7 @@ import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"io"
"os"
"path/filepath"
"syscall"
@@ -29,6 +30,8 @@ var (
proxyCAEnd = []byte("# buildkit proxy CA end\n")
)
const maxCertBundleBytes = 10 << 20
// InjectProxyCA appends caPEM to the rootfs trust bundle used by common Linux
// TLS stacks and returns a cleanup that removes only the injected CA.
func InjectProxyCA(rootfsPath string, caPEM []byte) (func() error, error) {
@@ -47,7 +50,7 @@ func InjectProxyCA(rootfsPath string, caPEM []byte) (func() error, error) {
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve certificate bundle %s", name)
}
if st, err := os.Stat(p); err == nil && !st.IsDir() {
if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() {
bundle = p
break
}
@@ -56,17 +59,13 @@ func InjectProxyCA(rootfsPath string, caPEM []byte) (func() error, error) {
return func() error { return nil }, nil
}
original, err := os.ReadFile(bundle)
original, st, err := readCertBundle(bundle)
if err != nil {
return nil, errors.WithStack(err)
return nil, err
}
if containsCertificate(original, certSum) {
return func() error { return nil }, nil
}
st, err := os.Stat(bundle)
if err != nil {
return nil, errors.WithStack(err)
}
next := append([]byte{}, original...)
if len(next) > 0 && next[len(next)-1] != '\n' {
next = append(next, '\n')
@@ -82,25 +81,47 @@ func InjectProxyCA(rootfsPath string, caPEM []byte) (func() error, error) {
}
return func() error {
current, err := os.ReadFile(bundle)
current, st, err := readCertBundle(bundle)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return errors.WithStack(err)
return err
}
cleaned := removeInjectedCA(current, certSum)
if bytes.Equal(current, cleaned) {
return nil
}
st, err := os.Stat(bundle)
if err != nil {
return errors.WithStack(err)
}
return writeCertBundle(bundle, cleaned, st)
}, nil
}
func readCertBundle(path string) ([]byte, os.FileInfo, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, errors.WithStack(err)
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return nil, nil, errors.WithStack(err)
}
if !st.Mode().IsRegular() {
return nil, nil, errors.Errorf("%s is not a regular file", path)
}
limited := &io.LimitedReader{R: f, N: maxCertBundleBytes + 1}
dt, err := io.ReadAll(limited)
if err != nil {
return nil, nil, errors.WithStack(err)
}
if limited.N == 0 {
return nil, nil, errors.Errorf("%s exceeds %d bytes", path, maxCertBundleBytes)
}
return dt, st, nil
}
func firstCertificate(dt []byte) (*x509.Certificate, error) {
for {
block, rest := pem.Decode(dt)

View File

@@ -3,6 +3,7 @@ package attestation
import (
"context"
"encoding/json"
"io"
"os"
"github.com/containerd/continuity/fs"
@@ -16,6 +17,8 @@ import (
"golang.org/x/sync/errgroup"
)
const maxAttestationBytes int64 = 40 << 20
// ReadAll reads the content of an attestation.
func ReadAll(ctx context.Context, s session.Group, att exporter.Attestation) ([]byte, error) {
var content []byte
@@ -41,7 +44,7 @@ func ReadAll(ctx context.Context, s session.Group, att exporter.Attestation) ([]
if err != nil {
return nil, err
}
content, err = os.ReadFile(p)
content, err = readRegularFile(p)
if err != nil {
return nil, errors.Wrap(err, "cannot read in-toto attestation")
}
@@ -54,6 +57,51 @@ func ReadAll(ctx context.Context, s session.Group, att exporter.Attestation) ([]
return content, nil
}
func readRegularFile(p string) ([]byte, error) {
f, err := openRegularFile(p)
if err != nil {
return nil, err
}
defer f.Close()
dt, err := readAllLimited(f, p, maxAttestationBytes)
if err != nil {
return nil, err
}
return dt, nil
}
func readAllLimited(r io.Reader, name string, limit int64) ([]byte, error) {
limited := &io.LimitedReader{R: r, N: limit + 1}
dt, err := io.ReadAll(limited)
if err != nil {
return nil, errors.WithStack(err)
}
if limited.N == 0 {
return nil, errors.Errorf("%s exceeds %d bytes", name, limit)
}
return dt, nil
}
func openRegularFile(p string) (*os.File, error) {
f, err := os.Open(p)
if err != nil {
return nil, errors.WithStack(err)
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, errors.WithStack(err)
}
if !st.Mode().IsRegular() {
f.Close()
return nil, errors.Errorf("%s is not a regular file", p)
}
return f, nil
}
// MakeInTotoStatements iterates over all provided result attestations and
// generates intoto attestation statements.
func MakeInTotoStatements(ctx context.Context, s session.Group, attestations []exporter.Attestation, defaultSubjects []intoto.Subject) ([]intoto.Statement, error) {

View File

@@ -132,17 +132,15 @@ func unbundle(root string, bundle exporter.Attestation) ([]exporter.Attestation,
if err != nil {
return nil, err
}
f, err := os.Open(p)
f, err := openRegularFile(p)
if err != nil {
return nil, err
}
dec := json.NewDecoder(f)
var stmt intoto.Statement
if err := dec.Decode(&stmt); err != nil {
return nil, errors.Wrap(err, "cannot decode in-toto statement")
}
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
return nil, errors.New("in-toto statement is not a single JSON object")
stmt, err = decodeStatement(f, p)
f.Close()
if err != nil {
return nil, err
}
if bundle.InToto.PredicateType != "" && stmt.PredicateType != bundle.InToto.PredicateType {
return nil, errors.Errorf("bundle entry %s does not match required predicate type %s", stmt.PredicateType, bundle.InToto.PredicateType)
@@ -175,6 +173,29 @@ func unbundle(root string, bundle exporter.Attestation) ([]exporter.Attestation,
return unbundled, nil
}
func decodeStatement(r io.Reader, name string) (intoto.Statement, error) {
limited := &io.LimitedReader{R: r, N: maxAttestationBytes + 1}
dec := json.NewDecoder(limited)
var stmt intoto.Statement
if err := dec.Decode(&stmt); err != nil {
if limited.N == 0 {
return stmt, errors.Errorf("%s exceeds %d bytes", name, maxAttestationBytes)
}
return stmt, errors.Wrap(err, "cannot decode in-toto statement")
}
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
if limited.N == 0 {
return stmt, errors.Errorf("%s exceeds %d bytes", name, maxAttestationBytes)
}
return stmt, errors.New("in-toto statement is not a single JSON object")
}
if limited.N == 0 {
return stmt, errors.Errorf("%s exceeds %d bytes", name, maxAttestationBytes)
}
return stmt, nil
}
func Validate(atts []exporter.Attestation) error {
for _, att := range atts {
if err := validate(att); err != nil {

View File

@@ -120,6 +120,15 @@ func (b *BuildOp) Exec(ctx context.Context, job solver.JobContext, inputs []solv
if err != nil {
return nil, errors.Wrapf(err, "failed to open %s", newfn)
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, errors.WithStack(err)
}
if !st.Mode().IsRegular() {
f.Close()
return nil, errors.Errorf("%s is not a regular file", newfn)
}
def, err := llb.ReadFrom(f)
if err != nil {