From 13dbcd42eff173e577d32225df99e485c640de2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:04:33 +0000 Subject: [PATCH 1/4] Initial plan From 1f9a4743e9786cd032e67bdc088ea8e98ce75009 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:36:07 +0000 Subject: [PATCH 2/4] BUG/MEDIUM: ssl: fix wolfSSL use-after-free in X509_STORE_getX_objects wolfSSL's X509_STORE_get0_objects() decrements the reference count of X509 objects in store->objs on every call via X509StoreFreeObjList (when WOLFSSL_SIGNER_DER_CERT is defined). Since haproxy calls X509_STORE_getX_objects() on the same store from multiple places (ssl_ckch.c during initial load, ssl_sock.c:ssl_set_cert_crl_file, and ssl_sock.c:ssl_get_client_ca_file), the X509 objects' reference counts reach zero and get freed while still referenced, causing a heap-use-after-free. Fix this by adding a wolfSSL-specific ha_wolfssl_X509_STORE_get_objects() that bypasses wolfSSL_X509_STORE_get0_objects() entirely, instead building a fresh owned stack directly from store->certs (non-self-signed) and store->trusted (self-signed) with proper X509_up_ref() calls. Also update sk_X509_OBJECT_popX_free to actually free the returned stack for wolfSSL (instead of being a no-op), which properly decrements the reference counts when the caller is done. Agent-Logs-Url: https://github.com/chipitsine/haproxy/sessions/b2212b9c-1328-4a03-9fcc-835e2d2bc7e9 Co-authored-by: chipitsine <2217296+chipitsine@users.noreply.github.com> --- include/haproxy/openssl-compat.h | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/include/haproxy/openssl-compat.h b/include/haproxy/openssl-compat.h index fb859263eb85..81b3e1df9e01 100644 --- a/include/haproxy/openssl-compat.h +++ b/include/haproxy/openssl-compat.h @@ -383,6 +383,75 @@ static inline unsigned long ERR_peek_error_func(const char **func) #if (HA_OPENSSL_VERSION_NUMBER >= 0x40000000L) && !defined(OPENSSL_IS_AWSLC) && !defined(LIBRESSL_VERSION_NUMBER) && !defined(USE_OPENSSL_WOLFSSL) # define X509_STORE_getX_objects(x) X509_STORE_get1_objects(x) # define sk_X509_OBJECT_popX_free(x, y) sk_X509_OBJECT_pop_free(x,y) +#elif defined(USE_OPENSSL_WOLFSSL) +/* wolfSSL's X509_STORE_get0_objects() has a design issue: it decrements + * the reference count of X509 objects cached in store->objs on every call + * (via X509StoreFreeObjList). This causes use-after-free when called multiple + * times on the same store. We bypass it by building a fresh owned stack + * directly from store->certs (non-self-signed) and store->trusted + * (self-signed), with proper reference counting. + */ +static inline STACK_OF(X509_OBJECT) * +ha_wolfssl_X509_STORE_get_objects(X509_STORE *store) +{ + STACK_OF(X509_OBJECT) *ret; + int i, n; + + if (!store) + return NULL; + + ret = wolfSSL_sk_X509_OBJECT_new(); + if (!ret) + return NULL; + + for (i = 0, n = sk_X509_num(store->certs); i < n; i++) { + X509 *x = sk_X509_value(store->certs, i); + X509_OBJECT *obj; + + if (!x) + continue; + obj = X509_OBJECT_new(); + if (!obj) + goto err; + X509_up_ref(x); + obj->type = X509_LU_X509; + obj->data.x509 = x; + if (wolfSSL_sk_X509_OBJECT_push(ret, obj) <= 0) { + /* X509_OBJECT_free calls X509_free(obj->data.x509), + * which undoes the X509_up_ref above. */ + X509_OBJECT_free(obj); + goto err; + } + } + + for (i = 0, n = sk_X509_num(store->trusted); i < n; i++) { + X509 *x = sk_X509_value(store->trusted, i); + X509_OBJECT *obj; + + if (!x) + continue; + obj = X509_OBJECT_new(); + if (!obj) + goto err; + X509_up_ref(x); + obj->type = X509_LU_X509; + obj->data.x509 = x; + if (wolfSSL_sk_X509_OBJECT_push(ret, obj) <= 0) { + /* X509_OBJECT_free calls X509_free(obj->data.x509), + * which undoes the X509_up_ref above. */ + X509_OBJECT_free(obj); + goto err; + } + } + + return ret; + +err: + sk_X509_OBJECT_pop_free(ret, X509_OBJECT_free); + return NULL; +} +# define X509_STORE_getX_objects(x) ha_wolfssl_X509_STORE_get_objects(x) +# define sk_X509_OBJECT_popX_free(x, y) sk_X509_OBJECT_pop_free(x, y) #else # define X509_STORE_getX_objects(x) X509_STORE_get0_objects(x) # define sk_X509_OBJECT_popX_free(x, y) ({}) From 87b20dc054316c38e0e57fa774772116decbd5c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:49:21 +0000 Subject: [PATCH 3/4] BUG/MEDIUM: ssl: fix wolfSSL X509_STORE_getX_objects to not access struct internals The previous fix used store->certs and store->trusted fields that are only present in newer wolfSSL builds, causing compilation errors with the wolfSSL version cached in the CI: 'X509_STORE' {aka 'WOLFSSL_X509_STORE'} has no member named 'certs' Replace the direct struct-field access with a public-API-only approach: call wolfSSL_X509_STORE_get0_objects() to get the current snapshot and build an independent caller-owned copy using X509_up_ref()/X509_CRL_up_ref() for each object. This protects against wolfSSL's destructive rebuild (triggered by WOLFSSL_SIGNER_DER_CERT / --enable-haproxy) where X509StoreFreeObjList frees the previous call's X509 objects, causing use-after-free when callers still hold pointers into the old result. The approach works with both old and new wolfSSL: - Old wolfSSL: X509StoreFreeObjList decrements the X509 ref; our up_ref ensures it stays >= 1 while the copy is live; when the copy is freed via sk_X509_OBJECT_pop_free, the ref drops to 0 and the object is freed correctly. - New wolfSSL (with store->certs/trusted fields): X509StoreFreeObjList NULLs the non-CM cert refs instead of freeing them; our up_refs provide the same protection at no extra cost. Agent-Logs-Url: https://github.com/chipitsine/haproxy/sessions/35e7d3c8-3c1f-4609-beeb-5a96a7887ad0 Co-authored-by: chipitsine <2217296+chipitsine@users.noreply.github.com> --- include/haproxy/openssl-compat.h | 78 +++++++++++++++++++------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/include/haproxy/openssl-compat.h b/include/haproxy/openssl-compat.h index 81b3e1df9e01..5cbab41e56ad 100644 --- a/include/haproxy/openssl-compat.h +++ b/include/haproxy/openssl-compat.h @@ -384,61 +384,77 @@ static inline unsigned long ERR_peek_error_func(const char **func) # define X509_STORE_getX_objects(x) X509_STORE_get1_objects(x) # define sk_X509_OBJECT_popX_free(x, y) sk_X509_OBJECT_pop_free(x,y) #elif defined(USE_OPENSSL_WOLFSSL) -/* wolfSSL's X509_STORE_get0_objects() has a design issue: it decrements - * the reference count of X509 objects cached in store->objs on every call - * (via X509StoreFreeObjList). This causes use-after-free when called multiple - * times on the same store. We bypass it by building a fresh owned stack - * directly from store->certs (non-self-signed) and store->trusted - * (self-signed), with proper reference counting. +/* wolfSSL's X509_STORE_get0_objects() rebuilds store->objs on every call when + * WOLFSSL_SIGNER_DER_CERT is active (enabled by --enable-haproxy), freeing + * the objects from the previous call. Any caller that holds a pointer into + * the old result therefore gets a use-after-free on the next call. + * + * Work around this by wrapping wolfSSL_X509_STORE_get0_objects() to return an + * independent, caller-owned copy: each X509/CRL in the snapshot is protected + * with X509_up_ref/X509_CRL_up_ref so that wolfSSL's internal cleanup cannot + * drop the ref to zero while the copy is still live. Callers must release the + * returned stack with sk_X509_OBJECT_pop_free(..., X509_OBJECT_free). */ static inline STACK_OF(X509_OBJECT) * ha_wolfssl_X509_STORE_get_objects(X509_STORE *store) { + STACK_OF(X509_OBJECT) *orig; STACK_OF(X509_OBJECT) *ret; int i, n; if (!store) return NULL; + orig = wolfSSL_X509_STORE_get0_objects(store); + if (!orig) + return NULL; + ret = wolfSSL_sk_X509_OBJECT_new(); if (!ret) return NULL; - for (i = 0, n = sk_X509_num(store->certs); i < n; i++) { - X509 *x = sk_X509_value(store->certs, i); + n = sk_X509_OBJECT_num(orig); + for (i = 0; i < n; i++) { + X509_OBJECT *src = sk_X509_OBJECT_value(orig, i); X509_OBJECT *obj; + int type; - if (!x) + if (!src) continue; + + type = X509_OBJECT_get_type(src); obj = X509_OBJECT_new(); if (!obj) goto err; - X509_up_ref(x); - obj->type = X509_LU_X509; - obj->data.x509 = x; - if (wolfSSL_sk_X509_OBJECT_push(ret, obj) <= 0) { - /* X509_OBJECT_free calls X509_free(obj->data.x509), - * which undoes the X509_up_ref above. */ + + if (type == X509_LU_X509) { + X509 *x = X509_OBJECT_get0_X509(src); + if (!x) { + X509_OBJECT_free(obj); + continue; + } + X509_up_ref(x); + obj->type = X509_LU_X509; + obj->data.x509 = x; + } + else if (type == X509_LU_CRL) { + X509_CRL *crl = X509_OBJECT_get0_X509_CRL(src); + if (!crl) { + X509_OBJECT_free(obj); + continue; + } + X509_CRL_up_ref(crl); + obj->type = X509_LU_CRL; + obj->data.crl = crl; + } + else { X509_OBJECT_free(obj); - goto err; + continue; } - } - - for (i = 0, n = sk_X509_num(store->trusted); i < n; i++) { - X509 *x = sk_X509_value(store->trusted, i); - X509_OBJECT *obj; - if (!x) - continue; - obj = X509_OBJECT_new(); - if (!obj) - goto err; - X509_up_ref(x); - obj->type = X509_LU_X509; - obj->data.x509 = x; if (wolfSSL_sk_X509_OBJECT_push(ret, obj) <= 0) { - /* X509_OBJECT_free calls X509_free(obj->data.x509), - * which undoes the X509_up_ref above. */ + /* X509_OBJECT_free calls X509_free or X509_CRL_free, + * which undoes the up_ref above. */ X509_OBJECT_free(obj); goto err; } From 7267d036ce2048e10104f92e309e26e92ca99450 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:17:05 +0000 Subject: [PATCH 4/4] BUG/MINOR: ssl: guard X509_CRL_up_ref in wolfSSL wrapper against old builds wolfSSL_X509_CRL_up_ref is guarded by #ifdef OPENSSL_ALL in wolfSSL's src/crl.c, and the corresponding #define X509_CRL_up_ref in wolfssl/openssl/ssl.h was removed between Dec 2025 and Mar 2026 ('remove unimplemented function macro'). A CI build cached during that window therefore sees the symbol as undeclared: error: call to undeclared function 'X509_CRL_up_ref' Wrap the X509_LU_CRL branch in ha_wolfssl_X509_STORE_get_objects() with #ifdef X509_CRL_up_ref so that older wolfSSL builds skip CRL objects (which are rarely if ever present in a wolfSSL CA X509_STORE anyway) while newer builds that have the symbol still copy CRL entries correctly. Agent-Logs-Url: https://github.com/chipitsine/haproxy/sessions/26b1a3e4-deb8-4f64-9458-0ee19b5053e3 Co-authored-by: chipitsine <2217296+chipitsine@users.noreply.github.com> --- include/haproxy/openssl-compat.h | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/include/haproxy/openssl-compat.h b/include/haproxy/openssl-compat.h index 5cbab41e56ad..c73f827f9927 100644 --- a/include/haproxy/openssl-compat.h +++ b/include/haproxy/openssl-compat.h @@ -390,10 +390,12 @@ static inline unsigned long ERR_peek_error_func(const char **func) * the old result therefore gets a use-after-free on the next call. * * Work around this by wrapping wolfSSL_X509_STORE_get0_objects() to return an - * independent, caller-owned copy: each X509/CRL in the snapshot is protected - * with X509_up_ref/X509_CRL_up_ref so that wolfSSL's internal cleanup cannot - * drop the ref to zero while the copy is still live. Callers must release the - * returned stack with sk_X509_OBJECT_pop_free(..., X509_OBJECT_free). + * independent, caller-owned copy: each X509 in the snapshot is protected with + * X509_up_ref so that wolfSSL's internal cleanup cannot drop the ref to zero + * while the copy is still live. CRL objects are included when the build + * provides X509_CRL_up_ref (OPENSSL_ALL or recent wolfSSL); older wolfSSL + * builds that lack the symbol silently omit CRL entries. Callers must release + * the returned stack with sk_X509_OBJECT_pop_free(..., X509_OBJECT_free). */ static inline STACK_OF(X509_OBJECT) * ha_wolfssl_X509_STORE_get_objects(X509_STORE *store) @@ -438,6 +440,10 @@ ha_wolfssl_X509_STORE_get_objects(X509_STORE *store) obj->data.x509 = x; } else if (type == X509_LU_CRL) { +#ifdef X509_CRL_up_ref + /* wolfSSL_X509_CRL_up_ref requires OPENSSL_ALL and was + * absent from older wolfSSL builds; skip CRL objects + * when the symbol is unavailable. */ X509_CRL *crl = X509_OBJECT_get0_X509_CRL(src); if (!crl) { X509_OBJECT_free(obj); @@ -446,6 +452,10 @@ ha_wolfssl_X509_STORE_get_objects(X509_STORE *store) X509_CRL_up_ref(crl); obj->type = X509_LU_CRL; obj->data.crl = crl; +#else + X509_OBJECT_free(obj); + continue; +#endif } else { X509_OBJECT_free(obj);