Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<abi>_<compiler>`.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@ 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
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
Expand All @@ -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 $@ $^
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions stackman/stackman_guard.c
Original file line number Diff line number Diff line change
@@ -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;
}
60 changes: 60 additions & 0 deletions stackman/stackman_guard.h
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>

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
139 changes: 139 additions & 0 deletions tests/test_guard.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <stdio.h>
#include <stdint.h>

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;
}
Loading