Coverage Report

Created: 2026-08-13 07:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}