Skip to content

fix(go/adbc/pkg): cgo handle pattern#4287

Open
zeroshade wants to merge 2 commits intoapache:mainfrom
zeroshade:fix/cgo-handle-pattern
Open

fix(go/adbc/pkg): cgo handle pattern#4287
zeroshade wants to merge 2 commits intoapache:mainfrom
zeroshade:fix/cgo-handle-pattern

Conversation

@zeroshade
Copy link
Copy Markdown
Member

Looking into adbc-drivers/mysql#99 (comment) resulted in finding some of these issues, along with apache/arrow-go#793

Summary

Three bugs found and fixed in the CGO driver template (_tmpl/driver.go.tmpl) and all generated drivers
(flightsql, snowflake, panicdummy).

Bug 1 — off-by-one in exportStringOption (buffer overwrite)

exportStringOption wrote the null terminator to sink[lenWithTerminator]
(= sink[len(val)+1]) instead of sink[len(val)]. When the caller supplied
exactly the minimum buffer size (len(val)+1), this wrote one byte past the
end of the allocated buffer.

Bug 2 — fragile cgo.Handle recovery in Release functions

The four Release functions (ArrayStreamRelease, DatabaseRelease,
ConnectionRelease, StatementRelease) recovered the handle from
private_data using (*(*cgo.Handle)(ptr)) — reinterpreting the
C-allocated uintptr_t wrapper as a *cgo.Handle. This worked by
coincidence (both are uintptr-sized) but was inconsistent with
getFromHandle and relied on an undocumented type-size coincidence.

Introduces handleFromPtr(ptr unsafe.Pointer) cgo.Handle as the single
canonical read-back path, used by both getFromHandle and all Release
functions. The misleading comment claiming the GC would corrupt the handle
is replaced with an accurate explanation of the actual CGO rule being
satisfied.

Bug 3 — unnecessary C allocation for handle storage; wrong Delete ordering

createHandle allocated a uintptr_t via C.calloc to hold the handle's
numeric value, then stored a pointer to that allocation in private_data.
This was not necessary: cgo.Handle is type Handle uintptr — an integer,
not a Go heap pointer — so the CGO checker does not object to storing it
directly in a pointer-sized void* field.

The C allocation is eliminated entirely. createHandle now stores the handle
value directly via unsafe.Pointer(uintptr(hndl)), and handleFromPtr casts
it back with cgo.Handle(uintptr(ptr)). This removes a calloc/free pair
from every New/Release call path and eliminates any possibility of a leak from
an early return between allocation and free.

Additionally, the Release functions were calling h.Delete() before
h.Value() in some paths, which would panic — Delete removes the entry from
the handle map, invalidating any subsequent Value call. The correct sequence
is now applied consistently in all four Release functions:

  1. Nil private_data (idempotence guard for double-release)
  2. h.Value() — extract the Go object while the handle is still live
  3. h.Delete() — remove from the map
  4. Use the extracted object

…e access pattern

Two bugs fixed in the CGO driver template (and all generated drivers):

1. exportStringOption wrote the null terminator to sink[lenWithTerminator]
   (= sink[len(val)+1]) instead of sink[len(val)], causing a one-byte
   buffer overwrite when the caller supplied exactly the minimum buffer size.

2. The Release functions recovered the cgo.Handle from private_data using
   (*(*cgo.Handle)(ptr)), which reinterprets the C-allocated uintptr_t as
   a *cgo.Handle. This worked by coincidence (both are uintptr-sized) but
   was fragile and inconsistent with getFromHandle. Introduce handleFromPtr
   so all read-back paths go through the same explicit uintptr cast, and
   replace the misleading "GC will corrupt the handle" comment with an
   accurate explanation of the CGO pointer rule.
…e ordering

cgo.Handle is a uintptr integer, not a Go pointer, so the CGO checker
does not object to storing it directly in a void* field. No C allocation
is needed; createHandle now packs the handle value into the pointer-sized
private_data field directly and handleFromPtr casts it back.

This removes the calloc/free pair from every New/Release path and
eliminates the risk of a leak if a Release function were to exit early
before reaching the C.free call.

Also fix the ordering in all four Release functions: private_data is
cleared first (so a concurrent caller sees nil and exits early), then
h.Value() extracts the Go object while the handle is still valid, then
h.Delete() removes it from the global map. The previous code called
h.Delete() before h.Value() in some paths, which would panic.
@zeroshade zeroshade requested a review from lidavidm April 30, 2026 17:41
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.

1 participant