aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-07-30 19:22:17 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-07-30 22:06:04 +0200
commit73b0780321acb14bab6707006f5136ea9998951d (patch)
tree30c5d0cff89913185df7f5fb7ebe1636827cfd58 /src
parente13de2013749961edaa126697f3290d3dca91823 (diff)
downloadluasocket-73b0780321acb14bab6707006f5136ea9998951d.tar.gz
luasocket-73b0780321acb14bab6707006f5136ea9998951d.tar.bz2
luasocket-73b0780321acb14bab6707006f5136ea9998951d.zip
feat(receive): add maxsize argument to bound memory usage
client:receive("*l") and receive("*a") are unbounded: a peer that never sends a newline, or never closes, makes LuaSocket buffer until the process runs out of memory (e.g. src/http.lua reading response headers in a loop). Add an optional maxsize argument that caps the payload a single call may accumulate, including prefix. - Hoist all argument validation ahead of timeout_markstart() so bad calls (maxsize < 1, #prefix >= maxsize, numeric pattern > maxsize) raise before any I/O and leave the socket untouched. - recvline/recvall take a budget and return a new internal BUF_OVERSIZED code, surfaced to Lua as the "oversized" error alongside "timeout"/"closed", with the partial held in the 3rd return value. - recvraw is left untouched: argument checks make the cap unreachable for numeric patterns. - Preserve three invariants: a timeout partial is always shorter than maxsize (safe to retry as prefix), completion beats the cap for *a, and no bytes are lost or skipped on overflow. - tcp.c, unixstream.c and serial.c all share this code path unchanged. Adds test coverage (argument errors, *l/*a boundaries, timeout/close at the cap, the drain idiom, numeric patterns, unix-stream mirror) and documents the new argument, error, and recovery idioms in docs/tcp.html.
Diffstat (limited to 'src')
-rw-r--r--src/buffer.c108
1 files changed, 88 insertions, 20 deletions
diff --git a/src/buffer.c b/src/buffer.c
index 7148be3..05d6fb3 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -9,12 +9,24 @@
9* Internal function prototypes 9* Internal function prototypes
10\*=========================================================================*/ 10\*=========================================================================*/
11static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b); 11static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b);
12static int recvline(p_buffer buf, luaL_Buffer *b); 12static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget);
13static int recvall(p_buffer buf, luaL_Buffer *b); 13static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget);
14static int buffer_get(p_buffer buf, const char **data, size_t *count); 14static int buffer_get(p_buffer buf, const char **data, size_t *count);
15static void buffer_skip(p_buffer buf, size_t count); 15static void buffer_skip(p_buffer buf, size_t count);
16static int sendraw(p_buffer buf, const char *data, size_t count, size_t *sent); 16static int sendraw(p_buffer buf, const char *data, size_t count, size_t *sent);
17 17
18/* Internal completion code for buffer_meth_receive. err is not confined to
19 * the IO_* enum: socket_recv/socket_send (usocket.c/wsocket.c) propagate raw
20 * platform errors (POSIX errno, Windows WSA codes) straight through, and
21 * those are always positive, so a positive sentinel here could collide with
22 * a genuine transport error (e.g. errno 1 == EPERM) and get misreported as
23 * "oversized". Chosen negative and outside {IO_DONE, IO_TIMEOUT, IO_CLOSED,
24 * IO_UNKNOWN} (0, -1, -2, -3) so it can never collide with anything err
25 * legitimately takes.
26 * MUST be handled before buf->io->error() is called -- it is not a transport
27 * error. */
28#define BUF_OVERSIZED (-1000)
29
18/* min and max macros */ 30/* min and max macros */
19#ifndef MIN 31#ifndef MIN
20#define MIN(x, y) ((x) < (y) ? x : y) 32#define MIN(x, y) ((x) < (y) ? x : y)
@@ -105,8 +117,33 @@ int buffer_meth_send(lua_State *L, p_buffer buf) {
105int buffer_meth_receive(lua_State *L, p_buffer buf) { 117int buffer_meth_receive(lua_State *L, p_buffer buf) {
106 int err = IO_DONE, top; 118 int err = IO_DONE, top;
107 luaL_Buffer b; 119 luaL_Buffer b;
108 size_t size; 120 size_t size, wanted = 0, maxsize = 0;
121 size_t budget = 0; /* 0 == unlimited */
122 int numeric = lua_isnumber(L, 2);
109 const char *part = luaL_optlstring(L, 3, "", &size); 123 const char *part = luaL_optlstring(L, 3, "", &size);
124
125 /* ---- validation: must precede timeout_markstart() and any I/O ---- */
126 if (numeric) {
127 double n = lua_tonumber(L, 2);
128 luaL_argcheck(L, n >= 0, 2, "invalid receive pattern");
129 wanted = (size_t) n;
130 } else {
131 const char *p = luaL_optstring(L, 2, "*l");
132 luaL_argcheck(L, p[0] == '*' && (p[1] == 'l' || p[1] == 'a'),
133 2, "invalid receive pattern");
134 }
135 if (!lua_isnoneornil(L, 4)) {
136 double m = luaL_checknumber(L, 4);
137 luaL_argcheck(L, m >= 1, 4, "maxsize must be a positive number");
138 maxsize = (size_t) m;
139 luaL_argcheck(L, size < maxsize, 4,
140 "prefix length >= maxsize (drain with prefix=\"\" or raise maxsize)");
141 if (numeric)
142 luaL_argcheck(L, wanted <= maxsize, 4,
143 "maxsize smaller than requested byte count");
144 budget = maxsize - size;
145 }
146
110 timeout_markstart(buf->tm); 147 timeout_markstart(buf->tm);
111 /* make sure we don't confuse buffer stuff with arguments */ 148 /* make sure we don't confuse buffer stuff with arguments */
112 lua_settop(L, 3); 149 lua_settop(L, 3);
@@ -116,24 +153,28 @@ int buffer_meth_receive(lua_State *L, p_buffer buf) {
116 luaL_buffinit(L, &b); 153 luaL_buffinit(L, &b);
117 luaL_addlstring(&b, part, size); 154 luaL_addlstring(&b, part, size);
118 /* receive new patterns */ 155 /* receive new patterns */
119 if (!lua_isnumber(L, 2)) { 156 if (!numeric) {
120 const char *p= luaL_optstring(L, 2, "*l"); 157 const char *p= luaL_optstring(L, 2, "*l");
121 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b); 158 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b, budget);
122 else if (p[0] == '*' && p[1] == 'a') err = recvall(buf, &b); 159 else err = recvall(buf, &b, budget);
123 else luaL_argcheck(L, 0, 2, "invalid receive pattern");
124 /* get a fixed number of bytes (minus what was already partially 160 /* get a fixed number of bytes (minus what was already partially
125 * received) */ 161 * received) */
126 } else { 162 } else {
127 double n = lua_tonumber(L, 2);
128 size_t wanted = (size_t) n;
129 luaL_argcheck(L, n >= 0, 2, "invalid receive pattern");
130 if (size == 0 || wanted > size) 163 if (size == 0 || wanted > size)
131 err = recvraw(buf, wanted-size, &b); 164 err = recvraw(buf, wanted-size, &b);
132 } 165 }
133 /* check if there was an error */ 166 /* check if there was an error */
134 if (err != IO_DONE) { 167 /* luaL_pushresult(&b) must come first (its accumulator lives on the
135 /* we can't push anyting in the stack before pushing the 168 * stack), but the partial it produces belongs in slot 3, not 1 -- so
136 * contents of the buffer. this is the reason for the complication */ 169 * both error branches push buffer/error/buffer-copy/nil, then
170 * lua_replace the nil into slot 1. */
171 if (err == BUF_OVERSIZED) {
172 luaL_pushresult(&b);
173 lua_pushliteral(L, "oversized");
174 lua_pushvalue(L, -2);
175 lua_pushnil(L);
176 lua_replace(L, -4);
177 } else if (err != IO_DONE) {
137 luaL_pushresult(&b); 178 luaL_pushresult(&b);
138 lua_pushstring(L, buf->io->error(buf->io->ctx, err)); 179 lua_pushstring(L, buf->io->error(buf->io->ctx, err));
139 lua_pushvalue(L, -2); 180 lua_pushvalue(L, -2);
@@ -201,36 +242,61 @@ static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b) {
201 242
202/*-------------------------------------------------------------------------*\ 243/*-------------------------------------------------------------------------*\
203* Reads everything until the connection is closed (buffered) 244* Reads everything until the connection is closed (buffered)
245* budget == 0 means unlimited; otherwise the number of payload bytes still
246* allowed. Completion (connection closed) beats the cap: filling the cap
247* exactly and then seeing EOF means the whole stream was received.
204\*-------------------------------------------------------------------------*/ 248\*-------------------------------------------------------------------------*/
205static int recvall(p_buffer buf, luaL_Buffer *b) { 249static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget) {
206 int err = IO_DONE; 250 int err = IO_DONE;
207 size_t total = 0; 251 size_t total = 0;
208 while (err == IO_DONE) { 252 while (err == IO_DONE) {
209 const char *data; size_t count; 253 const char *data; size_t count;
210 err = buffer_get(buf, &data, &count); 254 err = buffer_get(buf, &data, &count);
255 if (budget && count > budget - total) { /* strictly more than fits */
256 count = budget - total;
257 luaL_addlstring(b, data, count);
258 buffer_skip(buf, count);
259 return BUF_OVERSIZED;
260 }
211 total += count; 261 total += count;
212 luaL_addlstring(b, data, count); 262 luaL_addlstring(b, data, count);
213 buffer_skip(buf, count); 263 buffer_skip(buf, count);
214 } 264 }
215 if (err == IO_CLOSED) { 265 if (err == IO_CLOSED) { /* completion beats the cap */
216 if (total > 0) return IO_DONE; 266 if (total > 0) return IO_DONE;
217 else return IO_CLOSED; 267 else return IO_CLOSED;
218 } else return err; 268 }
269 if (budget && total == budget) return BUF_OVERSIZED;
270 return err;
219} 271}
220 272
221/*-------------------------------------------------------------------------*\ 273/*-------------------------------------------------------------------------*\
222* Reads a line terminated by a CR LF pair or just by a LF. The CR and LF 274* Reads a line terminated by a CR LF pair or just by a LF. The CR and LF
223* are not returned by the function and are discarded from the buffer 275* are not returned by the function and are discarded from the buffer
276* budget == 0 means unlimited; otherwise the number of payload bytes still
277* allowed. The cap test sits before consuming a byte, so a line of exactly
278* budget payload bytes succeeds while budget+1 reports oversized. A timeout
279* or close with the payload exactly at the cap and no terminator yet also
280* resolves to oversized, never to timeout/closed.
224\*-------------------------------------------------------------------------*/ 281\*-------------------------------------------------------------------------*/
225static int recvline(p_buffer buf, luaL_Buffer *b) { 282static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget) {
226 int err = IO_DONE; 283 int err = IO_DONE;
284 size_t total = 0;
227 while (err == IO_DONE) { 285 while (err == IO_DONE) {
228 size_t count, pos; const char *data; 286 size_t count, pos; const char *data;
229 err = buffer_get(buf, &data, &count); 287 err = buffer_get(buf, &data, &count);
230 pos = 0; 288 pos = 0;
231 while (pos < count && data[pos] != '\n') { 289 while (pos < count && data[pos] != '\n') {
232 /* we ignore all \r's */ 290 /* we ignore all \r's -- they are consumed but never counted */
233 if (data[pos] != '\r') luaL_addchar(b, data[pos]); 291 if (data[pos] != '\r') {
292 if (budget && total == budget) {
293 /* leave the offending byte in the buffer for the next call */
294 buffer_skip(buf, pos);
295 return BUF_OVERSIZED;
296 }
297 luaL_addchar(b, data[pos]);
298 total++;
299 }
234 pos++; 300 pos++;
235 } 301 }
236 if (pos < count) { /* found '\n' */ 302 if (pos < count) { /* found '\n' */
@@ -239,7 +305,9 @@ static int recvline(p_buffer buf, luaL_Buffer *b) {
239 } else /* reached the end of the buffer */ 305 } else /* reached the end of the buffer */
240 buffer_skip(buf, pos); 306 buffer_skip(buf, pos);
241 } 307 }
242 return err; 308 if (err == IO_DONE) return IO_DONE; /* '\n' found: success, regardless of total */
309 if (budget && total == budget) return BUF_OVERSIZED; /* stalled/closed exactly at the cap: I1 */
310 return err; /* real timeout/closed, below the cap */
243} 311}
244 312
245/*-------------------------------------------------------------------------*\ 313/*-------------------------------------------------------------------------*\