/src/postgis/fuzzers/gserialized_from_bytea_fuzzer.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: PostGIS |
4 | | * Purpose: GSERIALIZED input fuzzer |
5 | | * |
6 | | ****************************************************************************** |
7 | | * Copyright (C) 2026 Darafei Praliaskouski <me@komzpa.net> |
8 | | * |
9 | | * This program is free software; you can redistribute it and/or |
10 | | * modify it under the terms of the GNU General Public License |
11 | | * as published by the Free Software Foundation; either version 2 |
12 | | * of the License, or (at your option) any later version. |
13 | | * |
14 | | ****************************************************************************/ |
15 | | |
16 | | #include <assert.h> |
17 | | #include <stddef.h> |
18 | | #include <stdint.h> |
19 | | #include <stdlib.h> |
20 | | #include <string.h> |
21 | | |
22 | | extern "C" { |
23 | | #include "geos_stub.h" |
24 | | #include "proj_stub.h" |
25 | | } |
26 | | |
27 | | #include "liblwgeom_fuzzer.hpp" |
28 | | |
29 | | extern "C" int |
30 | | LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/) |
31 | 18 | { |
32 | 18 | postgis_lwgeom_fuzzer_initialize(); |
33 | 18 | return 0; |
34 | 18 | } |
35 | | |
36 | | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len); |
37 | | |
38 | | static const size_t POSTGIS_FUZZER_MAX_GSERIALIZED_SIZE = 16 * 1024 * 1024; |
39 | | |
40 | | static void |
41 | | postgis_fuzzer_assert(int condition) |
42 | 3.94k | { |
43 | 3.94k | if (!condition) |
44 | 0 | abort(); |
45 | 3.94k | } |
46 | | |
47 | | static lwvarlena_t * |
48 | | geometry_to_bytea(const LWGEOM *lwgeom) |
49 | 453 | { |
50 | 453 | return lwgeom_to_wkb_varlena(lwgeom, WKB_NDR | WKB_EXTENDED); |
51 | 453 | } |
52 | | |
53 | | static void |
54 | | assert_gserialized_bytea_roundtrip(LWGEOM *lwgeom) |
55 | 453 | { |
56 | 453 | size_t first_size = 0; |
57 | 453 | size_t second_size = 0; |
58 | 453 | GSERIALIZED *first = gserialized_from_lwgeom(lwgeom, &first_size); |
59 | 453 | postgis_fuzzer_assert(first != NULL); |
60 | | |
61 | 453 | lwvarlena_t *bytea = geometry_to_bytea(lwgeom); |
62 | 453 | postgis_fuzzer_assert(bytea != NULL); |
63 | | |
64 | 453 | LWGEOM *from_bytea = |
65 | 453 | lwgeom_from_wkb((uint8_t *)bytea->data, LWSIZE_GET(bytea->size) - LWVARHDRSZ, LW_PARSER_CHECK_ALL); |
66 | 453 | postgis_fuzzer_assert(from_bytea != NULL); |
67 | | |
68 | 453 | GSERIALIZED *second = gserialized_from_lwgeom(from_bytea, &second_size); |
69 | 453 | postgis_fuzzer_assert(second != NULL); |
70 | 453 | postgis_fuzzer_assert(first_size == LWSIZE_GET(first->size)); |
71 | 453 | postgis_fuzzer_assert(second_size == LWSIZE_GET(second->size)); |
72 | 453 | postgis_fuzzer_assert(lwgeom->type == from_bytea->type); |
73 | 453 | postgis_fuzzer_assert(clamp_srid(lwgeom->srid) == from_bytea->srid); |
74 | 453 | postgis_fuzzer_assert(FLAGS_GET_Z(lwgeom->flags) == FLAGS_GET_Z(from_bytea->flags)); |
75 | 453 | postgis_fuzzer_assert(FLAGS_GET_M(lwgeom->flags) == FLAGS_GET_M(from_bytea->flags)); |
76 | 453 | postgis_fuzzer_assert(FLAGS_GET_GEODETIC(lwgeom->flags) == FLAGS_GET_GEODETIC(from_bytea->flags)); |
77 | | |
78 | | /* WKB bytea does not preserve GSERIALIZED-only metadata. Arbitrary |
79 | | * GSERIALIZED input can carry a bbox or out-of-range SRID that is |
80 | | * normalized after geometry->bytea->geometry, so compare the semantic |
81 | | * geometry after applying the same normalization. |
82 | | */ |
83 | 453 | lwgeom_drop_bbox(lwgeom); |
84 | 453 | lwgeom_drop_bbox(from_bytea); |
85 | 453 | lwgeom_set_srid(lwgeom, from_bytea->srid); |
86 | 453 | postgis_fuzzer_assert(lwgeom_same(lwgeom, from_bytea)); |
87 | | |
88 | 453 | lwgeom_free(from_bytea); |
89 | 453 | lwfree(first); |
90 | 453 | lwfree(second); |
91 | 453 | lwfree(bytea); |
92 | 453 | } |
93 | | |
94 | | int |
95 | | LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) |
96 | 954 | { |
97 | 954 | if (len < offsetof(GSERIALIZED, data)) |
98 | 4 | return 0; |
99 | | |
100 | 950 | if (POSTGIS_LWGEOM_FUZZER_SETJMP()) |
101 | 612 | { |
102 | 612 | postgis_lwgeom_fuzzer_cleanup_allocations(); |
103 | 612 | return 0; |
104 | 612 | } |
105 | | |
106 | 338 | uint32_t size_header; |
107 | 338 | memcpy(&size_header, buf, sizeof(size_header)); |
108 | 338 | const size_t declared_size = LWSIZE_GET(size_header); |
109 | 338 | if (declared_size > POSTGIS_FUZZER_MAX_GSERIALIZED_SIZE) |
110 | 34 | return 0; |
111 | | |
112 | 18.4E | const size_t allocation_size = declared_size > len ? declared_size : len; |
113 | | |
114 | | /* Leave the varlena size header untouched. The first four bytes are |
115 | | * attacker-controlled when GSERIALIZED arrives from a damaged page, |
116 | | * binary COPY, bytea cast, or hostile dump. When the declared size is |
117 | | * larger than the supplied testcase, zero-fill the missing tail so the |
118 | | * parser can validate the declared buffer without UBSAN builds reading |
119 | | * past the fuzzer allocation. |
120 | | */ |
121 | 304 | GSERIALIZED *gserialized = static_cast<GSERIALIZED *>(postgis_lwgeom_fuzzer_malloc(allocation_size)); |
122 | 304 | if (gserialized == NULL) |
123 | 0 | return 0; |
124 | 304 | memset(gserialized, 0, allocation_size); |
125 | 304 | memcpy(gserialized, buf, len); |
126 | | |
127 | 304 | LWGEOM *lwgeom = lwgeom_from_gserialized(gserialized); |
128 | 304 | if (lwgeom != NULL) |
129 | 453 | { |
130 | 453 | assert_gserialized_bytea_roundtrip(lwgeom); |
131 | 453 | lwgeom_free(lwgeom); |
132 | 453 | } |
133 | | |
134 | 304 | postgis_lwgeom_fuzzer_free(gserialized); |
135 | 304 | postgis_lwgeom_fuzzer_cleanup_allocations(); |
136 | 304 | return 0; |
137 | 304 | } |