Line | Count | Source (jump to first uncovered line) |
1 | | /* $OpenBSD: sshbuf.c,v 1.19 2022/12/02 04:40:27 djm Exp $ */ |
2 | | /* |
3 | | * Copyright (c) 2011 Damien Miller |
4 | | * |
5 | | * Permission to use, copy, modify, and distribute this software for any |
6 | | * purpose with or without fee is hereby granted, provided that the above |
7 | | * copyright notice and this permission notice appear in all copies. |
8 | | * |
9 | | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
10 | | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
11 | | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
12 | | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
13 | | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
14 | | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
15 | | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
16 | | */ |
17 | | |
18 | | #include "includes.h" |
19 | | |
20 | | #include <sys/types.h> |
21 | | #include <signal.h> |
22 | | #include <stdlib.h> |
23 | | #include <stdio.h> |
24 | | #include <string.h> |
25 | | |
26 | | #include "ssherr.h" |
27 | | #define SSHBUF_INTERNAL |
28 | | #include "sshbuf.h" |
29 | | #include "misc.h" |
30 | | /* #include "log.h" */ |
31 | | |
32 | 834k | #define BUF_WATERSHED 256*1024 |
33 | | |
34 | | #ifdef SSHBUF_DEBUG |
35 | | # define SSHBUF_TELL(what) do { \ |
36 | | printf("%s:%d %s: %s size %zu alloc %zu off %zu max %zu\n", \ |
37 | | __FILE__, __LINE__, __func__, what, \ |
38 | | buf->size, buf->alloc, buf->off, buf->max_size); \ |
39 | | fflush(stdout); \ |
40 | | } while (0) |
41 | | #else |
42 | | # define SSHBUF_TELL(what) |
43 | | #endif |
44 | | |
45 | | struct sshbuf { |
46 | | u_char *d; /* Data */ |
47 | | const u_char *cd; /* Const data */ |
48 | | size_t off; /* First available byte is buf->d + buf->off */ |
49 | | size_t size; /* Last byte is buf->d + buf->size - 1 */ |
50 | | size_t max_size; /* Maximum size of buffer */ |
51 | | size_t alloc; /* Total bytes allocated to buf->d */ |
52 | | int readonly; /* Refers to external, const data */ |
53 | | u_int refcount; /* Tracks self and number of child buffers */ |
54 | | struct sshbuf *parent; /* If child, pointer to parent */ |
55 | | char label[MAX_LABEL_LEN]; /* String for buffer label - debugging use */ |
56 | | int type; /* type of buffer enum (sshbuf_types)*/ |
57 | | }; |
58 | | |
59 | | /* update the label string for a given sshbuf. Useful |
60 | | * for debugging */ |
61 | | void |
62 | | sshbuf_relabel(struct sshbuf *buf, const char *label) |
63 | 280k | { |
64 | 280k | if (label != NULL) |
65 | 280k | strncpy(buf->label, label, MAX_LABEL_LEN-1); |
66 | 280k | } |
67 | | |
68 | | /* set the type (from enum sshbuf_type) of the given sshbuf. |
69 | | * The purpose is to allow different classes of buffers to |
70 | | * follow different code paths if necessary */ |
71 | | void |
72 | | sshbuf_type(struct sshbuf *buf, int type) |
73 | 280k | { |
74 | 280k | if (type < BUF_MAX_TYPE) |
75 | 280k | buf->type = type; |
76 | 280k | } |
77 | | |
78 | | static inline int |
79 | | sshbuf_check_sanity(const struct sshbuf *buf) |
80 | 135M | { |
81 | 135M | SSHBUF_TELL("sanity"); |
82 | 135M | if (__predict_false(buf == NULL || |
83 | 135M | (!buf->readonly && buf->d != buf->cd) || |
84 | 135M | buf->refcount < 1 || buf->refcount > SSHBUF_REFS_MAX || |
85 | 135M | buf->cd == NULL || |
86 | 135M | buf->max_size > SSHBUF_SIZE_MAX || |
87 | 135M | buf->alloc > buf->max_size || |
88 | 135M | buf->size > buf->alloc || |
89 | 135M | buf->off > buf->size)) { |
90 | | /* Do not try to recover from corrupted buffer internals */ |
91 | 0 | SSHBUF_DBG(("SSH_ERR_INTERNAL_ERROR")); |
92 | 0 | ssh_signal(SIGSEGV, SIG_DFL); |
93 | 0 | raise(SIGSEGV); |
94 | 0 | return SSH_ERR_INTERNAL_ERROR; |
95 | 0 | } |
96 | 135M | return 0; |
97 | 135M | } |
98 | | |
99 | | static void |
100 | | sshbuf_maybe_pack(struct sshbuf *buf, int force) |
101 | 40.8M | { |
102 | 40.8M | SSHBUF_DBG(("force %d", force)); |
103 | 40.8M | SSHBUF_TELL("pre-pack"); |
104 | 40.8M | if (buf->off == 0 || buf->readonly || buf->refcount > 1) |
105 | 40.8M | return; |
106 | 0 | if (force || |
107 | 0 | (buf->off >= SSHBUF_PACK_MIN && buf->off >= buf->size / 2)) { |
108 | 0 | memmove(buf->d, buf->d + buf->off, buf->size - buf->off); |
109 | 0 | buf->size -= buf->off; |
110 | 0 | buf->off = 0; |
111 | 0 | SSHBUF_TELL("packed"); |
112 | 0 | } |
113 | 0 | } |
114 | | |
115 | | struct sshbuf * |
116 | | sshbuf_new_label (const char *label) |
117 | 632k | { |
118 | 632k | struct sshbuf *ret; |
119 | | |
120 | 632k | if ((ret = calloc(sizeof(*ret), 1)) == NULL) |
121 | 0 | return NULL; |
122 | 632k | ret->alloc = SSHBUF_SIZE_INIT; |
123 | 632k | ret->max_size = SSHBUF_SIZE_MAX; |
124 | 632k | ret->readonly = 0; |
125 | 632k | ret->refcount = 1; |
126 | 632k | ret->parent = NULL; |
127 | 632k | if (label != NULL) |
128 | 632k | strncpy(ret->label, label, MAX_LABEL_LEN-1); |
129 | 632k | if ((ret->cd = ret->d = calloc(1, ret->alloc)) == NULL) { |
130 | 0 | free(ret); |
131 | 0 | return NULL; |
132 | 0 | } |
133 | 632k | return ret; |
134 | 632k | } |
135 | | |
136 | | struct sshbuf * |
137 | | sshbuf_from(const void *blob, size_t len) |
138 | 62.3k | { |
139 | 62.3k | struct sshbuf *ret; |
140 | | |
141 | 62.3k | if (blob == NULL || len > SSHBUF_SIZE_MAX || |
142 | 62.3k | (ret = calloc(sizeof(*ret), 1)) == NULL) |
143 | 0 | return NULL; |
144 | 62.3k | ret->alloc = ret->size = ret->max_size = len; |
145 | 62.3k | ret->readonly = 1; |
146 | 62.3k | ret->refcount = 1; |
147 | 62.3k | ret->parent = NULL; |
148 | 62.3k | ret->cd = blob; |
149 | 62.3k | ret->d = NULL; |
150 | 62.3k | return ret; |
151 | 62.3k | } |
152 | | |
153 | | int |
154 | | sshbuf_set_parent(struct sshbuf *child, struct sshbuf *parent) |
155 | 62.3k | { |
156 | 62.3k | int r; |
157 | | |
158 | 62.3k | if ((r = sshbuf_check_sanity(child)) != 0 || |
159 | 62.3k | (r = sshbuf_check_sanity(parent)) != 0) |
160 | 0 | return r; |
161 | 62.3k | if (child->parent != NULL && child->parent != parent) |
162 | 0 | return SSH_ERR_INTERNAL_ERROR; |
163 | 62.3k | child->parent = parent; |
164 | 62.3k | child->parent->refcount++; |
165 | 62.3k | return 0; |
166 | 62.3k | } |
167 | | |
168 | | struct sshbuf * |
169 | | sshbuf_fromb(struct sshbuf *buf) |
170 | 62.3k | { |
171 | 62.3k | struct sshbuf *ret; |
172 | | |
173 | 62.3k | if (sshbuf_check_sanity(buf) != 0) |
174 | 0 | return NULL; |
175 | 62.3k | if ((ret = sshbuf_from(sshbuf_ptr(buf), sshbuf_len(buf))) == NULL) |
176 | 0 | return NULL; |
177 | 62.3k | if (sshbuf_set_parent(ret, buf) != 0) { |
178 | 0 | sshbuf_free(ret); |
179 | 0 | return NULL; |
180 | 0 | } |
181 | 62.3k | return ret; |
182 | 62.3k | } |
183 | | |
184 | | void |
185 | | sshbuf_free(struct sshbuf *buf) |
186 | 1.59M | { |
187 | 1.59M | if (buf == NULL) |
188 | 842k | return; |
189 | | /* |
190 | | * The following will leak on insane buffers, but this is the safest |
191 | | * course of action - an invalid pointer or already-freed pointer may |
192 | | * have been passed to us and continuing to scribble over memory would |
193 | | * be bad. |
194 | | */ |
195 | 757k | if (sshbuf_check_sanity(buf) != 0) |
196 | 0 | return; |
197 | | |
198 | | /* |
199 | | * If we are a parent with still-extant children, then don't free just |
200 | | * yet. The last child's call to sshbuf_free should decrement our |
201 | | * refcount to 0 and trigger the actual free. |
202 | | */ |
203 | 757k | buf->refcount--; |
204 | 757k | if (buf->refcount > 0) |
205 | 62.3k | return; |
206 | | |
207 | | /* |
208 | | * If we are a child, the free our parent to decrement its reference |
209 | | * count and possibly free it. |
210 | | */ |
211 | 694k | sshbuf_free(buf->parent); |
212 | 694k | buf->parent = NULL; |
213 | | |
214 | 694k | if (!buf->readonly) { |
215 | 632k | explicit_bzero(buf->d, buf->alloc); |
216 | 632k | free(buf->d); |
217 | 632k | } |
218 | 694k | freezero(buf, sizeof(*buf)); |
219 | 694k | } |
220 | | |
221 | | void |
222 | | sshbuf_reset(struct sshbuf *buf) |
223 | 2.52M | { |
224 | 2.52M | u_char *d; |
225 | | |
226 | 2.52M | if (buf->readonly || buf->refcount > 1) { |
227 | | /* Nonsensical. Just make buffer appear empty */ |
228 | 35.0k | buf->off = buf->size; |
229 | 35.0k | return; |
230 | 35.0k | } |
231 | 2.48M | if (sshbuf_check_sanity(buf) != 0) |
232 | 0 | return; |
233 | 2.48M | buf->off = buf->size = 0; |
234 | 2.48M | if (buf->alloc != SSHBUF_SIZE_INIT) { |
235 | 78.3k | if ((d = recallocarray(buf->d, buf->alloc, SSHBUF_SIZE_INIT, |
236 | 78.3k | 1)) != NULL) { |
237 | 78.3k | buf->cd = buf->d = d; |
238 | 78.3k | buf->alloc = SSHBUF_SIZE_INIT; |
239 | 78.3k | } |
240 | 78.3k | } |
241 | 2.48M | explicit_bzero(buf->d, buf->alloc); |
242 | 2.48M | } |
243 | | |
244 | | size_t |
245 | | sshbuf_max_size(const struct sshbuf *buf) |
246 | 0 | { |
247 | 0 | return buf->max_size; |
248 | 0 | } |
249 | | |
250 | | size_t |
251 | | sshbuf_alloc(const struct sshbuf *buf) |
252 | 0 | { |
253 | 0 | return buf->alloc; |
254 | 0 | } |
255 | | |
256 | | const struct sshbuf * |
257 | | sshbuf_parent(const struct sshbuf *buf) |
258 | 0 | { |
259 | 0 | return buf->parent; |
260 | 0 | } |
261 | | |
262 | | u_int |
263 | | sshbuf_refcount(const struct sshbuf *buf) |
264 | 0 | { |
265 | 0 | return buf->refcount; |
266 | 0 | } |
267 | | |
268 | | int |
269 | | sshbuf_set_max_size(struct sshbuf *buf, size_t max_size) |
270 | 0 | { |
271 | 0 | size_t rlen; |
272 | 0 | u_char *dp; |
273 | 0 | int r; |
274 | |
|
275 | 0 | SSHBUF_DBG(("set max buf = %p len = %zu", buf, max_size)); |
276 | 0 | if ((r = sshbuf_check_sanity(buf)) != 0) |
277 | 0 | return r; |
278 | 0 | if (max_size == buf->max_size) |
279 | 0 | return 0; |
280 | 0 | if (buf->readonly || buf->refcount > 1) |
281 | 0 | return SSH_ERR_BUFFER_READ_ONLY; |
282 | 0 | if (max_size > SSHBUF_SIZE_MAX) |
283 | 0 | return SSH_ERR_NO_BUFFER_SPACE; |
284 | | /* pack and realloc if necessary */ |
285 | 0 | sshbuf_maybe_pack(buf, max_size < buf->size); |
286 | 0 | if (max_size < buf->alloc && max_size > buf->size) { |
287 | 0 | if (buf->size < SSHBUF_SIZE_INIT) |
288 | 0 | rlen = SSHBUF_SIZE_INIT; |
289 | 0 | else |
290 | 0 | rlen = ROUNDUP(buf->size, SSHBUF_SIZE_INC); |
291 | 0 | if (rlen > max_size) |
292 | 0 | rlen = max_size; |
293 | 0 | SSHBUF_DBG(("new alloc = %zu", rlen)); |
294 | 0 | if ((dp = recallocarray(buf->d, buf->alloc, rlen, 1)) == NULL) |
295 | 0 | return SSH_ERR_ALLOC_FAIL; |
296 | 0 | buf->cd = buf->d = dp; |
297 | 0 | buf->alloc = rlen; |
298 | 0 | } |
299 | 0 | SSHBUF_TELL("new-max"); |
300 | 0 | if (max_size < buf->alloc) |
301 | 0 | return SSH_ERR_NO_BUFFER_SPACE; |
302 | 0 | buf->max_size = max_size; |
303 | 0 | return 0; |
304 | 0 | } |
305 | | |
306 | | size_t |
307 | | sshbuf_len(const struct sshbuf *buf) |
308 | 83.8M | { |
309 | 83.8M | if (sshbuf_check_sanity(buf) != 0) |
310 | 0 | return 0; |
311 | 83.8M | return buf->size - buf->off; |
312 | 83.8M | } |
313 | | |
314 | | size_t |
315 | | sshbuf_avail(const struct sshbuf *buf) |
316 | 0 | { |
317 | 0 | if (sshbuf_check_sanity(buf) != 0 || buf->readonly || buf->refcount > 1) |
318 | 0 | return 0; |
319 | | /* we need to reserve a small amount of overhead on the input buffer |
320 | | * or we can enter into a pathological state during bulk |
321 | | * data transfers. We use a fraction of the max size as we want it to scale |
322 | | * with the size of the input buffer. If we do it for all of the buffers |
323 | | * we fail the regression unit tests. This seems like a reasonable |
324 | | * solution. Of course, I still need to figure out *why* this is |
325 | | * happening and come up with an actual fix. TODO |
326 | | * cjr 4/19/2024 */ |
327 | 0 | if (buf->type == BUF_CHANNEL_INPUT) |
328 | 0 | return buf->max_size / 1.05 - (buf->size - buf->off); |
329 | 0 | else |
330 | 0 | return buf->max_size - (buf->size - buf->off); |
331 | 0 | } |
332 | | |
333 | | const u_char * |
334 | | sshbuf_ptr(const struct sshbuf *buf) |
335 | 5.44M | { |
336 | 5.44M | if (sshbuf_check_sanity(buf) != 0) |
337 | 0 | return NULL; |
338 | 5.44M | return buf->cd + buf->off; |
339 | 5.44M | } |
340 | | |
341 | | u_char * |
342 | | sshbuf_mutable_ptr(const struct sshbuf *buf) |
343 | 248k | { |
344 | 248k | if (sshbuf_check_sanity(buf) != 0 || buf->readonly || buf->refcount > 1) |
345 | 0 | return NULL; |
346 | 248k | return buf->d + buf->off; |
347 | 248k | } |
348 | | |
349 | | int |
350 | | sshbuf_check_reserve(const struct sshbuf *buf, size_t len) |
351 | 41.2M | { |
352 | 41.2M | int r; |
353 | | |
354 | 41.2M | if ((r = sshbuf_check_sanity(buf)) != 0) |
355 | 0 | return r; |
356 | 41.2M | if (buf->readonly || buf->refcount > 1) |
357 | 0 | return SSH_ERR_BUFFER_READ_ONLY; |
358 | 41.2M | SSHBUF_TELL("check"); |
359 | | /* Check that len is reasonable and that max_size + available < len */ |
360 | 41.2M | if (len > buf->max_size || buf->max_size - len < buf->size - buf->off) |
361 | 0 | return SSH_ERR_NO_BUFFER_SPACE; |
362 | 41.2M | return 0; |
363 | 41.2M | } |
364 | | |
365 | | int |
366 | | sshbuf_allocate(struct sshbuf *buf, size_t len) |
367 | 40.8M | { |
368 | 40.8M | size_t rlen, need; |
369 | 40.8M | u_char *dp; |
370 | 40.8M | int r; |
371 | | |
372 | 40.8M | SSHBUF_DBG(("allocate buf = %p len = %zu", buf, len)); |
373 | 40.8M | if ((r = sshbuf_check_reserve(buf, len)) != 0) |
374 | 0 | return r; |
375 | | /* |
376 | | * If the requested allocation appended would push us past max_size |
377 | | * then pack the buffer, zeroing buf->off. |
378 | | */ |
379 | 40.8M | sshbuf_maybe_pack(buf, buf->size + len > buf->max_size); |
380 | 40.8M | SSHBUF_TELL("allocate"); |
381 | 40.8M | if (len + buf->size <= buf->alloc) |
382 | 40.4M | return 0; /* already have it. */ |
383 | | |
384 | | /* |
385 | | * Prefer to alloc in SSHBUF_SIZE_INC units, but |
386 | | * allocate less if doing so would overflow max_size. |
387 | | */ |
388 | 417k | need = len + buf->size - buf->alloc; |
389 | 417k | rlen = ROUNDUP(buf->alloc + need, SSHBUF_SIZE_INC); |
390 | | /* With the changes in 8.9 the output buffer end up growing pretty |
391 | | * slowly. It's knows that it needs to grow but it only does so 32K |
392 | | * at a time. This means a lot of calls to realloc and memcpy which |
393 | | * kills performance until the buffer reaches some maximum size. |
394 | | * So we explicitly test for a buffer that's trying to grow and |
395 | | * if it is then we push the growth by 4MB at a time. This can result in |
396 | | * the buffer being over allocated (in terms of actual needs) but the |
397 | | * process is fast. This significantly reduces overhead |
398 | | * and improves performance. In this case we look for a buffer that is trying |
399 | | * to grow larger than BUF_WATERSHED (256*1024 taken from PACKET_MAX_SIZE) |
400 | | * and explcitly check that the buffer is being used for inbound outbound |
401 | | * channel buffering. |
402 | | * Updated for 18.4.1 -cjr 04/20/24 |
403 | | */ |
404 | 417k | if (rlen > BUF_WATERSHED && (buf->type == BUF_CHANNEL_OUTPUT || buf->type == BUF_CHANNEL_INPUT)) { |
405 | | /* debug_f ("Prior: label: %s, %p, rlen is %zu need is %zu max_size is %zu", |
406 | | buf->label, buf, rlen, need, buf->max_size); */ |
407 | | /* easiest thing to do is grow the nuffer by 4MB each time. It might end |
408 | | * up being somewhat overallocated but works quickly */ |
409 | 94 | need = (4*1024*1024); |
410 | 94 | rlen = ROUNDUP(buf->alloc + need, SSHBUF_SIZE_INC); |
411 | | /* debug_f ("Post: label: %s, %p, rlen is %zu need is %zu max_size is %zu", */ |
412 | | /* buf->label, buf, rlen, need, buf->max_size); */ |
413 | 94 | } |
414 | 417k | SSHBUF_DBG(("need %zu initial rlen %zu", need, rlen)); |
415 | | |
416 | | /* rlen might be above the max allocation */ |
417 | 417k | if (rlen > buf->max_size) |
418 | 0 | rlen = buf->max_size; |
419 | | |
420 | 417k | SSHBUF_DBG(("adjusted rlen %zu", rlen)); |
421 | 417k | if ((dp = recallocarray(buf->d, buf->alloc, rlen, 1)) == NULL) { |
422 | 0 | SSHBUF_DBG(("realloc fail")); |
423 | 0 | return SSH_ERR_ALLOC_FAIL; |
424 | 0 | } |
425 | 417k | buf->alloc = rlen; |
426 | 417k | buf->cd = buf->d = dp; |
427 | 417k | if ((r = sshbuf_check_reserve(buf, len)) < 0) { |
428 | | /* shouldn't fail */ |
429 | 0 | return r; |
430 | 0 | } |
431 | 417k | SSHBUF_TELL("done"); |
432 | 417k | return 0; |
433 | 417k | } |
434 | | |
435 | | int |
436 | | sshbuf_reserve(struct sshbuf *buf, size_t len, u_char **dpp) |
437 | 40.8M | { |
438 | 40.8M | u_char *dp; |
439 | 40.8M | int r; |
440 | | |
441 | 40.8M | if (dpp != NULL) |
442 | 40.8M | *dpp = NULL; |
443 | | |
444 | 40.8M | SSHBUF_DBG(("reserve buf = %p len = %zu", buf, len)); |
445 | 40.8M | if ((r = sshbuf_allocate(buf, len)) != 0) |
446 | 0 | return r; |
447 | | |
448 | 40.8M | dp = buf->d + buf->size; |
449 | 40.8M | buf->size += len; |
450 | 40.8M | if (dpp != NULL) |
451 | 40.8M | *dpp = dp; |
452 | 40.8M | return 0; |
453 | 40.8M | } |
454 | | |
455 | | int |
456 | | sshbuf_consume(struct sshbuf *buf, size_t len) |
457 | 1.44M | { |
458 | 1.44M | int r; |
459 | | |
460 | 1.44M | SSHBUF_DBG(("len = %zu", len)); |
461 | 1.44M | if ((r = sshbuf_check_sanity(buf)) != 0) |
462 | 0 | return r; |
463 | 1.44M | if (len == 0) |
464 | 0 | return 0; |
465 | 1.44M | if (len > sshbuf_len(buf)) |
466 | 1.12k | return SSH_ERR_MESSAGE_INCOMPLETE; |
467 | 1.44M | buf->off += len; |
468 | | /* deal with empty buffer */ |
469 | 1.44M | if (buf->off == buf->size) |
470 | 264k | buf->off = buf->size = 0; |
471 | 1.44M | SSHBUF_TELL("done"); |
472 | 1.44M | return 0; |
473 | 1.44M | } |
474 | | |
475 | | int |
476 | | sshbuf_consume_end(struct sshbuf *buf, size_t len) |
477 | 302k | { |
478 | 302k | int r; |
479 | | |
480 | 302k | SSHBUF_DBG(("len = %zu", len)); |
481 | 302k | if ((r = sshbuf_check_sanity(buf)) != 0) |
482 | 0 | return r; |
483 | 302k | if (len == 0) |
484 | 0 | return 0; |
485 | 302k | if (len > sshbuf_len(buf)) |
486 | 260 | return SSH_ERR_MESSAGE_INCOMPLETE; |
487 | 301k | buf->size -= len; |
488 | 301k | SSHBUF_TELL("done"); |
489 | 301k | return 0; |
490 | 302k | } |