/* ------------------------------------------------------------ */ /* HTTrack Website Copier, Offline Browser for Windows and Unix Copyright (C) 1998 Xavier Roche and other contributors SPDX-License-Identifier: GPL-3.0-or-later This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Please visit our Website: http://www.httrack.com */ /* Drives the String allocation-failure path through a realloc stub: asking a real allocator for a size it should refuse is a guess about the machine, not a test (#915). One case per run, named by argv[1]. */ #include #include #include #include static int alloc_fails = 0; static int oom_jumps = 0; static int oom_calls = 0; static size_t oom_size = 0; static size_t last_request = 0; static jmp_buf oom_jump; /* Hands back a pointer no caller may dereference, so a growth loop can be run to its end without allocating; alloc_budget caps a loop that never ends. */ static int alloc_pretends = 0; static int alloc_budget = 0; static int alloc_calls = 0; static char pretend_buffer[1]; static char *test_realloc(char *buff, size_t size) { last_request = size; alloc_calls++; if (alloc_fails) { return NULL; } if (alloc_pretends) { return alloc_calls > alloc_budget ? NULL : pretend_buffer; } return (char *) realloc(buff, size); } /* Declared before the include: StringBuffN_ and StringSprintf_ expand STRING_OOM inside the header itself. */ static void test_oom(size_t size); #define STRING_REALLOC(BUFF, SIZE) test_realloc(BUFF, SIZE) #define STRING_FREE(BUFF) free(BUFF) #define STRING_OOM(SIZE) test_oom(SIZE) #include "htsstrings.h" /* Either returns to the caller, leaving the String observable, or runs the shipped handler. */ static void test_oom(size_t size) { oom_calls++; oom_size = size; if (oom_jumps) { longjmp(oom_jump, 1); } StringOom_(size); } /* File scope, so longjmp cannot leave them indeterminate. */ static String room = STRING_EMPTY; static const char *kept = NULL; static size_t kept_capacity = 0; /* The handler must run once, and be told the size that was actually asked of the allocator: an under-allocation reports a size the String does not have. Compared against the stub's own record rather than a literal, so the initial capacity stays a policy the test does not pin. */ static int failure_reported(const char *name) { if (oom_calls != 1) { printf("%s: FAIL (handler ran %d times)\n", name, oom_calls); return 0; } if (oom_size != last_request) { printf("%s: FAIL (reported %u bytes, allocator was asked for %u)\n", name, (unsigned) oom_size, (unsigned) last_request); return 0; } return 1; } /* Control: with the stub allocating for real, nothing must reach the handler. Without this, a stub stuck in failing mode would "prove" every case. */ static int grow_case(void) { size_t cap, i; StringRoomTotal(room, 100); if (oom_calls != 0) { printf("grow: FAIL (handler ran %d times)\n", oom_calls); return 1; } cap = StringCapacity(room); if (StringBuff(room) == NULL || cap < 100) { printf("grow: FAIL (capacity %u)\n", (unsigned) cap); return 1; } /* A capacity never written to is a number, not a buffer: fill it to the last byte, so an allocation short of the announced capacity is a heap overflow the sanitizer legs catch. */ for (i = 0; i + 1 < cap; i++) { StringBuffRW(room)[i] = (char) ('a' + (i % 26)); } StringBuffRW(room)[cap - 1] = '\0'; for (i = 0; i + 1 < cap; i++) { if (StringBuff(room)[i] != (char) ('a' + (i % 26))) { printf("grow: FAIL (byte %u of %u read back as 0x%02x)\n", (unsigned) i, (unsigned) cap, (unsigned char) StringBuff(room)[i]); return 1; } } if (strlen(StringBuff(room)) != cap - 1) { printf("grow: FAIL (%u bytes readable, capacity %u)\n", (unsigned) strlen(StringBuff(room)), (unsigned) cap); return 1; } StringFree(room); printf("grow: OK\n"); return 0; } static int hook_case(void) { alloc_fails = oom_jumps = 1; if (setjmp(oom_jump) == 0) { StringRoomTotal(room, 100); printf("hook: FAIL (grew through a failing allocator)\n"); return 1; } if (!failure_reported("hook")) { return 1; } if (StringBuff(room) != NULL || StringCapacity(room) != 0 || StringLength(room) != 0) { printf("hook: FAIL (capacity %u, buffer %s)\n", (unsigned) StringCapacity(room), StringBuff(room) == NULL ? "null" : "set"); return 1; } printf("hook: OK\n"); return 0; } /* The one the old code got wrong: a failed realloc must not overwrite the live buffer with NULL nor bump the capacity past what was allocated. */ static int keep_case(void) { StringCopy(room, "abc"); kept = StringBuff(room); kept_capacity = StringCapacity(room); alloc_fails = oom_jumps = 1; if (setjmp(oom_jump) == 0) { StringRoomTotal(room, 1000); printf("keep: FAIL (grew through a failing allocator)\n"); return 1; } if (!failure_reported("keep")) { return 1; } if (StringBuff(room) != kept || StringCapacity(room) != kept_capacity || StringLength(room) != 3 || strcmp(StringBuff(room), "abc") != 0) { printf("keep: FAIL (buffer %s, capacity %u was %u)\n", StringBuff(room) == kept ? "kept" : "moved", (unsigned) StringCapacity(room), (unsigned) kept_capacity); return 1; } alloc_fails = 0; StringFree(room); printf("keep: OK\n"); return 0; } /* The header's own growers expand STRING_OOM as well, so drive each of them into the same failure rather than only the macro they call. */ static int sprintf_case(void) { alloc_fails = oom_jumps = 1; if (setjmp(oom_jump) == 0) { StringSprintf(room, "%s", "x"); printf("sprintf: FAIL (formatted through a failing allocator)\n"); return 1; } if (!failure_reported("sprintf") || StringBuff(room) != NULL) { return 1; } printf("sprintf: OK\n"); return 0; } static int buffn_case(void) { alloc_fails = oom_jumps = 1; if (setjmp(oom_jump) == 0) { (void) StringBuffN(room, 10); printf("buffn: FAIL (reserved through a failing allocator)\n"); return 1; } if (!failure_reported("buffn") || StringBuff(room) != NULL) { return 1; } printf("buffn: OK\n"); return 0; } /* Nothing reaches the halfway point through the API, so it is forced here. */ static int saturate_case(void) { StringCapacity(room) = ((size_t) -1) / 2 + 1; alloc_fails = oom_jumps = 1; if (setjmp(oom_jump) == 0) { StringRoomTotal(room, (size_t) -1); printf("saturate: FAIL (grew through a failing allocator)\n"); return 1; } if (!failure_reported("saturate")) { return 1; } if (last_request != (size_t) -1) { printf("saturate: FAIL (allocator asked for %lu bytes, want SIZE_MAX)\n", (unsigned long) last_request); return 1; } alloc_fails = 0; StringBuffRW(room) = NULL; StringFree(room); printf("saturate: OK\n"); return 0; } /* saturate_case dies on the first allocation, so it never sees a second step. Here the allocator keeps saying yes: a clamp that fails to raise the capacity spins, and the budget turns that spin into a failure. */ static int growloop_case(void) { enum { maxSteps = 8 * sizeof(size_t) + 4 }; alloc_pretends = oom_jumps = 1; alloc_budget = maxSteps; alloc_calls = 0; if (setjmp(oom_jump) != 0) { printf("growloop: FAIL (still growing after %d steps)\n", maxSteps); return 1; } StringRoomTotal(room, (size_t) -1); if (StringCapacity(room) != (size_t) -1) { printf("growloop: FAIL (stopped at capacity %lu)\n", (unsigned long) StringCapacity(room)); return 1; } alloc_pretends = 0; StringBuffRW(room) = NULL; /* the stub's pointer never came from malloc */ StringFree(room); printf("growloop: OK\n"); return 0; } /* Runs the shipped handler, which must print and abort. */ static int abort_case(void) { alloc_fails = 1; StringRoomTotal(room, 100); printf("abort: NOT aborted\n"); return 1; } int main(int argc, char **argv) { const char *const mode = argc > 1 ? argv[1] : ""; if (strcmp(mode, "grow") == 0) { return grow_case(); } else if (strcmp(mode, "hook") == 0) { return hook_case(); } else if (strcmp(mode, "keep") == 0) { return keep_case(); } else if (strcmp(mode, "sprintf") == 0) { return sprintf_case(); } else if (strcmp(mode, "buffn") == 0) { return buffn_case(); } else if (strcmp(mode, "saturate") == 0) { return saturate_case(); } else if (strcmp(mode, "growloop") == 0) { return growloop_case(); } else if (strcmp(mode, "abort") == 0) { return abort_case(); } fprintf(stderr, "usage: %s grow|hook|keep|sprintf|buffn|saturate|growloop|abort\n", argv[0]); return 2; }