September 27, 2026
I Found Two More Segfaults in DBI, Same Bug Class, Different Functions | CVE-2026-88816 &…
CVE-2026–88815 and CVE-2026–88816 in Perl’s DBI module

By Harsh Raj Singhania
5 min read
CVE-2026–88815 and CVE-2026–88816 in Perl's DBI module
I keep coming back to DBI.
The first time I was reading someone else's fix and noticed a second path they hadn't covered. That became CVE-2026–73194. The second time I went back on purpose and found CVE-2026–78030. Now this is the third time, and it produced two more.
Both of these come from exactly one thing: reading SvPVX without first checking that the SV is actually in string form. This is the same pattern that gave DBI CVE-2019-20919 and CVE-2026-60082. It keeps showing up in different functions because the check is easy to forget and nothing in the compiler or the test suite catches it when you do.
What DBI Is (For New Readers)
DBI is Perl's database interface layer. It's the module that sits between your Perl code and whatever database you're connecting to — MySQL, Postgres, SQLite, anything with a driver. Almost every Perl application that talks to a database uses it. It's been around since the 1990s, has hundreds of millions of CPAN installations, and is maintained by robrwo, who published both of these advisories.
When DBI is installed, its core logic lives in a file called DBI.xs, which is C code that gets compiled into a shared library. That compiled C is what actually runs when you call database functions from Perl. The bugs in this post are both inside DBI.xs.
A Bit of Perl Internals, Kept Short
Perl stores every value in a structure called an SV — Scalar Value. An SV can hold a string (called a PV internally), an integer (IV), or a float (NV). The same SV can have more than one form at once: for example, after you do my $x = "42"; $x + 0, Perl stores both the string "42" and the integer 42 inside the same SV.
The C API has two ways to get the string form from an SV:
SvPVX(sv)— grabs the raw string pointer directly, assumes it's already there. If the SV is a pure integer that was never stringified, this gives you NULL or garbage.SvPV(sv, len)— the safe version. It creates a string representation if one doesn't exist yet, then returns it.
There's also SvPOK(sv), which asks whether the SV currently has a valid string form. If the answer is no and you call SvPVX anyway, you're reading from an uninitialised or NULL pointer.
That's the entire bug class. Both of these CVEs are SvPVX called where SvPV should have been used.
Bug 1 — CVE-2026–88815 | sql_type_cast on a Pure Integer
GHSA-c8vq-w3wr-6979 · CWE-476 + CWE-843 · Patched in DBI 1.654
DBI::sql_type_cast is a documented public function. It takes a scalar value and a SQL type code and tries to coerce the value toward that type. The SQL_NUMERIC branch in sql_type_cast_svpv() looks like this, around line 1961 on master:
case SQL_NUMERIC:
uv = 0;
grok_flags = grok_number(SvPVX(sv), SvCUR(sv), &uv);case SQL_NUMERIC:
uv = 0;
grok_flags = grok_number(SvPVX(sv), SvCUR(sv), &uv);grok_number is Perl's internal function for parsing a number from a string. It's handed SvPVX(sv) — the raw string pointer — without any check that the SV actually has one. If sv is a pure integer like 42 that was never turned into a string, SvPVX(sv) returns NULL or a garbage pointer. grok_number then tries to read from that address and the process dies with SIGSEGV.
The reproducer is a single line:
perl -MDBI -e 'my $x=42; DBI::sql_type_cast($x, DBI::SQL_NUMERIC(), 0)'perl -MDBI -e 'my $x=42; DBI::sql_type_cast($x, DBI::SQL_NUMERIC(), 0)'Exit 139. 100% of the time. Also crashes on 3.14 (NV), 1<<40 (large IV), -1 (negative IV). It's safe on "42" + 0 because that SV already has a string cache — it's PVIV, not pure IV.
The fix is one line:
case SQL_NUMERIC: {
STRLEN len;
const char *p = SvPV(sv, len); /* stringify first */
uv = 0;
grok_flags = grok_number(p, len, &uv);
...
}case SQL_NUMERIC: {
STRLEN len;
const char *p = SvPV(sv, len); /* stringify first */
uv = 0;
grok_flags = grok_number(p, len, &uv);
...
}SvPV forces stringification, handles magic, and never returns a bad pointer. This is in DBI 1.654.
Bug 2 — CVE-2026–88816 | FetchHashKeyName Set to a Number
GHSA-f4qx-mr9m-q2hq · CWE-125 + CWE-843 · Patched in DBI 1.654
This one is more interesting because it can be triggered accidentally.
FetchHashKeyName is a documented, writeable per-handle attribute. It controls what column name key fetchrow_hashref uses when building its result hash — the default is "NAME", but drivers can override it to "NAME_lc" or "NAME_uc" for case normalization. Perfectly normal feature.
Inside fetchrow_hashref in DBI.xs, around line 5307, there's code that reads the attribute back to figure out which key to use:
if (!keyattrib || !*keyattrib) {
SV *kn = DBIc_FetchHashKeyName(imp_sth);
if (kn && SvOK(kn))
keyattrib = SvPVX(kn);
else
keyattrib = "NAME";
}
ka_rv = *hv_fetch((HV*)DBIc_MY_H(imp_sth), keyattrib, strlen(keyattrib), TRUE);if (!keyattrib || !*keyattrib) {
SV *kn = DBIc_FetchHashKeyName(imp_sth);
if (kn && SvOK(kn))
keyattrib = SvPVX(kn);
else
keyattrib = "NAME";
}
ka_rv = *hv_fetch((HV*)DBIc_MY_H(imp_sth), keyattrib, strlen(keyattrib), TRUE);The guard is SvOK(kn). This checks that the SV is defined and not undef — but it doesn't check SvPOK(kn). A numeric SV is SvOK. It passes the check. Then SvPVX(kn) runs on it, hands a garbage pointer to strlen, which walks off into unmapped memory, and the process crashes.
The reproducer uses only DBD::ExampleP, a test driver that ships inside the DBI distribution itself:
perl -MDBI -e '
my $dbh = DBI->connect("dbi:ExampleP:", "", "",
{RaiseError=>0, PrintError=>0});
$dbh->{FetchHashKeyName} = 42;
my $s = $dbh->prepare("select mode,size,name from .");
$s->execute;
$s->fetchrow_hashref;
'perl -MDBI -e '
my $dbh = DBI->connect("dbi:ExampleP:", "", "",
{RaiseError=>0, PrintError=>0});
$dbh->{FetchHashKeyName} = 42;
my $s = $dbh->prepare("select mode,size,name from .");
$s->execute;
$s->fetchrow_hashref;
'Exit 139. No third-party database required.
Why this one matters beyond the crash: FetchHashKeyName is a handle attribute that can be set through configuration, through ORMs, through code that processes options generically and doesn't distinguish strings from integers. An ORM that reads database options from a config file and hands them to DBI as-is could set FetchHashKeyName to a number without meaning to — for example if the option parser reads name: 1 from YAML and hands you an integer. The next call to fetchrow_hashref then crashes the Perl process.
The fix:
if (kn && SvOK(kn)) {
STRLEN kn_len;
keyattrib = SvPV(kn, kn_len); /* force stringify */
/* pass kn_len to hv_fetch instead of strlen(keyattrib) */
}if (kn && SvOK(kn)) {
STRLEN kn_len;
keyattrib = SvPV(kn, kn_len); /* force stringify */
/* pass kn_len to hv_fetch instead of strlen(keyattrib) */
}CVE-2026–88816 was patched in DBI 1.654, released on September 25, 2026, the same day as the fix for CVE-2026–88815. The GHSA metadata incorrectly indicated that no patched version was available at the time of writing.
How These Were Found
Not from reading a diff this time. I ran a manual sweep of SvPVX(...) call sites in DBI.xs looking for places where the code uses SvPVX without either checking SvPOK first or having called SvPV earlier in the same code path to ensure the string form exists. Both of these were missing that guarantee.
The advisory notes that the same pattern may exist in other places in DBI.xs — these are the two I could turn into reliable, self-contained crashes without needing a compiled third-party database driver. The "not in scope" section of the advisory lists three more adjacent issues worth being aware of even if they're not crashes.
The Pattern Across Four CVEs in the Same Repo
Going back through all of this:
- CVE-2026–73194 — heap out-of-bounds write in the
preparse()function. Found because another CVE's fix left a second code path uncovered. - CVE-2026–78030 — arbitrary module load in DBD::DBM, three separate
require()call sites with no sanitization on the argument. - CVE-2026–88815 —
SvPVXwithoutSvPOKinsql_type_cast_svpv. - CVE-2026–88816 —
SvPVXwithoutSvPOKinfetchrow_hashref.
The first two were found by reading what was around other people's fixes. The last two were found by looking for a class of bug that DBI's own history told me was there. CVE-2019–20919 and CVE-2026–60082 both involved the same kind of unsafe SV access. If the pattern showed up twice before, there was a reasonable chance it was somewhere else in the same file.
It was.
What To Do
If you're running DBI 1.653 or an earlier affected version: upgrade to DBI 1.654, which contains fixes for both CVE-2026–88815 and CVE-2026–88816.
References
- GHSA-c8vq-w3wr-6979 — CVE-2026–88815 (
sql_type_cast+ pure numeric SV): https://github.com/perl5-dbi/dbi/security/advisories/GHSA-c8vq-w3wr-6979 - GHSA-f4qx-mr9m-q2hq — CVE-2026–88816 (
FetchHashKeyName+fetchrow_hashref): https://github.com/perl5-dbi/dbi/security/advisories/GHSA-f4qx-mr9m-q2hq - DBI 1.654 on CPAN (fixes for CVE-2026–88815 and CVE-2026–88816): https://metacpan.org/release/DBI-1.654
- perl5-dbi/dbi source repository: https://github.com/perl5-dbi/dbi
- My first DBI post, CVE-2026–73194: https://medium.com/@harshrajsinghania/i-found-my-first-cve-by-trying-to-understand-someone-elses-bug-the-story-of-cve-2026-73194-1702f8bde5c1
- My second DBI post, CVE-2026–78030: https://medium.com/@harshrajsinghania/i-went-back-into-the-same-repo-on-purpose-and-found-something-worse