buffer overflow
WSASend
asynchronous programming
non-blocking I/O
network security

How to avoid buffer overflow on asynchronous non-blocking WSASend calls

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

With asynchronous WSASend, the biggest risk is usually not a classic stack smash. It is sending from memory that gets reused, freed, or overwritten before the overlapped send actually completes. In other words, the safe design is about buffer ownership, queue limits, and completion handling.

What WSASend Guarantees and What It Does Not

When you call WSASend with overlapped I/O, the function may return before the data is physically transmitted. That means the memory referenced by your WSABUF must remain valid until the send completes.

This is the core rule:

  • do not modify the send buffer until completion
  • do not free the send buffer until completion
  • do not reuse the OVERLAPPED structure until completion

If you violate that rule, the socket layer may read corrupted or unrelated bytes later.

The Real Fix: Own the Buffer Per Pending Send

A common safe pattern is to allocate one send object per pending operation.

cpp
1#include <winsock2.h>
2#include <mswsock.h>
3#include <vector>
4
5struct PendingSend {
6    OVERLAPPED ov{};
7    WSABUF buf{};
8    std::vector<char> storage;
9
10    explicit PendingSend(const char* data, size_t len) : storage(data, data + len) {
11        buf.buf = storage.data();
12        buf.len = static_cast<ULONG>(storage.size());
13    }
14};

Here the std::vector<char> owns the bytes. As long as the PendingSend object stays alive, the buffer stays valid.

Queue Outbound Data Instead of Sending From Temporary Memory

In non-blocking code, it is tempting to do this:

cpp
1char tmp[1024];
2// fill tmp
3WSABUF b{1024, tmp};
4WSASend(sock, &b, 1, nullptr, 0, &ov, nullptr);

That is unsafe if tmp goes out of scope or gets reused before completion.

A better approach is a per-connection send queue.

cpp
1#include <deque>
2#include <memory>
3
4std::deque<std::shared_ptr<PendingSend>> sendQueue;

Push each outbound message into the queue and pop it only after completion is reported.

Handle Partial Sends and Completion Correctly

Even asynchronous APIs can complete with fewer bytes than expected in some designs. Your completion path must inspect the byte count and decide whether another send is needed for the remaining bytes.

That means your send object should track progress, not just raw storage.

Limit Queue Growth

Avoiding corruption is only half the story. If the peer is slow and you keep enqueuing data forever, memory usage can explode.

Use backpressure:

  • cap the number of pending bytes per connection
  • stop reading or producing data when the queue is too large
  • disconnect misbehaving peers if necessary

That is how you avoid a memory exhaustion bug that feels like a buffer problem at runtime.

Example Send Submission

cpp
1int submitSend(SOCKET s, std::shared_ptr<PendingSend> ps) {
2    DWORD sent = 0;
3    DWORD flags = 0;
4    int rc = WSASend(s, &ps->buf, 1, &sent, flags, &ps->ov, nullptr);
5
6    if (rc == 0) {
7        return 0; // completed immediately
8    }
9
10    int err = WSAGetLastError();
11    if (err == WSA_IO_PENDING) {
12        return 0; // completion will arrive later
13    }
14
15    return err;
16}

The important part is that ps must stay alive after this call if completion is pending.

Completion Ports Make Ownership Easier

If you use I/O completion ports, keep a pointer to the send context and release it only after the completion callback or worker thread processes the finished send.

That gives you a clean ownership lifecycle:

  1. allocate send context
  2. submit WSASend
  3. wait for completion
  4. release send context

Common Pitfalls

A common mistake is passing a pointer to stack memory or to a mutable shared buffer and then reusing it immediately after calling WSASend.

Another mistake is reusing the same OVERLAPPED structure for multiple in-flight sends on the same socket.

Developers also often forget backpressure. Even if every buffer is technically safe, an unbounded queue can still exhaust memory under load.

Summary

  • The send buffer for overlapped WSASend must remain valid until completion.
  • Give each pending send its own owned buffer and OVERLAPPED state.
  • Use a per-connection send queue instead of temporary memory.
  • Apply backpressure so slow peers cannot grow memory without limit.
  • Think in terms of buffer ownership and completion lifecycle, not just API syntax.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.