Skip to content

Commit 95bf642

Browse files
committed
Fix TempDirForURL leaks for --build-context and ADD git
Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent 769d311 commit 95bf642

4 files changed

Lines changed: 90 additions & 25 deletions

File tree

add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,12 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
602602
go func() {
603603
defer wg.Done()
604604
defer pipeWriter.Close()
605-
// TODO: the returned cloneDir is never cleaned up, leaking disk space.
606605
var cloneDir, subdir string
607606
cloneDir, subdir, getErr = define.TempDirForURL(tmpdir.GetTempDir(), "", src)
608607
if getErr != nil {
609608
return
610609
}
610+
defer os.RemoveAll(cloneDir)
611611
getOptions := copier.GetOptions{
612612
UIDMap: srcUIDMap,
613613
GIDMap: srcGIDMap,

imagebuildah/executor.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,17 @@ import (
4040
"go.podman.io/image/v5/types"
4141
"go.podman.io/storage"
4242
"go.podman.io/storage/pkg/archive"
43+
"golang.org/x/sync/errgroup"
4344
"golang.org/x/sync/semaphore"
4445
)
4546

47+
type additionalBuildContext struct {
48+
define.AdditionalBuildContext
49+
// downloadedTempDir is the temporary directory created for the additional build context.
50+
// It should be removed when the build ends.
51+
downloadedTempDir string
52+
}
53+
4654
// builtinAllowedBuildArgs is list of built-in allowed build args. Normally we
4755
// complain if we're given values for arguments which have no corresponding ARG
4856
// instruction in the Dockerfile, since that's usually an indication of a user
@@ -161,7 +169,7 @@ type executor struct {
161169
imageInfoLock sync.Mutex
162170
imageInfoCache map[string]imageTypeAndHistoryAndDiffIDs
163171
fromOverride string
164-
additionalBuildContexts map[string]*define.AdditionalBuildContext
172+
additionalBuildContexts map[string]*additionalBuildContext
165173
manifest string
166174
secrets map[string]define.Secret
167175
sshsources map[string]*sshagent.Source
@@ -268,6 +276,11 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
268276
buildOutputs = append(buildOutputs, options.BuildOutput) //nolint:staticcheck
269277
}
270278

279+
wrappedAdditionalBuildContexts := make(map[string]*additionalBuildContext, len(options.AdditionalBuildContexts))
280+
for name, ctx := range options.AdditionalBuildContexts {
281+
wrappedAdditionalBuildContexts[name] = &additionalBuildContext{AdditionalBuildContext: *ctx}
282+
}
283+
271284
exec := executor{
272285
args: options.Args,
273286
cacheFrom: options.CacheFrom,
@@ -358,7 +371,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
358371
rusageLogFile: rusageLogFile,
359372
imageInfoCache: make(map[string]imageTypeAndHistoryAndDiffIDs),
360373
fromOverride: options.From,
361-
additionalBuildContexts: options.AdditionalBuildContexts,
374+
additionalBuildContexts: wrappedAdditionalBuildContexts,
362375
manifest: options.Manifest,
363376
secrets: secrets,
364377
sshsources: sshsources,
@@ -857,6 +870,21 @@ func (b *executor) Build(ctx context.Context, stages imagebuilder.Stages) (image
857870
}
858871
cleanupImages = nil
859872

873+
var g errgroup.Group
874+
for _, additionalBuildContext := range b.additionalBuildContexts {
875+
if additionalBuildContext.downloadedTempDir != "" {
876+
dir := additionalBuildContext.downloadedTempDir
877+
g.Go(func() error {
878+
logrus.Debugf("Removing additional build context temp dir %q", dir)
879+
return os.RemoveAll(dir)
880+
})
881+
}
882+
}
883+
if err := g.Wait(); err != nil {
884+
logrus.Debugf("Failed to cleanup additional build context temp dir: %v", err)
885+
lastErr = err
886+
}
887+
860888
if b.rusageLogFile != nil && b.rusageLogFile != b.out {
861889
// we deliberately ignore the error here, as this
862890
// function can be called multiple times

imagebuildah/stage_executor.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
484484
if fromErr != nil {
485485
return fmt.Errorf("unable to resolve argument %q: %w", copy.From, fromErr)
486486
}
487-
var additionalBuildContext *define.AdditionalBuildContext
487+
var additionalBuildContext *additionalBuildContext
488488
if foundContext, ok := s.executor.additionalBuildContexts[from]; ok {
489489
additionalBuildContext = foundContext
490490
} else {
@@ -512,18 +512,14 @@ func (s *stageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
512512
// additional context contains a tar file
513513
// so download and explode tar to buildah
514514
// temp and point context to that.
515-
// TODO: the returned path is never cleaned up, leaking disk space.
516515
path, subdir, err := define.TempDirForURL(tmpdir.GetTempDir(), internal.BuildahExternalArtifactsDir, additionalBuildContext.Value)
517516
if err != nil {
518517
return fmt.Errorf("unable to download context from external source %q: %w", additionalBuildContext.Value, err)
519518
}
520-
// point context dir to the extracted path
521-
contextDir = filepath.Join(path, subdir)
522-
// populate cache for next RUN step
523-
additionalBuildContext.DownloadedCache = contextDir
524-
} else {
525-
contextDir = additionalBuildContext.DownloadedCache
519+
additionalBuildContext.downloadedTempDir = path
520+
additionalBuildContext.DownloadedCache = filepath.Join(path, subdir)
526521
}
522+
contextDir = additionalBuildContext.DownloadedCache
527523
} else {
528524
// This points to a path on the filesystem
529525
// Check to see if there's a .containerignore
@@ -733,18 +729,14 @@ func (s *stageExecutor) runStageMountPoints(mountList []string) (map[string]inte
733729
// additional context contains a tar file
734730
// so download and explode tar to buildah
735731
// temp and point context to that.
736-
// TODO: the returned path is never cleaned up, leaking disk space.
737732
path, subdir, err := define.TempDirForURL(tmpdir.GetTempDir(), internal.BuildahExternalArtifactsDir, additionalBuildContext.Value)
738733
if err != nil {
739734
return nil, fmt.Errorf("unable to download context from external source %q: %w", additionalBuildContext.Value, err)
740735
}
741-
// point context dir to the extracted path
742-
mountPoint = filepath.Join(path, subdir)
743-
// populate cache for next RUN step
744-
additionalBuildContext.DownloadedCache = mountPoint
745-
} else {
746-
mountPoint = additionalBuildContext.DownloadedCache
736+
additionalBuildContext.downloadedTempDir = path
737+
additionalBuildContext.DownloadedCache = filepath.Join(path, subdir)
747738
}
739+
mountPoint = additionalBuildContext.DownloadedCache
748740
}
749741
stageMountPoints[from] = internal.StageMountDetails{
750742
IsAdditionalBuildContext: true,

tests/bud.bats

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3396,6 +3396,13 @@ _EOF
33963396
done
33973397
}
33983398

3399+
# assert_no_tempdir_leaks checks $1 has no leftover temp download directories.
3400+
function assert_no_tempdir_leaks() {
3401+
local tmpdir=$1
3402+
local leftovers=$(find $tmpdir -mindepth 1 -maxdepth 1 -print)
3403+
assert "$leftovers" == ""
3404+
}
3405+
33993406
# Helper function for several of the tests which pull from http.
34003407
#
34013408
# Usage: _test_http SUBDIRECTORY URL_PATH [EXTRA ARGS]
@@ -3416,11 +3423,14 @@ function _test_http() {
34163423

34173424
starthttpd "$BUDFILES/$testdir"
34183425
target=scratch-image
3419-
run_buildah build $WITH_POLICY_JSON \
3426+
local tmpdir=${TEST_SCRATCH_DIR}/tmpdir
3427+
mkdir -p $tmpdir
3428+
TMPDIR=$tmpdir run_buildah build $WITH_POLICY_JSON \
34203429
-t ${target} \
34213430
"$@" \
34223431
http://0.0.0.0:${HTTP_SERVER_PORT}/$urlpath
34233432
stophttpd
3433+
assert_no_tempdir_leaks $tmpdir
34243434
run_buildah from ${target}
34253435
}
34263436

@@ -3500,9 +3510,7 @@ function validate_instance_compression {
35003510
mkdir -p "${tmpdir}"
35013511
TMPDIR="${tmpdir}" run_buildah build $WITH_POLICY_JSON -t ${target} "${gitrepo}"
35023512
run_buildah from "${target}"
3503-
run find "${tmpdir}" -type d -print
3504-
echo "$output"
3505-
test "${#lines[*]}" -le 2
3513+
assert_no_tempdir_leaks $tmpdir
35063514
}
35073515

35083516
@test "bud-git-context-failure" {
@@ -8180,7 +8188,10 @@ _EOF
81808188
file.txt
81818189
_EOF
81828190

8183-
run_buildah build -f $contextdir/Dockerfile -t add-git-ignoring-files $contextdir
8191+
local tmpdir=${TEST_SCRATCH_DIR}/tmpdir
8192+
mkdir -p $tmpdir
8193+
TMPDIR=$tmpdir run_buildah build -f $contextdir/Dockerfile -t add-git-ignoring-files $contextdir
8194+
assert_no_tempdir_leaks $tmpdir
81848195
}
81858196

81868197
@test "bud with ADD with git repository source escape directory" {
@@ -8214,9 +8225,11 @@ ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/test-bug.git#main:proj${secretdir} /m
82148225
RUN cat /mydir/secretfile
82158226
_EOF
82168227

8217-
run_buildah 125 build -f $contextdir/Containerfile -t escape-image --no-cache $contextdir
8228+
local tmpdir=${TEST_SCRATCH_DIR}/tmpdir
8229+
mkdir -p $tmpdir
8230+
TMPDIR=$tmpdir run_buildah 125 build -f $contextdir/Containerfile -t escape-image --no-cache $contextdir
82188231
assert "$output" !~ "mysecret"
8219-
expect_output --substring "no such file or directory"
8232+
assert_no_tempdir_leaks $tmpdir
82208233
}
82218234

82228235
@test "bud with http context symlinked Dockerfile does not write through symlink on fallback" {
@@ -10510,3 +10523,35 @@ _EOF
1051010523
# Verify final image IDs match (complete cache hit)
1051110524
assert "$first_build_final_image_id" "==" "$second_build_final_image_id" "final image ID should match when cache is fully reused"
1051210525
}
10526+
10527+
@test "build-with-additional-build-context URL temp dirs are cleaned up" {
10528+
_prefetch alpine
10529+
10530+
local tmpdir=${TEST_SCRATCH_DIR}/tmpdir
10531+
mkdir -p $tmpdir
10532+
local tarball_content=${TEST_SCRATCH_DIR}/tarball-src
10533+
mkdir -p $tarball_content/myproject
10534+
echo secret-content > $tarball_content/myproject/file.txt
10535+
local tarball=${TEST_SCRATCH_DIR}/context.tar.gz
10536+
tar -czf $tarball -C $tarball_content .
10537+
local httpdir=${TEST_SCRATCH_DIR}/http
10538+
mkdir -p $httpdir
10539+
cp $tarball $httpdir/context.tar.gz
10540+
10541+
starthttpd $httpdir
10542+
local contextdir=${TEST_SCRATCH_DIR}/bud/context
10543+
mkdir -p $contextdir
10544+
cat > $contextdir/Dockerfile << _EOF
10545+
FROM alpine
10546+
COPY --from=ctx myproject/file.txt /file.txt
10547+
RUN cat /file.txt
10548+
RUN --mount=type=bind,src=myproject,from=ctx2,target=/mnt,z cmp /file.txt /mnt/file.txt
10549+
_EOF
10550+
10551+
TMPDIR=$tmpdir run_buildah build $WITH_POLICY_JSON \
10552+
--build-context ctx=http://0.0.0.0:${HTTP_SERVER_PORT}/context.tar.gz \
10553+
--build-context ctx2=http://0.0.0.0:${HTTP_SERVER_PORT}/context.tar.gz \
10554+
-t source -f $contextdir/Dockerfile $contextdir
10555+
expect_output --substring "secret-content"
10556+
assert_no_tempdir_leaks $tmpdir
10557+
}

0 commit comments

Comments
 (0)