Stack buffer overflow in `metacall.c` — is the 16-argument limit intentional?
Maintainers usually reply within 1 day
@10vulture1005 is already working on this.
Since Oct 11, 2026.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 28/100
Research direction
Start with the argument-filling loop in source/metacall/source/metacall.c (lines 603-635), where a fixed METACALL_ARGS_SIZE array is written using signature_count(s), and compare it with the malloc-based pattern at py_loader_impl.c (268-273). The maintainers have not yet chosen between allocating to fit and checking and failing, and the same pattern appears in about twelve entry points, so get a decision before editing. Done means a chosen approach applied consistently, with a test that passes 17 or more arguments.
Written by the indexing model from the issue text.
Description
🐛 Bug Report
What I'm proposing
I'm not opening a PR for this yet. I found something that looks like a real memory-safety bug, but
I can't tell whether the 16-argument limit is deliberate. So I'd like to ask:
- Is the limit intentional?
- If not, would you accept a fix, and which approach do you prefer?
Once we've agreed on scope, I'm happy to put together a solution.
What's happening
MetaCall lets a C program call functions written in other languages:
metacall_load_from_file("py", "script.py");
metacall("greet", "Alice"); // runs greet("Alice") in the Python script
To make that call, the C code gathers the arguments into an array.
The array is fixed-size:
#define METACALL_ARGS_SIZE 0x10 // 16
void *args[METACALL_ARGS_SIZE]; // holds 16
But the loop filling it uses a different number:
size_t args_count = signature_count(s); // however many params the function declares
for (iterator = 0; iterator < args_count; ++iterator) {
args[iterator] = value_create_bool(...);
}
s is the target function's signature, so args_count is however many parameters it takes.
Nothing compares args_count against METACALL_ARGS_SIZE.
A function declaring 17 parameters runs the loop 17 times, writing one element past the array and
overwriting adjacent stack memory — including the return address.
source/metacall/source/metacall.c:603-635
Why I think it's a bug rather than a designed limit
The limit is published as an API, but never enforced. metacall.h:222-228 exposes a function whose
entire job is to report it:
/**
* @brief
* Amount of function call arguments supported by MetaCall
*/
METACALL_API size_t metacall_args_size(void);
It answers "how many arguments do you support?" — and nothing on the receiving end checks. The
contract is documented, but the callee trusts every caller to honour it.
The same codebase already does this correctly. The Python loader handles the identical count,
three directories away, by allocating to fit (py_loader_impl.c:268-273):
const size_t args_size = signature_count(s);
if (args_size > 0)
{
py_func->values = malloc(sizeof(PyObject *) * args_size);
Same signature_count(s) value, but the buffer is sized to match. To me this reads like the correct
pattern was understood in the codebase and just didn't carry over to the public C layer.
It's easy to trigger on purpose. Not an obscure edge case — a Python function with 17 declared
parameters is enough. The values written past the end are heap pointers from value_create_*, not
zeros.
It doesn't even need a loaded script. metacallfs() takes the count from a parsed JSON array
instead (metacall.c:1359/1377), so a document with a 20+ element array triggers it too.
Scope
Twelve entry points share the pattern — a fixed array filled from signature_count():
| Function | Array | Count | Count comes from |
|---|---|---|---|
metacall |
615 | 621 | signature |
metacallt |
722 | 728 | signature |
metacallt_s |
821 | 821 | signature |
metacallht_s |
927 | 927 | signature |
metacallf |
1243 | 1249 | signature |
metacallfmv |
1430 | 1434 | signature |
metacallfms |
1576 | 1594 | signature |
metacallfmv_await |
1773 | 1773 | signature |
metacallfms_await |
2023 | 2042 | signature |
metacall_register |
1655 | 1663 | caller parameter |
metacallfs |
1359 | 1377 | JSON document |
metacallfs_await |
1919 | 1937 | JSON document |
Possible fixes
I'd rather not pick one unilaterally, since each has a tradeoff:
- Allocate to fit —
malloc()fromsignature_count(s), mirroringpy_loader_impl.c:268-273.
Behaviour-preserving; high-arity calls keep working. - Check and fail — return an error when
args_count > METACALL_ARGS_SIZE. Smaller diff, but it
turns a currently-working high-arity call into a hard failure, so it's an API behaviour change. - Raise the constant — I'd argue against this alone. It doesn't add the missing check, it just
moves the cliff from 16 to 32.
What I've done and haven't
Done: traced the count end-to-end — from PyObject_Size(args) in the Python loader, through
function_create() and signature_create(), to the loop writing into the array. Checked existing
issues first; the nearest are #907/#904 (off-by-one writes in portability_path_get_fullname/
get_directory and adt_set_small_insert), #855/#859, #636, #618, #811, #134 — none touch
metacall.c's argument handling. Searching METACALL_ARGS_SIZE and signature_count turns up
nothing.
Not done: I have not compiled this or run it under ASan, so I'm not claiming to have observed
a crash — this is from reading the code. I'd welcome a pointer to the right test harness, or
corrections if I've misread the intended semantics.
Context
- Repo
metacall/core@8f74e1fe(develop) - Static source review only; no build performed
Questions
- Is the 16-argument limit intentional? If so, is there a requirement that makes a fixed stack array
appropriate here? - If not, is this in scope for a contribution, and do you prefer option 1 or option 2?
- Also spotted:
metacallfmv(1430) andmetacallfmv_await(1773) declareargs
uninitialized and fill it sparsely atargs[signature_get_index(s, key)], so any position not
covered by a caller-supplied key reaches the loader as raw stack contents. This looks independent
of the limit issue — should it be a separate report? - Happy to split this into smaller issues if that's preferred.
- Dominant language
- C
- Stars
- 1.8k
- Forks
- 266
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 14
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from metacall/core
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
metacall/core#964 · 1 comment ·
Maintainers usually reply within 1 day
-
pre-push hook empties VERSION when the clone has no tagsPossibly taken @vedant21-oss claimed this 17 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
metacall/core#939 · 3 comments ·
Maintainers usually reply within 1 day
-
TOCTOU race condition vulnerability!Possibly taken @akshatchitransh claimed this 1 day ago. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 35/100
metacall/core#961 · 2 comments ·
Maintainers usually reply within 1 day
-
bug
Difficulty 3/5 Half a day Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Go Port Race Conditions on MacOSPossibly taken A pull request linked to this issue is open or already merged. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 35/100
metacall/core#942 · 3 comments ·
Maintainers usually reply within 1 day
Similar issues
-
agent:WSL code-review linux pri:low refactoring tooling
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
DOC BUG Missing
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SDL-Hercules-390/hyperion#911 ·
Maintainers usually reply within 1 day
-
build: man page directory doubledPossibly taken @zeusgoose claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
esp-mosaico/esp-mosaico-bsp#40 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 62/100