Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/bbox-init/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ func main() {
"error", err)
}

if err := mountExtras(logger); err != nil {
logger.Warn("failed to mount extras", "error", err)
}

if err := harden.ApplySeccomp(); err != nil {
logger.Error("seccomp filter failed", "error", err)
halt()
Expand Down
100 changes: 100 additions & 0 deletions cmd/bbox-init/mounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

//go:build linux

package main

import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"syscall"
"time"
)

// guestMountEntry matches the JSON written by InjectMountConfig on the host.
type guestMountEntry struct {
Tag string `json:"tag"`
GuestPath string `json:"guest_path"`
ReadOnly bool `json:"read_only"`
}

// mountConfigPath is the guest path where the host writes extra mount config.
const mountConfigPath = "/etc/broodbox-mounts.json"

// mountExtras reads /etc/broodbox-mounts.json and mounts each virtiofs share.
// If the config file does not exist, it returns nil (no extra mounts needed).
func mountExtras(logger *slog.Logger) error {
data, err := os.ReadFile(mountConfigPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("reading mount config: %w", err)
}

var entries []guestMountEntry
if err := json.Unmarshal(data, &entries); err != nil {
return fmt.Errorf("parsing mount config: %w", err)
}

for _, entry := range entries {
if err := mountOne(logger, entry); err != nil {
return err
}
}

return nil
}

// mountRetries is the number of attempts for each virtiofs mount.
const mountRetries = 5

// mountRetrySleep is the delay between mount retries.
const mountRetrySleep = 500 * time.Millisecond

// sandboxUID and sandboxGID are the UID/GID of the sandbox user in the guest.
const (
sandboxUID = 1000
sandboxGID = 1000
)

func mountOne(logger *slog.Logger, entry guestMountEntry) error {
if err := os.MkdirAll(entry.GuestPath, 0o755); err != nil {
return fmt.Errorf("creating mount point %s: %w", entry.GuestPath, err)
}

flags := uintptr(syscall.MS_NOSUID | syscall.MS_NODEV)
if entry.ReadOnly {
flags |= syscall.MS_RDONLY
}

var mountErr error
for attempt := 1; attempt <= mountRetries; attempt++ {
mountErr = syscall.Mount(entry.Tag, entry.GuestPath, "virtiofs", flags, "")
if mountErr == nil {
break
}
logger.Debug("virtiofs mount attempt failed, retrying",
"tag", entry.Tag,
"guest_path", entry.GuestPath,
"attempt", attempt,
"error", mountErr,
)
time.Sleep(mountRetrySleep)
}
if mountErr != nil {
return fmt.Errorf("mounting virtiofs tag %q at %s after %d attempts: %w",
entry.Tag, entry.GuestPath, mountRetries, mountErr)
}

if err := os.Chown(entry.GuestPath, sandboxUID, sandboxGID); err != nil {
logger.Warn("failed to chown mount point", "path", entry.GuestPath, "error", err)
}

logger.Info("mounted extra virtiofs share", "tag", entry.Tag, "guest_path", entry.GuestPath, "read_only", entry.ReadOnly)
return nil
}
3 changes: 2 additions & 1 deletion cmd/bbox/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,8 +737,9 @@ func run(parentCtx context.Context, agentName string, flags runFlags) error {
deps.Flusher = review.NewFSFlusher()
deps.Differ = diff.NewFSDiffer()

// Wire snapshot post-processors (git config sanitizer).
// Wire snapshot post-processors (worktree reconstruction, then git config sanitizer).
deps.SnapshotPostProcessors = []workspace.SnapshotPostProcessor{
infragit.NewWorktreeProcessor(logger),
infragit.NewConfigSanitizer(logger),
}

Expand Down
18 changes: 9 additions & 9 deletions internal/infra/git/sanitizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,45 +52,45 @@ func NewConfigSanitizer(logger *slog.Logger) *ConfigSanitizer {
//
// For external worktrees (where git metadata lives outside the workspace),
// sanitization is skipped because the config is not present in the snapshot.
func (s *ConfigSanitizer) Process(_ context.Context, originalPath, snapshotPath string) error {
func (s *ConfigSanitizer) Process(_ context.Context, originalPath, snapshotPath string) (*workspace.PostProcessResult, error) {
// Find the git config source on the host filesystem.
srcPath, err := resolveGitConfigPath(originalPath)
if err != nil {
s.logger.Warn("could not resolve git config path, skipping sanitization",
"path", originalPath, "error", err)
return nil
return nil, nil
}
if srcPath == "" {
return nil
return nil, nil
}

data, err := os.ReadFile(srcPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
return nil, nil
}
return fmt.Errorf("reading git config: %w", err)
return nil, fmt.Errorf("reading git config: %w", err)
}

sanitized := SanitizeConfig(string(data))

// Determine where the sanitized config should be written in the snapshot.
dstPath := s.resolveSnapshotConfigDest(originalPath, snapshotPath)
if dstPath == "" {
return nil
return nil, nil
}

// Ensure parent directory exists. Normally the snapshot creator copies
// .git/ first, but be defensive for edge cases and tests.
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return fmt.Errorf("creating git config directory in snapshot: %w", err)
return nil, fmt.Errorf("creating git config directory in snapshot: %w", err)
}

if err := os.WriteFile(dstPath, []byte(sanitized), 0o644); err != nil {
return fmt.Errorf("writing sanitized git config: %w", err)
return nil, fmt.Errorf("writing sanitized git config: %w", err)
}

return nil
return nil, nil
}

// resolveSnapshotConfigDest determines where to write the sanitized git config
Expand Down
22 changes: 11 additions & 11 deletions internal/infra/git/sanitizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func TestProcess_ReadsAndWritesSanitizedConfig(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// Read the sanitized config from the snapshot.
Expand Down Expand Up @@ -356,7 +356,7 @@ func TestProcess_NoGitConfig_NoOp(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// Snapshot should not have a .git directory.
Expand Down Expand Up @@ -688,7 +688,7 @@ func TestProcess_ExternalWorktree_SkipsSanitization(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// The .git file should be PRESERVED (not replaced with a directory).
Expand Down Expand Up @@ -756,7 +756,7 @@ func TestProcess_InWorkspaceWorktree_SanitizesConfig(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), workspace, snapshotDir)
_, err := sanitizer.Process(t.Context(), workspace, snapshotDir)
require.NoError(t, err)

// The .git file should remain a file (not converted to directory).
Expand Down Expand Up @@ -805,7 +805,7 @@ func TestProcess_ExternalWorktreeNoCommondir_SkipsSanitization(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// No config should be written for external worktrees.
Expand All @@ -828,7 +828,7 @@ func TestProcess_NormalRepo_NoDoubleCreate(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// For normal repos, Process should NOT create HEAD/objects/refs
Expand Down Expand Up @@ -868,7 +868,7 @@ func TestProcess_Worktree_MaliciousGitdir(t *testing.T) {
sanitizer := NewConfigSanitizer(logger)

// Process should succeed — external worktrees are skipped.
err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
_, err := sanitizer.Process(t.Context(), originalDir, snapshotDir)
require.NoError(t, err)

// No config should be written since gitdir is external.
Expand Down Expand Up @@ -922,7 +922,7 @@ func TestProcess_InWorkspaceWorktree_RelativeGitdir(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), workspace, snapshotDir)
_, err := sanitizer.Process(t.Context(), workspace, snapshotDir)
require.NoError(t, err)

// Config should be sanitized at the correct location.
Expand Down Expand Up @@ -978,7 +978,7 @@ func TestProcess_InWorkspaceWorktree_NoCommondir(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
sanitizer := NewConfigSanitizer(logger)

err := sanitizer.Process(t.Context(), workspace, snapshotDir)
_, err := sanitizer.Process(t.Context(), workspace, snapshotDir)
require.NoError(t, err)

// Config should be sanitized at gitdir/config (no commondir fallback).
Expand Down Expand Up @@ -1030,7 +1030,7 @@ func TestProcess_Worktree_CommondirEscapesSnapshot(t *testing.T) {
sanitizer := NewConfigSanitizer(logger)

// Process should succeed (escaping commondir is non-fatal, just skipped).
err := sanitizer.Process(t.Context(), workspace, snapshotDir)
_, err := sanitizer.Process(t.Context(), workspace, snapshotDir)
require.NoError(t, err)

// Should NOT have written to /tmp/config.
Expand Down Expand Up @@ -1063,6 +1063,6 @@ func TestProcess_Worktree_MalformedGitFileInSnapshot(t *testing.T) {
sanitizer := NewConfigSanitizer(logger)

// Process should succeed (malformed snapshot .git is non-fatal).
err := sanitizer.Process(t.Context(), workspace, snapshotDir)
_, err := sanitizer.Process(t.Context(), workspace, snapshotDir)
require.NoError(t, err)
}
Loading