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():
- Offset restore (redirects only) and mismatch check,
http_connect():
|
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():
|
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():
|
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:
http_connect() saves off = s->off (= N, line 1588) and resets s->off = 0 (line 1723) before reading the response.
- 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.
- A 401/407 response carries no
Content-Range header, so parse_content_range() never runs and s->off stays 0.
- 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).
- 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 fail → return AVERROR(EIO).
- 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.
Summary
When an HTTP server (or proxy) answers a request made at a non-zero offset with a
401 Unauthorized(or407 Proxy Authentication Required) challenge,http_connect()'s offset-mismatch check converts the response into a genericAVERROR(EIO)beforehttp_open_cnx()ever reaches its 401/407 retry logic. Authentication negotiation therefore fails hard whenever the first challenge arrives atoff > 0— e.g. opening with the-offsetoption, the partial-request continuation path (request_size/initial_request_sizere-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 bareEIO.Location
http_connect():FFmpeg/libavformat/http.c
Line 1723 in 9a83bff
http_connect():FFmpeg/libavformat/http.c
Lines 1752 to 1761 in 9a83bff
http_open_cnx():FFmpeg/libavformat/http.c
Lines 497 to 513 in 9a83bff
check_http_code():FFmpeg/libavformat/http.c
Lines 923 to 937 in 9a83bff
Details
http_connect()captures the requested offset and resetss->offbefore parsing the response, then restores it only for redirect responses:Step-by-step trace for a request at
off > 0(e.g.-offset N, which maps directly ontos->offvia theoffsetAVOption at line 215) against a server requiring authentication:http_connect()savesoff = s->off(= N, line 1588) and resetss->off = 0(line 1723) before reading the response.401with aWWW-Authenticatechallenge.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"), sohttp_read_header()returns 0.Content-Rangeheader, soparse_content_range()never runs ands->offstays 0.s->new_location). For 401/407,off(N) !=s->off(0), so lines 1755-1760 log "Unexpected offset" and returnAVERROR(EIO).http_open_cnx(), line 464if (ret < 0)routes this into the reconnect/fail path.http_should_reconnect(s, AVERROR(EIO))hits thedefault:case (lines 374-375) and returnss->reconnect_on_network_error(0 by default), so it isgoto fail→return AVERROR(EIO).goto redowith the freshly parsedWWW-Authenticate/Proxy-Authenticatestate, requiresret == 0and is therefore never reached when the challenge arrived atoff > 0.Triggers:
ffmpeg -offset N -i http://user:pass@host/file(auth_type autodetect, the default) — the very first request is atoff > 0and fails with EIO instead of retrying with credentials.request_size/initial_request_sizeis set, exhausting a partial response yieldsAVERROR(EAGAIN)andhttp_read_stream()re-opens viahttp_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.Note: at
off == 0the bug is masked becauseoff != s->offis false, which is why normal authenticated playback works and this has gone unnoticed.Impact
request_size/initial_request_sizesegmented-request feature, which issues many ranged requests per session.AVERROR(EIO)instead ofAVERROR_HTTP_UNAUTHORIZED/AVERROR_HTTP_PROXY_AUTH_REQUIRED, which also defeatsreconnect_on_http_error=4xxhandling inhttp_should_reconnect()(the EIO falls through to thedefault: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 andhttp_open_cnx()can run its retry logic. The original offset survives the retry becausehttp_open_cnx()re-capturesoff = s->offat line 462 on eachredo, andhttp_connect()re-saves it at line 1588:This is safe for the non-tolerated 401/407 cases (credentials already sent and rejected): there
check_http_code()makeshttp_read_header()return an error, sohttp_connect()bails out at line 1745-1746 before the offset check is reached. With the fix, if the retry budget inhttp_open_cnx()is exhausted, thefail:path (lines 543-549) returnsff_http_averror(s->http_code, AVERROR(EIO)), i.e. the properAVERROR_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 commit5f998e304dfd3fd6c0455447bffba45145ba027d) has the identical pattern inlibavformat/http.c(offset reset at line 1659, redirect-only restore at lines 1688-1689, mismatch check at lines 1691-1697, and the sameret == 0precondition on the 401/407 retry inhttp_open_cnx()). The fork'srequest_size/initial_request_sizepartial-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.