genbindings: fix use-after-free in virtual-override callbacks returning heap CABI types#338
genbindings: fix use-after-free in virtual-override callbacks returning heap CABI types#3385k3105 wants to merge 3 commits into
Conversation
…ng heap CABI types A virtual override implemented by a Go callback that returns a heap-allocated CABI type (string / []string / QList<T> / map) crashed the C++ caller with std::bad_alloc. The clearest trigger is a custom QAbstractItemModel in a QTreeView with InternalMove drag-drop: drag-enter calls mimeTypes(), whose generated trampoline reads the returned miqt_array of miqt_string AFTER the Go callback has already freed it. Root cause: the virtual-callback emission reused emitParameterGo2CABIForwarding to marshal the RETURN value, and that helper emits `defer C.free(...)`. That lifetime is correct for arguments (freed after the C call returns) but a use-after-free for callback return values — the deferred frees run when the Go callback returns, before the C++ trampoline copies the data. Fix, both halves so ownership transfers cleanly and nothing leaks: - emitgo.go: stripDeferCFree() removes the `defer C.free(...)` lines from the return-value marshaling, so the CABI memory outlives the Go callback's return. - emitcabi.go: emitCABI2CppFreeReturn() frees that CABI memory in the C++ trampoline AFTER copying it into the real Qt return type (per-element data + the top-level buffer), mirroring the heap cases of emitCABI2CppForwarding. Systemic: covers every overridden virtual returning a heap CABI type, not just mimeTypes. Includes a unit test for the Go-side strip; regenerating qt6 produces the expected output on both sides (no defer-free in the Go callback; the free loop + free(callback_return_value.data) in the C++ trampoline).
| // run on THIS function's return — before the C++ caller reads the data — | ||
| // a use-after-free (e.g. QAbstractItemModel::mimeTypes -> qBadAlloc). | ||
| // Strip them so the memory outlives the return; the C++ trampoline frees. | ||
| binding = stripDeferCFree(binding) |
There was a problem hiding this comment.
Q: Instead of generating this code and then stripping it out, how about adding a parameter to emitParameterGo2CABIForwarding that controls whether the C.free call is added?
|
Fixes: #86 |
|
Thank you for the PR and for taking a look at the problem! It's a real bug in miqt. A passing CI will require the branch to include a committed, regenerated genbindings output - it should be possible to use the makefile's It looks like AI tools were used to help make this PR. That's not a problem necessarily. But can I ask what AI tools are you using? |
|
opus 4.8 |
emitCABI2CppFreeReturn() emitted the element cast + for-loop unconditionally. For QList<T> returns whose element has no per-element free (borrowed pointer types such as QModelIndex*), the inner free is empty, so the generated C++ trampoline contained a do-nothing loop plus an unused `<T>* ..._arr` local (-Wunused-variable). Only emit the per-element loop when there is actually an element free to do; always emit the top-level free(callback_return_value.data). No behavioural change — purely drops dead code from the generated output. Follow-up to 8c8efab (virtual-callback UAF fix).
|
Done — pushed the regenerated bindings plus a small follow-up. The branch now has three commits:
That should get CI green now. Thanks for the quick review! |
In plain English
If you subclass a Qt virtual method in Go and that method returns a heap type
(a
string,[]string,QList<T>, or a map), the binding frees the memory amoment too early — so C++ reads memory that's already been freed. The most
visible symptom: a custom
QAbstractItemModelin aQTreeViewwithInternalMovedrag-drop crashes the instant you start a drag(
std::bad_alloc). AQStandardItemModel-backed view is fine, because its dragpath is pure C++ with no Go callback.
The cause is a one-line lifetime mistake in the code generator: when a Go
callback hands its return value back to C++, the generated Go code adds a
defer C.free(...). For a normal argument that's correct (free it after theC call). For a return value it's wrong — the
deferruns the moment the Gocallback returns, which is before C++ has copied the data out. C++ then reads
freed memory.
The fix moves the free from the wrong place to the right place. Two small,
coordinated changes:
defer C.free(...)onthe return value, so the memory survives until C++ has it.
that memory after it's been copied into the real Qt type.
Change 1 alone stops the crash but would leak the memory; change 2 reclaims it.
Together: no crash, no leak. This is systemic — it covers every overridden
virtual that returns a heap type, not just
mimeTypes(which is simply the onethe drag-drop path happens to call first).
Details
Reproduction
A custom
QAbstractItemModelin aQTreeViewwithInternalMovedrag-dropcrashes with
std::bad_alloc/SIGABRTthe moment a drag starts. Tested withmiqt v0.14.0 (qt6), Qt 6.11.1, Go 1.26, linux/amd64.
Drag any row → crash.
C++ backtrace (gdb
catch throw)On drag-enter the view calls the model's
mimeTypes(). The generated C++trampoline reads the
miqt_arrayofmiqt_stringreturned by the Go callbackand does
QString::fromUtf8(arr[i].data, arr[i].len)— butarr[i].lenisgarbage (freed heap), so Qt throws
qBadAlloc.Root cause
In
gen_qabstractitemmodel.go,miqt_exec_callback_QAbstractItemModel_mimeTypes:Generator origin:
cmd/genbindings/emitgo.go, the virtual-callback emissionpath reuses
emitParameterGo2CABIForwardingto marshal the return value —and that helper emits
defer C.free(...). That lifetime is correct forarguments (freed after the C call returns) but a use-after-free for callback
return values (the C++ caller reads them after the Go callback has returned).
Systemic: every overridden virtual returning a heap CABI type —
string,[]string,QList<T>, maps — is affected.mimeTypesis just the oneexercised first by drag-enter.
The fix
emitgo.go—stripDeferCFree()removes thedefer C.free(...)linesfrom the return-value marshaling, so the CABI memory outlives the Go
callback's return.
emitcabi.go—emitCABI2CppFreeReturn()frees that CABI memory in theC++ trampoline after copying it into the real Qt return type (each element's
data+ the top-level buffer), mirroring the heap cases ofemitCABI2CppForwarding.Verification
go build ./cmd/genbindings— passes.go test ./cmd/genbindings -run TestStripDeferCFree— passes (strips exactlythe
defer C.freelines, leaves the allocations intact).as intended:
gen_qabstractitemmodel.go: themimeTypescallback now has zerodefer C.free(was 2).gen_qabstractitemmodel.cpp: themimeTypestrampoline now frees after thecopy —
the minimal repro and a real custom-model app drag/drop cleanly, with
drag-reparent + save persisting correctly, and no leak.
Note on regenerated files
This PR contains the generator change + a unit test; I haven't committed the
regenerated
gen_*for the whole tree. My environment (Qt 6.11 + clang 22 vsmiqt's pinned Qt 6.9 + clang 18) needs allowlist tweaks for a clean full
regenerate, so I've left the committed-file regeneration to your CI /
miqt-docker. I did run a targeted regenerate of the affected file to confirmthe generator emits the correct output on both sides (shown above).