esbmc icon indicating copy to clipboard operation
esbmc copied to clipboard

[C] ESBMC shows false positive(?) comparing to CBMC

Open ChenfengWei0 opened this issue 10 months ago • 9 comments

  • I am testing a mapping library from rxi/map which I am planning to implement in Solidity
struct map_node_t;
typedef struct map_node_t map_node_t;

typedef struct
{
	map_node_t **buckets;
	unsigned nbuckets, nnodes;
} map_base_t;

typedef struct
{
	unsigned bucketidx;
	map_node_t *node;
} map_iter_t;

#define map_t(T)         \
	struct               \
	{                    \
		map_base_t base; \
		T *ref;          \
		T tmp;           \
	}

#define map_init(m) \
	memset(m, 0, sizeof(*(m)))

#define map_deinit(m) \
	map_deinit_(&(m)->base)

#define map_get(m, key) \
	((m)->ref = map_get_(&(m)->base, key))

#define map_set(m, key, value) \
	((m)->tmp = (value),       \
	 map_set_(&(m)->base, key, &(m)->tmp, sizeof((m)->tmp)))

#define map_remove(m, key) \
	map_remove_(&(m)->base, key)

#define map_iter(m) \
	map_iter_()

#define map_next(m, iter) \
	map_next_(&(m)->base, iter)

void map_deinit_(map_base_t *m);
void *map_get_(map_base_t *m, const char *key);
int map_set_(map_base_t *m, const char *key, void *value, int vsize);
void map_remove_(map_base_t *m, const char *key);
map_iter_t map_iter_(void);
const char *map_next_(map_base_t *m, map_iter_t *iter);

typedef map_t(void *) map_void_t;
typedef map_t(char *) map_str_t;
typedef map_t(int) map_int_t;
typedef map_t(char) map_char_t;

struct map_node_t
{
	unsigned hash;
	void *value;
	map_node_t *next;
	/* char key[]; */
	/* char value[]; */
};

static unsigned map_hash(const char *str)
{
	unsigned hash = 5381;
	while (*str)
	{
		hash = ((hash << 5) + hash) ^ *str++;
	}
	return hash;
}

static map_node_t *map_newnode(const char *key, void *value, int vsize)
{
	map_node_t *node;
	int ksize = strlen(key) + 1;
	int voffset = ksize + ((sizeof(void *) - ksize) % sizeof(void *));
	node = malloc(sizeof(*node) + voffset + vsize);
	if (!node)
		return NULL;
	memcpy(node + 1, key, ksize);
	node->hash = map_hash(key);
	node->value = ((char *)(node + 1)) + voffset;
	memcpy(node->value, value, vsize);
	return node;
}

static int map_bucketidx(map_base_t *m, unsigned hash)
{
	/* If the implementation is changed to allow a non-power-of-2 bucket count,
	 * the line below should be changed to use mod instead of AND */
	return hash & (m->nbuckets - 1);
}

static void map_addnode(map_base_t *m, map_node_t *node)
{
	int n = map_bucketidx(m, node->hash);
	node->next = m->buckets[n];
	m->buckets[n] = node;
}

static int map_resize(map_base_t *m, int nbuckets)
{
	map_node_t *nodes, *node, *next;
	map_node_t **buckets;
	int i;
	/* Chain all nodes together */
	nodes = NULL;
	i = m->nbuckets;
	while (i--)
	{
		node = (m->buckets)[i];
		while (node)
		{
			next = node->next;
			node->next = nodes;
			nodes = node;
			node = next;
		}
	}
	/* Reset buckets */
	buckets = realloc(m->buckets, sizeof(*m->buckets) * nbuckets);
	if (buckets != NULL)
	{
		m->buckets = buckets;
		m->nbuckets = nbuckets;
	}
	if (m->buckets)
	{
		memset(m->buckets, 0, sizeof(*m->buckets) * m->nbuckets);
		/* Re-add nodes to buckets */
		node = nodes;
		while (node)
		{
			next = node->next;
			map_addnode(m, node);
			node = next;
		}
	}
	/* Return error code if realloc() failed */
	return (buckets == NULL) ? -1 : 0;
}

static map_node_t **map_getref(map_base_t *m, const char *key)
{
	unsigned hash = map_hash(key);
	map_node_t **next;
	if (m->nbuckets > 0)
	{
		next = &m->buckets[map_bucketidx(m, hash)];
		while (*next)
		{
			if ((*next)->hash == hash && !strcmp((char *)(*next + 1), key))
			{
				return next;
			}
			next = &(*next)->next;
		}
	}
	return NULL;
}

void map_deinit_(map_base_t *m)
{
	map_node_t *next, *node;
	int i;
	i = m->nbuckets;
	while (i--)
	{
		node = m->buckets[i];
		while (node)
		{
			next = node->next;
			free(node);
			node = next;
		}
	}
	free(m->buckets);
}

void *map_get_(map_base_t *m, const char *key)
{
	map_node_t **next = map_getref(m, key);
	return next ? (*next)->value : NULL;
}

int map_set_(map_base_t *m, const char *key, void *value, int vsize)
{
	int n, err;
	map_node_t **next, *node;
	/* Find & replace existing node */
	next = map_getref(m, key);
	if (next)
	{
		memcpy((*next)->value, value, vsize);
		return 0;
	}
	/* Add new node */
	node = map_newnode(key, value, vsize);
	if (node == NULL)
		goto fail;
	if (m->nnodes >= m->nbuckets)
	{
		n = (m->nbuckets > 0) ? (m->nbuckets << 1) : 1;
		err = map_resize(m, n);
		if (err)
			goto fail;
	}
	map_addnode(m, node);
	m->nnodes++;
	return 0;
fail:
	if (node)
		free(node);
	return -1;
}

void map_remove_(map_base_t *m, const char *key)
{
	map_node_t *node;
	map_node_t **next = map_getref(m, key);
	if (next)
	{
		node = *next;
		*next = (*next)->next;
		free(node);
		m->nnodes--;
	}
}

map_iter_t map_iter_(void)
{
	map_iter_t iter;
	iter.bucketidx = -1;
	iter.node = NULL;
	return iter;
}

const char *map_next_(map_base_t *m, map_iter_t *iter)
{
	if (iter->node)
	{
		iter->node = iter->node->next;
		if (iter->node == NULL)
			goto nextBucket;
	}
	else
	{
	nextBucket:
		do
		{
			if (++iter->bucketidx >= m->nbuckets)
			{
				return NULL;
			}
			iter->node = m->buckets[iter->bucketidx];
		} while (iter->node == NULL);
	}
	return (char *)(iter->node + 1);
}

int main()
{
	map_int_t m;
	map_init(&m);
	map_set(&m, "testkey", 123);
	int *val = map_get(&m, "testkey");
	if (val)
	{
		assert(1);
	}
	else
	{
		assert(0);
	}
}

ESBMC: esbmc file.c --unwind 8 --no-unwinding-assertions or esbmc file.c --k-induction

[Counterexample]


State 1 file file.c line 289 column 2 function main thread 0
----------------------------------------------------
  m = { .base={ .buckets = 0 }, .ref=INVALID1048576, .tmp=0,
    .anon_pad$3=0 }

State 3 file file.c line 291 column 2 function main thread 0
----------------------------------------------------
  (&m)->tmp = 123 (00000000 00000000 00000000 01111011)

State 4 file file.c line 102 column 2 function map_newnode thread 0
----------------------------------------------------
  node = ( struct map_node_t *)0

State 5 file file.c line 207 column 2 function map_get_ thread 0
----------------------------------------------------
  next = 0

State 10 file file.c line 292 column 13 function main thread 0
----------------------------------------------------
  (&m)->ref = (signed int *)0

State 11 file file.c line 292 column 2 function main thread 0
----------------------------------------------------
  val = m.ref

State 12 file file.c line 299 column 3 function main thread 0
----------------------------------------------------
Violated property:
  file file.c line 299 column 3 function main
  assertion
  (_Bool)0


VERIFICATION FAILED

CBMC: cbmc file.c --pointer-check --pointer-overflow-check --unwind 10 --no-unwinding-assertions

VERIFICATION SUCCESSFUL

According to the program logic, we should reach the assert(1) rather than assert(0). So I assume in this case ESBMC generates a false positive.

ChenfengWei0 avatar May 02 '24 12:05 ChenfengWei0

The cause might be that, whatever value we set to the key, the return val in ESBMC will always be NULL (assert(val == NULL) always hold).

ChenfengWei0 avatar May 02 '24 13:05 ChenfengWei0

You can use the option --force-malloc-success to ensure that malloc always succeeds.

lucasccordeiro avatar May 02 '24 13:05 lucasccordeiro

You can use the option --force-malloc-success to ensure that malloc always succeeds.

Thanks for the suggestions. I tried --k-induction --force-malloc-success and --unwind 9 --no-unwinding-assertions --force-malloc-success --no-pointer-check and still get the incorrect result

[Counterexample]


State 1 file file.c line 289 column 2 function main thread 0
----------------------------------------------------
  m = { .base = { .buckets = 0 } }

State 2 file file.c line 291 column 2 function main thread 0
----------------------------------------------------
  (&m)->tmp = 123 (00000000 00000000 00000000 01111011)

State 3 file file.c line 228 column 3 function map_set_ thread 0
----------------------------------------------------
  n = (signed int)(m.base.nbuckets > 0 ? m.base.nbuckets << 1 : 1)

State 4 file file.c line 133 column 2 function map_resize thread 0
----------------------------------------------------
  i = (signed int)m.base.nbuckets

State 5 file file.c line 134 column 9 function map_resize thread 0
----------------------------------------------------
  i = -1 (11111111 11111111 11111111 11111111)

State 6 file file.c line 146 column 2 function map_resize thread 0
----------------------------------------------------
  buckets = ( struct map_node_t * *)0

State 7 file file.c line 149 column 3 function map_resize thread 0
----------------------------------------------------
  m->buckets = 0

State 8 file file.c line 150 column 3 function map_resize thread 0
----------------------------------------------------
  m->nbuckets = 1 (00000000 00000000 00000000 00000001)

State 9 file file.c line 156 column 3 function map_resize thread 0
----------------------------------------------------
  node = 0

State 10 file file.c line 234 column 2 function map_set_ thread 0
----------------------------------------------------
  m->nnodes = m.base.nnodes + 1

State 11 file file.c line 174 column 3 function map_getref thread 0
----------------------------------------------------
  next = m.base.buckets

State 12 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 13 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 14 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 15 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 16 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 17 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 18 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(0 + 16)

State 19 file file.c line 181 column 4 function map_getref thread 0
----------------------------------------------------
  next = ( struct map_node_t * *)(INVALID4294967295 + 16)

State 20 file file.c line 207 column 2 function map_get_ thread 0
----------------------------------------------------
  next = 0

State 21 file file.c line 292 column 13 function main thread 0
----------------------------------------------------
  (&m)->ref = (signed int *)0

State 22 file file.c line 292 column 2 function main thread 0
----------------------------------------------------
  val = m.ref

State 23 file file.c line 299 column 3 function main thread 0
----------------------------------------------------
Violated property:
  file file.c line 299 column 3 function main
  assertion
  (_Bool)0


VERIFICATION FAILED

Bug found (k = 9)

ChenfengWei0 avatar May 02 '24 13:05 ChenfengWei0

You can use c-reduce to reduce the program so that we can find out the root cause of this issue:

https://github.com/esbmc/esbmc/wiki/Reducing-C-Programs

#!/bin/bash

esbmc race-1_1-join.c --incremental-bmc --context-bound 2 --no-pointer-check --no-bounds-check  --no-div-by-zero-check --no-slice --yices --no-por >esbmc_out.txt 2>&1
cbmc race-1_1-join.c >cbmc_out.txt 2>&1
grep 'VERIFICATION FAILED' esbmc_out.txt >/dev/null 2>&1 &&\
grep 'VERIFICATION SUCCESSFUL' cbmc_out.txt >/dev/null 2>&1

lucasccordeiro avatar May 02 '24 13:05 lucasccordeiro

typedef struct {
  int *a;
} b;
typedef struct {
  b base;
} c;
c e;
d(b *i) {
  b f = *i;
  while (f.a)
    ;
}
g(b *i) {
  b h = *i;
  int *a = realloc(h.a, a);
  i->a = a;
}
main() {
  g(&(&e)->base);
  d(&(&e)->base);
  assert(0);
}

ChenfengWei0 avatar May 02 '24 20:05 ChenfengWei0

I further simplified it a little bit

#include <stdlib.h>
#include <assert.h>
typedef struct
{
	int *a;
} b;
typedef struct
{
	b base;
} c;
c e;
void g(b *i)
{
	b h = *i;
	int *a = realloc(h.a, a);
	assert(a != NULL);
}
int main()
{
	g(&(&e)->base);
}

I also compiled and executed the binary, and no assertion failure occurred. CBMC shows SUCCESSFUL, ESBMC shows FAILED.

ChenfengWei0 avatar May 02 '24 21:05 ChenfengWei0

Further simplified:

#include <stdlib.h>
#include <assert.h>

int main()
{
	void *p = realloc(NULL, 0);
	assert(p);
}

Related: #1182, #1462.

Edit: Note, for this specific example with new_size being zero, there are (new in C23) caveats: image

fbrausse avatar May 03 '24 09:05 fbrausse

What is the verdict for this issue here? My view: CBMC defaults to --force-malloc-success (which for us should imply realloc() success as well, I believe), therefore without that flag there would be no discrepancy.

With the flag, and the default C standard (gnu17 for clang-11 through 18), we have a bug in that the pointer returned from realloc() is not valid. That's #1462, right? Is there a case I missed?

fbrausse avatar May 13 '24 15:05 fbrausse

What is the verdict for this issue here? My view: CBMC defaults to --force-malloc-success (which for us should imply realloc() success as well, I believe), therefore without that flag there would be no discrepancy.

With the flag, and the default C standard (gnu17 for clang-11 through 18), we have a bug in that the pointer returned from realloc() is not valid. That's #1462, right? Is there a case I missed?

Yes, I believe it is the #1462. In this case, I tried to disable the esbmc pointer-check, and the value of (*p) seems to be a random value.

ChenfengWei0 avatar May 13 '24 15:05 ChenfengWei0