Skip to content

Refactor/safe memory wrappers#13

Open
map588 wants to merge 5 commits intomainfrom
refactor/safe-memory-wrappers
Open

Refactor/safe memory wrappers#13
map588 wants to merge 5 commits intomainfrom
refactor/safe-memory-wrappers

Conversation

@map588
Copy link
Copy Markdown
Owner

@map588 map588 commented Apr 15, 2026

No description provided.

map588 and others added 4 commits April 5, 2026 16:03
…row to platform.h

Centralize heap memory management into four safe wrappers alongside
existing safe_realloc:

- safe_free(ptr): frees and NULLs any pointer to prevent double-free
- safe_str_free(&str): frees const char* with NULL-out (replaces free((void*)str))
- safe_buf_free(buf, &count): frees array and zeros its count
- safe_grow(arr, n, cap, factor): one-line capacity-doubling realloc

Applied across cypher.c, store.c, mcp.c, and pass_githistory.c,
eliminating ~60 lines of repetitive free/grow boilerplate.
Copilot AI review requested due to automatic review settings April 15, 2026 18:12
@map588
Copy link
Copy Markdown
Owner Author

map588 commented Apr 15, 2026

@copilot resolve the merge conflicts in this pull request

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add safe memory wrapper functions to platform.h

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add four safe memory wrapper functions to centralize heap management
• safe_free() prevents double-free by NULLing pointers after freeing
• safe_str_free() safely frees const char* without casting
• safe_grow() macro simplifies dynamic array capacity doubling
• Replace ~60 lines of repetitive free/realloc boilerplate across codebase
Diagram
flowchart LR
  A["platform.h<br/>New Wrappers"] -->|safe_free| B["cypher.c"]
  A -->|safe_str_free| B
  A -->|safe_grow| B
  A -->|safe_buf_free| C["store.c"]
  A -->|safe_grow| C
  A -->|safe_grow| D["mcp.c"]
  A -->|safe_grow| E["pass_githistory.c"]
  B -->|Eliminate<br/>boilerplate| F["Cleaner<br/>Code"]
  C -->|Eliminate<br/>boilerplate| F
  D -->|Eliminate<br/>boilerplate| F
  E -->|Eliminate<br/>boilerplate| F
Loading

Grey Divider

File Changes

1. src/foundation/platform.h ✨ Enhancement +42/-0

Introduce four safe memory management wrappers

• Add safe_free_impl() inline function with macro wrapper to free and NULL any pointer
• Add safe_str_free() inline function to safely free const char* pointers
• Add safe_buf_free_impl() with macro to free buffers and zero their count
• Add safe_grow() macro for capacity-doubling realloc with growth factor parameter

src/foundation/platform.h


2. src/cypher/cypher.c ✨ Enhancement +129/-156

Refactor to use safe memory wrapper functions

• Replace free((void*)ptr) calls with safe_str_free(&ptr) for const char* strings
• Replace free(ptr) calls with safe_free(ptr) for general pointers
• Replace manual capacity-doubling logic with safe_grow() macro calls
• Applied across lexer, parser, expression, pattern, and result handling functions

src/cypher/cypher.c


3. src/store/store.c ✨ Enhancement +189/-252

Refactor store.c to use safe memory wrappers

• Replace free((void*)ptr) with safe_str_free(&ptr) for string fields
• Replace free(ptr) with safe_free(ptr) throughout memory cleanup
• Replace manual capacity checks with safe_grow() macro in query result collection
• Applied in store initialization, search, schema, architecture, and traversal functions

src/store/store.c


View more (2)
4. src/mcp/mcp.c ✨ Enhancement +1/-4

Use safe_grow macro for grep matches

• Replace manual capacity-doubling logic with safe_grow() macro in grep_match collection

src/mcp/mcp.c


5. src/pipeline/pass_githistory.c ✨ Enhancement +3/-12

Use safe_grow macro for commit array growth

• Replace three instances of manual capacity-doubling with safe_grow() macro
• Applied in git log parsing for commit collection

src/pipeline/pass_githistory.c


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 15, 2026

Code Review by Qodo

🐞 Bugs (1)   📘 Rule violations (0)   📎 Requirement gaps (0)
🐞\ ≡ Correctness (1)

Grey Divider


Action required

1. safe_free void** type-pun 🐞
Description
safe_free casts the address of an arbitrary typed pointer (e.g., cbm_store_t*) to void** and writes
through it, which is undefined behavior under C strict-aliasing/type rules and can miscompile under
optimization. This macro is now used broadly (e.g., store_open_internal uses safe_free(s)), so any
UB here becomes a systemic reliability/correctness risk.
Code

src/foundation/platform.h[R34-42]

+/* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free.
+ * Accepts void** so it works with any pointer type via the macro. */
+static inline void safe_free_impl(void **pp) {
+    if (pp && *pp) {
+        free(*pp);
+        *pp = NULL;
+    }
+}
+#define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr))
Evidence
platform.h defines safe_free as a void** cast of &(ptr) and then dereferences/writes through that
void** in safe_free_impl, which is not a valid way to alias and mutate an object of type T* per C
aliasing rules. The macro is actively used on typed pointers in the codebase (e.g., freeing a
cbm_store_t* in store_open_internal), so the UB is on a hot path.

src/foundation/platform.h[34-42]
src/store/store.c[501-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`safe_free` currently type-puns arbitrary pointer lvalues through a `void**` and writes via that pointer. This is undefined behavior under C strict aliasing and can miscompile under optimization.

### Issue Context
The macro is used widely after this refactor (e.g., `safe_free(s)` in store open failure paths), so any UB in the macro affects many components.

### Fix Focus Areas
- src/foundation/platform.h[34-42]

### Recommended fix
Replace the `safe_free_impl(void **pp)` + `#define safe_free(ptr) ...void** cast...` approach with a macro that frees and NULLs the *original typed lvalue* without reinterpreting it as `void**`.

One portable C approach (requires passing an assignable pointer lvalue) is:
```c
#define safe_free(p) do {
   if ((p) != NULL) {
       free((void *)(p));
       (p) = NULL;
   }
} while (0)
```
If you need single-evaluation semantics for expressions, consider documenting that `safe_free()` must be passed a simple lvalue (no side effects), or (if your toolchain allows) use a compiler extension-based temporary to ensure single evaluation while keeping correct typing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

…ts keeping safe_grow wrappers

Co-authored-by: map588 <122550757+map588@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented Apr 15, 2026

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit 3926ddd. The conflicts in src/cypher/cypher.c were between the safe_grow() macro calls (from this refactor branch) and the inline realloc code (from main). Kept the safe_grow wrappers consistent with the purpose of this branch.

Comment thread src/foundation/platform.h
Comment on lines +34 to +42
/* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free.
* Accepts void** so it works with any pointer type via the macro. */
static inline void safe_free_impl(void **pp) {
if (pp && *pp) {
free(*pp);
*pp = NULL;
}
}
#define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Safe_free void** type-pun 🐞 Bug ≡ Correctness

safe_free casts the address of an arbitrary typed pointer (e.g., cbm_store_t*) to void** and writes
through it, which is undefined behavior under C strict-aliasing/type rules and can miscompile under
optimization. This macro is now used broadly (e.g., store_open_internal uses safe_free(s)), so any
UB here becomes a systemic reliability/correctness risk.
Agent Prompt
### Issue description
`safe_free` currently type-puns arbitrary pointer lvalues through a `void**` and writes via that pointer. This is undefined behavior under C strict aliasing and can miscompile under optimization.

### Issue Context
The macro is used widely after this refactor (e.g., `safe_free(s)` in store open failure paths), so any UB in the macro affects many components.

### Fix Focus Areas
- src/foundation/platform.h[34-42]

### Recommended fix
Replace the `safe_free_impl(void **pp)` + `#define safe_free(ptr) ...void** cast...` approach with a macro that frees and NULLs the *original typed lvalue* without reinterpreting it as `void**`.

One portable C approach (requires passing an assignable pointer lvalue) is:
```c
#define safe_free(p) do {
    if ((p) != NULL) {
        free((void *)(p));
        (p) = NULL;
    }
} while (0)
```
If you need single-evaluation semantics for expressions, consider documenting that `safe_free()` must be passed a simple lvalue (no side effects), or (if your toolchain allows) use a compiler extension-based temporary to ensure single evaluation while keeping correct typing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors memory management across the C codebase by introducing “safe” free/grow helpers in platform.h and updating store/pipeline/cypher/mcp code to use them, aiming to reduce double-free/use-after-free risk and standardize dynamic array growth patterns.

Changes:

  • Add safe_free, safe_str_free, safe_buf_free, and safe_grow helpers to src/foundation/platform.h.
  • Replace many free(...) calls with safe free helpers to NULL-out pointers after freeing.
  • Replace repeated manual “grow + realloc” blocks with safe_grow(...) in several hot paths.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/store/store.c Replaces manual frees/realloc growth with safe_str_free/safe_free and safe_grow across store operations and cleanup helpers.
src/pipeline/pass_githistory.c Uses safe_grow for commit array growth in git history parsing.
src/mcp/mcp.c Uses safe_grow when collecting grep matches.
src/foundation/platform.h Introduces new safe memory helpers/macros used by the refactor.
src/cypher/cypher.c Replaces many frees with safe wrappers and uses safe_grow for parser/executor dynamic arrays.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/foundation/platform.h
Comment on lines +34 to +64
/* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free.
* Accepts void** so it works with any pointer type via the macro. */
static inline void safe_free_impl(void **pp) {
if (pp && *pp) {
free(*pp);
*pp = NULL;
}
}
#define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr))

/* Safe string free: frees a const char* and NULLs it.
* Casts away const so callers don't need the (void*) dance. */
static inline void safe_str_free(const char **sp) {
if (sp && *sp) {
free((void *)*sp);
*sp = NULL;
}
}

/* Safe buffer free: frees a heap array and zeros its element count.
* Use for dynamic arrays paired with a size_t count. */
static inline void safe_buf_free_impl(void **buf, size_t *count) {
if (buf && *buf) {
free(*buf);
*buf = NULL;
}
if (count) {
*count = 0;
}
}
#define safe_buf_free(buf, countp) safe_buf_free_impl((void **)(void *)&(buf), (countp))
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safe_free/safe_buf_free rely on casting the address of an arbitrary pointer object to void ** and then writing through it. This is undefined behavior under C strict-aliasing rules (accessing a T* object via a void* lvalue) and can miscompile at -O2 with strict-aliasing enabled. Prefer a macro that frees the expression and assigns it to NULL directly (requiring an lvalue), or use _Generic to dispatch to type-correct helpers without void ** type punning.

Copilot uses AI. Check for mistakes.
Comment thread src/foundation/platform.h
Comment on lines +66 to +71
/* Safe grow: doubles capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */
#define safe_grow(arr, n, cap, factor) do { \
if ((size_t)(n) >= (size_t)(cap)) { \
(cap) *= (factor); \
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safe_grow breaks if cap is 0: cap *= factor keeps it at 0, and the safe_realloc(..., cap * sizeof(*arr)) path ends up allocating 1 byte (due to safe_realloc's size==0 handling), which is not enough for arr[0]. Either enforce/ASSERT that cap is nonzero before calling, or have safe_grow treat cap==0 as a minimum initial capacity (e.g., set it to 1 or a small default before multiplying).

Suggested change
/* Safe grow: doubles capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOMold memory freed by safe_realloc). */
#define safe_grow(arr, n, cap, factor) do { \
if ((size_t)(n) >= (size_t)(cap)) { \
(cap) *= (factor); \
/* Safe grow: increases capacity and reallocs when count reaches cap.
* If cap is 0, initializes it to 1 so the allocation is large enough
* for at least one element before applying normal growth on later calls.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOMold memory freed by safe_realloc). */
#define safe_grow(arr, n, cap, factor) do { \
if ((size_t)(n) >= (size_t)(cap)) { \
if ((size_t)(cap) == 0) { \
(cap) = 1; \
} else { \
(cap) *= (factor); \
} \

Copilot uses AI. Check for mistakes.
Comment thread src/foundation/platform.h
Comment on lines +66 to +68
/* Safe grow: doubles capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safe_grow doc comment is misleading: it says it “doubles capacity” and “evaluates to the new arr”, but the macro is a statement and the growth factor is caller-provided (not necessarily 2). Updating the comment to match the actual semantics will reduce misuse by future callers.

Suggested change
/* Safe grow: doubles capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOMold memory freed by safe_realloc). */
/* Safe grow: grows capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Statement macro: multiplies cap by growth_factor, then updates arr in place
* via safe_realloc (arr becomes NULL on OOM, and old memory is freed). */

Copilot uses AI. Check for mistakes.
Comment thread src/foundation/platform.h
Comment on lines +34 to +74
/* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free.
* Accepts void** so it works with any pointer type via the macro. */
static inline void safe_free_impl(void **pp) {
if (pp && *pp) {
free(*pp);
*pp = NULL;
}
}
#define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr))

/* Safe string free: frees a const char* and NULLs it.
* Casts away const so callers don't need the (void*) dance. */
static inline void safe_str_free(const char **sp) {
if (sp && *sp) {
free((void *)*sp);
*sp = NULL;
}
}

/* Safe buffer free: frees a heap array and zeros its element count.
* Use for dynamic arrays paired with a size_t count. */
static inline void safe_buf_free_impl(void **buf, size_t *count) {
if (buf && *buf) {
free(*buf);
*buf = NULL;
}
if (count) {
*count = 0;
}
}
#define safe_buf_free(buf, countp) safe_buf_free_impl((void **)(void *)&(buf), (countp))

/* Safe grow: doubles capacity and reallocs when count reaches cap.
* Usage: safe_grow(arr, count, cap, growth_factor)
* Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */
#define safe_grow(arr, n, cap, factor) do { \
if ((size_t)(n) >= (size_t)(cap)) { \
(cap) *= (factor); \
(arr) = safe_realloc((arr), (size_t)(cap) * sizeof(*(arr))); \
} \
} while (0)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platform.h is covered by tests/test_platform.c, but these newly introduced safe-memory helpers (safe_free, safe_str_free, safe_grow, safe_buf_free) don’t appear to have any direct tests. Adding a small test that verifies “free + NULL” behavior and that safe_grow preserves contents/capacity expectations would help prevent regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants