Commit 80b7e49c27 for openssl.org

commit 80b7e49c273f9977394de54f338baec9273dd47a
Author: herbenderbler <johnclaus@gmail.com>
Date:   Wed Mar 25 00:49:06 2026 -0600

    Use mmap for pkeyutl -rawin and dgst one-shot input

    When using openssl pkeyutl -rawin or openssl dgst for one-shot sign/verify
    (e.g. Ed25519, Ed448), file input is now read via mmap() on Unix where
    supported, avoiding a full buffer allocation and copy. Large files are
    supported without doubling memory use; on failure of the mmap path we
    do not fall back to the buffer path.

    - Add app_mmap_file() in apps/lib/apps.c: stat/open/mmap/close, tri-state
      return (1 mapped, 0 size zero, -1 error). Parameter err_bio avoids
      shadowing global bio_err (-Wshadow).
    - apps/pkeyutl.c and apps/dgst.c: use app_mmap_file(); single exit for
      mmap path in pkeyutl; dgst includes apps.h first for _FILE_OFFSET_BITS;
      do_fp_oneshot_sign returns EXIT_SUCCESS/EXIT_FAILURE like do_fp(); no
      fallback when mmap attempted but fails.
    - pkeyutl mmap/buffer path: pass filesize to EVP_DigestVerify and
      EVP_DigestSign (review suggestion, avoids casting buf_len).
    - Error messages: per-file messages for stat/size (dgst, pkeyutl); CHANGES.md
      "Unix-like" and "16 MB" (documentation style).
    - Centralize _FILE_OFFSET_BITS and mmap includes in apps/include/apps.h.
    - Tests: pkeyutl/dgst oneshot from file, no-fallback regression tests;
      use srctop_dir for test paths; stderr patterns for mmap errors.
    - Docs: man pages and CHANGES.md.

    CI fixes: return failure from dgst one-shot sign when mmap fails; treat
    non-regular paths as mmap errors in app_mmap_file() and pkeyutl; reject
    directories before mmap.

    Addresses review feedback from DDvO, npajkovsky, and vdukhovni (PR #30429).

    Fixes #11677

    Co-authored-by: Viktor Dukhovni <viktor1ghub@dukhovni.org>
    Co-authored-by: David von Oheimb <DDvO@users.noreply.github.com>

    Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
    Reviewed-by: Viktor Dukhovni <viktor@openssl.org>
    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    MergeDate: Fri Mar 27 16:25:33 2026
    (Merged from https://github.com/openssl/openssl/pull/30429)

diff --git a/CHANGES.md b/CHANGES.md
index 4cd74cea87..8672a909b6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -31,6 +31,17 @@ OpenSSL Releases

 ### Changes between 4.0 and 4.1 [xx XXX xxxx]

+ * The `openssl pkeyutl` command now uses memory-mapped I/O when reading
+   raw input from a file for oneshot sign/verify operations (such as Ed25519,
+   Ed448, and ML-DSA) on platforms that support it (Unix-like). The
+   `openssl dgst` command uses the same approach for one-shot sign/verify
+   when the input is from a file, removing the previous 16 MB limit for
+   file-based input. This improves performance and supports large files
+   without doubling memory use. Other platforms and stdin input continue to
+   use the existing buffer-based path.
+
+   *John Claus*
+
  * Added AVX2 optimized ML-DSA NTT operations on `x86_64`.

    *Marcel Cornu and Tomasz Kantecki*
diff --git a/apps/dgst.c b/apps/dgst.c
index 0243042073..68e9f3f43f 100644
--- a/apps/dgst.c
+++ b/apps/dgst.c
@@ -7,10 +7,10 @@
  * https://www.openssl.org/source/license.html
  */

+#include "apps.h"
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
-#include "apps.h"
 #include "progs.h"
 #include <openssl/bio.h>
 #include <openssl/err.h>
@@ -20,6 +20,7 @@
 #include <openssl/pem.h>
 #include <openssl/hmac.h>
 #include <ctype.h>
+#include <sys/stat.h>

 #undef BUFSIZE
 #define BUFSIZE 1024 * 8
@@ -482,7 +483,7 @@ int dgst_main(int argc, char **argv)
         BIO_set_fp(in, stdin, BIO_NOCLOSE);
         if (oneshot_sign)
             ret = do_fp_oneshot_sign(out, signctx, in, separator, out_bin,
-                sigkey, sigbuf, siglen, NULL, "stdin");
+                sigkey, sigbuf, siglen, NULL, NULL);
         else
             ret = do_fp(out, buf, inp, separator, out_bin, xoflen,
                 sigkey, sigbuf, siglen, NULL, md_name, "stdin");
@@ -723,52 +724,88 @@ end:
 }

 /*
- * Some new algorithms only support one shot operations.
- * For these we need to buffer all input and then do the sign on the
- * total buffered input. These algorithms set a NULL digest name which is
- * then used inside EVP_DigestVerify() and EVP_DigestSign().
+ * Perform one-shot verify or sign on a contiguous data buffer.
+ * Returns 0 on failure, 1 on success.
  */
-static int do_fp_oneshot_sign(BIO *out, EVP_MD_CTX *ctx, BIO *in, int sep, int binout,
-    EVP_PKEY *key, unsigned char *sigin, int siglen,
-    const char *sig_name, const char *file)
+static int do_oneshot_verify_sign(EVP_MD_CTX *ctx, BIO *out,
+    unsigned char *sigin, int siglen, EVP_PKEY *key,
+    const unsigned char *data, size_t len,
+    int sep, int binout, const char *sig_name, const char *file)
 {
-    int res, ret = EXIT_FAILURE;
-    size_t len = 0;
-    size_t buflen = 0;
-    size_t maxlen = 16 * 1024 * 1024;
-    uint8_t *buf = NULL, *sig = NULL;
+    int res;
+    size_t siglen_out = 0;
+    unsigned char *sig = NULL;

-    if (!bio_to_mem(&buf, &buflen, maxlen, in)) {
-        BIO_printf(bio_err, "Read error in %s\n", file);
-        return ret;
-    }
     if (sigin != NULL) {
-        res = EVP_DigestVerify(ctx, sigin, siglen, buf, buflen);
+        res = EVP_DigestVerify(ctx, sigin, siglen, data, len);
         print_verify_result(out, res);
-        if (res > 0)
-            ret = EXIT_SUCCESS;
-        goto end;
+        return res > 0;
     }
     if (key != NULL) {
-        if (EVP_DigestSign(ctx, NULL, &len, buf, buflen) != 1) {
+        if (EVP_DigestSign(ctx, NULL, &siglen_out, data, len) != 1) {
             BIO_puts(bio_err, "Error getting maximum length of signed data\n");
-            goto end;
+            return 0;
         }
-        sig = app_malloc(len, "Signature buffer");
-        if (EVP_DigestSign(ctx, sig, &len, buf, buflen) != 1) {
+        sig = app_malloc(siglen_out, "Signature buffer");
+        if (EVP_DigestSign(ctx, sig, &siglen_out, data, len) != 1) {
             BIO_puts(bio_err, "Error signing data\n");
-            goto end;
+            OPENSSL_free(sig);
+            return 0;
         }
-        print_out(out, sig, len, sep, binout, sig_name, NULL, file);
-        ret = EXIT_SUCCESS;
-    } else {
-        BIO_puts(bio_err, "key must be set for one-shot algorithms\n");
-        goto end;
+        print_out(out, sig, siglen_out, sep, binout, sig_name, NULL, file);
+        OPENSSL_free(sig);
+        return 1;
     }
+    BIO_puts(bio_err, "key must be set for one-shot algorithms\n");
+    return 0;
+}

-end:
-    OPENSSL_free(sig);
-    OPENSSL_clear_free(buf, buflen);
+/*
+ * Some new algorithms only support one shot operations.
+ * For these we need to buffer all input and then do the sign on the
+ * total buffered input. These algorithms set a NULL digest name which is
+ * then used inside EVP_DigestVerify() and EVP_DigestSign().
+ */
+static int do_fp_oneshot_sign(BIO *out, EVP_MD_CTX *ctx, BIO *in, int sep, int binout,
+    EVP_PKEY *key, unsigned char *sigin, int siglen,
+    const char *sig_name, const char *file)
+{
+    int ret = EXIT_FAILURE;
+    size_t buflen = 0;
+    size_t maxlen = 16 * 1024 * 1024;
+    uint8_t *buf = NULL;
+
+#if defined(OPENSSL_SYS_UNIX) && defined(_POSIX_MAPPED_FILES) && _POSIX_MAPPED_FILES > 0
+    if (file != NULL) {
+        const unsigned char *data = NULL;
+        size_t filesize = 0;
+        int r = app_mmap_file(file, bio_err, (size_t)-1, &data, &filesize);
+
+        if (r == 1) {
+            ret = do_oneshot_verify_sign(ctx, out, sigin, siglen, key, data,
+                      filesize, sep, binout, sig_name, file)
+                ? EXIT_SUCCESS
+                : EXIT_FAILURE;
+            munmap((void *)data, filesize);
+            return ret;
+        }
+        if (r == -1)
+            return EXIT_FAILURE; /* error already printed */
+        /* r == 0: empty file, fall through to buffer path */
+    }
+#endif
+
+    {
+        const char *display_file = file != NULL ? file : "stdin";
+
+        if (!bio_to_mem(&buf, &buflen, maxlen, in))
+            return EXIT_FAILURE;
+        ret = do_oneshot_verify_sign(ctx, out, sigin, siglen, key, buf, buflen,
+                  sep, binout, sig_name, display_file)
+            ? EXIT_SUCCESS
+            : EXIT_FAILURE;
+        OPENSSL_clear_free(buf, buflen);
+    }

     return ret;
 }
diff --git a/apps/include/apps.h b/apps/include/apps.h
index 16669ed040..43097c013e 100644
--- a/apps/include/apps.h
+++ b/apps/include/apps.h
@@ -10,6 +10,16 @@
 #ifndef OSSL_APPS_H
 #define OSSL_APPS_H

+#if defined(__linux__) || defined(__sun__) || defined(__hpux)
+/*
+ * Allow open() and stat() to work with files larger than 2GB on 32-bit
+ * systems.  See crypto/o_fopen.c and crypto/bio/bss_file.c.
+ */
+#ifndef _FILE_OFFSET_BITS
+#define _FILE_OFFSET_BITS 64
+#endif
+#endif
+
 #include "internal/common.h" /* for HAS_PREFIX */
 #include "internal/nelem.h"
 #include <assert.h>
@@ -22,6 +32,19 @@
 #endif

 #include <openssl/e_os2.h>
+#if defined(OPENSSL_SYS_UNIX) && defined(_POSIX_MAPPED_FILES) && _POSIX_MAPPED_FILES > 0
+#include <sys/mman.h>
+#include <unistd.h>
+/*
+ * Map a file read-only into memory.  Returns 1 on success (*out_data and
+ * *out_size set; caller must munmap when done), 0 when file size is 0 (no
+ * error, caller may use buffer path), or -1 on error (message printed to
+ * bio_err).  known_size: (size_t)-1 = stat to get size; 0 = do not map
+ * (return 0); > 0 = use this size (caller obtained it from stat of same path).
+ */
+int app_mmap_file(const char *path, BIO *err_bio, size_t known_size,
+    const unsigned char **out_data, size_t *out_size);
+#endif
 #include <openssl/types.h>
 #include <openssl/bio.h>
 #include <openssl/x509.h>
diff --git a/apps/lib/apps.c b/apps/lib/apps.c
index 6e8167b7e9..46e78ecebd 100644
--- a/apps/lib/apps.c
+++ b/apps/lib/apps.c
@@ -2202,6 +2202,63 @@ int bio_to_mem(unsigned char **out, size_t *outlen, size_t maxlen, BIO *in)
     return 1;
 }

+#if defined(OPENSSL_SYS_UNIX) && defined(_POSIX_MAPPED_FILES) && _POSIX_MAPPED_FILES > 0
+int app_mmap_file(const char *path, BIO *err_bio, size_t known_size,
+    const unsigned char **out_data, size_t *out_size)
+{
+    struct stat st;
+    size_t filesize;
+    int fd;
+    void *p;
+
+    *out_data = NULL;
+    *out_size = 0;
+
+    if (known_size == 0)
+        return 0;
+
+    if (known_size == (size_t)-1) {
+        if (stat(path, &st) != 0 || st.st_size < 0) {
+            BIO_printf(err_bio, "Error: failed to get size of file '%s'\n", path);
+            return -1;
+        }
+        if (!S_ISREG(st.st_mode)) {
+            /*
+             * mmap() is only for regular files. Directories and other non-regular
+             * paths can report st_size == 0; do not treat those like empty files
+             * and fall back to the buffer path in callers.
+             */
+            BIO_puts(err_bio, "Error: failed to use memory-mapped file\n");
+            return -1;
+        }
+        filesize = (size_t)st.st_size;
+        if ((off_t)filesize != st.st_size) {
+            BIO_puts(err_bio, "Error: failed to convert file size, likely too big\n");
+            return -1;
+        }
+        if (filesize == 0)
+            return 0;
+    } else {
+        filesize = known_size;
+    }
+
+    fd = open(path, O_RDONLY);
+    if (fd < 0) {
+        BIO_puts(err_bio, "Error opening file for memory mapping\n");
+        return -1;
+    }
+    p = mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, fd, 0);
+    (void)close(fd);
+    if (p == MAP_FAILED) {
+        BIO_puts(err_bio, "Error: failed to use memory-mapped file\n");
+        return -1;
+    }
+    *out_data = (const unsigned char *)p;
+    *out_size = filesize;
+    return 1;
+}
+#endif
+
 int pkey_ctrl_string(EVP_PKEY_CTX *ctx, const char *value)
 {
     int rv = 0;
diff --git a/apps/pkeyutl.c b/apps/pkeyutl.c
index d30bae99f2..0ffd92f8c0 100644
--- a/apps/pkeyutl.c
+++ b/apps/pkeyutl.c
@@ -9,6 +9,7 @@

 #include "apps.h"
 #include "progs.h"
+#include <limits.h>
 #include <string.h>
 #include <openssl/err.h>
 #include <openssl/pem.h>
@@ -37,8 +38,8 @@ static int do_keyop(EVP_PKEY_CTX *ctx, int pkey_op,
     unsigned char *secret, size_t *psecretlen);

 static int do_raw_keyop(int pkey_op, EVP_MD_CTX *mctx,
-    EVP_PKEY *pkey, BIO *in,
-    int filesize, unsigned char *sig, size_t siglen,
+    EVP_PKEY *pkey, BIO *in, const char *infile,
+    size_t filesize, unsigned char *sig, size_t siglen,
     unsigned char **out, size_t *poutlen);

 static int only_nomd(EVP_PKEY *pkey)
@@ -162,7 +163,7 @@ int pkeyutl_main(int argc, char **argv)
     int rawin = 0;
     EVP_MD_CTX *mctx = NULL;
     EVP_MD *md = NULL;
-    int filesize = -1;
+    size_t filesize = (size_t)-1; /* (size_t)-1 means unknown */
     OSSL_LIB_CTX *libctx = app_get0_libctx();

     prog = opt_init(argc, argv, pkeyutl_options);
@@ -454,8 +455,11 @@ int pkeyutl_main(int argc, char **argv)
         if (infile != NULL) {
             struct stat st;

-            if (stat(infile, &st) == 0 && st.st_size <= INT_MAX)
-                filesize = (int)st.st_size;
+            if (stat(infile, &st) == 0 && st.st_size >= 0) {
+                filesize = (size_t)st.st_size;
+                if ((off_t)filesize != st.st_size)
+                    filesize = (size_t)-1;
+            }
         }
         if (in == NULL)
             goto end;
@@ -539,7 +543,7 @@ int pkeyutl_main(int argc, char **argv)

     if (pkey_op == EVP_PKEY_OP_VERIFY) {
         if (rawin) {
-            rv = do_raw_keyop(pkey_op, mctx, pkey, in, filesize, sig, siglen,
+            rv = do_raw_keyop(pkey_op, mctx, pkey, in, infile, filesize, sig, siglen,
                 NULL, 0);
         } else {
             rv = EVP_PKEY_verify(ctx, sig, siglen, buf_in, buf_inlen);
@@ -554,7 +558,7 @@ int pkeyutl_main(int argc, char **argv)
     }
     if (rawin) {
         /* rawin allocates the buffer in do_raw_keyop() */
-        rv = do_raw_keyop(pkey_op, mctx, pkey, in, filesize, NULL, 0,
+        rv = do_raw_keyop(pkey_op, mctx, pkey, in, infile, filesize, NULL, 0,
             &buf_out, &buf_outlen);
     } else {
         if (kdflen != 0) {
@@ -821,8 +825,8 @@ static int do_keyop(EVP_PKEY_CTX *ctx, int pkey_op,
 #define TBUF_MAXSIZE 2048

 static int do_raw_keyop(int pkey_op, EVP_MD_CTX *mctx,
-    EVP_PKEY *pkey, BIO *in,
-    int filesize, unsigned char *sig, size_t siglen,
+    EVP_PKEY *pkey, BIO *in, const char *infile,
+    size_t filesize, unsigned char *sig, size_t siglen,
     unsigned char **out, size_t *poutlen)
 {
     int rv = 0;
@@ -832,32 +836,72 @@ static int do_raw_keyop(int pkey_op, EVP_MD_CTX *mctx,

     /* Some algorithms only support oneshot digests */
     if (only_nomd(pkey)) {
-        if (filesize < 0) {
+        if (filesize == (size_t)-1) {
+            BIO_printf(bio_err,
+                "Error: unable to determine size of file '%s' for oneshot operation\n",
+                infile);
+            goto end;
+        }
+#if defined(OPENSSL_SYS_UNIX) && defined(_POSIX_MAPPED_FILES) && _POSIX_MAPPED_FILES > 0
+        if (infile != NULL) {
+            struct stat st;
+
+            if (stat(infile, &st) == 0 && !S_ISREG(st.st_mode)) {
+                BIO_puts(bio_err, "Error: failed to use memory-mapped file\n");
+                goto end;
+            }
+        }
+        if (filesize > 0 && infile != NULL) {
+            const unsigned char *data = NULL;
+            size_t mapped_size = 0;
+
+            if (app_mmap_file(infile, bio_err, filesize, &data, &mapped_size) == 1) {
+                switch (pkey_op) {
+                case EVP_PKEY_OP_VERIFY:
+                    rv = EVP_DigestVerify(mctx, sig, siglen, data, mapped_size);
+                    break;
+                case EVP_PKEY_OP_SIGN:
+                    rv = EVP_DigestSign(mctx, NULL, poutlen, data, mapped_size);
+                    if (rv == 1 && out != NULL) {
+                        *out = app_malloc(*poutlen, "buffer output");
+                        rv = EVP_DigestSign(mctx, *out, poutlen, data, mapped_size);
+                    }
+                    break;
+                default:
+                    break;
+                }
+                munmap((void *)data, mapped_size);
+            }
+            /* Success or mmap failure: do not fall back to buffer path */
+            goto end;
+        }
+#endif
+        if (filesize > INT_MAX) {
             BIO_puts(bio_err,
-                "Error: unable to determine file size for oneshot operation\n");
+                "Error: file too large for oneshot operation without memory mapping\n");
             goto end;
         }
         if (filesize > 0)
             mbuf = app_malloc(filesize, "oneshot sign/verify buffer");
         switch (pkey_op) {
         case EVP_PKEY_OP_VERIFY:
-            buf_len = BIO_read(in, mbuf, filesize);
-            if (buf_len != filesize) {
+            buf_len = BIO_read(in, mbuf, (int)filesize);
+            if (buf_len < 0 || (size_t)buf_len != filesize) {
                 BIO_puts(bio_err, "Error reading raw input data\n");
                 goto end;
             }
-            rv = EVP_DigestVerify(mctx, sig, siglen, mbuf, buf_len);
+            rv = EVP_DigestVerify(mctx, sig, siglen, mbuf, filesize);
             break;
         case EVP_PKEY_OP_SIGN:
-            buf_len = BIO_read(in, mbuf, filesize);
-            if (buf_len != filesize) {
+            buf_len = BIO_read(in, mbuf, (int)filesize);
+            if (buf_len < 0 || (size_t)buf_len != filesize) {
                 BIO_puts(bio_err, "Error reading raw input data\n");
                 goto end;
             }
-            rv = EVP_DigestSign(mctx, NULL, poutlen, mbuf, buf_len);
+            rv = EVP_DigestSign(mctx, NULL, poutlen, mbuf, filesize);
             if (rv == 1 && out != NULL) {
                 *out = app_malloc(*poutlen, "buffer output");
-                rv = EVP_DigestSign(mctx, *out, poutlen, mbuf, buf_len);
+                rv = EVP_DigestSign(mctx, *out, poutlen, mbuf, filesize);
             }
             break;
         }
diff --git a/doc/man1/openssl-dgst.pod.in b/doc/man1/openssl-dgst.pod.in
index 8aa2cf79c1..73e439ad14 100644
--- a/doc/man1/openssl-dgst.pod.in
+++ b/doc/man1/openssl-dgst.pod.in
@@ -118,10 +118,11 @@ Filename to output to, or standard output by default.
 Digitally sign the digest using the given private key.

 Note that for algorithms that only support one-shot signing
-(such as Ed25519, ED448, ML-DSA-44, ML-DSA-65 andML-DSA-87) the digest must not
+(such as Ed25519, ED448, ML-DSA-44, ML-DSA-65 and ML-DSA-87) the digest must not
 be set. For these algorithms the input is buffered (and not digested) before
-signing. For these algorithms, if the input is larger than 16MB an error
-will occur.
+signing. When the input is from a file, memory-mapped I/O is used on
+supported platforms (Unix\-like), allowing large files without a size limit; when
+input is from stdin or on unsupported platforms, input is limited to 16MB.

 =item B<-keyform> B<DER>|B<PEM>|B<P12>

@@ -143,6 +144,10 @@ see L<openssl-passphrase-options(1)>.

 Verify the signature using the public key in "filename".
 The output is either "Verified OK" or "Verification Failure".
+For one-shot verification algorithms (e.g. Ed25519, Ed448), when the input
+is from a file, memory-mapped I/O is used on supported platforms (Unix\-like),
+allowing large files; when input is from stdin or on unsupported platforms,
+input is limited to 16MB.

 =item B<-prverify> I<filename>

@@ -331,6 +336,11 @@ The B<-engine> and B<-engine_impl> options were removed in OpenSSL 4.0.

 The B<-hmac-env> and B<-hmac-stdin> options were added in OpenSSL 4.0.

+Since OpenSSL 4.1, one-shot sign and verify (e.g. Ed25519, Ed448) with input
+from a file uses memory-mapped I/O on supported platforms (Unix\-like), allowing
+large files to be processed without the previous 16MB limit for file-based
+input.
+
 =head1 COPYRIGHT

 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/doc/man1/openssl-pkeyutl.pod.in b/doc/man1/openssl-pkeyutl.pod.in
index 5c080bf51f..053385ca0b 100644
--- a/doc/man1/openssl-pkeyutl.pod.in
+++ b/doc/man1/openssl-pkeyutl.pod.in
@@ -77,6 +77,10 @@ is implied since OpenSSL 3.5, and required in earlier versions.

 The B<-digest> option implies B<-rawin> since OpenSSL 3.5.

+When the input is read from a file (B<-in> I<filename>), the command may
+use memory-mapped I/O on supported platforms for better performance and
+to handle large files without loading the entire file into memory.
+
 =item B<-digest> I<algorithm>

 This option can only be used with B<-sign> and B<-verify>.
@@ -681,6 +685,11 @@ the supported algorithms, the only supported B<mode> is now the default.

 The B<-engine> option was removed in OpenSSL 4.0.

+Since OpenSSL 4.1, when reading raw input from a file (B<-in> I<filename>) for
+oneshot sign/verify (such as Ed25519, Ed448, and ML-DSA), the command uses
+memory-mapped I/O on supported platforms, allowing large files to be processed
+without loading the entire file into memory.
+
 =head1 COPYRIGHT

 Copyright 2006-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/test/recipes/20-test_dgst.t b/test/recipes/20-test_dgst.t
index fe801b298e..e287bd32ff 100644
--- a/test/recipes/20-test_dgst.t
+++ b/test/recipes/20-test_dgst.t
@@ -12,13 +12,13 @@ use warnings;

 use File::Spec;
 use File::Basename;
-use OpenSSL::Test qw/:DEFAULT with srctop_file data_file bldtop_dir/;
+use OpenSSL::Test qw/:DEFAULT with srctop_file srctop_dir data_file bldtop_dir/;
 use OpenSSL::Test::Utils;
 use Cwd qw(abs_path);

 setup("test_dgst");

-plan tests => 24;
+plan tests => 25;

 sub tsignverify {
     my $testtext = shift;
@@ -89,91 +89,143 @@ sub tsignverify_sha512 {
        $testtext.": Expect failure verifying mismatching data");
 }

-SKIP: {
-    skip "RSA is not supported by this OpenSSL build", 1
-        if disabled("rsa");
-
-    subtest "RSA signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("RSA",
-                    srctop_file("test","testrsa.pem"),
-                    srctop_file("test","testrsapub.pem"));
-    };
-
-    subtest "RSA signature generation and verification with `sha512` CLI" => sub {
-        tsignverify_sha512("RSA",
-                           srctop_file("test","testrsa2048.pem"),
-                           srctop_file("test","testrsa2048pub.pem"));
-    };
-}
+subtest "RSA signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("rsa")) {
+        plan tests => 1;
+        ok(1, "Skipped (RSA not supported)");
+        return;
+    }
+    tsignverify("RSA",
+                srctop_file("test","testrsa.pem"),
+                srctop_file("test","testrsapub.pem"));
+};

-SKIP: {
-    skip "DSA is not supported by this OpenSSL build", 1
-        if disabled("dsa");
+subtest "RSA signature generation and verification with `sha512` CLI" => sub {
+    if (disabled("rsa")) {
+        plan tests => 1;
+        ok(1, "Skipped (RSA not supported)");
+        return;
+    }
+    tsignverify_sha512("RSA",
+                       srctop_file("test","testrsa2048.pem"),
+                       srctop_file("test","testrsa2048pub.pem"));
+};

-    subtest "DSA signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("DSA",
-                    srctop_file("test","testdsa.pem"),
-                    srctop_file("test","testdsapub.pem"));
-    };
-}
+subtest "DSA signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("dsa")) {
+        plan tests => 1;
+        ok(1, "Skipped (DSA not supported)");
+        return;
+    }
+    tsignverify("DSA",
+                srctop_file("test","testdsa.pem"),
+                srctop_file("test","testdsapub.pem"));
+};

-SKIP: {
-    skip "ECDSA is not supported by this OpenSSL build", 1
-        if disabled("ec");
+subtest "ECDSA signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ec")) {
+        plan tests => 1;
+        ok(1, "Skipped (ECDSA not supported)");
+        return;
+    }
+    tsignverify("ECDSA",
+                srctop_file("test","testec-p256.pem"),
+                srctop_file("test","testecpub-p256.pem"));
+};

-    subtest "ECDSA signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("ECDSA",
-                    srctop_file("test","testec-p256.pem"),
-                    srctop_file("test","testecpub-p256.pem"));
-    };
-}
+subtest "Ed25519 signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ecx")) {
+        plan tests => 1;
+        ok(1, "Skipped (EdDSA not supported)");
+        return;
+    }
+    tsignverify("Ed25519",
+                srctop_file("test","tested25519.pem"),
+                srctop_file("test","tested25519pub.pem"));
+};

-SKIP: {
-    skip "EdDSA is not supported by this OpenSSL build", 2
-        if disabled("ecx");
-
-    subtest "Ed25519 signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("Ed25519",
-                    srctop_file("test","tested25519.pem"),
-                    srctop_file("test","tested25519pub.pem"));
-    };
-
-    subtest "Ed448 signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("Ed448",
-                    srctop_file("test","tested448.pem"),
-                    srctop_file("test","tested448pub.pem"));
-    };
-}
+subtest "Ed448 signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ecx")) {
+        plan tests => 1;
+        ok(1, "Skipped (EdDSA not supported)");
+        return;
+    }
+    tsignverify("Ed448",
+                srctop_file("test","tested448.pem"),
+                srctop_file("test","tested448pub.pem"));
+};

-SKIP: {
-    skip "ML-DSA is not supported by this OpenSSL build", 3
-        if disabled("ml-dsa");
-
-    subtest "ML-DSA-44 signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("Ml-DSA-44",
-                    srctop_file("test","testmldsa44.pem"),
-                    srctop_file("test","testmldsa44pub.pem"));
-    };
-    subtest "ML-DSA-65 signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("Ml-DSA-65",
-                    srctop_file("test","testmldsa65.pem"),
-                    srctop_file("test","testmldsa65pub.pem"));
-    };
-    subtest "ML-DSA-87 signature generation and verification with `dgst` CLI" => sub {
-        tsignverify("Ml-DSA-87",
-                    srctop_file("test","testmldsa87.pem"),
-                    srctop_file("test","testmldsa87pub.pem"));
-    };
-}
+subtest "dgst one-shot: no buffer fallback when mmap path fails (Unix)" => sub {
+    if ($^O eq 'MSWin32' || disabled("ecx")) {
+        plan tests => 1;
+        ok(1, "Skipped (Unix/mmap or EdDSA not available)");
+        return;
+    }
+    plan tests => 2;

-SKIP: {
-    skip "dgst with provider is not supported by this OpenSSL build", 1
-        if disabled("module");
+    # Use a directory with non-zero st_size so app_mmap_file() attempts open+mmap
+    # (curdir "." often has st_size 0 on some FS, which skips mmap and breaks this test).
+    # mmap() on a directory must fail; we must not fall back to bio_to_mem.
+    my $key = srctop_file("test", "tested25519.pem");
+    my $dir = srctop_dir("test");
+    my $stderr_file = "dgst_nofallback_err.txt";
+
+    with({ exit_checker => sub { return shift != 0; } },
+         sub {
+             ok(run(app(['openssl', 'dgst', '-sign', $key, $dir],
+                        stderr => $stderr_file)),
+                "dgst one-shot with un-mmapable file fails (no fallback)");
+         });
+    if (open(my $fh, '<', $stderr_file)) {
+        my $err = do { local $/; <$fh> };
+        close($fh);
+        ok($err =~ /Error: failed to use memory-mapped file/, "stderr mentions mmap failure");
+    } else {
+        ok(0, "could not read stderr file");
+    }
+    unlink($stderr_file) if -f $stderr_file;
+};

-    subtest "SHA1 generation by provider with `dgst` CLI" => sub {
+subtest "ML-DSA-44 signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ml-dsa")) {
+        plan tests => 1;
+        ok(1, "Skipped (ML-DSA not supported)");
+        return;
+    }
+    tsignverify("Ml-DSA-44",
+                srctop_file("test","testmldsa44.pem"),
+                srctop_file("test","testmldsa44pub.pem"));
+};
+subtest "ML-DSA-65 signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ml-dsa")) {
+        plan tests => 1;
+        ok(1, "Skipped (ML-DSA not supported)");
+        return;
+    }
+    tsignverify("Ml-DSA-65",
+                srctop_file("test","testmldsa65.pem"),
+                srctop_file("test","testmldsa65pub.pem"));
+};
+subtest "ML-DSA-87 signature generation and verification with `dgst` CLI" => sub {
+    if (disabled("ml-dsa")) {
         plan tests => 1;
+        ok(1, "Skipped (ML-DSA not supported)");
+        return;
+    }
+    tsignverify("Ml-DSA-87",
+                srctop_file("test","testmldsa87.pem"),
+                srctop_file("test","testmldsa87pub.pem"));
+};
+
+subtest "SHA1 generation by provider with `dgst` CLI" => sub {
+    if (disabled("module")) {
+        plan tests => 1;
+        ok(1, "Skipped (dgst with provider not supported)");
+        return;
+    }
+    plan tests => 1;

-        $ENV{OPENSSL_MODULES} = abs_path(bldtop_dir("test"));
+    $ENV{OPENSSL_MODULES} = abs_path(bldtop_dir("test"));
         my $testdata = srctop_file('test', 'data.bin');
         my @macdata = run(app(['openssl', 'dgst', '-sha1',
                                '-provider', "p_ossltest",
@@ -183,8 +235,7 @@ SKIP: {
         chomp(@macdata);
         my $expected = qr/SHA1\(\Q$testdata\E\)= 000102030405060708090a0b0c0d0e0f10111213/;
         ok($macdata[0] =~ $expected, "SHA1: Check HASH value is as expected ($macdata[0]) vs ($expected)");
-    }
-}
+};

 subtest "HMAC generation with `dgst` CLI" => sub {
     plan tests => 2;
@@ -357,33 +408,40 @@ subtest "SHAKE digest generation with no xoflen set `dgst` CLI" => sub {
     ok(!run(app(['openssl', 'dgst', '-shake256', $testdata])), "SHAKE256 must fail without xoflen");
 };

-SKIP: {
-    skip "ECDSA is not supported by this OpenSSL build", 2
-        if disabled("ec");
-
-    subtest "signing with xoflen is not supported `dgst` CLI" => sub {
+subtest "signing with xoflen is not supported `dgst` CLI" => sub {
+    if (disabled("ec")) {
         plan tests => 1;
-        my $data_to_sign = srctop_file('test', 'data.bin');
-
-        ok(!run(app(['openssl', 'dgst', '-shake256', '-xoflen', '64',
-                     '-sign', srctop_file("test","testec-p256.pem"),
-                     '-out', 'test.sig',
-                     srctop_file('test', 'data.bin')])),
-                     "Generating signature with xoflen should fail");
-    };
+        ok(1, "Skipped (ECDSA not supported)");
+        return;
+    }
+    plan tests => 1;
+    my $data_to_sign = srctop_file('test', 'data.bin');

-    skip "HMAC-DRBG-KDF is not supported by this OpenSSL build", 1
-        if disabled("hmac-drbg-kdf");
+    ok(!run(app(['openssl', 'dgst', '-shake256', '-xoflen', '64',
+                 '-sign', srctop_file("test","testec-p256.pem"),
+                 '-out', 'test.sig',
+                 srctop_file('test', 'data.bin')])),
+                 "Generating signature with xoflen should fail");
+};

-    subtest "signing using the nonce-type sigopt" => sub {
+subtest "signing using the nonce-type sigopt" => sub {
+    if (disabled("ec")) {
         plan tests => 1;
-        my $data_to_sign = srctop_file('test', 'data.bin');
-
-        ok(run(app(['openssl', 'dgst', '-sha256',
-                     '-sign', srctop_file("test","testec-p256.pem"),
-                     '-out', 'test.sig',
-                     '-sigopt', 'nonce-type:1',
-                     srctop_file('test', 'data.bin')])),
-                     "Sign using the nonce-type sigopt");
+        ok(1, "Skipped (ECDSA not supported)");
+        return;
     }
-}
+    if (disabled("hmac-drbg-kdf")) {
+        plan tests => 1;
+        ok(1, "Skipped (HMAC-DRBG-KDF not supported)");
+        return;
+    }
+    plan tests => 1;
+    my $data_to_sign = srctop_file('test', 'data.bin');
+
+    ok(run(app(['openssl', 'dgst', '-sha256',
+                 '-sign', srctop_file("test","testec-p256.pem"),
+                 '-out', 'test.sig',
+                 '-sigopt', 'nonce-type:1',
+                 srctop_file('test', 'data.bin')])),
+                 "Sign using the nonce-type sigopt");
+};
diff --git a/test/recipes/20-test_pkeyutl.t b/test/recipes/20-test_pkeyutl.t
index 757c404387..bbc37b87f0 100644
--- a/test/recipes/20-test_pkeyutl.t
+++ b/test/recipes/20-test_pkeyutl.t
@@ -11,13 +11,13 @@ use warnings;

 use File::Spec;
 use File::Basename;
-use OpenSSL::Test qw/:DEFAULT srctop_file ok_nofips with/;
+use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir ok_nofips with/;
 use OpenSSL::Test::Utils;
 use File::Compare qw/compare_text compare/;

 setup("test_pkeyutl");

-plan tests => 27;
+plan tests => 29;

 # For the tests below we use the cert itself as the TBS file

@@ -214,9 +214,65 @@ SKIP: {
 }

 SKIP: {
-    skip "EdDSA is not supported by this OpenSSL build", 4
+    skip "EdDSA is not supported by this OpenSSL build", 6
         if disabled("ecx");

+    subtest "pkeyutl -rawin oneshot with file input (mmap or buffer path)" => sub {
+        my $data = srctop_file("test", "data.bin");
+        my $ed25519_key = srctop_file("test", "tested25519.pem");
+        my $ed25519_pub = srctop_file("test", "tested25519pub.pem");
+        my $ed448_key = srctop_file("test", "tested448.pem");
+        my $ed448_pub = srctop_file("test", "tested448pub.pem");
+
+        plan tests => 4;
+
+        # -in <file> for oneshot: uses mmap on Unix when supported, else buffer+BIO_read
+        ok(run(app(['openssl', 'pkeyutl', '-sign', '-rawin', '-inkey', $ed25519_key,
+                    '-in', $data, '-out', 'rawin_file_ed25519.sig'])),
+           "Ed25519 -rawin sign from file");
+        ok(run(app(['openssl', 'pkeyutl', '-verify', '-rawin', '-pubin', '-inkey', $ed25519_pub,
+                    '-sigfile', 'rawin_file_ed25519.sig', '-in', $data])),
+           "Ed25519 -rawin verify from file");
+        ok(run(app(['openssl', 'pkeyutl', '-sign', '-rawin', '-inkey', $ed448_key,
+                    '-in', $data, '-out', 'rawin_file_ed448.sig'])),
+           "Ed448 -rawin sign from file");
+        ok(run(app(['openssl', 'pkeyutl', '-verify', '-rawin', '-pubin', '-inkey', $ed448_pub,
+                    '-sigfile', 'rawin_file_ed448.sig', '-in', $data])),
+           "Ed448 -rawin verify from file");
+    };
+
+    subtest "pkeyutl -rawin oneshot: no buffer fallback when mmap path fails (Unix)" => sub {
+        if ($^O eq 'MSWin32') {
+            plan tests => 1;
+            ok(1, "Skipped (Unix/mmap only)");
+            return;
+        }
+        plan tests => 2;
+
+        # Use a directory with non-zero st_size so the mmap path is attempted
+        # (curdir "." often has st_size 0 on some FS and skips mmap).
+        my $ed25519_key = srctop_file("test", "tested25519.pem");
+        my $dir = srctop_dir("test");
+        my $stderr_file = "pkeyutl_nofallback_err.txt";
+
+        with({ exit_checker => sub { return shift != 0; } },
+             sub {
+                 ok(run(app(['openssl', 'pkeyutl', '-sign', '-rawin', '-inkey', $ed25519_key,
+                             '-in', $dir, '-out', 'nofallback.sig'],
+                            stderr => $stderr_file)),
+                    "pkeyutl -rawin with un-mmapable input fails (no fallback)");
+             });
+        if (open(my $fh, '<', $stderr_file)) {
+            my $err = do { local $/; <$fh> };
+            close($fh);
+            ok($err =~ /Error(?: opening file for memory mapping|: failed to use memory-mapped file)/,
+               "stderr mentions mmap failure");
+        } else {
+            ok(0, "could not read stderr file");
+        }
+        unlink($stderr_file) if -f $stderr_file;
+    };
+
     subtest "Ed2559 CLI signature generation and verification" => sub {
         tsignverify("Ed25519",
                     srctop_file("test","tested25519.pem"),