From 961c9a52db75ecabefd8b055eb33ac9a1513f6f4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 27 Jul 2026 18:25:05 +0200 Subject: [PATCH] fix(sys): Enforce CGo safety rules in WinTrust API calls Every single Wintrust struct field is converting a Go pointer to uintptr and storing it. This is the exact pattern the Go spec explicitly calls out as unsafe. The moment the uintptr value is stored, the GC no longer considers the original object reachable through it and is free to collect or move it before WinVerifyTrust reads the field. The KeepAlive calls must come after verifyTrust() returns, not before. Their purpose is to extend the liveness of each object to that point. As an additional improvement, handle CryptCatalogAdminCalcHashFromFileHandle error code and resize the has buffer with the new size if the call fails with ERROR_INSUFFICIENT_BUFFER. --- pkg/sys/trust.go | 59 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/pkg/sys/trust.go b/pkg/sys/trust.go index 7cabeafae..ce28d867a 100644 --- a/pkg/sys/trust.go +++ b/pkg/sys/trust.go @@ -24,6 +24,7 @@ import ( "io" "os" "reflect" + "runtime" "sync" "time" "unsafe" @@ -182,7 +183,11 @@ func (t *WintrustData) VerifyFile(filename string) (SignatureStatus, error) { FilePath: windows.StringToUTF16Ptr(filename), } t.Union = uintptr(unsafe.Pointer(fileinfo)) - return t.verifyTrust() + + status, err := t.verifyTrust() + runtime.KeepAlive(fileinfo) + + return status, err } // VerifyCatalog verifies the trust of the file signed by catalog-based certificate. @@ -192,22 +197,40 @@ func (t *WintrustData) VerifyCatalog( catalogAdmin windows.Handle, catalog CatalogInfo, hash []byte, - hasSize uint32, + hashSize uint32, ) (SignatureStatus, error) { - // tag is a hexadecimal representation of the hash of the file - tag := windows.StringToUTF16Ptr(format.BytesToHex(hash[:hasSize])) + // convert strings to UTF-16 and hold explicit Go references. + // Converting directly to uintptr in the struct literal would + // allow the GC to collect these before WinVerifyTrust reads them + s := format.BytesToHex(hash[:hashSize]) + tag := windows.StringToUTF16Ptr(s) + path := windows.StringToUTF16Ptr(filename) + catinfo := &WintrustCatalogInfo{ Size: uint32(unsafe.Sizeof(WintrustCatalogInfo{})), CatalogFilePath: uintptr(unsafe.Pointer(&catalog.Name[0])), - MemberFilePath: uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(filename))), + MemberFilePath: uintptr(unsafe.Pointer(path)), MemberFile: windows.Handle(fd), MemberTag: uintptr(unsafe.Pointer(tag)), FileHash: uintptr(unsafe.Pointer(&hash[0])), - FileHashSize: hasSize, + FileHashSize: hashSize, CatalogAdmin: catalogAdmin, } t.Union = uintptr(unsafe.Pointer(catinfo)) - return t.verifyTrust() + + status, err := t.verifyTrust() + + // KeepAlive pins every object whose address was passed into + // WinVerifyTrust via a uintptr field. Without these, the GC + // is permitted to collect them as soon as their last Go-visible + // use is seenm which may be before verifyTrust returns + runtime.KeepAlive(tag) + runtime.KeepAlive(path) + runtime.KeepAlive(hash) + runtime.KeepAlive(catalog) + runtime.KeepAlive(catinfo) + + return status, err } // Close disposes state data by specifying the corresponding action @@ -264,8 +287,9 @@ type CatalogInfo struct { // CatalogFile returns the full path to the catalog file. func (c CatalogInfo) CatalogFile() string { - p := (*[unsafe.Sizeof(c.Name) / 2]uint16)(unsafe.Pointer(&c.Name[0])) - return windows.UTF16ToString(p[:]) + n := len(c.Name) / 2 + p := unsafe.Slice((*uint16)(unsafe.Pointer(&c.Name[0])), n) + return windows.UTF16ToString(p) } var isWintrustDLLFound bool @@ -299,11 +323,11 @@ type Cat struct { size uint32 } -const hashSize uint32 = 100 +const initialHashBufSize uint32 = 32 // SHA-1 is 20, SHA-256 is 32; start at 32 // NewCatalog creates an instance of the catalog with default hash size. func NewCatalog() Cat { - return Cat{size: hashSize, hash: make([]byte, hashSize)} + return Cat{size: initialHashBufSize, hash: make([]byte, initialHashBufSize)} } // Open opens the catalog and acquires the hash for the given file. If the @@ -325,9 +349,22 @@ func (c *Cat) Open(filename string) error { &c.size, uintptr(unsafe.Pointer(&c.hash[0])), 0, ) + if err == windows.ERROR_INSUFFICIENT_BUFFER { + // reallocate with the new buffer size, + // rewind the file position and retry + c.hash = make([]byte, c.size) + _, _ = c.file.Seek(0, io.SeekStart) + err = CryptCatalogAdminCalcHashFromFileHandle( + c.admin, + c.file.Fd(), + &c.size, + uintptr(unsafe.Pointer(&c.hash[0])), 0) + } + if err != nil { return err } + // enumerate catalogs that contain the calculated hash. // If no catalogs are found, we can deduce the file is // not catalog signed