Skip to content

avformat/http: 401/407 challenge at non-zero offset is turned into EIO before the auth retry in http_open_cnx can run #49

Description

@ronag

Summary

When an HTTP server (or proxy) answers a request made at a non-zero offset with a 401 Unauthorized (or 407 Proxy Authentication Required) challenge, http_connect()'s offset-mismatch check converts the response into a generic AVERROR(EIO) before http_open_cnx() ever reaches its 401/407 retry logic. Authentication negotiation therefore fails hard whenever the first challenge arrives at off > 0 — e.g. opening with the -offset option, the partial-request continuation path (request_size/initial_request_size re-requests), or any operation that issues a fresh ranged request against a server/CDN that re-challenges. As a side effect the correct error code (AVERROR_HTTP_UNAUTHORIZED / AVERROR_HTTP_PROXY_AUTH_REQUIRED) is also replaced by a bare EIO.

Location

  • Offset reset before reading the response, http_connect():
    s->off = 0;
  • Offset restore (redirects only) and mismatch check, http_connect():

    FFmpeg/libavformat/http.c

    Lines 1752 to 1761 in 9a83bff

    if (s->new_location)
    s->off = off;
    if (off != s->off) {
    av_log(h, AV_LOG_ERROR,
    "Unexpected offset: expected %"PRIu64", got %"PRIu64"\n",
    off, s->off);
    err = AVERROR(EIO);
    goto done;
    }
  • Unreachable auth retry, http_open_cnx():

    FFmpeg/libavformat/http.c

    Lines 497 to 513 in 9a83bff

    auth_attempts++;
    if (s->http_code == 401) {
    if ((cur_auth_type == HTTP_AUTH_NONE || s->auth_state.stale) &&
    s->auth_state.auth_type != HTTP_AUTH_NONE && auth_attempts < 4) {
    ffurl_closep(&s->hd);
    goto redo;
    } else
    goto fail;
    }
    if (s->http_code == 407) {
    if ((cur_proxy_auth_type == HTTP_AUTH_NONE || s->proxy_auth_state.stale) &&
    s->proxy_auth_state.auth_type != HTTP_AUTH_NONE && auth_attempts < 4) {
    ffurl_closep(&s->hd);
    goto redo;
    } else
    goto fail;
    }
  • First-401/407 leniency, check_http_code():

    FFmpeg/libavformat/http.c

    Lines 923 to 937 in 9a83bff

    static int check_http_code(URLContext *h, int http_code, const char *end)
    {
    HTTPContext *s = h->priv_data;
    /* error codes are 4xx and 5xx, but regard 401 and 416 as a success, so we
    * don't abort until all headers have been parsed. */
    if (http_code >= 400 && http_code < 600 &&
    (http_code != 401 || s->auth_state.auth_type != HTTP_AUTH_NONE) &&
    (http_code != 407 || s->proxy_auth_state.auth_type != HTTP_AUTH_NONE) &&
    (http_code != 416 || !s->reconnect_partial)) {
    end += strspn(end, SPACE_CHARS);
    av_log(h, AV_LOG_WARNING, "HTTP error %d %s\n", http_code, end);
    return ff_http_averror(http_code, AVERROR(EIO));
    }
    return 0;
    }

Details

http_connect() captures the requested offset and resets s->off before parsing the response, then restores it only for redirect responses:

static int http_connect(URLContext *h, const char *path, const char *local_path,
                        const char *hoststr, const char *auth,
                        const char *proxyauth)
{
    ...
    uint64_t off = s->off;                       // line 1588
    ...
    /* init input buffer */
    ...
    s->off              = 0;                     // line 1723
    ...
    err = http_read_header(h);
    ...
    if (s->new_location)                         // lines 1752-1753
        s->off = off;

    if (off != s->off) {                         // lines 1755-1761
        av_log(h, AV_LOG_ERROR,
               "Unexpected offset: expected %"PRIu64", got %"PRIu64"\n",
               off, s->off);
        err = AVERROR(EIO);
        goto done;
    }

Step-by-step trace for a request at off > 0 (e.g. -offset N, which maps directly onto s->off via the offset AVOption at line 215) against a server requiring authentication:

  1. http_connect() saves off = s->off (= N, line 1588) and resets s->off = 0 (line 1723) before reading the response.
  2. The server answers 401 with a WWW-Authenticate challenge. check_http_code() (lines 928-931) deliberately treats a first-time 401/407 as success ("regard 401 ... as a success, so we don't abort until all headers have been parsed"), so http_read_header() returns 0.
  3. A 401/407 response carries no Content-Range header, so parse_content_range() never runs and s->off stays 0.
  4. The restore at lines 1752-1753 only fires for redirects (s->new_location). For 401/407, off (N) != s->off (0), so lines 1755-1760 log "Unexpected offset" and return AVERROR(EIO).
  5. Back in http_open_cnx(), line 464 if (ret < 0) routes this into the reconnect/fail path. http_should_reconnect(s, AVERROR(EIO)) hits the default: case (lines 374-375) and returns s->reconnect_on_network_error (0 by default), so it is goto failreturn AVERROR(EIO).
  6. The auth retry block at lines 498-505 (and the 407 block at 506-513), which would close the connection and goto redo with the freshly parsed WWW-Authenticate/Proxy-Authenticate state, requires ret == 0 and is therefore never reached when the challenge arrived at off > 0.

Triggers:

  • ffmpeg -offset N -i http://user:pass@host/file (auth_type autodetect, the default) — the very first request is at off > 0 and fails with EIO instead of retrying with credentials.
  • The fork's partial-request continuation path: when request_size/initial_request_size is set, exhausting a partial response yields AVERROR(EAGAIN) and http_read_stream() re-opens via http_open_cnx() at the current offset (lines 1916-1926). If the server/CDN re-challenges at that point (expired token, node failover), playback aborts with EIO instead of re-authenticating.
  • Any new ranged request after a reconnect/seek against a server that re-challenges mid-session.

Note: at off == 0 the bug is masked because off != s->off is false, which is why normal authenticated playback works and this has gone unnoticed.

Impact

  • Authenticated HTTP(S) sources are completely unusable with a non-zero start offset: the open fails even though the client has valid credentials and the retry machinery exists specifically for this case.
  • Mid-stream re-authentication (CDN token rotation, digest nonce expiry on a new ranged request) aborts playback/ingest instead of transparently retrying — relevant for the fork's request_size/initial_request_size segmented-request feature, which issues many ranged requests per session.
  • Error reporting is degraded: callers receive a generic AVERROR(EIO) instead of AVERROR_HTTP_UNAUTHORIZED/AVERROR_HTTP_PROXY_AUTH_REQUIRED, which also defeats reconnect_on_http_error=4xx handling in http_should_reconnect() (the EIO falls through to the default: network-error case).

Severity: medium — no memory safety issue, but a hard functional failure of authentication on common configurations, with no workaround other than reconnect_on_network_error=1 (which retries blindly without ever attaching credentials, so it loops until the retry budget is exhausted).

Suggested fix

Treat auth-challenge responses like redirects in the offset restore, so http_connect() returns 0 for the tolerated first 401/407 and http_open_cnx() can run its retry logic. The original offset survives the retry because http_open_cnx() re-captures off = s->off at line 462 on each redo, and http_connect() re-saves it at line 1588:

--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1749,7 +1749,7 @@ static int http_connect(URLContext *h, const char *path, const char *local_path
     s->max_latency = FFMAX(s->max_latency, latency);
 
-    if (s->new_location)
+    if (s->new_location || s->http_code == 401 || s->http_code == 407)
         s->off = off;
 
     if (off != s->off) {

This is safe for the non-tolerated 401/407 cases (credentials already sent and rejected): there check_http_code() makes http_read_header() return an error, so http_connect() bails out at line 1745-1746 before the offset check is reached. With the fix, if the retry budget in http_open_cnx() is exhausted, the fail: path (lines 543-549) returns ff_http_averror(s->http_code, AVERROR(EIO)), i.e. the proper AVERROR_HTTP_UNAUTHORIZED/AVERROR_HTTP_PROXY_AUTH_REQUIRED, instead of generic EIO.

Upstream status

This bug is not fork-specific. Upstream FFmpeg (upstream/master, checked at commit 5f998e304dfd3fd6c0455447bffba45145ba027d) has the identical pattern in libavformat/http.c (offset reset at line 1659, redirect-only restore at lines 1688-1689, mismatch check at lines 1691-1697, and the same ret == 0 precondition on the 401/407 retry in http_open_cnx()). The fork's request_size/initial_request_size partial-request feature merely widens the exposure surface. Worth reporting upstream as well.


Consolidates duplicate findings from the review: avformat/http: offset-mismatch check in http_connect aborts 401/407 auth retries for any nonzero-offset request.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghttplibavformat/http.cupstreamAlso present in upstream FFmpeg

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions