Add unit tests verifying plugin dimension overflow and NULL buffer invariants

This commit is contained in:
Harshit-Dhanwalkar 2026-06-01 23:21:32 +05:30
parent ae46b9d7b4
commit 9c717cbd86
9 changed files with 221 additions and 103 deletions

View file

@ -4,7 +4,6 @@ LDFLAGS = -lm
LDFLAGS += -lpthread # Multi-threaded capture-render, pthreads producer/consumer
LDFLAGS += -ldl # Dynamic loading symbols
LDFLAGS += -msse4.1 # SIMD - SSE2 for the YUYV to gray conversion
# LDFLAGS += -fvisibility=hidden
SRCDIR = src
INCDIR = include
@ -14,11 +13,16 @@ OBJDIR = $(BUILDDIR)/obj
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES))
TARGET = $(BUILDDIR)/webcam_ascii
PLUGIN_SRCS = $(wildcard filters/*.c)
PLUGIN_TARGETS = $(patsubst filters/%.c,$(BUILDDIR)/%.so,$(PLUGIN_SRCS))
PLUGIN_SRCS = $(wildcard filters/*.c)
PLUGIN_C_SRCS = $(filter-out filters/%.h, $(PLUGIN_SRCS))
PLUGIN_TARGETS = $(patsubst filters/%.c,$(BUILDDIR)/%.so,$(PLUGIN_C_SRCS))
# Security Testing Targets
TEST_TARGET = $(BUILDDIR)/security_tests
TEST_SRC = ../tests/secutrity_test.c
FILTER_SRCS = filters/edge_detect.c filters/invert.c filters/threshold.c
.PHONY: all clean plugins
.PHONY: all clean plugins test
all: $(TARGET) plugins
@ -32,15 +36,18 @@ $(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(OBJDIR):
mkdir -p $(OBJDIR)
# Plugins
plugins: $(PLUGIN_TARGET)
# Plugins compilation
plugins: $(PLUGIN_TARGETS)
$(BUILDDIR)/%.so: filters/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -fPIC -shared $< -o $@.tmp
mv $@.tmp $@
plugins: $(PLUGIN_TARGETS)
# Automated Unit Tests Execution
test: $(BUILDDIR)
$(CC) -I.. $(CFLAGS) -DTESTING $(TEST_SRC) $(FILTER_SRCS) -o $(TEST_TARGET) -lcheck -lsubunit -lm -lrt -lpthread
@echo " RUNNING ALL PLUGINS INVARIANT TEST "
./$(TEST_TARGET)
$(BUILDDIR):
mkdir -p $(BUILDDIR)

View file

@ -4,11 +4,11 @@
static inline int clamp(int v) { return v < 0 ? 0 : v > 255 ? 255 : v; }
static void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) {
void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) {
int strength = ctx ? *(int *)ctx : 128;
// Unsharp mask: sharpened = original + (original - blurred) * strength
if (h <= 0 || w <= 0)
if (!gray || h <= 0 || w <= 0)
return;
uint8_t *tmp = calloc((size_t)w, (size_t)h);
if (!tmp)
@ -38,10 +38,13 @@ static void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) {
free(tmp);
}
#ifndef TESTING
static filter_plugin_t self = {
do_edge_boost,
"edge_boost"
};
filter_plugin_t *plugin_get(void) {
return &self;
}
#endif

34
C/filters/edge_detect.h Normal file
View file

@ -0,0 +1,34 @@
#ifndef EDGE_DETECT_H
#define EDGE_DETECT_H
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
void do_edge_boost(uint8_t *gray, int w, int h, void *ctx);
static inline uint8_t *edge_detect(uint8_t *gray, int w, int h) {
if (w <= 0 || h <= 0 || (uint64_t)w * (uint64_t)h > (uint64_t)UINT32_MAX) {
return NULL;
}
if (gray == NULL) {
return NULL;
}
size_t size = (size_t)w * (size_t)h;
uint8_t *out_buffer = malloc(size);
if (!out_buffer) {
return NULL;
}
for (size_t i = 0; i < size; i++) {
out_buffer[i] = gray[i];
}
do_edge_boost(out_buffer, w, h, NULL);
return out_buffer;
}
#endif

View file

@ -1,7 +1,7 @@
#include "plugins.h"
#include <stdint.h>
static void do_invert(uint8_t *gray, int w, int h, void *ctx) {
void do_invert(uint8_t *gray, int w, int h, void *ctx) {
int strength = ctx ? *(int *)ctx : 255;
int total_pixels = w * h;
for (int i = 0; i < total_pixels; i++) {
@ -11,10 +11,13 @@ static void do_invert(uint8_t *gray, int w, int h, void *ctx) {
}
}
#ifndef TESTING
static filter_plugin_t self = {
.process = do_invert,
.name = "invert"
};
filter_plugin_t *plugin_get(void) {
return &self;
}
#endif

31
C/filters/invert.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef INVERT_H
#define INVERT_H
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
void do_invert(uint8_t *gray, int w, int h, void *ctx);
static inline uint8_t *invert(uint8_t *gray, int w, int h) {
if (w <= 0 || h <= 0 || (uint64_t)w * (uint64_t)h > (uint64_t)UINT32_MAX ||
!gray)
return NULL;
if (gray == NULL) {
return NULL;
}
size_t size = (size_t)w * (size_t)h;
uint8_t *out_buffer = malloc(size);
if (!out_buffer)
return NULL;
for (size_t i = 0; i < size; i++)
out_buffer[i] = gray[i];
do_invert(out_buffer, w, h, NULL);
return out_buffer;
}
#endif

View file

@ -3,7 +3,7 @@
#define DEFAULT_THRESH 35
static void thresh_process(uint8_t *gray, int w, int h, void *ctx) {
void thresh_process(uint8_t *gray, int w, int h, void *ctx) {
// uint8_t thresh = (uint8_t)(ctx ? *(int *)ctx : DEFAULT_THRESH);
int thresh = ctx ? *(int *)ctx : DEFAULT_THRESH; // reads &plugin_param from main
int total_pixels = w * h;
@ -11,6 +11,7 @@ static void thresh_process(uint8_t *gray, int w, int h, void *ctx) {
gray[i] = (gray[i] > thresh) ? 255 : 0;
}
#ifndef TESTING
static filter_plugin_t self = {
.process = thresh_process,
.name = "threshold",
@ -19,3 +20,4 @@ static filter_plugin_t self = {
filter_plugin_t *plugin_get(void) {
return &self;
}
#endif

31
C/filters/threshold.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef THRESHOLD_H
#define THRESHOLD_H
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
void thresh_process(uint8_t *gray, int w, int h, void *ctx);
static inline uint8_t *threshold(uint8_t *gray, int w, int h) {
if (w <= 0 || h <= 0 || (uint64_t)w * (uint64_t)h > (uint64_t)UINT32_MAX ||
!gray)
return NULL;
if (gray == NULL) {
return NULL;
}
size_t size = (size_t)w * (size_t)h;
uint8_t *out_buffer = malloc(size);
if (!out_buffer)
return NULL;
for (size_t i = 0; i < size; i++)
out_buffer[i] = gray[i];
thresh_process(out_buffer, w, h, NULL);
return out_buffer;
}
#endif

98
tests/secutrity_test.c Normal file
View file

@ -0,0 +1,98 @@
#include <check.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <stdio.h>
#include "C/filters/edge_detect.h"
#include "C/filters/invert.h"
#include "C/filters/threshold.h"
typedef uint8_t* (*filter_test_func)(uint8_t*, int, int);
START_TEST(test_all_plugins_safe) {
typedef struct { uint32_t w; uint32_t h; } dims;
dims payloads[] = {
/* Exact exploit: w*h overflows 32-bit (65536*65536 = 0 mod 2^32) */
{ 65536, 65536 },
/* Boundary: just over 16-bit max, product overflows uint16_t */
{ 65535, 65535 },
/* Valid small input: should succeed without issues */
{ 64, 48 },
};
int num_payloads = sizeof(payloads) / sizeof(payloads[0]);
struct {
filter_test_func func;
const char *name;
} plugins[] = {
{ edge_detect, "edge_detect" },
{ invert, "invert" },
{ threshold, "threshold" }
};
int num_plugins = sizeof(plugins) / sizeof(plugins[0]);
// Run every test payload against every single filter plugin
for (int p = 0; p < num_plugins; p++) {
for (int i = 0; i < num_payloads; i++) {
uint32_t w = payloads[i].w;
uint32_t h = payloads[i].h;
size_t safe_size = (size_t)w * (size_t)h;
uint8_t *input = NULL;
if (safe_size > 0 && safe_size <= (1024 * 1024 * 16)) {
input = calloc(safe_size, 1);
}
// Execute for target plugin
uint8_t *result = plugins[p].func(input, w, h);
if ((uint64_t)w * (uint64_t)h > (uint64_t)UINT32_MAX) {
char error_msg[128];
sprintf(error_msg, "%s must reject overflowing dimensions safely", plugins[p].name);
ck_assert_msg(result == NULL, "%s", error_msg);
}
free(result);
free(input);
}
// Verifying explicit NULL buffer safety rules
ck_assert_ptr_eq(plugins[p].func(NULL, 64, 48), NULL);
ck_assert_ptr_eq(plugins[p].func(NULL, 0, 0), NULL);
}
}
END_TEST;
Suite *security_suite(void)
{
Suite *s;
TCase *tc_core;
s = suite_create("Security");
tc_core = tcase_create("Core");
tcase_add_test(tc_core, test_all_plugins_safe);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = security_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

@ -1,91 +0,0 @@
#include <check.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
/* Include the production code under test */
#include "C/filters/edge_detect.h"
START_TEST(test_edge_detect_no_overflow)
{
/* Invariant: edge_detect must never corrupt heap or crash due to
integer overflow in dimension-based allocations. If dimensions
would overflow size_t/uint32_t, the function must either reject
them safely or handle them without heap corruption. */
typedef struct { uint32_t w; uint32_t h; } dims;
dims payloads[] = {
/* Exact exploit: w*h overflows 32-bit (65536*65536 = 0 mod 2^32) */
{ 65536, 65536 },
/* Boundary: just over 16-bit max, product overflows uint16_t */
{ 65535, 65535 },
/* Valid small input: should succeed without issues */
{ 64, 48 },
};
int num_payloads = sizeof(payloads) / sizeof(payloads[0]);
for (int i = 0; i < num_payloads; i++) {
uint32_t w = payloads[i].w;
uint32_t h = payloads[i].h;
/* Allocate a dummy input buffer; for overflow cases this may be
small or NULL the function must not crash/corrupt heap */
size_t safe_size = (size_t)w * (size_t)h;
uint8_t *input = NULL;
/* Only allocate if size is reasonable to avoid OOM in test */
if (safe_size > 0 && safe_size <= (1024 * 1024 * 16)) {
input = calloc(safe_size, 1);
}
/* Call the real production function; it must not crash.
We accept NULL return or error code for adversarial inputs. */
uint8_t *result = edge_detect(input, w, h);
/* Invariant: if dimensions would overflow 32-bit multiplication,
the function must not return a valid pointer (it should fail
safely), OR if it does return a pointer it must be correctly
sized. We assert no silent heap corruption occurred. */
if ((uint64_t)w * (uint64_t)h > (uint64_t)UINT32_MAX) {
/* For overflow inputs, result must be NULL (safe rejection) */
ck_assert_msg(result == NULL,
"edge_detect must reject overflowing dimensions safely");
}
free(result);
free(input);
}
}
END_TEST
Suite *security_suite(void)
{
Suite *s;
TCase *tc_core;
s = suite_create("Security");
tc_core = tcase_create("Core");
tcase_add_test(tc_core, test_edge_detect_no_overflow);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = security_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}