Skip to content

IoUring: generic FileRegion drain loop calls transferTo() past transferred() == count() #16825

@LuciferYang

Description

@LuciferYang

Netty version

4.2.13.Final (and current 4.2 branch)

Expected behavior

FileRegion#transferTo() is not invoked once the region reports transferred() == count(). epoll's AbstractEpollStreamChannel#writeFileRegion honors this:

if (region.transferred() >= region.count()) {
    in.remove();
    return 0;
}

https://github.com/netty/netty/blob/netty-4.2.13.Final/transport-classes-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java#L403-L406

so it never calls transferTo() past completion.

Actual behavior

The generic FileRegion fallback added in #16571 drains the region into a direct ByteBuf chunk and exits the drain loop only when the chunk buffer has no more writable bytes:

while (buf.writableBytes() > 0) {
    long t = region.transferTo(ch, region.transferred());
    if (t <= 0) {
        break;
    }
}

https://github.com/netty/netty/blob/netty-4.2.13.Final/transport-classes-io_uring/src/main/java/io/netty/channel/uring/AbstractIoUringStreamChannel.java#L344-L349

There is no check on region.transferred() < region.count(). If a single transferTo() advances transferred to count while leaving spare capacity in the chunk buffer, the loop calls transferTo() again past transferred() == count().

Why it matters

The FileRegion contract lets implementations assume transferTo() is not invoked once the region reports completion. Adapters that wrap an inner source with per-chunk framing — encryption, compression, length-prefixed framing, MACs, etc. — lazily build the next chunk on each transferTo() call. When the loop invokes them again after the region has been fully transferred, they build an empty next chunk and ship its framing into the buffer, which then lands on the wire and breaks the receiver's parsing of subsequent frames.

The same FileRegion patterns work without surprises on NIO and epoll, and the same kind of fallback exists on epoll for the same purpose, but only io_uring exhibits the overshoot.

Reproducer

A FileRegion that writes a single byte on its first transferTo() call and advances transferred() to count() in one shot (a legal pattern for any layer that flushes a completed inner chunk in one call) receives chunkSize - 1 additional transferTo() invocations from the io_uring transport before the chunk buffer fills — for the default 64 KiB chunk size and a 16-byte region the extra calls are 15. None of those are licensed by the contract.

A regression test and a one-line fix mirroring epoll's guard are in the accompanying PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions