Tighten opaque plugin content boundaries

This commit is contained in:
npc0-hue
2026-09-03 18:24:39 +08:00
parent 80cddbf19d
commit 14cbc63e61
31 changed files with 452 additions and 558 deletions
+81 -26
View File
@@ -20,6 +20,7 @@ import (
"time"
"browser.local/platform/domain"
"browser.local/platform/validator"
)
// DistributionBuilder executes a distribution build inside a platform-owned
@@ -40,10 +41,28 @@ type DistributionBuildProgress struct {
Message string
}
type DistributionBuildOutput struct {
Path string
SizeBytes int64
Checksum string
cleanup func() error
}
func (output DistributionBuildOutput) Cleanup() error {
if output.cleanup == nil {
return nil
}
return output.cleanup()
}
type distributionBuilderWithProgress interface {
BuildWithProgress(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) ([]byte, error)
}
type distributionBuilderWithFileOutput interface {
BuildFile(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) (DistributionBuildOutput, error)
}
// DockerDistributionBuilderConfig configures a container-per-build builder.
type DockerDistributionBuilderConfig struct {
DockerBinary string
@@ -116,8 +135,7 @@ func runCommandStreamCombined(ctx context.Context, name string, args []string, o
for scanner.Scan() {
line := scanner.Text()
mu.Lock()
output.WriteString(line)
output.WriteByte('\n')
appendBoundedBuilderOutput(&output, []byte(line+"\n"))
mu.Unlock()
if onLine != nil {
onLine(line)
@@ -197,17 +215,33 @@ func (builder *DockerDistributionBuilder) Build(input domain.DistributionBuildIn
}
func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) ([]byte, error) {
output, err := builder.BuildFile(input, progress)
if err != nil {
return nil, err
}
defer func() { _ = output.Cleanup() }()
binary, err := os.ReadFile(output.Path)
if err != nil {
return nil, validationError("platform builder did not produce a readable distribution executable")
}
if int64(len(binary)) != output.SizeBytes || validator.BytesChecksum(binary) != output.Checksum {
return nil, validationError("platform builder output changed before it could be read")
}
return binary, nil
}
func (builder *DockerDistributionBuilder) BuildFile(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) (DistributionBuildOutput, error) {
if ready, reason := builder.Readiness(); !ready {
return nil, validationError(reason)
return DistributionBuildOutput{}, validationError(reason)
}
reportBuilderProgress(progress, 8, "env_check: platform builder readiness verified")
sourceDir, err := filepath.Abs(strings.TrimSpace(builder.config.SourceDir))
if err != nil {
return nil, validationError("platform builder run source directory is invalid")
return DistributionBuildOutput{}, validationError("platform builder run source directory is invalid")
}
workspaceDir, err := filepath.Abs(strings.TrimSpace(builder.config.WorkspaceDir))
if err != nil {
return nil, validationError("platform builder workspace directory is invalid")
return DistributionBuildOutput{}, validationError("platform builder workspace directory is invalid")
}
ctx, cancel := context.WithTimeout(context.Background(), builder.config.Timeout)
defer cancel()
@@ -220,14 +254,20 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib
}
cacheDir, err := builder.cacheDir(workspaceDir)
if err != nil {
return nil, validationError("platform builder cache directory is invalid")
return DistributionBuildOutput{}, validationError("platform builder cache directory is invalid")
}
// Workspaces stay isolated per plugin and per job as required by
// run-build-download-flow.
jobDir := filepath.Join(workspaceDir, sanitizeIDPart(input.PluginID), sanitizeIDPart(input.JobID))
if err := os.RemoveAll(jobDir); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
cleanupJob := true
defer func() {
if cleanupJob {
_ = os.RemoveAll(jobDir)
}
}()
outputDir := filepath.Join(jobDir, "output")
inputDir := filepath.Join(jobDir, "input")
buildDir := filepath.Join(jobDir, "build")
@@ -236,15 +276,14 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib
goModCacheDir := filepath.Join(cacheDir, "go-mod")
for _, directory := range []string{outputDir, inputDir, buildDir, sourceMountDir, goBuildCacheDir, goModCacheDir} {
if err := os.MkdirAll(directory, 0o700); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
}
defer func() { _ = os.RemoveAll(jobDir) }()
reportBuilderProgress(progress, 14, "env_check: platform builder workspace prepared")
if input.ComponentKind == domain.DistributionComponentRun {
preparedSourceDir, err := builder.prepareRunSource(ctx, sourceDir, sourceMountDir, input, progress)
if err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
sourceMountDir = preparedSourceDir
}
@@ -253,59 +292,61 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib
// through a job-channel response to a machine-side endpoint or a container
// command-line argument.
if strings.TrimSpace(input.AuthKey) == "" {
return nil, validationError("distribution build input is missing a component auth key")
return DistributionBuildOutput{}, validationError("distribution build input is missing a component auth key")
}
if err := os.WriteFile(filepath.Join(inputDir, "auth-key"), []byte(input.AuthKey), 0o600); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
seedPayload := []byte("[]")
if strings.TrimSpace(input.WorkspaceSeed) != "" {
decoded, err := base64.StdEncoding.DecodeString(input.WorkspaceSeed)
if err != nil {
return nil, validationError("distribution build input has an invalid workspace seed")
return DistributionBuildOutput{}, validationError("distribution build input has an invalid workspace seed")
}
seedPayload = decoded
}
if err := os.WriteFile(filepath.Join(inputDir, "workspace-seed.json"), seedPayload, 0o600); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
lifecyclePlanPayload := []byte("{}")
if input.AutonomousLifecycle != nil {
encoded, err := json.Marshal(input.AutonomousLifecycle)
if err != nil {
return nil, validationError("distribution build input has an invalid autonomous lifecycle plan")
return DistributionBuildOutput{}, validationError("distribution build input has an invalid autonomous lifecycle plan")
}
lifecyclePlanPayload = encoded
}
if err := os.WriteFile(filepath.Join(inputDir, "autonomous-lifecycle-plan.json"), lifecyclePlanPayload, 0o600); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
if err := os.WriteFile(filepath.Join(inputDir, "build.sh"), []byte(distributionBuildScript), 0o500); err != nil {
return nil, err
return DistributionBuildOutput{}, err
}
outputName := strings.TrimSpace(input.OutputFilename)
if outputName == "" || filepath.Base(outputName) != outputName {
return nil, validationError("distribution build input has an invalid output filename")
return DistributionBuildOutput{}, validationError("distribution build input has an invalid output filename")
}
args := builder.containerArgs(input, sourceMountDir, inputDir, buildDir, outputDir, goBuildCacheDir, goModCacheDir, outputName)
reportBuilderProgress(progress, 18, "git_sync: platform builder container starting")
output, err := builder.runBuildCommand(ctx, args, progress)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, validationError("platform builder timed out while building the distribution")
return DistributionBuildOutput{}, validationError("platform builder timed out while building the distribution")
}
return nil, validationError("platform builder failed: " + safeBuilderFailureWithCommandError(output, err, input.AuthKey, builder.config.SourceDir, sourceDir, jobDir))
return DistributionBuildOutput{}, validationError("platform builder failed: " + safeBuilderFailureWithCommandError(output, err, input.AuthKey, builder.config.SourceDir, sourceDir, jobDir))
}
reportBuilderProgress(progress, 92, "package_finalize: reading platform builder output")
binary, err := os.ReadFile(filepath.Join(outputDir, outputName))
reportBuilderProgress(progress, 92, "package_finalize: validating platform builder output")
outputPath := filepath.Join(outputDir, outputName)
sizeBytes, checksum, err := fileSizeAndChecksum(outputPath)
if err != nil {
return nil, validationError("platform builder did not produce a distribution executable")
return DistributionBuildOutput{}, validationError("platform builder did not produce a distribution executable")
}
if len(binary) == 0 {
return nil, validationError("platform builder produced an empty distribution executable")
if sizeBytes == 0 {
return DistributionBuildOutput{}, validationError("platform builder produced an empty distribution executable")
}
return binary, nil
cleanupJob = false
return DistributionBuildOutput{Path: outputPath, SizeBytes: sizeBytes, Checksum: checksum, cleanup: func() error { return os.RemoveAll(jobDir) }}, nil
}
// prepareRunSource implements the source phase of a Jenkins-style build. A
@@ -422,6 +463,20 @@ func (builder *DockerDistributionBuilder) runBuildCommand(ctx context.Context, a
const builderProgressMarker = "__platform_builder_progress__|"
const maxBuilderDiagnosticOutputBytes = 1024 * 1024
func appendBoundedBuilderOutput(output *bytes.Buffer, payload []byte) {
if len(payload) >= maxBuilderDiagnosticOutputBytes {
output.Reset()
_, _ = output.Write(payload[len(payload)-maxBuilderDiagnosticOutputBytes:])
return
}
if overflow := output.Len() + len(payload) - maxBuilderDiagnosticOutputBytes; overflow > 0 {
_ = output.Next(overflow)
}
_, _ = output.Write(payload)
}
func parseBuilderProgressLine(line string) (DistributionBuildProgress, bool) {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, builderProgressMarker) {