Skip to content

avformat/shared: batch of low-severity verified findings (SEEK_END stale inner_pos, lock-free atomics assert, block_shift=30 pathologies, get_file_handle semantics, http Location carve-out) #32

Description

@ronag

Summary

Batch of five low-severity, code-verified findings from the second-pass review. Filed together to keep the tracker focused; split out any that get scheduled.

shared_seek SEEK_END: when set_filesize fails after a successful inner seek, s->inner_pos is left stale — a later cache miss can skip the corrective inner seek and cache wrong-offset data under a valid CRC

Location: libavformat/shared.c:825-838 (failure return at 835-836 without updating inner_pos), read-side trust at 686 and 738 — severity: low

In the SEEK_END branch taken when the spacemap filesize is unknown, the code physically seeks the inner protocol (res = ffurl_seek(s->inner, pos, whence), 831) and only afterwards calls set_filesize. If set_filesize fails (another process concurrently published a different size — the exact race window the set-once function exists for), it returns AVERROR(EINVAL) at 836 WITHOUT updating s->inner_pos, even though the inner protocol is now positioned at res. s->inner_pos is the sole guard deciding whether shared_read re-seeks the inner stream (686: if (s->inner_pos != inner_pos)), and the owner fetch path then asserts and trusts it (738: av_assert0(inner_pos == block_pos)). If the stale s->inner_pos happens to equal the next miss's target (e.g. both 0 right after open), the seek is skipped, ffurl_read pulls bytes from offset res instead, and the block is cached and published with a perfectly valid CRC over wrong-offset data — silent, persistent, shared corruption. Reachability is narrow with an http inner (http_seek_internal returns ENOSYS for SEEK_END when its filesize is unknown, http.c:2165-2166, and when http knows the size shared_open normally already populated the spacemap via ffurl_size), so this needs a non-http inner supporting SEEK_END without AVSEEK_SIZE, plus the concurrent size-publish race — hence low severity despite the silent-corruption outcome.

Evidence: shared.c:830-838 (verified):

res = ffurl_seek(s->inner, pos, whence);
if (res < 0)
    return res;
/* Opportunistically update known filesize */
if (set_filesize(h, res - pos) < 0)
    return AVERROR(EINVAL);      /* inner moved, s->inner_pos NOT updated */
av_log(h, AV_LOG_DEBUG, "Inner seek to 0x%"PRIx64"\n", res);
return s->pos = s->inner_pos = res;

shared.c:686-705 skips the corrective seek whenever s->inner_pos == inner_pos; 738 av_assert0(inner_pos == block_pos) then the fetch loop caches whatever the inner connection returns and publishes the CRC at 789. http.c:2165-2166 confirms the http-inner case usually errors out before the hazard.

Fix: Update s->inner_pos as soon as the inner seek succeeds, before any failure return: s->inner_pos = res; if (set_filesize(h, res - pos) < 0) return AVERROR(EINVAL); return s->pos = res;. Audit any other site where ffurl_seek(s->inner, ...) succeeds but the function then errors out.

No compile-time guarantee that the mmap'd cross-process atomics are lock-free/address-free; a non-lock-free fallback silently voids all synchronization

Location: libavformat/shared.c:106-119 (Block.state atomic_ushort, Spacemap.filesize atomic_ullong, hash atomic_uchar[32]) — severity: low

The protocol's correctness rests on C11 atomics in MAP_SHARED memory operating across processes. C11 (7.17.5) only guarantees address-free operation — the property required for cross-process atomics — for lock-free atomics. If the target's atomic_ullong (the 64-bit filesize) is not lock-free, the compiler lowers it to libatomic calls serializing through a PROCESS-LOCAL lock table: two processes then mutate filesize with no mutual exclusion or ordering at all (torn 64-bit reads possible), and the set-once CAS protocol plus the release(filesize)->release(state)/acquire(state) publication chain that tail-block clamping depends on silently stop working. Real on 32-bit targets without 64-bit CAS (older ARM, some MIPS/PPC configs FFmpeg supports) and invisible at runtime. No static assert or configure check exists anywhere in the file.

Evidence: shared.c:110-119:

typedef struct Spacemap {
    atomic_uint header_magic;
    atomic_ushort version;
    atomic_ushort block_shift;
    atomic_ullong filesize; /* byte offset of true EOF, or 0 if unknown */
    atomic_uchar hash[HASH_SIZE];
    ...

plus atomic_ushort state in Block (107), all accessed by multiple processes via the MAP_SHARED mappings created at shared.c:363/421. No occurrence of ATOMIC_*_LOCK_FREE, atomic_is_lock_free, or any static assertion in the file (verified by full read).

Fix: Enforce the precondition at compile time next to the struct definitions:

_Static_assert(ATOMIC_SHORT_LOCK_FREE == 2 && ATOMIC_INT_LOCK_FREE == 2 &&
               ATOMIC_CHAR_LOCK_FREE == 2 && ATOMIC_LLONG_LOCK_FREE == 2,
               "shared: requires address-free (lock-free) atomics for cross-process use");

or compile the protocol out via a configure dependency when ATOMIC_LLONG_LOCK_FREE != 2.

max_packet_size/min_packet_size/short_seek all pinned to block_size: block_shift=30 (allowed) forces 1 GiB AVIOContext buffers and turns forward seeks below ~2*block_size into full sequential downloads; block_shift=9 forces 512-byte I/O

Location: libavformat/shared.c:327-328, 856-861, 868 (option range 9..30, also accepted from foreign spacemaps via 502-505); cross: avio.c:422-435, aviobuf.c:269-289 — severity: low

shared_open sets h->max_packet_size = h->min_packet_size = s->block_size (327-328). ffio_fdopen sizes the AVIOContext buffer to max_packet_size (avio.c:424-426: buffer_size = max_packet_size; /* no need to bufferize more than one packet */): at -block_shift 30 that is a 1 GiB av_malloc per opened input, fill_buffer asks shared_read for the full block, and shared_read fetches the entire block from http before returning the first byte (739-763) — multi-second first-byte latency, plus a second 1 GiB tmp_buf in non-mmap fallback (320). The block_shift can also be imposed by a foreign spacemap (502-505 accepts 9..30). Additionally shared_get_short_seek returns at least block_size (856-861) and avio_seek honors it via short_seek_get (aviobuf.c:270-272 FFMAX), so any forward seek with offset1 <= buffer_size + short_seek (up to ~2*block_size) is satisfied by looping fill_buffer (aviobuf.c:281-286) — downloading and caching up to a full block (or far more at large shifts) of unwanted data per seek. Each cold whole-block fetch also lengthens the PENDING window, magnifying the #15 timeout-race pathologies. At the other extreme, block_shift=9 makes the avio buffer 512 bytes: one shared_read (CRC over the block + atomics) per 512 bytes demuxed.

Evidence: shared.c:327-328: h->max_packet_size = s->block_size; h->min_packet_size = s->block_size;. avio.c:424-426 (verified): max_packet_size = h->max_packet_size; if (max_packet_size) { buffer_size = max_packet_size; }. shared.c:860: return ret > 0 ? FFMAX(ret, s->block_size) : s->block_size;. aviobuf.c:269-272 (verified): short_seek = ctx->short_seek_threshold; if (ctx->short_seek_get) { int tmp = ctx->short_seek_get(s->opaque); short_seek = FFMAX(tmp, short_seek); }; 281-286: forward seeks with offset1 <= buffer_size + short_seek loop fill_buffer instead of seeking.

Fix: Decouple the avio buffer from the cache block: shared_read already serves arbitrary sizes and partial blocks, so cap or drop max_packet_size (e.g. FFMIN(s->block_size, 1<<20) or 0) and stop setting min_packet_size (read-side it is only consumed by avio_write_marker). Cap shared_get_short_seek similarly (e.g. FFMIN(block_size, 256<<10)). If whole-block avio requests are desired, narrow the option range (e.g. 12..22).

shared_get_file_handle exposes the inner http socket fd although shared serves data from the cache: poll/select semantics wrong by construction

Location: libavformat/shared.c:850-854, registered at 889; contract: url.h url_get_file_handle docs; http.c:2245-2249 — severity: low

shared_get_file_handle forwards to ffurl_get_file_handle(s->inner) — for http that is the current TCP socket (http.c:2245-2249). url_get_file_handle exists so callers can poll for readability; for shared that fd is meaningless: cache-hit reads are satisfied from mmap with zero socket activity (a poller waits forever on a fully-cached file, or gets a spurious POLLHUP when the idle server closes), and conversely the socket may hold data from a previous range request that shared_read will discard via seek/reconnect. http also replaces the socket on every backward seek (http_seek_internal reconnect, 2219-2236), invalidating any fd a caller cached. In-tree consumers of ffurl_get_file_handle are protocol wrappers (rtsp/rtpproto/sapdec) unlikely to sit above shared, so the blast radius today is url-layer/out-of-tree users — but the callback as implemented can only mislead them.

Evidence: shared.c:850-854:

static int shared_get_file_handle(URLContext *h)
{
    SharedContext *s = h->priv_data;
    return ffurl_get_file_handle(s->inner);
}

registered in ff_shared_protocol at 889. http.c:2245-2249 returns ffurl_get_file_handle(s->hd) — the live socket of the CURRENT connection.

Fix: Remove .url_get_file_handle from ff_shared_protocol (ffurl_get_file_handle then returns the documented 'not available' signal). Revisit with proper multi-handle semantics only if nonblocking support is ever implemented.

http_connect's new_location carve-out bypasses the 200-vs-206 offset check: a non-3xx response carrying a Location header lets offset-0 data be cached at block_pos with a valid CRC

Location: libavformat/http.c:1752-1761 (carve-out + offset check), 1309-1311 (parse_location on ANY response), 515-516 (redirect codes consumed elsewhere); amplification shared.c:737-789 — severity: low

http.c does detect range-ignoring servers: after the response headers, if (off != s->off) fails the request with EIO (1755-1760), because s->off is reset to 0 per request (1723) and only restored by a Content-Range header (959). That guard protects shared.c's core assumption that a successful seek+read yields bytes from block_pos. However, if (s->new_location) s->off = off; (1752-1753) forcibly satisfies the check whenever the response carries a Location header. That is correct for 3xx (the redirect branch at 515-540 follows it and the body is irrelevant), but parse_location sets new_location for ANY response containing Location (1309-1311) — a 200 with a Location header (emitted by some app servers/CDN edges as canonicalization metadata) passes the check, http_open_cnx returns success, and the body is the full entity from offset 0. shared.c then caches offset-0 bytes as block block_pos with a valid CRC (737-789): silent, persistent shared-cache corruption for all consumers, concentrated at whichever offsets get seeked to first (head/tail probe blocks).

Evidence: http.c:1752-1761 (verified):

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;
}

parse_location (939-948) sets new_location unconditionally on a Location header; the redirect-following branch consumes it only for 301/302/303/307/308 (http.c:515-516, verified). shared.c:737-789: owner fetch trusts the successful open and publishes the CRC.

Fix: Restrict the carve-out to actual redirects: if (s->new_location && s->http_code >= 300 && s->http_code < 400) s->off = off; so a 200+Location with the wrong offset still fails the off != s->off check.


Found by second-pass multi-lens review with adversarial verification.

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

    prio:lowsharedshared: block cache protocol (libavformat/shared.c)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions