new file mode 100644
@@ -0,0 +1,170 @@
+From 57d0a1f66b9ed1b54f11af661da4719c43409497 Mon Sep 17 00:00:00 2001
+From: "H.Merijn Brand - Tux" <linux@tux.freedom.nl>
+Date: Sat, 4 Jul 2026 11:38:24 +0200
+Subject: [PATCH] Set a hard limit of 99999 on '?' placeholders
+ (CVE-2026-14739)
+
+CVE: CVE-2026-14739
+Upstream-Status: Backport [https://github.com/perl5-dbi/dbi/commit/2b77c88b655e9539a592c71a61fb965fc0075395]
+
+Backport Changes:
+ - Omit ChangeLog, dbixs_rev.h, lib/DBI/Changes.pm, and the DBI.pm version-number hunks because the target remains DBI 1.643.
+ - Omit regenerated doc/DBI.3, doc/DBI.html, doc/DBI.man, and doc/DBI.md files; retain the DBI.pm source-documentation and DBI.xs security changes unchanged.
+ - Correct the upstream boundary check to accept exactly 99999 placeholders and reject values above that documented limit; add t/60preparse.t regression coverage for 99999 and 100000 placeholders.
+
+(cherry picked from commit 2b77c88b655e9539a592c71a61fb965fc0075395)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ DBI.pm | 6 ++++--
+ DBI.xs | 53 +++++++++++++++++++++++++++++++++--------------------
+ t/60preparse.t | 13 ++++++++++++-
+ 3 files changed, 49 insertions(+), 23 deletions(-)
+
+diff --git a/DBI.pm b/DBI.pm
+index d62a32d..b988e40 100644
+--- a/DBI.pm
++++ b/DBI.pm
+@@ -7123,7 +7123,8 @@ a ref to an empty hash because they can't pre-determine the names.
+ It is possible that the keys in the hash returned by C<ParamValues>
+ are not exactly the same as those implied by the prepared statement.
+ For example, DBD::Oracle translates 'C<?>' placeholders into 'C<:pN>'
+-where N is a sequence number starting at 1.
++where N is a sequence number starting at C<1> with a hard limit of
++C<99999>.
+
+ * Values:
+
+@@ -7225,7 +7226,8 @@ integer.
+ It is also possible that the keys in the hash returned by
+ C<ParamArrays> are not exactly the same as those implied by the
+ prepared statement. For example, DBD::Oracle translates 'C<?>'
+-placeholders into 'C<:pN>' where N is a sequence number starting at 1.
++placeholders into 'C<:pN>' where N is a sequence number starting at
++C<1> with a hard limit of C<99999>.
+
+ =head3 C<RowsInCache>
+
+diff --git a/DBI.xs b/DBI.xs
+index 8858e21..23ad34a 100644
+--- a/DBI.xs
++++ b/DBI.xs
+@@ -4201,7 +4201,14 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
+ }
+
+ /* XXX this allocation strategy won't work when we get to more advanced stuff */
+- new_stmt_sv = newSV(strlen(statement) * 6 + 16);
++ /* The 7 is for length increase from '?' (length 1) to :p99999 (length 7)
++ * which imposes a limit of 99999 '?' placeholders POSIX style. Actual counts
++ * are a bit higher:
++ * using factor 5: :p1 .. :p1107
++ * using factor 6: :p1 .. :p11106
++ * using factor 7: :p1 .. :p111105
++ * and that count is insane already */
++ new_stmt_sv = newSV(strlen(statement) * 7 + 16);
+ sv_setpv(new_stmt_sv,"");
+ src = statement;
+ dest = SvPVX(new_stmt_sv);
+@@ -4340,9 +4347,9 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
+ continue;
+ }
+
+- if ( !(*src==':' && (PS_accept(DBIpp_ph_cn) || PS_accept(DBIpp_ph_cs)))
+- && !(*src=='?' && PS_accept(DBIpp_ph_qm))
+- ){
++ if ( !(*src==':' && (PS_accept(DBIpp_ph_cn) || PS_accept(DBIpp_ph_cs)))
++ && !(*src=='?' && PS_accept(DBIpp_ph_qm))
++ ){
+ if (*src == '\'' || *src == '"')
+ in_quote = *src;
+ *dest++ = *src++;
+@@ -4361,12 +4368,18 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
+ if (PS_return(DBIpp_ph_qm))
+ ;
+ else if (PS_return(DBIpp_ph_cn)) { /* '?' -> ':p1' (etc) */
++ if (idx > 99999) {
++ char buf[99];
++ sprintf(buf, "preparse found more than 99999 '?' placeholders. Limit exceeded.");
++ set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
++ return &PL_sv_undef;
++ }
+ sprintf(start,":p%d", idx++);
+ dest = start+strlen(start);
+ }
+ else if (PS_return(DBIpp_ph_sp)) { /* '?' -> '%s' */
+- *start = '%';
+- *dest++ = 's';
++ *start = '%';
++ *dest++ = 's';
+ }
+ }
+ else if (isDIGIT(*src)) { /* :1 */
+@@ -4374,24 +4387,24 @@ preparse(SV *dbh, const char *statement, IV ps_return, IV ps_accept, void *foo)
+ style = ":1";
+
+ if (PS_return(DBIpp_ph_cn)) { /* ':1'->':p1' */
+- idx = pln;
+- *dest++ = 'p';
+- while(isDIGIT(*src))
+- *dest++ = *src++;
++ idx = pln;
++ *dest++ = 'p';
++ while(isDIGIT(*src))
++ *dest++ = *src++;
+ }
+ else if (PS_return(DBIpp_ph_qm) /* ':1' -> '?' */
+ || PS_return(DBIpp_ph_sp) /* ':1' -> '%s' */
+ ) {
+- PS_return(DBIpp_ph_qm) ? sprintf(start,"?") : sprintf(start,"%%s");
+- dest = start + strlen(start);
+- if (pln != idx) {
+- char buf[99];
+- sprintf(buf, "preparse found placeholder :%d out of sequence, expected :%d", pln, idx);
+- set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
+- return &PL_sv_undef;
+- }
+- while(isDIGIT(*src)) src++;
+- idx++;
++ PS_return(DBIpp_ph_qm) ? sprintf(start,"?") : sprintf(start,"%%s");
++ dest = start + strlen(start);
++ if (pln != idx) {
++ char buf[99];
++ sprintf(buf, "preparse found placeholder :%d out of sequence, expected :%d", pln, idx);
++ set_err_char(dbh, imp_xxh, "1", 1, buf, 0, "preparse");
++ return &PL_sv_undef;
++ }
++ while(isDIGIT(*src)) src++;
++ idx++;
+ }
+ }
+ else if (isALNUM(*src)) /* :name */
+diff --git a/t/60preparse.t b/t/60preparse.t
+index 6432feb..668dfa3 100755
+--- a/t/60preparse.t
++++ b/t/60preparse.t
+@@ -11,7 +11,7 @@ BEGIN {
+ plan skip_all => 'preparse not supported for DBI::PurePerl';
+ }
+ else {
+- plan tests => 39;
++ plan tests => 43;
+ }
+ }
+
+@@ -71,6 +71,17 @@ is( pp($dbh, "a = :name", DBIpp_ph_sp, DBIpp_ph_cs), "a = %s" );
+
+ is( pp($dbh, "a = ? b = ? c = ?", DBIpp_ph_cn, DBIpp_ph_XX), "a = :p1 b = :p2 c = :p3" );
+
++my $max_placeholders = pp(
++ $dbh, "?" x 99999, DBIpp_ph_cn, DBIpp_ph_qm
++);
++ok(defined $max_placeholders, "accepts the documented limit of 99999 placeholders");
++is(substr($max_placeholders, -7), ":p99999", "numbers the final allowed placeholder");
++
++is(pp($dbh, "?" x 100000, DBIpp_ph_cn, DBIpp_ph_qm), undef,
++ "rejects placeholders above the documented limit");
++is($DBI::errstr, "preparse found more than 99999 '?' placeholders. Limit exceeded.",
++ "reports the placeholder limit");
++
+ ## Placeholders inside comments (should be ignored where comments style is accepted):
+
+ is( pp( $dbh,
@@ -17,6 +17,7 @@ SRC_URI = "http://search.cpan.org/CPAN/authors/id/T/TI/TIMB/DBI-${PV}.tar.gz \
file://CVE-2026-14380_p2.patch \
file://CVE-2026-14380_p3.patch \
file://CVE-2026-14380_p4.patch \
+ file://CVE-2026-14739.patch \
"
SRC_URI[md5sum] = "352f80b1e23769c116082a90905d7398"
SRC_URI[sha256sum] = "8a2b993db560a2c373c174ee976a51027dd780ec766ae17620c20393d2e836fa"