Commit 833efe31fc for openssl.org

commit 833efe31fc163fb6216d6406396168cb87cea403
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Thu Jun 25 22:09:30 2026 +0900

    Remove remaining atoi()/atol() calls

    Remove the remaining direct atoi()/atol() uses across apps, libcrypto,
    libssl, providers, demos and tests, and route the replacements through
    checked helpers instead of open-coding the conversion at each call site:

      - apps use the existing opt_int()/opt_long()/opt_int_arg() parsers;
      - libcrypto and provider code use the new internal ossl_strtol()/
        ossl_strtoint() helpers, a checked (and, for ossl_strtoint(),
        narrowing) signed counterpart to the public OPENSSL_strtoul();
      - libssl, demos and tests use OPENSSL_strtoul(); the test code shares
        a single test_strtoint() helper rather than repeating the bound check
        and cast per file;
      - unsigned values use OPENSSL_strtoul(), with call-site-specific range
        checks before narrowing where the destination type requires it.

    Reject conversion failures and narrow through the helpers rather than
    acting on garbage, and preserve the signed RSA-PSS saltlen sentinels
    while rejecting invalid numeric strings. Add regression coverage for the
    invalid RSA-PSS saltlen string path.

    Fixes #8216

    Assisted-by: OpenCode:GLM-5.2

    Reviewed-by: Tom Cosgrove <tom.cosgrove@arm.com>
    Reviewed-by: Tim Hudson <tjh@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    MergeDate: Wed Jul 29 17:02:18 2026
    (Merged from https://github.com/openssl/openssl/pull/31731)

diff --git a/apps/ca.c b/apps/ca.c
index 8f98bc9a6a..c7b5a1c22d 100644
--- a/apps/ca.c
+++ b/apps/ca.c
@@ -484,13 +484,16 @@ int ca_main(int argc, char **argv)
             crl_nextupdate = opt_arg();
             break;
         case OPT_CRLDAYS:
-            crldays = atol(opt_arg());
+            if (!opt_long(opt_arg(), &crldays))
+                goto opthelp;
             break;
         case OPT_CRLHOURS:
-            crlhours = atol(opt_arg());
+            if (!opt_long(opt_arg(), &crlhours))
+                goto opthelp;
             break;
         case OPT_CRLSEC:
-            crlsec = atol(opt_arg());
+            if (!opt_long(opt_arg(), &crlsec))
+                goto opthelp;
             break;
         case OPT_INFILES:
             req = 1;
diff --git a/apps/cmp.c b/apps/cmp.c
index abe6de5cb9..e75ed35a48 100644
--- a/apps/cmp.c
+++ b/apps/cmp.c
@@ -3706,9 +3706,13 @@ static int handle_opts_upfront(int argc, char **argv)
                 opt_section = argv[++i];
             else if (strcmp(argv[i] + 1,
                          cmp_options[OPT_VERBOSITY - OPT_HELP].name)
-                    == 0
-                && !set_verbosity(atoi(argv[++i])))
-                return 0;
+                == 0) {
+                int level;
+
+                ++i;
+                if (!opt_int(argv[i], &level) || !set_verbosity(level))
+                    return 0;
+            }
         }
     }
     if (opt_section[0] == '\0') /* empty string */
diff --git a/apps/lib/apps.c b/apps/lib/apps.c
index 63a8c8c00d..5d082992ec 100644
--- a/apps/lib/apps.c
+++ b/apps/lib/apps.c
@@ -252,10 +252,8 @@ static char *app_get_pass(const char *arg, int keepbio)
         } else if (CHECK_AND_SKIP_PREFIX(arg, "fd:")) {
             BIO *btmp;

-            i = atoi(arg);
-            if (i >= 0)
-                pwdbio = BIO_new_fd(i, BIO_NOCLOSE);
-            if ((i < 0) || pwdbio == NULL) {
+            if (!opt_int(arg, &i) || i < 0
+                || (pwdbio = BIO_new_fd(i, BIO_NOCLOSE)) == NULL) {
                 BIO_printf(bio_err, "Can't access file descriptor %s\n", arg);
                 return NULL;
             }
diff --git a/apps/req.c b/apps/req.c
index 83ac38ef86..19933ea352 100644
--- a/apps/req.c
+++ b/apps/req.c
@@ -1557,10 +1557,12 @@ static EVP_PKEY_CTX *set_keygen_ctx(const char *gstr,
     /* Treat the second part of gstr, if there is one */
     if (gstr != NULL) {
         /* If the second part starts with a digit, we assume it's a size */
-        if (!expect_paramfile && gstr[0] >= '0' && gstr[0] <= '9')
-            keylen = atol(gstr);
-        else
+        if (!expect_paramfile && gstr[0] >= '0' && gstr[0] <= '9') {
+            if (!opt_long(gstr, &keylen))
+                return NULL;
+        } else {
             paramfile = gstr;
+        }
     }

     if (paramfile != NULL) {
diff --git a/apps/s_client.c b/apps/s_client.c
index 65aff3eb42..80bb0f5305 100644
--- a/apps/s_client.c
+++ b/apps/s_client.c
@@ -15,6 +15,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
+#include <limits.h>
 #include <openssl/e_os2.h>
 #include "internal/nelem.h"
 #include "internal/sockets.h" /* for openssl_fdset() */
@@ -1520,7 +1521,8 @@ int s_client_main(int argc, char **argv)
             break;
         case OPT_MTU:
 #ifndef OPENSSL_NO_DTLS
-            socket_mtu = atol(opt_arg());
+            if (!opt_long(opt_arg(), &socket_mtu))
+                goto opthelp;
 #endif
             break;
         case OPT_FALLBACKSCSV:
@@ -1625,7 +1627,20 @@ int s_client_main(int argc, char **argv)
             len = (int)strlen(p);
             for (start = 0, i = 0; i <= len; ++i) {
                 if (i == len || p[i] == ',') {
-                    serverinfo_types[serverinfo_count] = atoi(p + start);
+                    char *end;
+                    unsigned long ul;
+
+                    /*
+                     * Parse only the current comma-separated component.
+                     * OPENSSL_strtoul() with an endptr consumes the leading
+                     * digits; we require it to stop exactly at the delimiter
+                     * (or NUL) and to fit in an unsigned short.
+                     */
+                    if (!OPENSSL_strtoul(p + start, &end, 10, &ul)
+                        || end != p + i
+                        || ul > USHRT_MAX)
+                        goto opthelp;
+                    serverinfo_types[serverinfo_count] = (unsigned short)ul;
                     if (++serverinfo_count == MAX_SI_TYPES)
                         break;
                     start = i + 1;
diff --git a/apps/s_server.c b/apps/s_server.c
index 4d99e5442b..2fc193980c 100644
--- a/apps/s_server.c
+++ b/apps/s_server.c
@@ -1885,7 +1885,7 @@ int s_server_main(int argc, char *argv[])
             break;
 #endif
         case OPT_NACCEPT:
-            naccept = atol(opt_arg());
+            naccept = opt_int_arg();
             break;
         case OPT_VERIFY:
             s_server_verify = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
@@ -2254,7 +2254,8 @@ int s_server_main(int argc, char *argv[])
             break;
         case OPT_MTU:
 #ifndef OPENSSL_NO_DTLS
-            socket_mtu = atol(opt_arg());
+            if (!opt_long(opt_arg(), &socket_mtu))
+                goto opthelp;
 #endif
             break;
         case OPT_LISTEN:
diff --git a/apps/speed.c b/apps/speed.c
index b1732744d3..569f438a39 100644
--- a/apps/speed.c
+++ b/apps/speed.c
@@ -3946,11 +3946,15 @@ int speed_main(int argc, char **argv)
             }

             if (strncmp(sig_name, "dsa", 3) == 0) {
+                int dsa_nbits = 0;
+
+                if (!opt_int(sig_name + 3, &dsa_nbits))
+                    goto sig_err_break;
                 ctx_params = EVP_PKEY_CTX_new_id(EVP_PKEY_DSA, NULL);
                 if (ctx_params == NULL
                     || EVP_PKEY_paramgen_init(ctx_params) <= 0
                     || EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx_params,
-                           atoi(sig_name + 3))
+                           dsa_nbits)
                         <= 0
                     || EVP_PKEY_paramgen(ctx_params, &pkey_params) <= 0
                     || (sig_gen_ctx = EVP_PKEY_CTX_new(pkey_params, NULL)) == NULL
diff --git a/crypto/evp/ctrl_params_translate.c b/crypto/evp/ctrl_params_translate.c
index c515c6cbbf..d0529166d7 100644
--- a/crypto/evp/ctrl_params_translate.c
+++ b/crypto/evp/ctrl_params_translate.c
@@ -1046,9 +1046,19 @@ static int fix_dh_nid5114(enum state state,
     case PRE_CTRL_STR_TO_PARAMS:
         if (ctx->p2 == NULL)
             return 0;
-        if ((ctx->p2 = (char *)ossl_ffc_named_group_get_name(ossl_ffc_uid_to_dh_named_group(atoi(ctx->p2)))) == NULL) {
-            ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
-            return 0;
+        {
+            int uid;
+
+            if (!ossl_strtoint(ctx->p2, NULL, 10, &uid)) {
+                ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
+                return 0;
+            }
+            ctx->p2 = (char *)ossl_ffc_named_group_get_name(
+                ossl_ffc_uid_to_dh_named_group(uid));
+            if (ctx->p2 == NULL) {
+                ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
+                return 0;
+            }
         }

         ctx->p1 = 0;
@@ -1076,8 +1086,14 @@ static int fix_dh_paramgen_type(enum state state,
         return 0;

     if (state == PRE_CTRL_STR_TO_PARAMS) {
-        if ((ctx->p2 = (char *)ossl_dh_gen_type_id2name(atoi(ctx->p2)))
-            == NULL) {
+        int id;
+
+        if (!ossl_strtoint(ctx->p2, NULL, 10, &id)) {
+            ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
+            return 0;
+        }
+        ctx->p2 = (char *)ossl_dh_gen_type_id2name(id);
+        if (ctx->p2 == NULL) {
             ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
             return 0;
         }
@@ -1433,8 +1449,14 @@ static int fix_rsa_pss_saltlen(enum state state,
                 break;
         }

-        val = i == OSSL_NELEM(str_value_map) ? atoi(ctx->p2)
-                                             : (int)str_value_map[i].id;
+        if (i == OSSL_NELEM(str_value_map)) {
+            if (!ossl_strtoint(ctx->p2, NULL, 10, &val)) {
+                ERR_raise(ERR_LIB_EVP, EVP_R_INVALID_VALUE);
+                return 0;
+            }
+        } else {
+            val = (int)str_value_map[i].id;
+        }
         if (state == POST_CTRL_TO_PARAMS) {
             /*
              * EVP_PKEY_CTRL_GET_RSA_PSS_SALTLEN weirdness explained further
diff --git a/crypto/mem.c b/crypto/mem.c
index 476d6b2529..408c8edfc0 100644
--- a/crypto/mem.c
+++ b/crypto/mem.c
@@ -112,12 +112,19 @@ static void parseit(void)
 {
     char *semi = strchr(md_failstring, ';');
     char *atsign;
+    char *end;

     if (semi != NULL)
         *semi++ = '\0';

-    /* Get the count (atol will stop at the @ if there), and percentage */
-    md_count = atol(md_failstring);
+    /*
+     * Get the count (parsing stops at the '@' if present), and percentage.
+     * Ignore an unparsable/overflowing count rather than acting on garbage.
+     * Validate that the count is followed by '@' or end-of-string.
+     */
+    if (!ossl_strtol(md_failstring, &end, 10, &md_count)
+        || (*end != '\0' && *end != '@'))
+        md_count = 0;
     atsign = strchr(md_failstring, '@');
     md_fail_percent = atsign == NULL ? 0 : (int)(atof(atsign + 1) * 100 + 0.5);

@@ -179,10 +186,19 @@ void ossl_malloc_setup_failures(void)
             parseit();
         }
     }
-    if ((cp = getenv("OPENSSL_MALLOC_FD")) != NULL)
-        md_tracefd = atoi(cp);
-    if ((cp = getenv("OPENSSL_MALLOC_SEED")) != NULL)
-        srandom(atoi(cp));
+    if ((cp = getenv("OPENSSL_MALLOC_FD")) != NULL) {
+        int fd;
+
+        if (ossl_strtoint(cp, NULL, 10, &fd) && fd >= 0)
+            md_tracefd = fd;
+    }
+    if ((cp = getenv("OPENSSL_MALLOC_SEED")) != NULL) {
+        unsigned long seed;
+
+        /* Any value is a usable seed; just truncate it to the srandom() type. */
+        if (OPENSSL_strtoul(cp, NULL, 10, &seed))
+            srandom((unsigned int)seed);
+    }
 }
 #endif

diff --git a/crypto/o_str.c b/crypto/o_str.c
index 2192d48775..d48b43df17 100644
--- a/crypto/o_str.c
+++ b/crypto/o_str.c
@@ -161,6 +161,52 @@ int OPENSSL_strtoul(const char *str, char **endptr, int base,
     return 1;
 }

+/*
+ * Internal signed counterpart to OPENSSL_strtoul().  Parses a signed long with
+ * the same validation rules: it fails on a conversion error (including
+ * overflow/underflow), if no digits were consumed, or, when |endptr| is NULL,
+ * if the whole string was not consumed.  Returns 1 on success (storing the
+ * result in |*result|), 0 otherwise.
+ */
+int ossl_strtol(const char *str, char **endptr, int base, long *result)
+{
+    char *tmp_endptr;
+    char **internal_endptr = endptr == NULL ? &tmp_endptr : endptr;
+
+    errno = 0;
+
+    *internal_endptr = (char *)str;
+
+    if (result == NULL || str == NULL)
+        return 0;
+
+    *result = strtol(str, internal_endptr, base);
+    if (errno != 0
+        || (endptr == NULL && **internal_endptr != '\0')
+        || str == *internal_endptr)
+        return 0;
+
+    return 1;
+}
+
+/*
+ * As ossl_strtol() but stores the result in an int, additionally failing if
+ * the parsed value does not fit in an int.  Returns 1 on success (storing the
+ * result in |*result|), 0 otherwise.
+ */
+int ossl_strtoint(const char *str, char **endptr, int base, int *result)
+{
+    long l;
+
+    if (result == NULL || !ossl_strtol(str, endptr, base, &l))
+        return 0;
+    if (l < INT_MIN || l > INT_MAX)
+        return 0;
+
+    *result = (int)l;
+    return 1;
+}
+
 int OPENSSL_hexchar2int(unsigned char c)
 {
 #ifdef CHARSET_EBCDIC
diff --git a/crypto/ppccap.c b/crypto/ppccap.c
index e029eb3051..189c9e03d4 100644
--- a/crypto/ppccap.c
+++ b/crypto/ppccap.c
@@ -160,11 +160,16 @@ void OPENSSL_cpuid_setup(void)

     if (sizeof(size_t) == 4) {
         struct utsname uts;
+        long major;
+        char *end;
 #if defined(_SC_AIX_KERNEL_BITMODE)
         if (sysconf(_SC_AIX_KERNEL_BITMODE) != 64)
             return;
 #endif
-        if (uname(&uts) != 0 || atoi(uts.version) < 6)
+        if (uname(&uts) != 0
+            || !ossl_strtol(uts.version, &end, 10, &major)
+            || (*end != '\0' && *end != '.')
+            || major < 6)
             return;
     }

diff --git a/crypto/ts/ts_conf.c b/crypto/ts/ts_conf.c
index f8d01813df..1c23eaca89 100644
--- a/crypto/ts/ts_conf.c
+++ b/crypto/ts/ts_conf.c
@@ -358,14 +358,26 @@ int TS_CONF_set_accuracy(CONF *conf, const char *section, TS_RESP_CTX *ctx)
     for (i = 0; i < sk_CONF_VALUE_num(list); ++i) {
         CONF_VALUE *val = sk_CONF_VALUE_value(list, i);
         if (strcmp(val->name, ENV_VALUE_SECS) == 0) {
-            if (val->value)
-                secs = atoi(val->value);
+            if (val->value != NULL
+                && (!ossl_strtoint(val->value, NULL, 10, &secs)
+                    || secs < 0)) {
+                ts_CONF_invalid(section, ENV_ACCURACY);
+                goto err;
+            }
         } else if (strcmp(val->name, ENV_VALUE_MILLISECS) == 0) {
-            if (val->value)
-                millis = atoi(val->value);
+            if (val->value != NULL
+                && (!ossl_strtoint(val->value, NULL, 10, &millis)
+                    || millis < 0)) {
+                ts_CONF_invalid(section, ENV_ACCURACY);
+                goto err;
+            }
         } else if (strcmp(val->name, ENV_VALUE_MICROSECS) == 0) {
-            if (val->value)
-                micros = atoi(val->value);
+            if (val->value != NULL
+                && (!ossl_strtoint(val->value, NULL, 10, &micros)
+                    || micros < 0)) {
+                ts_CONF_invalid(section, ENV_ACCURACY);
+                goto err;
+            }
         } else {
             ts_CONF_invalid(section, ENV_ACCURACY);
             goto err;
diff --git a/demos/digest/EVP_MD_xof.c b/demos/digest/EVP_MD_xof.c
index f0c9de3e5f..d31bd82d12 100644
--- a/demos/digest/EVP_MD_xof.c
+++ b/demos/digest/EVP_MD_xof.c
@@ -9,8 +9,10 @@

 #include <stdio.h>
 #include <string.h>
+#include <limits.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/crypto.h>
 #include <openssl/core_names.h>

 /*
@@ -53,7 +55,13 @@ int main(int argc, char **argv)

     /* Allow digest length to be changed for demonstration purposes. */
     if (argc > 1) {
-        digest_len_i = atoi(argv[1]);
+        unsigned long ul;
+
+        if (!OPENSSL_strtoul(argv[1], NULL, 10, &ul) || ul > INT_MAX) {
+            fprintf(stderr, "Specify a non-negative digest length\n");
+            goto end;
+        }
+        digest_len_i = (int)ul;
         if (digest_len_i <= 0) {
             fprintf(stderr, "Specify a non-negative digest length\n");
             goto end;
diff --git a/demos/pkey/EVP_PKEY_RSA_keygen.c b/demos/pkey/EVP_PKEY_RSA_keygen.c
index a889ab6f77..343bc5f296 100644
--- a/demos/pkey/EVP_PKEY_RSA_keygen.c
+++ b/demos/pkey/EVP_PKEY_RSA_keygen.c
@@ -17,8 +17,10 @@

 #include <string.h>
 #include <stdio.h>
+#include <limits.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/crypto.h>
 #include <openssl/rsa.h>
 #include <openssl/core_names.h>
 #include <openssl/pem.h>
@@ -253,7 +255,13 @@ int main(int argc, char **argv)
     }

     if (argc > 1) {
-        bits_i = atoi(argv[1]);
+        unsigned long ul;
+
+        if (!OPENSSL_strtoul(argv[1], NULL, 10, &ul) || ul > INT_MAX) {
+            fprintf(stderr, "Invalid RSA key size\n");
+            return EXIT_FAILURE;
+        }
+        bits_i = (int)ul;
         if (bits_i < 512) {
             fprintf(stderr, "Invalid RSA key size\n");
             return EXIT_FAILURE;
diff --git a/doc/man3/OPENSSL_malloc.pod b/doc/man3/OPENSSL_malloc.pod
index 1907469fcd..a22099ac3e 100644
--- a/doc/man3/OPENSSL_malloc.pod
+++ b/doc/man3/OPENSSL_malloc.pod
@@ -47,7 +47,7 @@ OPENSSL_MALLOC_SEED
  char *OPENSSL_strndup(const char *str, size_t s);
  size_t OPENSSL_strlcat(char *dst, const char *src, size_t size);
  size_t OPENSSL_strlcpy(char *dst, const char *src, size_t size);
- int OPENSSL_strtoul(char *src, char **endptr, int base, unsigned long *num);
+ int OPENSSL_strtoul(const char *str, char **endptr, int base, unsigned long *num);
  void *OPENSSL_memdup(void *data, size_t s);
  void *OPENSSL_clear_realloc(void *p, size_t old_len, size_t num);
  void *OPENSSL_clear_realloc_array(void *p, size_t old_len, size_t num,
@@ -226,8 +226,8 @@ work on all platforms):
   ...app invocation... 3>/tmp/log$$

 If the environment variable B<OPENSSL_MALLOC_SEED> is set, its value
-is interpreted as an integer using atoi(3) and supplied to the srandom(3)
-call for the random number generator initialisation.
+is parsed using OPENSSL_strtoul() and supplied to the srandom(3) call for
+the random number generator initialisation.

 =head1 RETURN VALUES

diff --git a/include/internal/cryptlib.h b/include/internal/cryptlib.h
index 0dcc6b6acd..8bc22e39ce 100644
--- a/include/internal/cryptlib.h
+++ b/include/internal/cryptlib.h
@@ -162,6 +162,19 @@ char *ossl_buf2hexstr_sep(const unsigned char *buf, long buflen, char sep);
 unsigned char *ossl_hexstr2buf_sep(const char *str, long *buflen,
     const char sep);

+/*
+ * Parse a signed long with validation; see OPENSSL_strtoul() for the rules.
+ * Returns 1 on success (and stores the result in |*result|), 0 on failure.
+ */
+int ossl_strtol(const char *str, char **endptr, int base, long *result);
+
+/*
+ * As ossl_strtol() but stores the result in an int, additionally failing if
+ * the parsed value does not fit in an int.
+ * Returns 1 on success (and stores the result in |*result|), 0 on failure.
+ */
+int ossl_strtoint(const char *str, char **endptr, int base, int *result);
+
 /**
  *  Writes |n| value in hex format into |buf|,
  *  and returns the number of bytes written
diff --git a/providers/implementations/rands/seeding/rand_unix.c b/providers/implementations/rands/seeding/rand_unix.c
index 67b38cb719..d1bda5c68e 100644
--- a/providers/implementations/rands/seeding/rand_unix.c
+++ b/providers/implementations/rands/seeding/rand_unix.c
@@ -451,9 +451,19 @@ static int wait_random_seeded(void)
              * this alternative but essentially identical source moot.
              */
             if (uname(&un) == 0) {
-                kernel[0] = atoi(un.release);
+                int val;
+                char *end;
+
+                kernel[0] = (ossl_strtoint(un.release, &end, 10, &val)
+                                && (*end == '.' || *end == '\0'))
+                    ? val
+                    : 0;
                 p = strchr(un.release, '.');
-                kernel[1] = p == NULL ? 0 : atoi(p + 1);
+                kernel[1] = (p != NULL
+                                && ossl_strtoint(p + 1, &end, 10, &val)
+                                && (*end == '.' || *end == '\0'))
+                    ? val
+                    : 0;
                 if (kernel[0] > kernel_version[0]
                     || (kernel[0] == kernel_version[0]
                         && kernel[1] >= kernel_version[1])) {
diff --git a/providers/implementations/signature/rsa_sig.c b/providers/implementations/signature/rsa_sig.c
index 232414935f..7b35fec028 100644
--- a/providers/implementations/signature/rsa_sig.c
+++ b/providers/implementations/signature/rsa_sig.c
@@ -1695,8 +1695,11 @@ static int rsa_set_ctx_params(void *vprsactx, const OSSL_PARAM params[])
                 saltlen = RSA_PSS_SALTLEN_AUTO;
             else if (strcmp(p.slen->data, OSSL_PKEY_RSA_PSS_SALT_LEN_AUTO_DIGEST_MAX) == 0)
                 saltlen = RSA_PSS_SALTLEN_AUTO_DIGEST_MAX;
-            else
-                saltlen = atoi(p.slen->data);
+            else if (!ossl_strtoint(p.slen->data, NULL, 10, &saltlen)) {
+                ERR_raise_data(ERR_LIB_PROV, PROV_R_INVALID_SALT_LENGTH,
+                    "invalid RSA-PSS saltlen value");
+                return 0;
+            }
         }

         /*
diff --git a/ssl/ssl_conf.c b/ssl/ssl_conf.c
index a7293512b9..d065b649d5 100644
--- a/ssl/ssl_conf.c
+++ b/ssl/ssl_conf.c
@@ -9,6 +9,7 @@

 #include "internal/e_os.h"

+#include <limits.h>
 #include <stdio.h>
 #include "ssl_local.h"
 #include <openssl/conf.h>
@@ -723,9 +724,11 @@ out:
 static int cmd_NumTickets(SSL_CONF_CTX *cctx, const char *value)
 {
     int rv = 0;
-    int num_tickets = atoi(value);
+    unsigned long ul;
+
+    if (OPENSSL_strtoul(value, NULL, 10, &ul) && ul <= INT_MAX) {
+        int num_tickets = (int)ul;

-    if (num_tickets >= 0) {
         if (cctx->ctx)
             rv = SSL_CTX_set_num_tickets(cctx->ctx, num_tickets);
         if (cctx->ssl)
diff --git a/test/evp_test.c b/test/evp_test.c
index 77029f40db..ef6dea9664 100644
--- a/test/evp_test.c
+++ b/test/evp_test.c
@@ -475,10 +475,9 @@ static int evp_test_buffer_ncopy(const char *value,
     EVP_TEST_BUFFER *db;
     unsigned char *tbuf, *p;
     size_t tbuflen;
-    int ncopy = atoi(value);
-    int i;
+    int ncopy = 0, i;

-    if (ncopy <= 0)
+    if (!test_strtoint(value, &ncopy) || ncopy <= 0)
         return 0;
     if (sk == NULL || sk_EVP_TEST_BUFFER_num(sk) == 0)
         return 0;
@@ -501,9 +500,9 @@ static int evp_test_buffer_set_count(const char *value,
     STACK_OF(EVP_TEST_BUFFER) *sk)
 {
     EVP_TEST_BUFFER *db;
-    int count = atoi(value);
+    int count = 0;

-    if (count <= 0)
+    if (!test_strtoint(value, &count) || count <= 0)
         return 0;

     if (sk == NULL || sk_EVP_TEST_BUFFER_num(sk) == 0)
@@ -764,14 +763,13 @@ static int digest_test_parse(EVP_TEST *t,
     if (strcmp(keyword, "Ncopy") == 0)
         return evp_test_buffer_ncopy(value, mdata->input);
     if (strcmp(keyword, "Padding") == 0)
-        return (mdata->pad_type = atoi(value)) > 0;
+        return test_strtoint(value, &mdata->pad_type) && mdata->pad_type > 0;
     if (strcmp(keyword, "XOF") == 0)
-        return (mdata->xof = atoi(value)) > 0;
+        return test_strtoint(value, &mdata->xof) && mdata->xof > 0;
     if (strcmp(keyword, "OutputSize") == 0) {
         int sz;

-        sz = atoi(value);
-        if (sz < 0)
+        if (!test_strtoint(value, &sz))
             return -1;
         mdata->digest_size = sz;
         return 1;
@@ -1065,8 +1063,7 @@ static int cipher_test_parse(EVP_TEST *t, const char *keyword,
     if (strcmp(keyword, "Key") == 0)
         return parse_bin(value, &cdat->key, &cdat->key_len);
     if (strcmp(keyword, "Rounds") == 0) {
-        i = atoi(value);
-        if (i < 0)
+        if (!test_strtoint(value, &i))
             return -1;
         cdat->rounds = (unsigned int)i;
         return 1;
@@ -1080,8 +1077,7 @@ static int cipher_test_parse(EVP_TEST *t, const char *keyword,
     if (strcmp(keyword, "Ciphertext") == 0)
         return parse_bin(value, &cdat->ciphertext, &cdat->ciphertext_len);
     if (strcmp(keyword, "KeyBits") == 0) {
-        i = atoi(value);
-        if (i < 0)
+        if (!test_strtoint(value, &i))
             return -1;
         cdat->key_bits = (size_t)i;
         return 1;
@@ -1838,14 +1834,12 @@ static int mac_test_parse(EVP_TEST *t,
     if (strcmp(keyword, "Ctrl") == 0)
         return ctrladd(mdata->controls, value);
     if (strcmp(keyword, "OutputSize") == 0) {
-        mdata->output_size = atoi(value);
-        if (mdata->output_size < 0)
+        if (!test_strtoint(value, &mdata->output_size))
             return -1;
         return 1;
     }
     if (strcmp(keyword, "BlockSize") == 0) {
-        mdata->block_size = atoi(value);
-        if (mdata->block_size < 0)
+        if (!test_strtoint(value, &mdata->block_size))
             return -1;
         return 1;
     }
@@ -3451,8 +3445,7 @@ static int pbkdf2_test_parse(EVP_TEST *t,
     PBE_DATA *pdata = t->data;

     if (strcmp(keyword, "iter") == 0) {
-        pdata->iter = atoi(value);
-        if (pdata->iter <= 0)
+        if (!test_strtoint(value, &pdata->iter) || pdata->iter <= 0)
             return -1;
         return 1;
     }
@@ -3471,8 +3464,7 @@ static int pkcs12_test_parse(EVP_TEST *t,
     PBE_DATA *pdata = t->data;

     if (strcmp(keyword, "id") == 0) {
-        pdata->id = atoi(value);
-        if (pdata->id <= 0)
+        if (!test_strtoint(value, &pdata->id) || pdata->id <= 0)
             return -1;
         return 1;
     }
@@ -3901,7 +3893,8 @@ static int rand_test_parse(EVP_TEST *t,
     int n;

     if ((p = strchr(keyword, '.')) != NULL) {
-        n = atoi(++p);
+        if (!test_strtoint(++p, &n))
+            return 0;
         if (n >= MAX_RAND_REPEATS)
             return 0;
         if (n > rdata->n)
@@ -3935,17 +3928,21 @@ static int rand_test_parse(EVP_TEST *t,
         if (strcmp(keyword, "Digest") == 0)
             return TEST_ptr(rdata->digest = OPENSSL_strdup(value));
         if (strcmp(keyword, "DerivationFunction") == 0) {
-            rdata->use_df = atoi(value) != 0;
+            n = 0;
+            (void)test_strtoint(value, &n);
+            rdata->use_df = n != 0;
             return 1;
         }
         if (strcmp(keyword, "GenerateBits") == 0) {
-            if ((n = atoi(value)) <= 0 || n % 8 != 0)
+            if (!test_strtoint(value, &n) || n <= 0 || n % 8 != 0)
                 return 0;
             rdata->generate_bits = (unsigned int)n;
             return 1;
         }
         if (strcmp(keyword, "PredictionResistance") == 0) {
-            rdata->prediction_resistance = atoi(value) != 0;
+            n = 0;
+            (void)test_strtoint(value, &n);
+            rdata->prediction_resistance = n != 0;
             return 1;
         }
         if (strcmp(keyword, "CtrlInit") == 0)
@@ -5612,7 +5609,10 @@ start:
             }
             t->reason = take_value(pp);
         } else if (strcmp(pp->key, "Threads") == 0) {
-            if (OSSL_set_max_threads(libctx, atoi(pp->value)) == 0) {
+            int nthreads = 0;
+
+            (void)test_strtoint(pp->value, &nthreads);
+            if (OSSL_set_max_threads(libctx, nthreads) == 0) {
                 TEST_info("skipping, '%s' threads not available: %s:%d",
                     pp->value, t->s.test_file, t->s.start);
                 t->skip = 1;
@@ -5630,8 +5630,9 @@ start:
                 TEST_info("Line %d: multiple security category lines", t->s.curr);
                 return 0;
             }
-            t->security_category = atoi(pp->value);
-            if (t->security_category < 0 || t->security_category > 5) {
+            t->security_category = 0;
+            if (!test_strtoint(pp->value, &t->security_category)
+                || t->security_category > 5) {
                 TEST_info("Line %d: invalid security category, should be 0..5",
                     t->s.curr);
                 return 0;
diff --git a/test/helpers/ssl_test_ctx.c b/test/helpers/ssl_test_ctx.c
index 321d1a378a..f8b88df861 100644
--- a/test/helpers/ssl_test_ctx.c
+++ b/test/helpers/ssl_test_ctx.c
@@ -56,7 +56,8 @@ static int parse_boolean(const char *value, int *result)
 #define IMPLEMENT_SSL_TEST_INT_OPTION(struct_type, name, field)            \
     static int parse_##name##_##field(struct_type *ctx, const char *value) \
     {                                                                      \
-        ctx->field = atoi(value);                                          \
+        if (!test_strtoint(value, &ctx->field))                            \
+            ctx->field = 0;                                                \
         return 1;                                                          \
     }

diff --git a/test/lhash_test.c b/test/lhash_test.c
index 87dc71f829..394321151f 100644
--- a/test/lhash_test.c
+++ b/test/lhash_test.c
@@ -590,14 +590,16 @@ static int setup_num_workers(void)
     char *lhash_workers = getenv("LHASH_WORKERS");
     /* If we have HARNESS_JOBS set, don't eat more than a quarter */
     if (harness_jobs != NULL) {
-        int jobs = atoi(harness_jobs);
-        if (jobs > 0)
+        int jobs;
+
+        if (test_strtoint(harness_jobs, &jobs) && jobs > 0)
             num_workers = jobs / 4;
     }
     /* But if we have explicitly set LHASH_WORKERS use that */
     if (lhash_workers != NULL) {
-        int jobs = atoi(lhash_workers);
-        if (jobs > 0)
+        int jobs;
+
+        if (test_strtoint(lhash_workers, &jobs) && jobs > 0)
             num_workers = jobs;
     }

diff --git a/test/mfail/mfail.c b/test/mfail/mfail.c
index ec391c8e85..70f575afc9 100644
--- a/test/mfail/mfail.c
+++ b/test/mfail/mfail.c
@@ -12,6 +12,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <limits.h>
 #include <openssl/crypto.h>

 #if defined(__has_feature)
@@ -69,7 +70,12 @@ static int env_is_true(const char *name)
 static int env_int(const char *name, int dflt)
 {
     const char *value = getenv(name);
-    return (value != NULL && *value != '\0') ? atoi(value) : dflt;
+    unsigned long ul;
+
+    if (value != NULL && *value != '\0'
+        && OPENSSL_strtoul(value, NULL, 10, &ul) && ul <= INT_MAX)
+        return (int)ul;
+    return dflt;
 }

 static void mfail_print_bt(void)
diff --git a/test/recipes/30-test_evp_data/evppkey_rsa_common.txt b/test/recipes/30-test_evp_data/evppkey_rsa_common.txt
index e97ff64c09..58ac329afe 100644
--- a/test/recipes/30-test_evp_data/evppkey_rsa_common.txt
+++ b/test/recipes/30-test_evp_data/evppkey_rsa_common.txt
@@ -966,6 +966,12 @@ Input="0123456789ABCDEF0123456789ABCDEF"
 Output=4DE433D5844043EF08D354DA03CB29068780D52706D7D1E4D50EFB7D58C9D547D83A747DDD0635A96B28F854E50145518482CB49E963054621B53C60C498D07C16E9C2789C893CF38D4D86900DE71BDE463BD2761D1271E358C7480A1AC0BAB930DDF39602AD1BC165B5D7436B516B7A7858E8EB7AB1C420EEB482F4D207F0E462B1724959320A084E13848D11D10FB593E66BF680BF6D3F345FC3E9C3DE60ABBAC37E1C6EC80A268C8D9FC49626C679097AA690BC1AA662B95EB8DB70390861AA0898229F9349B4B5FDD030D4928C47084708A933144BE23BD3C6E661B85B2C0EF9ED36D498D5B7320E8194D363D4AD478C059BAE804181965E0B81B663158A
 Result = VERIFY_ERROR

+# Invalid numeric salt length string
+Verify = RSA-2048-PUBLIC
+Ctrl = rsa_padding_mode:pss
+Ctrl = rsa_pss_saltlen:1x
+Result = PKEY_CTRL_ERROR
+
 # Verify using default parameters, explicitly setting parameters
 Verify = RSA-PSS-DEFAULT
 Ctrl = rsa_padding_mode:pss
diff --git a/test/ssl_old_test.c b/test/ssl_old_test.c
index 40df6536e3..48e3f61cf3 100644
--- a/test/ssl_old_test.c
+++ b/test/ssl_old_test.c
@@ -1043,13 +1043,16 @@ int main(int argc, char *argv[])
         } else if (HAS_PREFIX(*argv, "-num")) {
             if (--argc < 1)
                 goto bad;
-            number = atoi(*(++argv));
+            (void)test_strtoint(*(++argv), &number);
             if (number == 0)
                 number = 1;
         } else if (strcmp(*argv, "-bytes") == 0) {
+            unsigned long ul = 0;
+
             if (--argc < 1)
                 goto bad;
-            bytes = atol(*(++argv));
+            if (OPENSSL_strtoul(*(++argv), NULL, 10, &ul) && ul <= LONG_MAX)
+                bytes = (long)ul;
             if (bytes == 0L)
                 bytes = 1L;
             i = (int)strlen(argv[0]);
@@ -1191,9 +1194,12 @@ int main(int argc, char *argv[])
                 goto bad;
             client_sess_in = *(++argv);
         } else if (strcmp(*argv, "-should_reuse") == 0) {
+            int reuse_arg = 0;
+
             if (--argc < 1)
                 goto bad;
-            should_reuse = !!atoi(*(++argv));
+            (void)test_strtoint(*(++argv), &reuse_arg);
+            should_reuse = reuse_arg != 0;
         } else if (strcmp(*argv, "-no_ticket") == 0) {
             no_ticket = 1;
         } else if (strcmp(*argv, "-client_ktls") == 0) {
diff --git a/test/testutil.h b/test/testutil.h
index 7e23f53827..cda7716cdf 100644
--- a/test/testutil.h
+++ b/test/testutil.h
@@ -712,6 +712,14 @@ STACK_OF(X509) *load_certs_pem(const char *file);
 X509_REQ *load_csr_der(const char *file, OSSL_LIB_CTX *libctx);
 int test_asn1_string_to_time_t(const char *asn1_string, time_t *out_time_t);
 int compare_with_reference_file(BIO *membio, const char *reffile);
+
+/*
+ * Parse a non-negative decimal integer from |value| into |*result|.  Returns 1
+ * on success, 0 on failure (NULL/empty/non-numeric/negative/overflowing input,
+ * or trailing garbage).  Test binaries link only against the public ABI, so
+ * this wraps OPENSSL_strtoul() rather than the libcrypto-internal helpers.
+ */
+int test_strtoint(const char *value, int *result);
 /*
  * Create an X509 from an array of strings.
  */
diff --git a/test/testutil/driver.c b/test/testutil/driver.c
index bfcb65b7bc..8e3637acfe 100644
--- a/test/testutil/driver.c
+++ b/test/testutil/driver.c
@@ -13,6 +13,7 @@

 #include <string.h>
 #include <assert.h>
+#include <limits.h>

 #include "internal/nelem.h"
 #include <openssl/bio.h>
@@ -138,14 +139,22 @@ int setup_test_framework(int argc, char *argv[])
     char *test_rand_seed = getenv("OPENSSL_TEST_RAND_SEED");
     char *TAP_levels = getenv("HARNESS_OSSL_LEVEL");

-    if (TAP_levels != NULL)
-        level = 4 * atoi(TAP_levels);
+    if (TAP_levels != NULL) {
+        int n;
+
+        if (test_strtoint(TAP_levels, &n) && n <= INT_MAX / 4)
+            level = 4 * n;
+    }
     test_adjust_streams_tap_level(level);
     if (test_rand_order != NULL) {
+        int n;
+
         rand_order = 1;
-        set_seed(atoi(test_rand_order));
+        set_seed(test_strtoint(test_rand_order, &n) ? n : 0);
     } else if (test_rand_seed != NULL) {
-        set_seed(atoi(test_rand_seed));
+        int n;
+
+        set_seed(test_strtoint(test_rand_seed, &n) ? n : 0);
     } else {
         set_seed(0);
     }
@@ -178,8 +187,12 @@ static int check_single_test_params(char *name, char *testname, char *itname)
                 break;
             }
         }
-        if (i >= num_tests)
-            single_test = atoi(name);
+        if (i >= num_tests) {
+            int n;
+
+            if (test_strtoint(name, &n))
+                single_test = n;
+        }
     }

     /* if only iteration is specified, assume we want the first test */
diff --git a/test/testutil/helper.c b/test/testutil/helper.c
index 551b9a200b..a23e8c2f23 100644
--- a/test/testutil/helper.c
+++ b/test/testutil/helper.c
@@ -7,12 +7,24 @@
  * https://www.openssl.org/source/license.html
  */
 #include <time.h>
+#include <limits.h>

 #include <openssl/asn1t.h>
 #include <openssl/posix_time.h>
+#include <openssl/crypto.h>

 #include "../testutil.h"

+int test_strtoint(const char *value, int *result)
+{
+    unsigned long ul;
+
+    if (!OPENSSL_strtoul(value, NULL, 10, &ul) || ul > INT_MAX)
+        return 0;
+    *result = (int)ul;
+    return 1;
+}
+
 int test_asn1_string_to_time_t(const char *asn1_string, time_t *out_time_t)
 {
     int ret = 0;
diff --git a/test/timing_load_creds.c b/test/timing_load_creds.c
index 5e9e2bf6c0..45cdc2e1da 100644
--- a/test/timing_load_creds.c
+++ b/test/timing_load_creds.c
@@ -9,8 +9,10 @@

 #include <stdio.h>
 #include <stdlib.h>
+#include <limits.h>

 #include <openssl/e_os2.h>
+#include <openssl/crypto.h>

 #ifdef OPENSSL_SYS_UNIX
 #include <sys/stat.h>
@@ -120,10 +122,14 @@ int main(int ac, char **av)
         default:
             usage();
             break;
-        case 'c':
-            if ((count = atoi(optarg)) < 0)
+        case 'c': {
+            unsigned long ul;
+
+            if (!OPENSSL_strtoul(optarg, NULL, 10, &ul) || ul > INT_MAX)
                 usage();
+            count = (int)ul;
             break;
+        }
         case 'd':
             debug = 1;
             break;
diff --git a/util/platform_symbols/unix-symbols.txt b/util/platform_symbols/unix-symbols.txt
index 62720f35fd..bfd60a51b7 100644
--- a/util/platform_symbols/unix-symbols.txt
+++ b/util/platform_symbols/unix-symbols.txt
@@ -1,7 +1,6 @@
 abort
 accept
 aligned_alloc
-atoi
 bcmp
 bind
 calloc
diff --git a/util/platform_symbols/windows-symbols.txt b/util/platform_symbols/windows-symbols.txt
index 7ae58a3f18..cd41c7ec02 100644
--- a/util/platform_symbols/windows-symbols.txt
+++ b/util/platform_symbols/windows-symbols.txt
@@ -125,7 +125,6 @@ _stat64i32
 _strdup
 _time64
 _wfopen
-atoi
 calloc
 clearerr
 fclose