Skip to content

FIX: Free exception messages and the reload file name - #5

Merged
Automation51D merged 5 commits into
mainfrom
fix/device-detection/free-exception-messages
Nov 2, 2026
Merged

Automation51D merged 5 commits into
mainfrom
fix/device-detection/free-exception-messages

Conversation

@drasmart

@drasmart drasmart commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Carried over from device-detection-go#102 by James Rosewell, a draft with no review yet, while device-detection-go is being merged into this repository. The branch is brought in as is, with its original commits, so the changes are under device-detection/.


Closes #3

Problem

fiftyoneDegreesExceptionGetMessage allocates its message with the library's Malloc and the caller must free it. Twelve places in dd copied the message into a Go error with C.GoString and never freed it, so every failed call leaked one message. The status message in InitManagerFromFile was freed with MemoryStandardFree rather than the Free that partners Malloc. All thirteen passed the message to fmt.Errorf as its format string, so a % in a data file path was garbled. ReloadFromFile also copied its file name with C.CString and never freed the copy. #3 has the details and a link to each place.

Change

  • dd/exception.go adds messageError, which copies a message the C library allocated into an error with errors.New and then frees it. Go cannot call a C function pointer, so the free goes through freeMessage, a static C function in the cgo preamble that calls fiftyoneDegreesFree.
  • dd/config_hash.go, dd/resource_hash.go and dd/results_hash.go call messageError at the twelve exception sites and the status site. config_hash.go no longer imports fmt.
  • ReloadFromFile frees its copy of the file name.
  • dd/memory.go adds allocatedDuring, which runs a call with the library's memory tracking switched on and returns the bytes still allocated afterwards.
  • dd/exception_test.go adds TestMessageError, which checks that the message for a missing file called missing-%d.hash holds the path unchanged, and TestMessageFreed, which checks that a failed InitManagerFromFile leaves nothing allocated through the exception path (LowMemory) and through the status path (InMemory).

Both tests use only a missing file. Like every test in dd, they need the Lite data file that TestMain looks for.

Verification

Windows 11, Go 1.25.1, MSYS2 UCRT64 gcc 16.2.0, CGO_ENABLED=1, with 51Degrees-LiteV4.1.hash at the repository root and in assets/, where TestMain and the onpremise tests look for it. "Before" is 100f55d and "after" is 4b5857d.

Check Before After
go build -p 2 ./... passes passes
go vet -p 2 ./dd ./onpremise clean clean
go vet -p 2 ./dd ./onpremise with go.mod temporarily set to go 1.24 13 findings, "non-constant format string in call to fmt.Errorf" clean
go test -p 2 -count=1 ./dd passes passes
go test -p 2 -count=1 ./onpremise passes passes
go test -p 2 -count=1 -v -run 'TestMessageError|TestMessageFreed' ./dd TestMessageError fails with missing-%!d(MISSING).hash, TestMessageFreed/exception leaves 218 bytes, TestMessageFreed/status leaves 193 bytes passes
Bytes left after 1,000 failed InitManagerFromFile calls, missing file, LowMemory 218,000 0
Bytes left after 1,000 MatchEvidence calls with freed evidence 195,000 0
Growth in private bytes over 1,000 ReloadFromFile calls with a 65,617 character path 66,797,568 790,528

For the "before" run of the two tests, the four production files were reset to 100f55d and the tests were kept. In the status case the old code did free the message, but with MemoryStandardFree, which the library's tracking does not see, so the tracker still counted its 193 bytes.

The last three rows come from local checks that are not committed. The first two wrap fiftyoneDegreesSetUpMemoryTracking and fiftyoneDegreesUnsetMemoryTracking around the calls. The third reads the process's private bytes through GetProcessMemoryInfo, so it only runs on Windows.

go vet ./... fails at 100f55d on four unkeyed struct literals in examples/uach/uach_test.go, which this change does not touch, so vet was run on the two library packages.

CI

The shared build and test workflow only picks up pull requests that are not drafts, so the build and test matrix has not run on this pull request. The UTM link lint is the only check that runs on a draft.

What remains

jwrosewell and others added 4 commits October 26, 2026 17:53
fiftyoneDegreesExceptionGetMessage allocates its message with the
library's Malloc and the caller must free it. The twelve places that
turn an exception into a Go error copied the message with C.GoString
and never freed it, so every failed call leaked one message. 1,000
calls of MatchEvidence with freed evidence left 195,000 bytes unfreed.

messageError copies a message into an error with errors.New and then
frees it with fiftyoneDegreesFree, which partners the Malloc that
allocated it. fiftyoneDegreesFree is a function pointer, which Go cannot
call, so a static C function in the cgo preamble calls it.

The status message in InitManagerFromFile uses the same helper. It was
freed with MemoryStandardFree, which only matches Malloc while the
library's memory tracking is off.

errors.New replaces fmt.Errorf at all thirteen places because a message
is not a format string. A data file called missing-%d.hash was reported
as missing-%!d(MISSING).hash, and go vet reports the pattern once the go
version in go.mod is 1.24 or later. TestMessageError covers this.
ReloadFromFile copied the file name into C memory with C.CString and
never freed it. 1,000 reloads with a 65,617 character path grew the
process's private bytes by 66,797,568 bytes, and by 675,840 bytes with
the copy freed.
allocatedDuring runs a call with the library's memory tracking switched
on and returns the bytes still allocated afterwards. TestMessageFreed
uses it on InitManagerFromFile with a missing file, once through the
exception path and once through the status path. Before the fix the
exception path left 218 bytes. The status path left 193 bytes because
its message was freed with MemoryStandardFree, which the tracker does
not see, rather than the library's Free.
ReloadFromFile and ReloadFromOriginalFile take main's version, from the
reload fix, which already frees the exception message and the file name and
also reports a failed status.
@drasmart
drasmart marked this pull request as ready for review November 2, 2026 16:32
@Automation51D
Automation51D merged commit e0842ac into main Nov 2, 2026
1 check passed
@drasmart
drasmart deleted the fix/device-detection/free-exception-messages branch November 2, 2026 16:51
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.

Exception messages and the reload file name are never freed

3 participants