diff --git a/AGENTS.md b/AGENTS.md index e6de47d..3cc5b6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,13 @@ same caller. A null stack is left unchanged. The pointer passed to the callback is the previous stack pointer. It only needs to preserve registers it modifies. +`stackman_guard.c` is optional and is not part of the switch. It is +plain C, with no library calls. `far` is the far boundary. `extent` is +the byte count toward near. `stackman_block_call` produces that pair +from a malloc block. Extent 0 means that bound is unknown, and the +helpers do not touch memory. A `stackman_call` block knows both ends, +so the extent is known and the guard sits at the near end. + User code includes `stackman.h`. The library implementation includes `stackman_impl.h` once. `platforms/platform.h` selects `switch__`. @@ -104,10 +111,12 @@ make PLATFORM_PREFIX=arm-linux-gnueabihf EMULATOR=qemu-arm test `make clean` only when switching `PLATFORMFLAGS` or `PLATFORM_PREFIX`. Success ends with `*** All test suites passed ***`. -Five programs: `bin/test`, `bin/test_cc`, `bin/test_static`, +Five switch programs: `bin/test`, `bin/test_cc`, `bin/test_static`, `bin/test_asm`, and `bin/test_block_asc`. The last one compiles `stackman_block_call` with `STACKMAN_STACK_DIR` set to 1. No supported -ABI grows upward. `tests/test.c` is `test_01` through `test_09`. `test_04` +ABI grows upward. `bin/test_guard` paints the optional guard unit, and +`bin/test_guard_up` rebuilds that unit with `STACKMAN_STACK_DIR=1`. +`tests/test.c` is `test_01` through `test_09`. `test_04` and `test_05` are the `stackman_call` contract. `test_07` and `test_08` pressure the compiler. They pass even when a callee-saved register is dropped, if the compiler kept the values in memory. `test_09` checks diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab5939..8659b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `stackman_block_call` turns a malloc block (low address and size) into the pointer `stackman_call` takes and the number of bytes toward near. +- Optional `stackman/stackman_guard.c`. Plain C, no library calls, and not part of the switch. Given a far boundary and a byte count toward near, it paints and checks a band at the near end. Extent 0 means the near bound is unknown and the memory is left alone. A `stackman_call` block knows both ends. ## [1.3.0] - 2026-10-15 diff --git a/Makefile b/Makefile index 0712226..5931b09 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,8 @@ test: tests $(EMULATOR) bin/test_static $(EMULATOR) bin/test_asm $(EMULATOR) bin/test_block_asc + $(EMULATOR) bin/test_guard + $(EMULATOR) bin/test_guard_up @echo "*** All test suites passed ***" tests: bin/test @@ -77,6 +79,8 @@ tests: bin/test_cc tests: bin/test_static tests: bin/test_asm tests: bin/test_block_asc +tests: bin/test_guard +tests: bin/test_guard_up tests: LDLIBS := -lstackman bin/test: tests/test.o $(LIB)/libstackman.a @@ -94,3 +98,17 @@ bin/test_asm: tests/test_asm.o tests/test_asm_s.o # Header-only. STACKMAN_STACK_DIR is set in the source. bin/test_block_asc: tests/test_block_asc.o $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $< ${DEBUG} + +bin/test_guard: tests/test_guard.o stackman/stackman_guard.o $(LIB)/libstackman.a + $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ tests/test_guard.o stackman/stackman_guard.o -lstackman + +# No supported ABI grows upward. This rebuilds the guard unit with that +# direction so the other branch of the address arithmetic is executed. +tests/test_guard_up.o: tests/test_guard.c + $(CC) $(CFLAGS) $(CPPFLAGS) -DSTACKMAN_STACK_DIR=1 -c -o $@ $< + +stackman/stackman_guard_up.o: stackman/stackman_guard.c + $(CC) $(CFLAGS) $(CPPFLAGS) -DSTACKMAN_STACK_DIR=1 -c -o $@ $< + +bin/test_guard_up: tests/test_guard_up.o stackman/stackman_guard_up.o + $(CC) $(LDFLAGS) $(STATIC_FLAG) -o $@ $^ diff --git a/README.md b/README.md index 193661c..ef4f855 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ platforms so that no assembly steps are required by users. - Simple api - - `stackman_switch()` and `stackman_call()` are the only functions. + - `stackman_switch()` and `stackman_call()` are the switch API. + - `stackman_guard.c` is optional and separate. It paints bytes in a + caller-owned block. It does not switch, and it is not in `libstackman`. - The caller provides a callback and context pointer to customize behaviour. - Simple implementation @@ -189,6 +191,51 @@ alignment leaves no usable byte. The last feature is useful to bypass any in-lining that a compiler may do, when one really wants a proper function call with stack, for example, when setting up a new stack entry point. +### Stack paint + +`stackman/stackman_guard.h` declares three functions implemented in +`stackman/stackman_guard.c`. Compile that file into the program that +wants them. They do not call the C library. + +The use is a block you allocated and then hand to `stackman_call`. +`far` is the pointer you pass as the new stack. `extent` is the bytes +from there toward near. Paint a band at that near end, run the call, +and check the band. Paint the whole extent first if you want +`stackman_stack_unused` to tell you how deep the call actually went. + +```c +void *far; +size_t extent; +char *base = malloc(size); /* low address, as malloc returns it */ +if (stackman_block_call(base, size, &far, &extent) == 0) + ; /* alignment left no usable byte */ + +stackman_guard_paint(far, extent, 64, 0xa5); +stackman_call(worker, ctx, far); +if (stackman_guard_check(far, extent, 64, 0xa5) != 1) + ; /* the near band was overwritten: this block is too small */ + +stackman_guard_paint(far, extent, extent, 0xa5); +stackman_call(worker, ctx, far); +size_t unused = stackman_stack_unused(far, extent, 0xa5); +size_t used = extent - unused; +``` + +Alignment can make `extent` a few bytes shorter than `size`. The +sliver outside `far` is not part of the stack. + +A `stackman_switch` snapshot is a different range. It is the bytes +already in use, from the saved near pointer out to `far`, and a copy +of that range has no untouched tail. The main stack's far is often +`STACKMAN_SP_FURTHEST`, which is not a paintable address. Extent 0 +is that case: the helpers leave memory alone. + +`stackman_stack_unused` takes the same `far` and `extent` as the paint. +`stackman_block_call` is the conversion from the malloc pair. On a +descending stack the low address is the near end. On an ascending +stack the low address is the far end. The paint functions stay in +`(far, extent)`, the same coordinates as `stackman_call`. + ## Usage - Include `stackman.h` for a decleration of the `stackman_switch()` function diff --git a/stackman/stackman_guard.c b/stackman/stackman_guard.c new file mode 100644 index 0000000..f8ac6ec --- /dev/null +++ b/stackman/stackman_guard.c @@ -0,0 +1,60 @@ +/* Optional stack paint. See stackman_guard.h. No library calls. */ + +#include "stackman_guard.h" + +/* from_near 0 is the byte nearest the overflow end. */ +static unsigned char * +byte_at(void *far, size_t extent, size_t from_near) +{ +#if STACKMAN_STACK_DIR == 0 + return (unsigned char *)far - extent + from_near; +#else + return (unsigned char *)far + extent - 1 - from_near; +#endif +} + +static int +guard_described(size_t extent, size_t guard_bytes) +{ + return extent != 0 && guard_bytes != 0 && guard_bytes <= extent; +} + +size_t +stackman_guard_paint(void *far, size_t extent, size_t guard_bytes, + unsigned char pattern) +{ + size_t i; + + if (!guard_described(extent, guard_bytes)) + return 0; + for (i = 0; i < guard_bytes; i++) + *byte_at(far, extent, i) = pattern; + return guard_bytes; +} + +int +stackman_guard_check(void *far, size_t extent, size_t guard_bytes, + unsigned char pattern) +{ + size_t i; + + if (!guard_described(extent, guard_bytes)) + return -1; + for (i = 0; i < guard_bytes; i++) { + if (*byte_at(far, extent, i) != pattern) + return 0; + } + return 1; +} + +size_t +stackman_stack_unused(void *far, size_t extent, unsigned char pattern) +{ + size_t i; + + for (i = 0; i < extent; i++) { + if (*byte_at(far, extent, i) != pattern) + return i; + } + return extent; +} diff --git a/stackman/stackman_guard.h b/stackman/stackman_guard.h new file mode 100644 index 0000000..38433f7 --- /dev/null +++ b/stackman/stackman_guard.h @@ -0,0 +1,60 @@ +#ifndef STACKMAN_GUARD_H +#define STACKMAN_GUARD_H + +/* + * Optional paint of a caller-owned stack block. Not part of the switch. + * Compile stackman_guard.c into the program that wants it. The switch + * library does not include this file. + * + * No library calls. The bytes are ordinary stores. Nothing is registered + * with the operating system, and nothing programs a hardware limit. + * + * The use is a pre-allocated block passed to stackman_call. + * stackman_block_call turns the malloc pair into `far` and `extent`. + * `far` is the pointer passed as the new stack. `extent` is the byte + * count toward near. For a range already held as two pointers, + * extent = STACKMAN_SP_DIFF(far, near). + * + * Paint the near band, call, then check it. Paint the whole extent + * and stackman_stack_unused reports how much of that block the call + * left untouched, so the next allocation can be sized from a real run. + * + * A stackman_switch snapshot is the other shape. It is the used range + * between the saved near pointer and far. There is no unused tail + * inside it. The main stack's far is often STACKMAN_SP_FURTHEST, + * which is not a paintable address. Pass extent 0 when the near bound + * is unknown. The functions then do not read or write memory. + * + * The direction comes from STACKMAN_STACK_DIR. On a descending stack the + * bytes are [far - extent, far). On an ascending stack they are + * [far, far + extent). The guard is the `guard_bytes` at the near end, + * the end a growing stack reaches last. + * + * A check only sees what was overwritten before the call. It does not + * stop the write. stackman_stack_unused is meaningful after the whole + * extent was painted with `pattern` and the stack then grew from `far` + * toward near, leaving a solid run of `pattern` at the near end. A + * pattern byte written by the program counts as untouched. + */ + +#include "stackman.h" +#include + +size_t stackman_guard_paint(void *far, size_t extent, size_t guard_bytes, + unsigned char pattern); + +/* 1: every guard byte still holds pattern. + * 0: a guard byte differs. + * -1: there is no guard (extent is 0, guard_bytes is 0, or + * guard_bytes is larger than extent). + */ +int stackman_guard_check(void *far, size_t extent, size_t guard_bytes, + unsigned char pattern); + +/* How many bytes from the near end toward far still hold pattern. + * Same coordinates as paint: far, then extent toward near. + * extent 0 returns 0. + */ +size_t stackman_stack_unused(void *far, size_t extent, unsigned char pattern); + +#endif diff --git a/tests/test_guard.c b/tests/test_guard.c new file mode 100644 index 0000000..d851b2a --- /dev/null +++ b/tests/test_guard.c @@ -0,0 +1,139 @@ +/* Paint tests for stackman_guard.c. + * Built twice: once for this ABI's STACKMAN_STACK_DIR, and once with + * STACKMAN_STACK_DIR=1. The ascending build does not call stackman_call. + * No supported ABI grows that way. The call below runs only on the + * descending build, which is every ABI this tree ships. + */ + +#include "stackman_guard.h" + +#include +#include +#include + +enum { N = 32, GUARD = 8 }; + +static void +test_math(void) +{ + unsigned char raw[N]; + void *far; + size_t i; + unsigned char pattern = 0xa5; + +#if STACKMAN_STACK_DIR == 0 + far = raw + N; +#else + far = raw; +#endif + + raw[0] = 0x11; + assert(stackman_guard_paint(0, 0, GUARD, pattern) == 0); + assert(stackman_guard_check(0, 0, GUARD, pattern) == -1); + assert(stackman_stack_unused(0, 0, pattern) == 0); + assert(raw[0] == 0x11); + + assert(stackman_guard_paint(far, 4, 8, pattern) == 0); + assert(stackman_guard_check(far, 4, 8, pattern) == -1); + + for (i = 0; i < N; i++) + raw[i] = 0x11; + assert(stackman_guard_paint(far, N, GUARD, pattern) == GUARD); + for (i = 0; i < N; i++) { + int in_guard; + +#if STACKMAN_STACK_DIR == 0 + in_guard = i < GUARD; +#else + in_guard = i >= N - GUARD; +#endif + assert(raw[i] == (in_guard ? pattern : 0x11)); + } + assert(stackman_guard_check(far, N, GUARD, pattern) == 1); + + /* The first byte farther than the guard is still in the block. + * Dirtying it leaves the guard intact. */ +#if STACKMAN_STACK_DIR == 0 + raw[GUARD] = 0; +#else + raw[N - GUARD - 1] = 0; +#endif + assert(stackman_guard_check(far, N, GUARD, pattern) == 1); + +#if STACKMAN_STACK_DIR == 0 + raw[0] = 0; +#else + raw[N - 1] = 0; +#endif + assert(stackman_guard_check(far, N, GUARD, pattern) == 0); + + assert(stackman_guard_paint(far, N, N, pattern) == N); + assert(stackman_stack_unused(far, N, pattern) == N); +#if STACKMAN_STACK_DIR == 0 + raw[N - 1] = 0; +#else + raw[0] = 0; +#endif + assert(stackman_stack_unused(far, N, pattern) == N - 1); +} + +#if STACKMAN_STACK_DIR == 0 +static void * +call_cb(void *context, int opcode, void *old_sp) +{ + volatile unsigned char scratch[256]; + size_t i; + + (void)context; + (void)opcode; + (void)old_sp; + for (i = 0; i < sizeof scratch; i++) + scratch[i] = (unsigned char)i; + return 0; +} + +static void +test_call_block(void) +{ + unsigned char raw[4096 + 16]; + void *far; + size_t extent; + unsigned char pattern = 0x5a; + + assert(stackman_block_call(raw, sizeof raw, &far, &extent) == 1); + assert(extent > 256 + 64); + + assert(stackman_guard_paint(far, extent, 64, pattern) == 64); + assert(stackman_call(call_cb, 0, far) == 0); + assert(stackman_guard_check(far, extent, 64, pattern) == 1); +} +#endif + +int +main(void) +{ + test_math(); +#if STACKMAN_STACK_DIR == 0 + test_call_block(); + printf("test_guard ok\n"); +#else + { + unsigned char raw[128 + STACKMAN_STACK_ALIGN]; + uintptr_t aligned; + unsigned char *base; + void *far; + size_t extent; + + aligned = ((uintptr_t)raw + STACKMAN_STACK_ALIGN - 1) & + ~(uintptr_t)(STACKMAN_STACK_ALIGN - 1); + base = (unsigned char *)aligned; + assert(stackman_block_call(base, 128, &far, &extent) == 1); + assert(far == (void *)base); + assert(extent == 128); + assert(stackman_guard_paint(far, extent, GUARD, 0xa5) == GUARD); + assert(stackman_guard_check(far, extent, GUARD, 0xa5) == 1); + } + printf("test_guard ascending ok\n"); +#endif + return 0; +}