GOGC dropping pointers allocated in C?

Hi,

I’m trying to get an https server working by overwriting the accept/read/write methods in the http module. I’m using go-wolfssl for TLS.

The server will accept a TLS 1.2 connection, then send some random data to the client connects, then wait for another connection.

The issue that I’m seeing is that whenever the server sends large payloads, it’ll do that successfully but it then can’t send data to the next client that connects (the send errors out with the TCP error EBADF). If dealing with smaller payloads (<30k), the server can send data successfully to each client that connects. Then as soon as a large one is sent, the next transmission fails.

If I disable the GOGC with debug.SetGCPercent(-1), the issue goes away and I can send as many large payloads as required. From the debugging I’ve done so far, this looks like an issue with the GO garbage collector dropping C pointers. go-wolfssl relies on the wolfSSL C library so it uses CGO. Does anyone have any other ideas or input?

Code is below. See this repo to actually run it GitHub - lealem47/go-wolfssl-https-server

Thank you!

package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	log "github.com/sirupsen/logrus"
	wolfSSL "github.com/wolfssl/go-wolfssl"
	"net"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"
)

const defaultPort = "8443"

type wolfSSLListener struct {
	listener net.Listener
	ctx      *wolfSSL.WOLFSSL_CTX
}

// Accept waits for and returns the next connection to the listener.
func (cl *wolfSSLListener) Accept() (net.Conn, error) {
	conn, err := cl.listener.Accept()
	if err != nil {
		return nil, err
	}
	fmt.Println("Accepted new connection from:", conn.RemoteAddr())

	ssl := wolfSSL.WolfSSL_new(cl.ctx)
	if ssl == nil {
		fmt.Println("WolfSSL_new Failed")
		os.Exit(1)
	}

	file, err := conn.(*net.TCPConn).File()
	if err != nil {
		panic(err)
	}
	fd := file.Fd()
	wolfSSL.WolfSSL_set_fd(ssl, int(fd))

	ret := wolfSSL.WolfSSL_accept(ssl)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("WolfSSL_accept error ", ret)
	} else {
		fmt.Println("Client Successfully Connected!")
	}

	return &wolfSSLConn{
		conn: conn,
		ssl:  ssl,
	}, nil
}

// Close closes the listener, making it stop accepting new connections.
func (cl *wolfSSLListener) Close() error {
	fmt.Println("Closing listener...")
	return cl.listener.Close()
}

// Addr returns the listener's network address.
func (cl *wolfSSLListener) Addr() net.Addr {
	return cl.listener.Addr()
}

type wolfSSLConn struct {
	conn   net.Conn
	ssl    *wolfSSL.WOLFSSL
	buffer bytes.Buffer
	mu     sync.Mutex
	closed bool
}

func (w *wolfSSLConn) Read(b []byte) (int, error) {
	log.Infof("Calling read: %d", len(b))

	ret := wolfSSL.WolfSSL_read(w.ssl, b, uintptr(len(b)))
	if ret < 0 {
		errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
		return 0, fmt.Errorf("read error: %d", errCode)
	}

	log.Infof("Read bytes: %s", string(b[:ret]))
	return int(ret), nil
}

func (w *wolfSSLConn) Write(b []byte) (int, error) {
	log.Infof("Calling write: %d", len(b))

	sz := uintptr(len(b))

	ret := wolfSSL.WolfSSL_write(w.ssl, b, sz)
	if ret < 0 {
		errCode := wolfSSL.WolfSSL_get_error(w.ssl, int(ret))
		return 0, fmt.Errorf("write error: %d", errCode)
	}

	return int(ret), nil
}

func (w *wolfSSLConn) Close() error {
	log.Infof("Closing connection")

	wolfSSL.WolfSSL_shutdown(w.ssl)
	wolfSSL.WolfSSL_free(w.ssl)
	return w.conn.Close()
}

func (w *wolfSSLConn) LocalAddr() net.Addr {
	return w.conn.LocalAddr()
}

func (w *wolfSSLConn) RemoteAddr() net.Addr {
	return w.conn.RemoteAddr()
}

func (w *wolfSSLConn) SetDeadline(t time.Time) error {
	return w.conn.SetDeadline(t)
}

func (w *wolfSSLConn) SetReadDeadline(t time.Time) error {
	return w.conn.SetReadDeadline(t)
}

func (w *wolfSSLConn) SetWriteDeadline(t time.Time) error {
	return w.conn.SetWriteDeadline(t)
}

// Handler for generating and base64 encoding 5KB of random data
func randomDataHandler(w http.ResponseWriter, r *http.Request) {
	// Get the "size" query parameter from the request
	sizeParam := r.URL.Query().Get("size")
	size := 500000 // default size

	// If the "size" parameter is provided, convert it to an integer
	if sizeParam != "" {
		parsedSize, err := strconv.Atoi(sizeParam)
		if err != nil || parsedSize <= 0 {
			http.Error(w, "Invalid size parameter", http.StatusBadRequest)
			return
		}
		size = parsedSize
	}

	// Generate random data of the specified size
	data := make([]byte, size)
	_, err := rand.Read(data)
	if err != nil {
		http.Error(w, "Could not generate random data", http.StatusInternalServerError)
		return
	}

	// Base64 encode the random data
	encodedData := base64.StdEncoding.EncodeToString(data)

	// Set content type and write the base64 encoded data
	w.Header().Set("Content-Type", "application/base64")
	w.Write([]byte(encodedData))
}

func main() {
	port := defaultPort

	// Set logging level
	log.SetLevel(log.InfoLevel)

	log.SetFormatter(&log.TextFormatter{
		DisableColors: false,
		FullTimestamp: true,
	})

	// Set up the HTTP server and routes
	http.HandleFunc("/", randomDataHandler)

	CERT_FILE := "./certs/server-cert.pem"
	KEY_FILE := "./certs/server-key.pem"

	/* Initialize wolfSSL */
	wolfSSL.WolfSSL_Init()

	/* Create WOLFSSL_CTX with tlsv12 */
	ctx := wolfSSL.WolfSSL_CTX_new(wolfSSL.WolfTLSv1_2_server_method())
	if ctx == nil {
		fmt.Println(" WolfSSL_CTX_new Failed")
		os.Exit(1)
	}

	/* Load server certificates into WOLFSSL_CTX */
	ret := wolfSSL.WolfSSL_CTX_use_certificate_file(ctx, CERT_FILE, wolfSSL.SSL_FILETYPE_PEM)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("Error: WolfSSL_CTX_use_certificate Failed")
		os.Exit(1)
	}

	/* Load server key into WOLFSSL_CTX */
	ret = wolfSSL.WolfSSL_CTX_use_PrivateKey_file(ctx, KEY_FILE, wolfSSL.SSL_FILETYPE_PEM)
	if ret != wolfSSL.WOLFSSL_SUCCESS {
		fmt.Println("Error: WolfSSL_CTX_use_PrivateKey Failed")
		os.Exit(1)
	}

	baseListener, err := net.Listen("tcp", ":"+port)
	if err != nil {
		fmt.Println("Error starting listener:", err)
		return
	}
	defer baseListener.Close()

	wolfSSLListener := &wolfSSLListener{
		listener: baseListener,
		ctx:      ctx,
	}

	log.Printf("Server listening on https://localhost:%s", port)
	err = http.Serve(wolfSSLListener, nil)
	if err != nil {
		fmt.Println("Error starting HTTP server:", err)
	}
}

cgo will not recycle c pointers at will. You can try to create a large number of c variables and then observe whether they will be recycled.