8000 Fix handle leak by Bbulatov · Pull Request #4567 · distribution/distribution · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix handle leak #4567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed

Conversation

Bbulatov
Copy link

Signed-off-by: Марк Булатов <markus7bulatenko@yandex.ru>
Copy link
Contributor
@Vad1mo Vad1mo left a comment

Choose a reason for hiding this comment

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

Why not use defer?

func (hrs *httpReadSeeker) reader() (io.Reader, error) {
    // ...

    if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
        if hrs.readerOffset > 0 {
            if resp.StatusCode != http.StatusPartialContent {
                defer resp.Body.Close()
                return nil, ErrWrongCodeForByteRange
            }

            contentRange := resp.Header.Get("Content-Range")
            if contentRange == "" {
                defer resp.Body.Close()
                return nil, errors.New("no Content-Range header found in HTTP 206 response")
            }

            submatches := contentRangeRegexp.FindStringSubmatch(contentRange)
            if len(submatches) < 4 {
                defer resp.Body.Close()
                return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange)
            }

            startByte, err := strconv.ParseUint(submatches[1], 10, 64)
            if err != nil {
                defer resp.Body.Close()
                return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange)
            }

            if startByte != uint64(hrs.readerOffset) {
                defer resp.Body.Close()
                return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset)
            }

            endByte, err := strconv.ParseUint(submatches[2], 10, 64)
            if err != nil {
                defer resp.Body.Close()
                return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange)
            }

            if submatches[3] == "*" {
                hrs.size = -1
            } else {
                size, err := strconv.ParseUint(submatches[3], 10, 64)
                if err != nil {
                    defer resp.Body.Close()
                    return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange)
                }

                if endByte+1 != size {
                    defer resp.Body.Close()
                    return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange)
                }

                hrs.size = int64(size)
            }
        } else if resp.StatusCode == http.StatusOK {
            hrs.size = resp.ContentLength
        } else {
            hrs.size = -1
        }
        hrs.rc = resp.Body
    } else {
        defer resp.Body.Close()
        if hrs.errorHandler != nil {
            return nil, hrs.errorHandler(resp)
        }
        return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status)
    }

    return hrs.rc, nil
}

Copy link
Member
@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

If we use a named return for the error, we can probably handle this with a single defer; from a quick glance over this function, it looks like we want to close the response body if an error is returned; in other cases, closing the body must be handled by the caller;

So if we change the signature to have a named return for the error (we don't need a name for the io.Reader); generally I prefer using a distinctive name for such error returns (instead of err) to prevent accidental shadowing;

- func (hrs *httpReadSeeker) reader() (io.Reader, error) {
+ func (hrs *httpReadSeeker) reader() (_ io.Reader, retErr error) {

Then we can define a defer after a non-error request was made;

resp, err := hrs.client.Do(req)
if err != nil {
    return nil, err
}
defer func() {
    if retErr != nil {
        _ = resp.Body.Close()
    }
}()

This means (AFAICS) that we can also remove the defer resp.Body.Close() at the end of the function, although I'm not 100% sure if that hrs.errorHandler(resp) could potentially return a nil error? It looks like NewHTTPReadSeeker is the only place where this errorHandler is set, and it's not used in our codebase, but (technically) it could be "anything", so perhaps we could keep it.

if hrs.errorHandler != nil {
    defer resp.Body.Close()
    return nil, hrs.errorHandler(resp)
}

Quick scratch I made locally with those changes applied looks like below (click "details" to expand);

func (hrs *httpReadSeeker) reader() (_ io.Reader, retErr error) {
	if hrs.err != nil {
		return nil, hrs.err
	}

	if hrs.rc != nil {
		return hrs.rc, nil
	}

	req, err := http.NewRequest("GET", hrs.url, nil)
	if err != nil {
		return nil, err
	}

	if hrs.readerOffset > 0 {
		// If we are at different offset, issue a range request from there.
		req.Header.Add("Range", fmt.Sprintf("bytes=%d-", hrs.readerOffset))
		// TODO: get context in here
		// context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range"))
	}

	resp, err := hrs.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() {
		if retErr != nil {
			_ = resp.Body.Close()
		}
	}()

	// Normally would use client.SuccessStatus, but that would be a cyclic
	// import
	if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
		if hrs.readerOffset > 0 {
			if resp.StatusCode != http.StatusPartialContent {
				return nil, ErrWrongCodeForByteRange
			}

			contentRange := resp.Header.Get("Content-Range")
			if contentRange == "" {
				return nil, errors.New("no Content-Range header found in HTTP 206 response")
			}

			submatches := contentRangeRegexp.FindStringSubmatch(contentRange)
			if len(submatches) < 4 {
				return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange)
			}

			startByte, err := strconv.ParseUint(submatches[1], 10, 64)
			if err != nil {
				return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange)
			}

			if startByte != uint64(hrs.readerOffset) {
				return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset)
			}

			endByte, err := strconv.ParseUint(submatches[2], 10, 64)
			if err != nil {
				return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange)
			}

			if submatches[3] == "*" {
				hrs.size = -1
			} else {
				size, err := strconv.ParseUint(submatches[3], 10, 64)
				if err != nil {
					return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange)
				}

				if endByte+1 != size {
					return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange)
				}

				hrs.size = int64(size)
			}
		} else if resp.StatusCode == http.StatusOK {
			hrs.size = resp.ContentLength
		} else {
			hrs.size = -1
		}
		hrs.rc = resp.Body
	} else {
		if hrs.errorHandler != nil {
			// Closing the body should be handled by the existing defer,
			// but in case a custom "errHandler" is used that doesn't return
			// an error, we close the body regardless.
			defer resp.Body.Close()
			return nil, hrs.errorHandler(resp)
		}
		return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status)
	}

	return hrs.rc, nil
}

@@ -190,30 +190,36 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
if hrs.readerOffset > 0 {
if resp.StatusCode != http.StatusPartialContent {
resp.Close()
Copy link
Member

Choose a reason for hiding this comment

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

At least these should be resp.Body.Close(), because resp.Close is a boolean (not a func).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah
Copy link
Member

Pushed my quick draft as a PR against your PR branch; if you merge (and squash), those change should show up here if you first push to update the PR;

@Vad1mo
Copy link
Contributor
8000 Vad1mo commented Feb 25, 2025

maybe we can get this even into v3??? @milosgajdos @stevvooe

@milosgajdos
Copy link
Member

@Bbulatov do you want to merge @thaJeztah 's PR into your branch so we can continue with this PR?

registry/client/transport: use defer to close response body
@milosgajdos
Copy link
Member

This needs a rebase now @Bbulatov

Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
@milosgajdos
Copy link
Member

OMG, I tried fixing this using GitHub's UI tools and it completely messed things up . @Bbulatov please remove my latest commit (it was committed via the extremely retarded resolve conflicts UI tool provided by GitHub) and then rebase. Sorry about this 🙄 🤦‍♂️

@milosgajdos
Copy link
Member

maybe we can get this even into v3??? @milosgajdos @stevvooe

@Vad1mo feel free to open a PR on main or maybe @Bbulatov should do; we won't be doing another 2.x release and I've just noticed that this PR has been opened against the 2.x branch...so yeah, if you want this, please open this against the main branch

@Vad1mo
Copy link
Contributor
Vad1mo commented Mar 20, 2025

@milosgajdos, ok I'll open up a new one

Vad1mo added a commit to container-registry/distribution that referenced this pull request Mar 20, 2025
Vad1mo added a commit to container-registry/distribution that referenced this pull request Mar 20, 2025
resolves distribution#4494 replaces distribution#4567

Signed-off-by: Vadim Bauer <vb@container-registry.com>
vitshev pushed a commit to tractoai/distribution that referenced this pull request Apr 10, 2025
resolves distribution#4494 replaces distribution#4567

Signed-off-by: Vadim Bauer <vb@container-registry.com>
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.

4 participants
0