Commit 0c2a196af8 for openssl.org

commit 0c2a196af8ab9c12da27826fa6024ca1b22f6db1
Author: Daniel Kubec <kubec@openssl.org>
Date:   Mon Nov 24 02:25:08 2025 +0100

    CONF: Add support for configurations per OSSL_LIB_CTX

    Add support for configurations per OSSL_LIB_CTX and fix cross-context overrides.

    Fixes #19248
    Fixes #19243

    Co-authored-by: Matt Caswell <matt@openssl.org>

    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    Reviewed-by: Matt Caswell <matt@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/29145)

diff --git a/crypto/conf/conf_mod.c b/crypto/conf/conf_mod.c
index 631eb3fec0..3559293f27 100644
--- a/crypto/conf/conf_mod.c
+++ b/crypto/conf/conf_mod.c
@@ -15,6 +15,7 @@
 #include <stdio.h>
 #include <ctype.h>
 #include <openssl/crypto.h>
+#include <openssl/conf.h>
 #include "internal/conf.h"
 #include <openssl/conf_api.h>
 #include "internal/dso.h"
@@ -30,39 +31,6 @@ DEFINE_STACK_OF(CONF_IMODULE)
 #define DSO_mod_init_name "OPENSSL_init"
 #define DSO_mod_finish_name "OPENSSL_finish"

-/*
- * This structure contains a data about supported modules. entries in this
- * table correspond to either dynamic or static modules.
- */
-
-struct conf_module_st {
-    /* DSO of this module or NULL if static */
-    DSO *dso;
-    /* Name of the module */
-    char *name;
-    /* Init function */
-    conf_init_func *init;
-    /* Finish function */
-    conf_finish_func *finish;
-    /* Number of successfully initialized modules */
-    int links;
-    void *usr_data;
-};
-
-/*
- * This structure contains information about modules that have been
- * successfully initialized. There may be more than one entry for a given
- * module.
- */
-
-struct conf_imodule_st {
-    CONF_MODULE *pmod;
-    char *name;
-    char *value;
-    unsigned long flags;
-    void *usr_data;
-};
-
 static CRYPTO_ONCE init_module_list_lock = CRYPTO_ONCE_STATIC_INIT;
 static CRYPTO_RCU_LOCK *module_list_lock = NULL;
 static STACK_OF(CONF_MODULE) *supported_modules = NULL; /* protected by lock */
@@ -447,6 +415,7 @@ static int module_init(CONF_MODULE *pmod, const char *name, const char *value,
     imod->name = OPENSSL_strdup(name);
     imod->value = OPENSSL_strdup(value);
     imod->usr_data = NULL;
+    imod->libctx = NULL;

     if (!imod->name || !imod->value)
         goto memerr;
diff --git a/crypto/conf/conf_ssl.c b/crypto/conf/conf_ssl.c
index 53ebc98c8b..bc1706aca5 100644
--- a/crypto/conf/conf_ssl.c
+++ b/crypto/conf/conf_ssl.c
@@ -11,51 +11,41 @@
 #include <string.h>
 #include <openssl/conf.h>
 #include <openssl/err.h>
-#include "internal/sslconf.h"
 #include "conf_local.h"
+#include "internal/sslconf.h"
+#include "internal/core.h"
+#include "internal/cryptlib.h"

-/*
- * SSL library configuration module placeholder. We load it here but defer
- * all decisions about its contents to libssl.
- */
-
-struct ssl_conf_name_st {
-    /* Name of this set of commands */
-    char *name;
-    /* List of commands */
-    SSL_CONF_CMD *cmds;
-    /* Number of commands */
-    size_t cmd_count;
-};
+typedef struct ssl_module_st SSL_MODULE;

-struct ssl_conf_cmd_st {
-    /* Command */
-    char *cmd;
-    /* Argument */
-    char *arg;
+struct ssl_module_st {
+    struct ssl_conf_name_st *names;
+    size_t names_count;
 };

-static struct ssl_conf_name_st *ssl_names;
-static size_t ssl_names_count;
-
 static void ssl_module_free(CONF_IMODULE *md)
 {
-    size_t i, j;
-    if (ssl_names == NULL)
+    SSL_MODULE *ssl = CONF_imodule_get_usr_data(md);
+
+    if (ssl == NULL)
         return;
-    for (i = 0; i < ssl_names_count; i++) {
-        struct ssl_conf_name_st *tname = ssl_names + i;
+
+    CONF_imodule_set_usr_data(md, NULL);
+    ossl_lib_ctx_detach_ssl_conf_imodule(NULL, md);
+
+    for (size_t i = 0; i < ssl->names_count; i++) {
+        struct ssl_conf_name_st *tname = ssl->names + i;

         OPENSSL_free(tname->name);
-        for (j = 0; j < tname->cmd_count; j++) {
+        for (size_t j = 0; j < tname->cmd_count; j++) {
             OPENSSL_free(tname->cmds[j].cmd);
             OPENSSL_free(tname->cmds[j].arg);
         }
         OPENSSL_free(tname->cmds);
     }
-    OPENSSL_free(ssl_names);
-    ssl_names = NULL;
-    ssl_names_count = 0;
+
+    OPENSSL_free(ssl->names);
+    OPENSSL_free(ssl);
 }

 static int ssl_module_init(CONF_IMODULE *md, const CONF *cnf)
@@ -64,6 +54,8 @@ static int ssl_module_init(CONF_IMODULE *md, const CONF *cnf)
     int rv = 0;
     const char *ssl_conf_section;
     STACK_OF(CONF_VALUE) *cmd_lists;
+    OSSL_LIB_CTX *libctx;
+    SSL_MODULE *ssl = NULL;

     ssl_conf_section = CONF_imodule_get_value(md);
     cmd_lists = NCONF_get_section(cnf, ssl_conf_section);
@@ -78,12 +70,21 @@ static int ssl_module_init(CONF_IMODULE *md, const CONF *cnf)
     }
     cnt = sk_CONF_VALUE_num(cmd_lists);
     ssl_module_free(md);
-    ssl_names = OPENSSL_calloc(cnt, sizeof(*ssl_names));
-    if (ssl_names == NULL)
+
+    ssl = OPENSSL_zalloc(sizeof(*ssl));
+    if (ssl == NULL)
         goto err;
-    ssl_names_count = cnt;
-    for (i = 0; i < ssl_names_count; i++) {
-        struct ssl_conf_name_st *ssl_name = ssl_names + i;
+    CONF_imodule_set_usr_data(md, ssl);
+
+    ssl->names = OPENSSL_calloc(cnt, sizeof(*ssl->names));
+    libctx = ossl_lib_ctx_get_concrete(cnf->libctx);
+    if (libctx == NULL || ssl->names == NULL)
+        goto err;
+
+    ossl_lib_ctx_attach_ssl_conf_imodule(libctx, md);
+    ssl->names_count = cnt;
+    for (i = 0; i < ssl->names_count; i++) {
+        struct ssl_conf_name_st *ssl_name = ssl->names + i;
         CONF_VALUE *sect = sk_CONF_VALUE_value(cmd_lists, (int)i);
         STACK_OF(CONF_VALUE) *cmds = NCONF_get_section(cnf, sect->value);

@@ -135,11 +136,16 @@ static int ssl_module_init(CONF_IMODULE *md, const CONF *cnf)
  * conf_ssl_name_find. Also stores the name of the set of commands in |*name|
  * and the number of commands in the set in |*cnt|.
  */
-const SSL_CONF_CMD *conf_ssl_get(size_t idx, const char **name, size_t *cnt)
+const SSL_CONF_CMD *conf_ssl_get(CONF_IMODULE *md, size_t idx, const char **name, size_t *cnt)
 {
-    *name = ssl_names[idx].name;
-    *cnt = ssl_names[idx].cmd_count;
-    return ssl_names[idx].cmds;
+    SSL_MODULE *ssl = md != NULL ? CONF_imodule_get_usr_data(md): NULL;
+
+    if (ssl == NULL || ssl->names == NULL)
+        return NULL;
+
+    *name = ssl->names[idx].name;
+    *cnt = ssl->names[idx].cmd_count;
+    return ssl->names[idx].cmds;
 }

 /*
@@ -147,14 +153,16 @@ const SSL_CONF_CMD *conf_ssl_get(size_t idx, const char **name, size_t *cnt)
  * index for the command set in |*idx|.
  * Returns 1 on success or 0 on failure.
  */
-int conf_ssl_name_find(const char *name, size_t *idx)
+int conf_ssl_name_find(CONF_IMODULE *md, const char *name, size_t *idx)
 {
-    size_t i;
+    SSL_MODULE *ssl = md != NULL ? CONF_imodule_get_usr_data(md): NULL;
     const struct ssl_conf_name_st *nm;
+    size_t i;

-    if (name == NULL)
+    if (ssl == NULL || ssl->names == NULL)
         return 0;
-    for (i = 0, nm = ssl_names; i < ssl_names_count; i++, nm++) {
+
+    for (i = 0, nm = ssl->names; i < ssl->names_count; i++, nm++) {
         if (strcmp(nm->name, name) == 0) {
             *idx = i;
             return 1;
diff --git a/crypto/context.c b/crypto/context.c
index 80a87562cd..80c29a2161 100644
--- a/crypto/context.c
+++ b/crypto/context.c
@@ -16,14 +16,14 @@
 #include "internal/core.h"
 #include "internal/bio.h"
 #include "internal/provider.h"
-#include "internal/threads_common.h"
+#include "internal/conf.h"
 #include "crypto/decoder.h"
 #include "crypto/context.h"

 struct ossl_lib_ctx_st {
     CRYPTO_RWLOCK *lock;
     OSSL_EX_DATA_GLOBAL global;
-
+    CONF_IMODULE *ssl_imod;
     void *property_string_data;
     void *evp_method_store;
     void *provider_store;
@@ -85,6 +85,34 @@ int ossl_lib_ctx_is_child(OSSL_LIB_CTX *ctx)
     return ctx->ischild;
 }

+int ossl_lib_ctx_attach_ssl_conf_imodule(OSSL_LIB_CTX *ctx, CONF_IMODULE *md)
+{
+    if (ctx == NULL || md == NULL || ctx->ssl_imod != NULL)
+        return 0;
+
+    ctx->ssl_imod = md;
+    md->libctx = ctx;
+    return 1;
+}
+
+int ossl_lib_ctx_detach_ssl_conf_imodule(OSSL_LIB_CTX *ctx, CONF_IMODULE *md)
+{
+    if (ctx != NULL && md != NULL)
+        return 0;
+
+    if (ctx != NULL && ctx->ssl_imod) {
+        ctx->ssl_imod->libctx = NULL;
+        ctx->ssl_imod = NULL;
+    }
+
+    if (md != NULL && md->libctx) {
+        md->libctx->ssl_imod = NULL;
+        md->libctx = NULL;
+    }
+
+    return 1;
+}
+
 static void context_deinit_objs(OSSL_LIB_CTX *ctx);

 static int context_init(OSSL_LIB_CTX *ctx)
@@ -362,6 +390,8 @@ static int context_deinit(OSSL_LIB_CTX *ctx)
     if (ctx == NULL)
         return 1;

+    ossl_lib_ctx_detach_ssl_conf_imodule(ctx, NULL);
+
     ossl_ctx_thread_stop(ctx);

     context_deinit_objs(ctx);
@@ -612,6 +642,9 @@ void *ossl_lib_ctx_get_data(OSSL_LIB_CTX *ctx, int index)
     case OSSL_LIB_CTX_COMP_METHODS:
         return (void *)&ctx->comp_methods;

+    case OSSL_LIB_CTX_SSL_CONF_IMODULE:
+        return (void *)ctx->ssl_imod;
+
     default:
         return NULL;
     }
diff --git a/include/internal/conf.h b/include/internal/conf.h
index 8c6c29cd2c..54794dc620 100644
--- a/include/internal/conf.h
+++ b/include/internal/conf.h
@@ -11,6 +11,9 @@
 # define OSSL_INTERNAL_CONF_H
 # pragma once

+# include "internal/dso.h"
+# include "internal/thread_once.h"
+
 # include <openssl/conf.h>

 # define DEFAULT_CONF_MFLAGS \
@@ -24,6 +27,40 @@ struct ossl_init_settings_st {
     unsigned long flags;
 };

+/*
+ * This structure contains a data about supported modules. entries in this
+ * table correspond to either dynamic or static modules.
+ */
+
+struct conf_module_st {
+    /* DSO of this module or NULL if static */
+    DSO *dso;
+    /* Name of the module */
+    char *name;
+    /* Init function */
+    conf_init_func *init;
+    /* Finish function */
+    conf_finish_func *finish;
+    /* Number of successfully initialized modules */
+    int links;
+    void *usr_data;
+};
+
+/*
+ * This structure contains information about modules that have been
+ * successfully initialized. There may be more than one entry for a given
+ * module.
+ */
+
+struct conf_imodule_st {
+    CONF_MODULE *pmod;
+    OSSL_LIB_CTX *libctx;
+    char *name;
+    char *value;
+    unsigned long flags;
+    void *usr_data;
+};
+
 int ossl_config_int(const OPENSSL_INIT_SETTINGS *);
 void ossl_no_config_int(void);
 void ossl_config_modules_free(void);
diff --git a/include/internal/core.h b/include/internal/core.h
index 03adb66bd3..1b559c22d1 100644
--- a/include/internal/core.h
+++ b/include/internal/core.h
@@ -11,6 +11,8 @@
 # define OSSL_INTERNAL_CORE_H
 # pragma once

+# include <openssl/conf.h>
+
 /*
  * namespaces:
  *
@@ -68,4 +70,7 @@ __owur int ossl_lib_ctx_write_lock(OSSL_LIB_CTX *ctx);
 __owur int ossl_lib_ctx_read_lock(OSSL_LIB_CTX *ctx);
 int ossl_lib_ctx_unlock(OSSL_LIB_CTX *ctx);
 int ossl_lib_ctx_is_child(OSSL_LIB_CTX *ctx);
+int ossl_lib_ctx_attach_ssl_conf_imodule(OSSL_LIB_CTX *ctx, CONF_IMODULE *md);
+int ossl_lib_ctx_detach_ssl_conf_imodule(OSSL_LIB_CTX *ctx, CONF_IMODULE *md);
+
 #endif
diff --git a/include/internal/cryptlib.h b/include/internal/cryptlib.h
index 11c1023b19..378375ba14 100644
--- a/include/internal/cryptlib.h
+++ b/include/internal/cryptlib.h
@@ -121,6 +121,7 @@ typedef struct ossl_ex_data_global_st {
 # define OSSL_LIB_CTX_COMP_METHODS                  21
 # define OSSL_LIB_CTX_INDICATOR_CB_INDEX            22
 # define OSSL_LIB_CTX_MAX_INDEXES                   22
+# define OSSL_LIB_CTX_SSL_CONF_IMODULE              23

 OSSL_LIB_CTX *ossl_lib_ctx_get_concrete(OSSL_LIB_CTX *ctx);
 int ossl_lib_ctx_is_default(OSSL_LIB_CTX *ctx);
diff --git a/include/internal/sslconf.h b/include/internal/sslconf.h
index fd7f7e3331..56176845ea 100644
--- a/include/internal/sslconf.h
+++ b/include/internal/sslconf.h
@@ -13,9 +13,30 @@

 typedef struct ssl_conf_cmd_st SSL_CONF_CMD;

-const SSL_CONF_CMD *conf_ssl_get(size_t idx, const char **name, size_t *cnt);
-int conf_ssl_name_find(const char *name, size_t *idx);
-void conf_ssl_get_cmd(const SSL_CONF_CMD *cmd, size_t idx, char **cmdstr,
-                      char **arg);
+/*
+ * SSL library configuration module placeholder. We load it here but defer
+ * all decisions about its contents to libssl.
+ */
+
+struct ssl_conf_name_st {
+    /* Name of this set of commands */
+    char *name;
+    /* List of commands */
+    SSL_CONF_CMD *cmds;
+    /* Number of commands */
+    size_t cmd_count;
+};
+
+struct ssl_conf_cmd_st {
+    /* Command */
+    char *cmd;
+    /* Argument */
+    char *arg;
+};
+
+const SSL_CONF_CMD *conf_ssl_get(CONF_IMODULE *m, size_t idx, const char **name,
+                                 size_t *cnt);
+int conf_ssl_name_find(CONF_IMODULE *m, const char *name, size_t *idx);
+void conf_ssl_get_cmd(const SSL_CONF_CMD *c, size_t idx, char **cmd, char **arg);

 #endif
diff --git a/ssl/ssl_mcnf.c b/ssl/ssl_mcnf.c
index 06a23344b2..7bb69d90b8 100644
--- a/ssl/ssl_mcnf.c
+++ b/ssl/ssl_mcnf.c
@@ -10,8 +10,10 @@
 #include <stdio.h>
 #include <openssl/conf.h>
 #include <openssl/ssl.h>
+#include <openssl/trace.h>
 #include "ssl_local.h"
 #include "internal/sslconf.h"
+#include "internal/cryptlib.h"

 /* SSL library configuration module. */

@@ -20,6 +22,17 @@ void SSL_add_ssl_module(void)
     /* Do nothing. This will be added automatically by libcrypto */
 }

+static CONF_IMODULE *ssl_do_lookup_module(OSSL_LIB_CTX *libctx)
+{
+    CONF_IMODULE *m = OSSL_LIB_CTX_get_data(libctx, OSSL_LIB_CTX_SSL_CONF_IMODULE);
+
+    if (m != NULL)
+        return m;
+
+    libctx = OSSL_LIB_CTX_get0_global_default();
+    return OSSL_LIB_CTX_get_data(libctx, OSSL_LIB_CTX_SSL_CONF_IMODULE);
+}
+
 static int ssl_do_config(SSL *s, SSL_CTX *ctx, const char *name, int system)
 {
     SSL_CONF_CTX *cctx = NULL;
@@ -29,32 +42,37 @@ static int ssl_do_config(SSL *s, SSL_CTX *ctx, const char *name, int system)
     unsigned int conf_diagnostics = 0;
     const SSL_METHOD *meth;
     const SSL_CONF_CMD *cmds;
-    OSSL_LIB_CTX *prev_libctx = NULL;
-    OSSL_LIB_CTX *libctx = NULL;
+    OSSL_LIB_CTX *libctx = NULL, *prev_libctx = NULL;
+    CONF_IMODULE *imod = NULL;

     if (s == NULL && ctx == NULL) {
         ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
         goto err;
     }
-
     if (name == NULL && system)
         name = "system_default";
-    if (!conf_ssl_name_find(name, &idx)) {
+
+    libctx = s != NULL ? s->ctx->libctx: ctx->libctx;
+    imod = ssl_do_lookup_module(libctx);
+    if (!conf_ssl_name_find(imod, name, &idx)) {
         if (!system)
             ERR_raise_data(ERR_LIB_SSL, SSL_R_INVALID_CONFIGURATION_NAME,
                            "name=%s", name);
         goto err;
     }
-    cmds = conf_ssl_get(idx, &name, &cmd_count);
+
+    cmds = conf_ssl_get(imod, idx, &name, &cmd_count);
+    flags = SSL_CONF_FLAG_FILE;
+    if (!system)
+        flags |= SSL_CONF_FLAG_CERTIFICATE | SSL_CONF_FLAG_REQUIRE_PRIVATE;
+
     cctx = SSL_CONF_CTX_new();
     if (cctx == NULL) {
         /* this is a fatal error, always report */
         system = 0;
         goto err;
     }
-    flags = SSL_CONF_FLAG_FILE;
-    if (!system)
-        flags |= SSL_CONF_FLAG_CERTIFICATE | SSL_CONF_FLAG_REQUIRE_PRIVATE;
+
     if (s != NULL) {
         meth = s->method;
         SSL_CONF_CTX_set_ssl(cctx, s);
@@ -64,6 +82,7 @@ static int ssl_do_config(SSL *s, SSL_CTX *ctx, const char *name, int system)
         SSL_CONF_CTX_set_ssl_ctx(cctx, ctx);
         libctx = ctx->libctx;
     }
+
     conf_diagnostics = OSSL_LIB_CTX_get_conf_diagnostics(libctx);
     if (conf_diagnostics)
         flags |= SSL_CONF_FLAG_SHOW_ERRORS;
diff --git a/test/build.info b/test/build.info
index 0fcb2c80fd..1392bb1a48 100644
--- a/test/build.info
+++ b/test/build.info
@@ -66,7 +66,7 @@ IF[{- !$disabled{tests} -}]
           context_internal_test aesgcmtest params_test evp_pkey_dparams_test \
           keymgmt_internal_test hexstr_test provider_status_test defltfips_test \
           bio_readbuffer_test user_property_test pkcs7_test upcallstest \
-          provfetchtest prov_config_test rand_test \
+          provfetchtest prov_config_test libctx_config_test rand_test \
           ca_internals_test bio_tfo_test membio_test bio_dgram_test list_test \
           fips_version_test x509_test hpke_test pairwise_fail_test \
           nodefltctxtest evp_xof_test x509_load_cert_file_test bio_meth_test \
@@ -253,6 +253,10 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[prov_config_test]=../include ../apps/include
   DEPEND[prov_config_test]=../libcrypto libtestutil.a

+  SOURCE[libctx_config_test]=libctx_config_test.c
+  INCLUDE[libctx_config_test]=../include ../apps/include
+  DEPEND[libctx_config_test]=../libcrypto ../libssl libtestutil.a
+
   SOURCE[evp_pkey_provided_test]=evp_pkey_provided_test.c
   INCLUDE[evp_pkey_provided_test]=../include ../apps/include
   DEPEND[evp_pkey_provided_test]=../libcrypto.a libtestutil.a
@@ -1176,6 +1180,7 @@ IF[{- !$disabled{tests} -}]
   IF[{- $disabled{module} || !$target{dso_scheme} -}]
     DEFINE[provider_test]=NO_PROVIDER_MODULE
     DEFINE[prov_config_test]=NO_PROVIDER_MODULE
+    DEFINE[libctx_config_test]=NO_PROVIDER_MODULE
     DEFINE[provider_internal_test]=NO_PROVIDER_MODULE
   ENDIF
   DEPEND[]=provider_internal_test.cnf
diff --git a/test/libctx_config_test.c b/test/libctx_config_test.c
new file mode 100644
index 0000000000..ad390c7273
--- /dev/null
+++ b/test/libctx_config_test.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <sys/stat.h>
+#include <openssl/conf.h>
+#include <openssl/ssl.h>
+#include "testutil.h"
+
+static char *cfg1 = NULL;
+static char *cfg2 = NULL;
+
+static const struct cfg_proto_version {
+    char **file;
+    long minver, maxver;
+} cfg_proto_version[] = {
+    { &cfg1, TLS1_1_VERSION, TLS1_1_VERSION },
+    { &cfg2, TLS1_2_VERSION, TLS1_2_VERSION }
+};
+
+static int test_config_libctx_global(int idx)
+{
+    const struct cfg_proto_version *cpv = &cfg_proto_version[idx];
+    SSL_CTX *ctx = NULL;
+    int test;
+
+    CONF_modules_load_file(*cpv->file, NULL, 0);
+
+    test = TEST_ptr(ctx = SSL_CTX_new(TLS_method()))
+        && TEST_int_eq(SSL_CTX_get_min_proto_version(ctx), cpv->minver)
+        && TEST_int_eq(SSL_CTX_get_max_proto_version(ctx), cpv->maxver);
+
+    CONF_modules_unload(0);
+    SSL_CTX_free(ctx);
+    return test;
+}
+
+static int test_config_libctx_local(void)
+{
+    OSSL_LIB_CTX *lib1 = NULL, *lib2 = NULL;
+    SSL_CTX *ctx1 = NULL, *ctx2 = NULL;
+    int test;
+
+    test = TEST_ptr(lib1 = OSSL_LIB_CTX_new())
+        && TEST_ptr(lib2 = OSSL_LIB_CTX_new())
+        && TEST_int_eq(OSSL_LIB_CTX_load_config(lib1, cfg1), 1)
+        && TEST_int_eq(OSSL_LIB_CTX_load_config(lib2, cfg2), 1)
+        && TEST_ptr(ctx1 = SSL_CTX_new_ex(lib1, NULL, TLS_server_method()))
+        && TEST_ptr(ctx2 = SSL_CTX_new_ex(lib2, NULL, TLS_server_method()))
+        && TEST_int_eq(SSL_CTX_get_min_proto_version(ctx1), TLS1_1_VERSION)
+        && TEST_int_eq(SSL_CTX_get_min_proto_version(ctx2), TLS1_2_VERSION)
+        && TEST_int_eq(SSL_CTX_get_max_proto_version(ctx1), TLS1_1_VERSION)
+        && TEST_int_eq(SSL_CTX_get_max_proto_version(ctx2), TLS1_2_VERSION);
+
+    SSL_CTX_free(ctx1);
+    SSL_CTX_free(ctx2);
+    OSSL_LIB_CTX_free(lib1);
+    OSSL_LIB_CTX_free(lib2);
+    return test;
+}
+
+OPT_TEST_DECLARE_USAGE("configfile\n")
+
+int setup_tests(void)
+{
+    int test;
+
+    test = TEST_true(test_skip_common_options())
+        && TEST_ptr(cfg1 = test_get_argument(0))
+        && TEST_ptr(cfg2 = test_get_argument(1));
+
+    ADD_TEST(test_config_libctx_local);
+    ADD_ALL_TESTS(test_config_libctx_global, OSSL_NELEM(cfg_proto_version));
+
+    return test;
+}
diff --git a/test/recipes/30-test_libctx_config.t b/test/recipes/30-test_libctx_config.t
new file mode 100644
index 0000000000..4c19112be7
--- /dev/null
+++ b/test/recipes/30-test_libctx_config.t
@@ -0,0 +1,25 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use OpenSSL::Test::Simple;
+use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_dir/;
+use OpenSSL::Test::Utils;
+
+BEGIN {
+setup("test_prov_config");
+}
+
+use lib srctop_dir('Configurations');
+use lib bldtop_dir('.');
+
+plan tests => 1;
+
+ok(run(test(["libctx_config_test", srctop_file("test", "tls-max-v11.cnf"),
+                                 srctop_file("test", "tls-max-v12.cnf")])),
+    "running libctx_config_test default.cnf");
diff --git a/test/tls-max-v11.cnf b/test/tls-max-v11.cnf
new file mode 100644
index 0000000000..ab764c19d9
--- /dev/null
+++ b/test/tls-max-v11.cnf
@@ -0,0 +1,14 @@
+openssl_conf = openssl_init
+
+# Comment out the next line to ignore configuration errors
+config_diagnostics = 1
+
+[openssl_init]
+ssl_conf = ssl_configuration
+
+[ssl_configuration]
+system_default = tls_system_default
+
+[tls_system_default]
+MinProtocol = TLSv1.1
+MaxProtocol = TLSv1.1
diff --git a/test/tls-max-v12.cnf b/test/tls-max-v12.cnf
new file mode 100644
index 0000000000..6b07369d13
--- /dev/null
+++ b/test/tls-max-v12.cnf
@@ -0,0 +1,14 @@
+openssl_conf = openssl_init
+
+# Comment out the next line to ignore configuration errors
+config_diagnostics = 1
+
+[openssl_init]
+ssl_conf = ssl_configuration
+
+[ssl_configuration]
+system_default = tls_system_default
+
+[tls_system_default]
+MinProtocol = TLSv1.2
+MaxProtocol = TLSv1.2