Skip to content

Commit d43eeb7

Browse files
authored
Merge pull request #122 from berkeleydb/security/pentest-final
fix(security): harden untrusted .db parse/verify surface
2 parents 0d265fd + 3711470 commit d43eeb7

16 files changed

Lines changed: 155 additions & 7 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
build_unix/**
22
build_asan/**
3+
build_asan_gate/**
34
compile_commands.json
45
test/tcl/tclIndex
56

src/btree/bt_search.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,24 @@ skip_lock: stack = set_stack;
10661066
if ((ret = __memp_fget(mpf, &pg,
10671067
dbc->thread_info, dbc->txn, get_mode, &h)) != 0)
10681068
goto err;
1069+
/*
1070+
* On an untrusted/corrupt file a BINTERNAL child pointer can
1071+
* point back up the tree (to itself, a sibling, or an ancestor)
1072+
* at the same or a higher level. The descent then never reaches
1073+
* LEAFLEVEL and this loop spins forever (a denial of service).
1074+
* A valid Btree always has strictly decreasing levels from root
1075+
* to leaf, so a child whose level is not below its parent's is
1076+
* corruption -- reject it as a clean page error rather than loop.
1077+
* (The lock-retry path above already enforces LEVEL(h)==level-1;
1078+
* this guards the common latch-coupling fast path.)
1079+
*/
1080+
if (LEVEL(h) >= level) {
1081+
(void)__memp_fput(mpf,
1082+
dbc->thread_info, h, dbc->priority);
1083+
h = NULL;
1084+
ret = DB_PAGE_NOTFOUND;
1085+
goto err;
1086+
}
10691087
/* Release the parent. */
10701088
if (parent_h != NULL && (ret = __memp_fput(mpf,
10711089
dbc->thread_info, parent_h, dbc->priority)) != 0)

src/db/partition.c

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,16 +1822,32 @@ __part_verify(dbp, vdp, fname, handle, callback, flags)
18221822
dbc = NULL;
18231823
ip = vdp->thread_info;
18241824

1825-
if (dbp->type == DB_BTREE) {
1825+
if (dbp->type == DB_BTREE || dbp->type == DB_RECNO) {
18261826
if ((ret = __bam_open(dbp, ip,
18271827
NULL, fname, PGNO_BASE_MD, flags)) != 0)
18281828
goto err;
18291829
}
18301830
#ifdef HAVE_HASH
1831-
else if ((ret = __ham_open(dbp, ip,
1832-
NULL, fname, PGNO_BASE_MD, flags)) != 0)
1833-
goto err;
1831+
else if (dbp->type == DB_HASH) {
1832+
if ((ret = __ham_open(dbp, ip,
1833+
NULL, fname, PGNO_BASE_MD, flags)) != 0)
1834+
goto err;
1835+
}
18341836
#endif
1837+
else {
1838+
/*
1839+
* Only Btree/Recno and Hash databases can be partitioned. A
1840+
* corrupt/hostile file whose meta page claims another type (e.g.
1841+
* Heap or Queue) while setting the partition flag must not be
1842+
* opened with the Hash access method: __db_cursor would allocate
1843+
* a cursor sized for dbp->type, which __ham_get_meta then casts
1844+
* to HASH_CURSOR, writing its hlock field past the end of the
1845+
* smaller allocation (a heap-buffer-overflow / type confusion).
1846+
* Reject the unexpected type instead.
1847+
*/
1848+
ret = __db_unknown_type(env, "__part_verify", dbp->type);
1849+
goto err;
1850+
}
18351851

18361852
/*
18371853
* Initalize partition db handles and get the names. Set DB_RDWRMASTER

src/heap/heap_open.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ __heap_read_meta(dbp, ip, txn, meta_pgno, flags)
172172
* metadata page will be created/initialized elsewhere.
173173
*/
174174
if (meta->dbmeta.magic == DB_HEAPMAGIC) {
175+
/*
176+
* region_size comes from the (possibly corrupt) on-disk meta
177+
* page and is used as a divisor via HEAP_REGION_SIZE(dbp)+1 in
178+
* HEAP_REGION_PGNO / HEAP_REGION_NUM. A region_size of 0 or of
179+
* UINT32_MAX (so the +1 wraps to 0) would divide by zero; any
180+
* value larger than the per-page region count is impossible for
181+
* this page size. Reject it before it is trusted, mirroring the
182+
* bound __heap_new_file already enforces on creation.
183+
*/
184+
if (meta->region_size == 0 ||
185+
meta->region_size > HEAP_REGION_COUNT(dbp, dbp->pgsize)) {
186+
__db_errx(dbp->env, DB_STR_A("1169",
187+
"region size may not be larger than %lu",
188+
"%lu"),
189+
(u_long)HEAP_REGION_COUNT(dbp, dbp->pgsize));
190+
ret = EINVAL;
191+
goto err;
192+
}
175193
h->curregion = meta->curregion;
176194
h->curpgindx = 0;
177195
h->gbytes = meta->gbytes;

src/heap/heap_verify.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ __heap_vrfy_meta(dbp, vdp, meta, pgno, flags)
7171
h = (HEAP *)dbp->heap_internal;
7272
h->region_size = meta->region_size;
7373
last_pgno = meta->dbmeta.last_pgno;
74+
/*
75+
* region_size is used as a divisor (HEAP_REGION_SIZE(dbp)+1) below and
76+
* in the structure pass; a corrupt 0 or UINT32_MAX value would divide
77+
* by zero (the +1 wraps). Reject it as bad rather than crash.
78+
*/
79+
if (meta->region_size == 0 ||
80+
meta->region_size > HEAP_REGION_COUNT(dbp, dbp->pgsize)) {
81+
EPRINT((dbp->env, DB_STR_A("1174",
82+
"Page %lu: invalid heap region size %lu",
83+
"%lu %lu"), (u_long)pgno, (u_long)meta->region_size));
84+
isbad = 1;
85+
goto err;
86+
}
7487
if (meta->nregions != HEAP_REGION_NUM(dbp, last_pgno)) {
7588
EPRINT((dbp->env, DB_STR_A("1157",
7689
"Page %lu: Number of heap regions incorrect",
@@ -124,6 +137,15 @@ __heap_vrfy(dbp, vdp, h, pgno, flags)
124137
int cnt, i, j, ret;
125138
db_indx_t *offsets, *offtbl, end;
126139

140+
/*
141+
* offsets is freed unconditionally at the err label. If
142+
* __db_vrfy_datapage below fails on a corrupt page we jump there
143+
* before offsets is assigned, so it must start NULL (a free of an
144+
* indeterminate pointer is otherwise undefined behavior / a wild
145+
* free on a hostile heap file).
146+
*/
147+
offsets = NULL;
148+
127149
if ((ret = __db_vrfy_datapage(dbp, vdp, h, pgno, flags)) != 0)
128150
goto err;
129151

src/qam/qam_open.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ __qam_open(dbp, ip, txn, name, base_pgno, mode, flags)
106106
t->re_len = qmeta->re_len;
107107
t->rec_page = qmeta->rec_page;
108108

109+
/*
110+
* rec_page (records per page) is trusted from the on-disk meta page
111+
* and used as a divisor throughout the queue access method via
112+
* QAM_RECNO_PAGE (records/page). A corrupt 0 divides by zero (SIGFPE).
113+
* A valid queue always has at least one record per page.
114+
*/
115+
if (t->rec_page == 0) {
116+
__db_errx(env, DB_STR_A("1136",
117+
"__qam_open: %s: unexpected file type or format", "%s"),
118+
name);
119+
ret = EINVAL;
120+
goto err;
121+
}
122+
109123
t->q_meta = base_pgno;
110124
t->q_root = base_pgno + 1;
111125

src/qam/qam_verify.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ __qam_vrfy_meta(dbp, vdp, meta, pgno, flags)
8181

8282
/*
8383
* re_len: If this is bad, we can't safely verify queue data pages, so
84-
* return DB_VERIFY_FATAL
84+
* return DB_VERIFY_FATAL. rec_page (records per page) must be non-zero:
85+
* it is used as a divisor via QAM_RECNO_PAGE below and throughout the
86+
* queue AM, so a corrupt 0 would divide by zero (SIGFPE).
8587
*/
86-
if (DB_ALIGN(meta->re_len + sizeof(QAMDATA) - 1, sizeof(u_int32_t)) *
88+
if (meta->rec_page == 0 ||
89+
DB_ALIGN(meta->re_len + sizeof(QAMDATA) - 1, sizeof(u_int32_t)) *
8790
meta->rec_page + QPAGE_SZ(dbp) > dbp->pgsize) {
8891
EPRINT((env, DB_STR_A("1147",
8992
"Page %lu: queue record length %lu too high for page size and recs/page",

test/fuzz/check-crashes.sh

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,18 @@
1717
# DB_ASSERT in __memp_fopen, or a recovery-failure panic), which is by-design
1818
# diagnostic behavior, not the OOB/FPE crash class this gate guards against.
1919
#
20+
# libdb ASan instrumentation:
21+
# Some crash classes (a heap-buffer-overflow / use-after-free / double-free
22+
# *inside* libdb's own allocations -- e.g. the __part_verify type-confusion
23+
# OOB write) are only observable when libdb itself is compiled with
24+
# AddressSanitizer; a harness-only ASan build (libdb.a plain) cannot see
25+
# them. If a build_unix built with `CFLAGS=-fsanitize=address` (ASan only,
26+
# NOT undefined -- UBSan flags libdb's pervasive base+offset pointer idioms)
27+
# is available, point LIBDB_BUILD at it to catch those. This gate
28+
# auto-builds one under build_asan_gate/ when LIBDB_ASAN=1 (default on).
29+
#
2030
# Usage: ./check-crashes.sh
21-
# Env: CC, LIBDB_BUILD (see run.sh)
31+
# Env: CC, LIBDB_BUILD (see run.sh), LIBDB_ASAN (1=build+use an ASan libdb)
2232
#
2333
# Run from test/fuzz/ inside a `nix develop` shell.
2434

@@ -27,6 +37,27 @@ set -eu
2737
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
2838
cd "$HERE"
2939

40+
CC=${CC:-clang}
41+
LIBDB_ASAN=${LIBDB_ASAN:-1}
42+
43+
# Build (once) an ASan-instrumented libdb so a memory fault *inside* libdb is
44+
# caught, then link the standalone harnesses against it. ASan only -- UBSan
45+
# would fire on libdb's legitimate base+offset pointer arithmetic. The
46+
# harness's own SAN flags in run.sh still add UBSan to the harness .c, so we
47+
# neutralise it for the lib by exporting an ASan-only LIBDB build here.
48+
if [ "$LIBDB_ASAN" = "1" ] && [ -z "${LIBDB_BUILD:-}" ]; then
49+
GATE_BUILD="$HERE/../../build_asan_gate"
50+
if [ ! -f "$GATE_BUILD/libdb.a" ]; then
51+
mkdir -p "$GATE_BUILD"
52+
( cd "$GATE_BUILD" &&
53+
../dist/configure --enable-debug \
54+
CC="$CC" CFLAGS="-fsanitize=address -g -O1" >configure.log 2>&1 &&
55+
make -j4 >build.log 2>&1 ) ||
56+
{ echo "warning: ASan libdb build failed; falling back to plain lib" >&2; }
57+
fi
58+
[ -f "$GATE_BUILD/libdb.a" ] && export LIBDB_BUILD="$GATE_BUILD"
59+
fi
60+
3061
# Build the standalone (no-libFuzzer) drivers for every harness once.
3162
FUZZ_STANDALONE=1 ./run.sh build
3263

1.5 KB
Binary file not shown.
1 KB
Binary file not shown.

0 commit comments

Comments
 (0)