diff --git a/.github/workflows/buildimage.yml b/.github/workflows/buildimage.yml index b25c0e49..f44589d5 100644 --- a/.github/workflows/buildimage.yml +++ b/.github/workflows/buildimage.yml @@ -16,7 +16,7 @@ env: jobs: build-osx: name: Build for Darwin x86_64 - runs-on: macos-13 + runs-on: macos-15-intel steps: - name: Install PostgreSQL@14 run: brew install --force postgresql@14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb618dc..fce3c961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v2.1.5 [2026-02-06] +_Bug fixes_ +- Fix memory leaks caused by unfreed `C.CString()` allocations across multiple functions. ([#618](https://github.com/turbot/steampipe-postgres-fdw/pull/618), [#620](https://github.com/turbot/steampipe-postgres-fdw/pull/620), [#622](https://github.com/turbot/steampipe-postgres-fdw/pull/622), [#624](https://github.com/turbot/steampipe-postgres-fdw/pull/624), [#631](https://github.com/turbot/steampipe-postgres-fdw/pull/631)) + ## v2.1.4 [2025-11-20] _Dependencies_ - Upgraded dependencies to remediate vulnerabilities. diff --git a/explain.go b/explain.go index 01286639..51677b09 100644 --- a/explain.go +++ b/explain.go @@ -6,6 +6,7 @@ package main #include "fdw_helpers.h" */ import "C" +import "unsafe" // Explainable is an optional interface for Iterator that can explain it's execution plan. type Explainable interface { @@ -20,5 +21,9 @@ type Explainer struct { // Property adds a key-value property to results of EXPLAIN query. func (e Explainer) Property(k, v string) { - C.ExplainPropertyText(C.CString(k), C.CString(v), e.ES) + ck := C.CString(k) + cv := C.CString(v) + defer C.free(unsafe.Pointer(ck)) + defer C.free(unsafe.Pointer(cv)) + C.ExplainPropertyText(ck, cv, e.ES) } diff --git a/fdw.go b/fdw.go index 5626ed58..6e721301 100644 --- a/fdw.go +++ b/fdw.go @@ -268,9 +268,11 @@ func goFdwExplainForeignScan(node *C.ForeignScanState, es *C.ExplainState) { //export goFdwBeginForeignScan func goFdwBeginForeignScan(node *C.ForeignScanState, eflags C.int) { + // Outer recover: catches panics during early initialization (before inner defer is registered) + // This provides defense-in-depth - if the inner defer's recovery fails, this catches it defer func() { if r := recover(); r != nil { - log.Printf("[WARN] goFdwExplainForeignScan failed with panic: %v", r) + log.Printf("[WARN] goFdwBeginForeignScan failed with panic during early init: %v", r) FdwError(fmt.Errorf("%v", r)) } }() @@ -285,6 +287,7 @@ func goFdwBeginForeignScan(node *C.ForeignScanState, eflags C.int) { log.Printf("[INFO] goFdwBeginForeignScan, connection '%s', table '%s', explain: %v \n", opts["connection"], opts["table"], explain) + // Inner recover: catches panics during main scan processing defer func() { if r := recover(); r != nil { log.Printf("[WARN] goFdwBeginForeignScan failed with panic: %v", r) diff --git a/helpers.go b/helpers.go index c7aef29d..4f3d1333 100644 --- a/helpers.go +++ b/helpers.go @@ -154,7 +154,11 @@ func ValToDatum(val interface{}, cinfo *C.ConversionInfo, buffer C.StringInfo) ( } }() // init an empty return result - datum := C.fdw_cStringGetDatum(C.CString("")) + // Allocate CString, use it, then immediately free to avoid memory leak + // Using explicit C.free() instead of defer because this is a hot path + emptyStr := C.CString("") + datum := C.fdw_cStringGetDatum(emptyStr) + C.free(unsafe.Pointer(emptyStr)) // write value into C buffer if err := valToBuffer(val, cinfo.atttypoid, buffer); err != nil { @@ -197,7 +201,12 @@ func valToBuffer(val interface{}, oid C.Oid, buffer C.StringInfo) (err error) { } C.resetStringInfo(buffer) - C.fdw_appendBinaryStringInfo(buffer, C.CString(valueString), C.int(len(valueString))) + // Allocate CString, use it, then immediately free to avoid memory leak + // Using explicit C.free() instead of defer because this is a hot path + // called for every string column in every row + cValueString := C.CString(valueString) + C.fdw_appendBinaryStringInfo(buffer, cValueString, C.int(len(valueString))) + C.free(unsafe.Pointer(cValueString)) return } diff --git a/schema.go b/schema.go index c3f00dc5..6d246a4a 100644 --- a/schema.go +++ b/schema.go @@ -66,7 +66,13 @@ func SchemaToSql(schema map[string]*proto.TableSchema, stmt *C.ImportForeignSche } log.Printf("[TRACE] SQL %s", sql) - commands = C.lappend(commands, unsafe.Pointer(C.CString(sql))) + // Use pstrdup to allocate in PostgreSQL memory context. + // This ensures the string is freed when the memory context is destroyed, + // avoiding a memory leak from C.CString which allocates on the C heap. + cSql := C.CString(sql) + pSql := C.pstrdup(cSql) + C.free(unsafe.Pointer(cSql)) + commands = C.lappend(commands, unsafe.Pointer(pSql)) } return commands