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.

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