Skip to content

Handle empty Windows event sets without calling WSAPoll #793

Description

@LunaStev

Problem

A newly created Windows event set, or one after its last registration is removed, passes a zero count to WSAPoll. The portable event API should return no events for a zero-timeout wait. Instead, this path can return a Winsock error even after Winsock initialization.

Microsoft's WSAPoll contract requires at least one valid socket and describes WSAEINVAL when none is valid. Starting points: std/sys/windows/event.wave, std/sys/windows/socket.wave, std/net/event.wave.

Reproduction

Audited at 4c3372157d8889fa8bbef0a7ccd2e72aab1c5c6c with LLVM 21.1.8. An executed Linux-hosted boundary mock retained the event provider and its poll/error-forwarding bodies, adapted only the two Winsock declarations to C ABI, and supplied allocation and OS-call mocks. At O0/O2:

new set:       WSAPoll(count=0, timeout=0) -> event_wait returns -10022
one entry:     WSAPoll(count=1, timeout=0) -> event_wait returns 0
entry removed: WSAPoll(count=0, timeout=0) -> event_wait returns -10022

The mock returns SOCKET_ERROR/WSAEINVAL for zero entries; this confirms forwarding, not native Windows execution. An empty Linux epoll control returned 0. The following native regression compiles to objects for both Windows amd64 and ARM64, but native execution remains to be done.

Prepare an alias of the matching checkout std from the repository root (this avoids using an older installed std):

Save this setup as prepare_std.py and run python prepare_std.py:

from pathlib import Path
out = Path(".tmp/additional-audit-std")
for source in Path("std").rglob("*.wave"):
    dest = out / source.relative_to("std")
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.write_text(source.read_text().replace('"std::', '"checkout_std::'))

Save as empty_event.wave:

import("checkout_std::sys::event")::{NativeEvent, event_create, event_wait, event_close};
import("checkout_std::sys::windows::socket")::{socket_runtime_init};
fun main() -> i32 {
    if (socket_runtime_init() < 0) { return 2; }
    var handle: i64 = event_create(1);
    if (handle < 0) { return 3; }
    var output: NativeEvent;
    var result: i64 = event_wait(handle, &output, 1, 0);
    event_close(handle);
    if (result == 0) { return 0; }
    return 1;
}

On a Windows host with the checkout-built compiler and its linker prerequisites:

target/debug/wavec.exe build empty_event.wave --dep checkout_std=.tmp/additional-audit-std --run -O0 --out-dir .tmp/empty-event-run

Expected exit 0. Repeat at O2 and after removing the last registration.

Scope

Handle the empty-set wait before invoking Winsock. Preserve zero, finite and infinite timeout semantics without introducing a busy loop. Keep this synchronous readiness provider separate from IOCP. #530 covers result fairness for nonempty sets, not this invalid native call.

Difficulty: medium.

Completion criteria

  • New and emptied sets return zero immediately for timeout 0.
  • Finite empty waits honor the timeout; infinite waits use a blocking path without spinning or an invalid WSAPoll call.
  • Boundary tests assert that WSAPoll is not called with zero entries; native amd64/ARM64 tests cover empty and nonempty sets with bounded test timeouts.

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

    bugA problem that causes incorrect behavior or crashes.help wantedThe issue requires extra attention or help from others.needs testingIssues that require additional testing or verification.os-specific bugA bug or issue that occurs only on a specific operating system.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions