Skip to content

core: keep the ring waist in bounds when a ring grows - #136

Open
munzzyy wants to merge 1 commit into
chimera-nas:mainfrom
munzzyy:iovec-ring-waist-resize
Open

core: keep the ring waist in bounds when a ring grows#136
munzzyy wants to merge 1 commit into
chimera-nas:mainfrom
munzzyy:iovec-ring-waist-resize

Conversation

@munzzyy

@munzzyy munzzyy commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

evpl_iovec_ring_resize() remaps head and tail into the freshly allocated array,
then does this to the waist:

    ring->head  = ring->size - 1;
    ring->waist = ((ring->waist + ring->size)  - ring->tail) - ring->size;
    ring->tail  = 0;

The + ring->size and the - ring->size cancel, so it is plain waist - tail
with no reduction. The sibling helper does the same remap correctly:

static inline uint64_t
evpl_iovec_ring_elements(const struct evpl_iovec_ring *ring)
{
    return ((ring->head + ring->size) - ring->tail) & ring->mask;
}

Resize only runs when the ring is full, so head is tail - 1 and the waist sits
somewhere between them. Where it has wrapped past zero and tail has not, its raw
index is below tail and the subtraction goes negative.

evpl_iovec_ring_waist() indexes the array with whatever came out:

    if (ring->waist == ring->head) {
        return NULL;
    } else {
        return &ring->iovec[ring->waist];
    }

so the caller gets a pointer in front of the block evpl_valloc() just returned.
evpl_rdmacm_flush_rdma_reads() and evpl_rdmacm_flush_datagram() both call it
and read through the result.

src/core/dgram_ring.h:74 carries the identical line, indexed the same way by
evpl_dgram_ring_waist(), and both rdmacm flush paths walk that ring too.

The fix is the & ring->mask the sibling already uses. mask still holds the
pre-resize size - 1 on that line; it is reassigned two lines further down,
after size. So the reduction is modulo the old size, which is the index space
the copy loop immediately above just remapped out of.

How wide is the bad range

Every tail/waist pair a full 8-slot ring can present, shipped expression against
the masked one:

ring size=8 mask=7

tail   head   waist  shipped    masked     note
1      0      0      -1         7          NEGATIVE INDEX
2      1      0      -2         6          NEGATIVE INDEX
2      1      1      -1         7          NEGATIVE INDEX
3      2      0      -3         5          NEGATIVE INDEX
3      2      1      -2         6          NEGATIVE INDEX
3      2      2      -1         7          NEGATIVE INDEX
4      3      0      -4         4          NEGATIVE INDEX
4      3      1      -3         5          NEGATIVE INDEX
4      3      2      -2         6          NEGATIVE INDEX
4      3      3      -1         7          NEGATIVE INDEX
5      4      0      -5         3          NEGATIVE INDEX
5      4      1      -4         4          NEGATIVE INDEX

pairs checked      : 64
disagreements      : 28
of those, negative : 28

28 of 64. Every disagreement is a negative index; there is no case where the
current expression is merely off by a slot.

Reproducer

Only the rdmacm paths advance a waist and I have no RDMA device here, so this
drives evpl_iovec_ring_add() and evpl_iovec_ring_resize() directly against a
Debug (ASan) build: fill the ring, drain most of it, refill so head wraps behind
tail, set the waist below tail, then add one more entry to force the grow.

before resize: size=8 mask=7 head=5 waist=1 tail=6 full=1
after  resize: size=16 mask=15 head=8 waist=-5 tail=0
ring base 0x7c42c5de0040, waist ptr 0x7c42c5ddffc8, delta -120 bytes
AddressSanitizer:DEADLYSIGNAL
=================================================================
==200831==ERROR: AddressSanitizer: SEGV on unknown address 0x7c42c5ddffd0
==200831==The signal is caused by a READ memory access.
    #0 in main oob_deref.c:61

SUMMARY: AddressSanitizer: SEGV oob_deref.c:61 in main
(libasan and libc frames trimmed)

Line 61 is waist->length. The waist should have come out at 3. With the patch:

before resize: size=8 mask=7 head=5 waist=1 tail=6 full=1
after  resize: size=16 mask=15 head=8 waist=3 tail=0
ring base 0x7cdb933e0040, waist ptr 0x7cdb933e0088, delta 72 bytes
waist->length = 64

Test

src/core/tests/iovec_ring_resize.c does the same setup and checks two things
across the grow: the waist lands inside the new array, and it still names the
entry it named before. On e27fb01:

waist -5 is outside a ring of 16 after resize

It builds against the internal header rather than a protocol because a waist
only moves in src/core/rdmacm/rdmacm.c, and putting it behind a protocol means
hardware in the loop.

Testing

Arch Linux, gcc 16.1.1. Both builds run under unshare -r -n because this box
has no CAP_NET_ADMIN for the netns wrapper, and http/basic and http/chunked
want port 80.

cd build/Debug   && unshare -r -n bash -c 'ip link set lo up; ctest --timeout 20'
100% tests passed out of 172

cd build/Release && unshare -r -n bash -c 'ip link set lo up; ctest --timeout 20'
100% tests passed out of 172

Both build clean under -Werror, zero warnings. uncrustify -c etc/uncrustify.cfg --check passes on the two headers and the new test, and reuse lint is still
compliant at 288/288.

Not tested on real RDMA hardware. I have none, so the read through the negative
index is shown above by driving the ring directly rather than by taking an RDMA
completion.

One thing I am unsure about: the test includes core/iovec_ring.h directly, which
nothing else under src/core/tests does. If you would rather this went in as a
conformance case or not at all, say so and I will drop it and leave the two-line
fix.

evpl_iovec_ring_resize() and evpl_dgram_ring_resize() move head and tail
into the new array. The waist they recompute as

    ((waist + size) - tail) - size

The added and the subtracted size cancel. That leaves waist - tail with
no reduction at all, so once the ring has wrapped far enough that waist
sits below tail it goes negative. evpl_iovec_ring_waist() then indexes
the array with it. The read lands in front of the allocation
evpl_valloc() just returned.

Mask it instead, the way evpl_iovec_ring_elements() already does for the
same remap. mask still holds the pre-resize size - 1 at that point. It
is reassigned two lines further down, so the reduction is modulo the old
size. That is the index space the copy loop above remapped out of.

Only the rdmacm flush paths advance a waist, so reaching this through a
protocol takes a device. The test drives the ring directly: fill, drain,
refill so head wraps behind tail, put the waist below tail, then add one
more entry to force the grow.
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.

1 participant