Skip to content

fix(backend): close the ReadCloser returned by PullBlob in push - #509

Merged
aftersnow merged 3 commits into
modelpack:mainfrom
SAY-5:fix/push-close-pull-blob-reader
Aug 14, 2026
Merged

fix(backend): close the ReadCloser returned by PullBlob in push#509
aftersnow merged 3 commits into
modelpack:mainfrom
SAY-5:fix/push-close-pull-blob-reader

Conversation

@SAY-5

@SAY-5 SAY-5 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Problem

pushIfNotExist pulls each blob's content from the source storage and
then hands it to Blobs().Push wrapped in io.NopCloser:

content, err := src.PullBlob(ctx, repo, desc.Digest.String())
if err != nil {
    return err
}

reader := pb.Add(prompt, desc.Digest.String(), desc.Size, content)
if err := dst.Blobs().Push(ctx, desc, io.NopCloser(reader)); err != nil {
    ...
}

The io.NopCloser is intentional: Close on the distribution reader
returns an error (see #50), so we don't let Push propagate that error.
But the wrapper also prevents the original content from ever being
closed. On both success and error paths, the underlying file descriptor
(or HTTP body) leaks for every blob we push, and Push is called once
per object in the image. #491.

Fix

Add defer content.Close() immediately after the nil-error check. The
existing NopCloser still prevents Push from calling Close on its
own, so the workaround for #50 is preserved. defer handles both the
success path and every early-return error path.

  content, err := src.PullBlob(ctx, repo, desc.Digest.String())
  if err != nil {
      return err
  }
+ defer content.Close()

  reader := pb.Add(prompt, desc.Digest.String(), desc.Size, content)

Fixes #491

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a resource leak in pkg/backend/push.go by ensuring that blob content is properly closed using a defer statement. Feedback was provided to simplify the accompanying comment for better readability and to move historical context to the commit message.

Comment thread pkg/backend/push.go Outdated
@SAY-5

SAY-5 commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Simplified the close comment per review feedback.

@SAY-5
SAY-5 force-pushed the fix/push-close-pull-blob-reader branch from 65d271c to d81ffec Compare May 27, 2026 04:52
SAY-5 added 2 commits June 2, 2026 14:05
pushIfNotExist pulled each blob's content from the source storage and then
handed it to Blobs().Push wrapped in io.NopCloser. The NopCloser is there
on purpose (Close on the distribution reader returns an error, see modelpack#50),
but it also means the original ReadCloser from PullBlob was never closed
on any path, leaking a file descriptor or HTTP body per blob.

Add a `defer content.Close()` immediately after the nil-error check so the
original reader is released on both success and error paths. The existing
NopCloser wrapper still prevents Push from calling Close itself, so the
workaround for modelpack#50 is preserved.

Fixes modelpack#491

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
Signed-off-by: say <say.apm35@gmail.com>
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@SAY-5
SAY-5 force-pushed the fix/push-close-pull-blob-reader branch from d81ffec to e8ce87c Compare June 2, 2026 21:06
@aftersnow
aftersnow enabled auto-merge (squash) August 14, 2026 09:52

@aftersnow aftersnow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against current main: pushIfNotExist never closes the ReadCloser from PullBlob, so every non-manifest blob leaks a file handle on push.

Checked that closing is actually the right call here. fileReader.Close() in distribution v3.1.0 goes through closeWithErr, which does fr.rc.Close() to release the reader chain and only then returns the fileReader: closed sentinel. So the handle is genuinely released, and ignoring the returned error is correct rather than sloppy. The guard on fr.err also makes a second Close a no-op, so there is no double-close risk alongside the existing io.NopCloser wrapper, which stays necessary to keep that sentinel out of Blobs().Push.

Test-merged onto main (9f887eb), built and ran ./pkg/backend/... ./pkg/storage/.... Failure set is identical to the baseline on clean main (all from a local sandbox blocking ~/.docker/config.json), so no regressions.

@aftersnow
aftersnow merged commit 3cb3138 into modelpack:main Aug 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Push leaks ReadCloser from PullBlob on both success and error paths

2 participants