cmakefilefreebsd

Bug only manifests on FreeBSD, but works perfectly fine on Windows, Linux, and MacOS


Here's the relevant part of the code from an arena allocator:

#ifdef DEBUG
    #include <string.h>
    #define D(x) x
#else
    #define D(x) (void) 0
#endif

/* Allocates a pointer from `arena`.
 *
 * The allocated pointer is at least aligned to `alignment`.
 *
 * `alignment` must be a power of 2.
 * 
 * `size` must be a multiple of `alignment`.
 *
 * If a request can not be entertained, i.e. would overflow, or `arena` is full,
 * the function returns `nullptr`. The function also returns a `nullptr` if the 
 * requested `size` or `alignment` is 0 or if `alignment` is not a power of 2, 
 * or if `size` is not a multiple of `alignment`.
 *
 * Any allocations made prior to this call are not freed on failure, and remain 
 * valid until the arena is either reset or destroyed. */
void *arena_alloc(Arena *arena, size_t alignment, size_t size)
{
    if (size == 0
        || alignment == 0 || (alignment != 1 && !is_power_of_two(alignment))
        || !is_multiple_of(size, alignment)) {
        return nullptr;
    }

    M_Pool *curr_pool = arena->pools[arena->current - 1];
    uint8_t *const p = curr_pool->buf + curr_pool->offset;
    const uintptr_t original = ((uintptr_t) p);

    if (original > UINTPTR_MAX - alignment) {
        return nullptr;
    }

    const uintptr_t remain = original & (alignment - 1);
    const uintptr_t aligned =
        remain != 0 ? original + (alignment - remain) : original;
    const size_t offset = aligned - original;

    if (size > SIZE_MAX - offset) {
        return nullptr;
    }

    size += offset;

    if (size > curr_pool->buf_len - curr_pool->offset) {
        return nullptr;
    }

    /* Set the optional padding for alignment immediately before a user block, 
     * and the bytes immediately following such a block to non-zero. 
     * The intent is to trigger OBOB failures to inappropiate app use of 
     * strlen()/strnlen(), which keep forging ahead till encountering ascii NUL. */
    D(
        /* 0xA5 is used in FreeBSD's PHK malloc for debugging purposes. */
        if (remain) {
            memset(p + (alignment - remain), 0xA5, alignment - remain);
        }
    );

    curr_pool->offset += size;
    D(memset(curr_pool->buf + curr_pool->offset, 0xA5,
            curr_pool->buf_len - curr_pool->offset));

    arena->last_alloc_size = size;

    /* Equal to "aligned", but preserves provenance. */
    return p + offset;
}

And here are the unit tests for this function:

static void test_arena_alloc(void)
{
    Arena *const arena = arena_new(nullptr, 100);

    TEST_ASSERT(arena);

    TEST_CHECK(arena_alloc(arena, 1, 112) == nullptr);
    TEST_CHECK(arena_alloc(arena, 0, 1) == nullptr);
    TEST_CHECK(arena_alloc(arena, 1, 0) == nullptr);
    TEST_CHECK(arena_alloc(arena, 2, 5) == nullptr);
    TEST_CHECK(arena_alloc(arena, 3, 5) == nullptr);

    TEST_CHECK(arena_alloc(arena, 1, 95));
    uint8_t *const curr_pool = arena->pools[0]->buf;

    TEST_CHECK(curr_pool[96] == 0xA5 && curr_pool[97] == 0xA5
        && curr_pool[98] == 0xA5 && curr_pool[99] == 0xA5);

    arena_reset(arena);

#ifdef HAVE_STDALIGN_H
    const int *const a = arena_alloc(arena, alignof (int), 5 * sizeof *a);
    const double *const b = arena_alloc(arena, alignof (double), 2 * sizeof *b);
    const char *const c = arena_alloc(arena, 1, 10);
    const short *const d = arena_alloc(arena, alignof (short), 5 * sizeof *d);

    TEST_CHECK(a && is_aligned(a, alignof (int)));
    TEST_CHECK(b && is_aligned(b, alignof (double)));
    TEST_CHECK(c && is_aligned(c, 1));
    TEST_CHECK(d && is_aligned(d, alignof (short)));
#endif
    arena_destroy(arena);
}

On Linux (Ubuntu and Linux Mint), Windows 10 and Windows 11, and MacOS (macos-latest, whatever version Github Actions use) all of these pass without a problem.

cc -std=c11 -fPIC -Wall -Wextra -Werror -Wwrite-strings -Wno-unused-variable -Wno-parentheses -Wpedantic -Warray-bounds -Wno-unused-function -Wstrict-prototypes -Wdeprecated -DDEBUG    tests.c   -o tests
./tests arena_alloc --verbose=3
Test arena_alloc:
  tests.c:92: arena... ok
  tests.c:94: arena_alloc(arena, 1, 112) == nullptr... ok
  tests.c:95: arena_alloc(arena, 0, 1) == nullptr... ok
  tests.c:96: arena_alloc(arena, 1, 0) == nullptr... ok
  tests.c:97: arena_alloc(arena, 2, 5) == nullptr... ok
  tests.c:98: arena_alloc(arena, 3, 5) == nullptr... ok
  tests.c:100: arena_alloc(arena, 1, 95)... ok
  tests.c:103: curr_pool[96] == 0xA5 && curr_pool[97] == 0xA5 && curr_pool[98] == 0xA5 && curr_pool[99] == 0xA5... ok
  tests.c:125: a && is_aligned(a, alignof (int))... ok
  tests.c:126: b && is_aligned(b, alignof (double))... ok
  tests.c:127: c && is_aligned(c, 1)... ok
  tests.c:128: d && is_aligned(d, alignof (short))... ok
  SUCCESS: All conditions have passed.

Summary:
  Count of run unit tests:           1
  Count of successful unit tests:    1
  Count of failed unit tests:        0
SUCCESS: No unit tests have failed.

Yet if I compile this code on FreeBSD, one test fail:

Test arena_alloc:
  tests.c:92: arena... ok
  tests.c:94: arena_alloc(arena, 1, 112) == nullptr... ok
  tests.c:95: arena_alloc(arena, 0, 1) == nullptr... ok
  tests.c:96: arena_alloc(arena, 1, 0) == nullptr... ok
  tests.c:97: arena_alloc(arena, 2, 5) == nullptr... ok
  tests.c:98: arena_alloc(arena, 3, 5) == nullptr... ok
  tests.c:100: arena_alloc(arena, 1, 95)... ok
  tests.c:104: curr_pool[96] == 0xA5 && curr_pool[97] == 0xA5 && curr_pool[98] == 0xA5 && curr_pool[99] == 0xA5... failed
  tests.c:114: a && is_aligned(a, alignof (int))... ok
  tests.c:115: b && is_aligned(b, alignof (double))... ok
  tests.c:116: c && is_aligned(c, 1)... ok
  tests.c:117: d && is_aligned(d, alignof (short))... ok
  FAILED: 1 condition has failed.

If I print out the values of curr_pool[96]-curr_pool[99], all of them are 0, and not 0xA5.

This is the Makefile I am using, which defines the DEBUG flag:

CFLAGS += -std=c11
CFLAGS += -fPIC
CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -Werror
CFLAGS += -Wwrite-strings
CFLAGS += -Wno-unused-variable
CFLAGS += -Wno-parentheses
CFLAGS += -Wpedantic
CFLAGS += -Warray-bounds
CFLAGS += -Wno-unused-function
CFLAGS += -Wstrict-prototypes
CFLAGS += -Wdeprecated


TARGET := arena
TEST_TARGET := tests
SLIB_TARGET := libarena.a
DLIB_TARGET := libarena.so

RM := /bin/rm -f

release: CFLAGS += -O2 -s -DTEST_MAIN
release: $(TARGET)

debug: CFLAGS += -DTEST_MAIN -DDEBUG -g3 -ggdb -fsanitize=address,leak,undefined
debug: $(TARGET)

static: $(SLIB_TARGET)

$(SLIB_TARGET): $(TARGET).o
    $(AR) rcs $@ $^

shared: $(DLIB_TARGET)
shared: LDFLAGS += -shared

$(DLIB_TARGET): $(TARGET).o
    $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

test: CFLAGS += -DDEBUG
test: $(TEST_TARGET)
    ./$(TEST_TARGET) --verbose=3

clean: 
    $(RM) $(TARGET) $(TEST_TARGET) $(TARGET).o $(SLIB_TARGET) $(DLIB_TARGET)

.PHONY: release debug static shared test clean
.DELETE_ON_ERROR:

The tests were compiled with:

make test

If anything's missing, or more code is required, kindly ask in the comments. Though, the whole code is present at: arena, assuming someone wants to compile it.

uname -a shows FreeBSD 14.0.


Solution

    1. BSD make does not support target-specific variable assignments such as:

      release: CFLAGS += -O2 -s -DTEST_MAIN
      

      One way around that is to pass the extra settings to a sub-make in a variable:

      CFLAGS += $(EXTRA_CFLAGS)
      
      release:
          $(MAKE) EXTRA_CFLAGS="-O2 -s -DTEST_MAIN" $(TARGET)
      
    2. BSD make does not support the $^ automatic variable, so the prerequisites need to be included explicitly in the command. For example, the original:

      $(SLIB_TARGET): $(TARGET).o
          $(AR) rcs $@ $^
      

      needs to be changed to:

      $(SLIB_TARGET): $(TARGET).o
          $(AR) rcs $@ $(TARGET).o
      

    After the adjustments, the Makefile may end up something like this (I also needed to add -fsanitize=address when building $(TARGET) from $(TARGET).o when using the debug options):

    CFLAGS += -std=c11
    CFLAGS += -fPIC
    CFLAGS += -Wall
    CFLAGS += -Wextra
    CFLAGS += -Werror
    CFLAGS += -Wwrite-strings
    CFLAGS += -Wno-unused-variable
    CFLAGS += -Wno-parentheses
    CFLAGS += -Wpedantic
    CFLAGS += -Warray-bounds
    CFLAGS += -Wno-unused-function
    CFLAGS += -Wstrict-prototypes
    CFLAGS += -Wdeprecated
    
    # Add extra flags from parent $(MAKE)
    CFLAGS += $(EXTRA_CFLAGS)
    LDFLAGS += $(EXTRA_LDFLAGS)
    
    
    TARGET := arena
    TEST_TARGET := tests
    SLIB_TARGET := libarena.a
    DLIB_TARGET := libarena.so
    
    RM := /bin/rm -f
    
    release:
        $(MAKE) EXTRA_CFLAGS="-O2 -s -DTEST_MAIN" $(TARGET)
    
    debug:
        $(MAKE) EXTRA_CFLAGS="-DTEST_MAIN -DDEBUG -g3 -ggdb -fsanitize=address,leak,undefined" \
            EXTRA_LDFLAGS="-fsanitize=address" $(TARGET)
    
    static: $(SLIB_TARGET)
    
    $(SLIB_TARGET): $(TARGET).o
        $(AR) rcs $@ $(TARGET).o
    
    shared: $(DLIB_TARGET)
    
    $(DLIB_TARGET): $(TARGET).o
        $(CC) $(CFLAGS) $(TARGET).o -o $@ $(LDFLAGS) -shared
    
    test:
        $(MAKE) EXTRA_CFLAGS="-DDEBUG" $(TEST_TARGET)
        ./$(TEST_TARGET) --verbose=3
    
    clean: 
        $(RM) $(TARGET) $(TEST_TARGET) $(TARGET).o $(SLIB_TARGET) $(DLIB_TARGET)
    
    .PHONY: release debug static shared test clean
    .DELETE_ON_ERROR:
    

    Other problems are due to the conditional aspects of the build environment. I recommend changing it so that the release and debug built object files and target executable have different names (or at least different paths), so that both can be built without interfering with each other. Similarly, the code conditionally compiled with -DTEST_MAIN should be changed so that the code is placed in a separate object file than the one that is placed in the library, otherwise the main function and other stuff could and up included in the library!