Skip to content

1.5.0 regression: SIGSEGV on MSVC/Windows the moment a WebSocket client connects (clean on 1.4.0) #36

Description

@binaryduke

Summary

1.5.0 segfaults on MSVC / Windows (IOCP) the moment a WebSocket client connects to a ws::server.

1.4.0 is clean with identical source. Reproduced with your own examples/15-WS.cpp + examples/16-WS-Client.cpp, unmodified, against a pristine git worktree of tag 1.5.0 (60290b4)git status clean, no local patches of any kind.

Flagging it because 1.5.0 is tagged stable and frozen, and — from #35 — you're testing on MinGW + Wine, so this may be invisible on your side. We have a real MSVC box and are happy to run anything you want against it.

Environment

MSVC 2022 (v19.x), x64, /std:c++20 /EHsc /GR /O2 /MT, -DNOMINMAX -DWIN32_LEAN_AND_MEAN. OpenSSL + zlib from vcpkg x64-windows-static.

Result

Pristine tag 1.5.0 (60290b4):

[t= 0 ms] server started at http://localhost:8000
[t= 511 ms] alive
[t= 1012 ms] alive
[t= 1513 ms] alive
[t= 2014 ms] alive
[t= 2280 ms] connected
SIGSEGV: Segmentation Violation
[t= 2280 ms] closed

Control — identical source, identical compiler and flags, only the nodepp include path changed — pristine tag 1.4.0 (e9b1866):

[t= 2073 ms] connected
ping from client                      <-- client -> server data arrives fine
[t= 8073 ms] closed
[t= 10002 ms] survived to 10s -- clean exit
tree WS client connects
1.4.0 (e9b1866) clean — bidirectional traffic, 10s, exit 0
1.5.0 (60290b4) SIGSEGV, immediately, every run

Layer isolation on 1.5.0: an HTTP client hitting a plain http::server is clean. The crash only appears once ws::server() is layered on. So the accept/socket path is healthy; it's the WS upgrade path.

Where we'd look — a suspect, not a diagnosis

1.5.0 changed onSocket from event_t<socket_t> to event_t<ptr_t<tcp_t>, socket_t> (tcp.h:44, tls.h:45), and ws::server() now does its work inside a deferred process::add(...) task that captures that self pointer:

skt.onSocket([=]( ptr_t<tcp_t> self, socket_t raw ){
    http_t hrv = raw;
    if( !generator::ws::server( hrv ) ){ self->onConnect.skip(); return; }
    ws_t cli = raw;
    process::add([=](){
        cli.set_timeout(0); cli.resume();
        cli.set_mask(0);
        self->onConnect.resume();
        self->onConnect.emit(cli);   // <-- self used after onSocket has already returned
        stream::pipe(cli);
    return -1; });
});

The crash lands right at onConnect. self being dereferenced from the deferred task, after the onSocket handler has returned, is our first suspect — but we have no debugger on this box and have not confirmed it. Treat it as a pointer, not a conclusion.

One trap worth knowing about

nodepp's own SIGSEGV handler (signal.h:36) prints and then calls process::exit(0)so a crashed process still exits with status 0. Any CI or test harness grading on exit code will score a segfault as a pass. It briefly fooled us. Grade on the log line.

Repro

Your examples/15-WS.cpp and examples/16-WS-Client.cpp, split into two processes so the client is genuinely separate, with a heartbeat and a timed exit added so the run terminates. Nothing else changed.

server (your 15-WS.cpp)
#include <nodepp/nodepp.h>
#include <nodepp/timer.h>
#include <nodepp/http.h>
#include <nodepp/ws.h>

using namespace nodepp;

static ulong T0 = 0;
static void mark( const char* w ){ console::log("[t=",(ulong)(process::millis()-T0),"ms]",string_t(w)); }

void onMain() {
    T0 = process::millis();

    auto server = http::server([=]( http_t cli ){
        cli.write_header( 200, header_t({ { "Content-Security-Policy", "*" } }) );
        cli.write("Hello World!");
    }); ws::server( server );

    server.onConnect([=]( ws_t cli ){
        mark("connected");
        cli.onData([=]( string_t data ){ cli.write( "<: received" ); console::log( data ); });
        cli.onClose([=](){ mark("closed"); });
    });

    server.onError([=]( except_t err ){ console::log( ">>", err.what() ); });

    server.listen( "localhost", 8000, [=]( socket_t server ){
        mark("server started at http://localhost:8000");
    });

    timer::interval( [](){ mark("alive"); }, 500 );
    timer::timeout ( [](){ mark("survived to 10s -- clean exit"); process::exit(0); }, 10000 );
}
client (your 16-WS-Client.cpp)
#include <nodepp/nodepp.h>
#include <nodepp/timer.h>
#include <nodepp/ws.h>

using namespace nodepp;

void onMain() {
    auto client = ws::client( "ws://localhost:8000/" );

    client.onConnect([=]( ws_t cli ){
        console::log("connected" );
        cli.onData([]( string_t data ){ console::log( data ); });
        cli.onClose([=](){ console::log("closed"); });
        timer::timeout([=](){ ws_t c = cli; c.write("ping from client"); }, 500 );
    });

    client.onError([=]( except_t err ){ console::log( "<>", err.data() ); });
    timer::timeout( [](){ console::log("client done"); process::exit(0); }, 6000 );
}

Start the server, then run the client.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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