diff --git a/Makefile b/Makefile index 61576a1e..ba9b0710 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ deps += $(LIB_OBJS:%.o=%.o.d) APPS := coop echo hello mqueues semaphore mutex cond \ pipes pipes_small pipes_struct prodcons progress \ rtsched suspend test64 timer timer_kill \ - cpubench + cpubench test_libc # Output files for __link target IMAGE_BASE := $(BUILD_DIR)/image diff --git a/app/mqueues.c b/app/mqueues.c index d632a6c7..5816a1ce 100644 --- a/app/mqueues.c +++ b/app/mqueues.c @@ -36,7 +36,7 @@ void task1(void) /* Prepare and enqueue second message (string payload) for task 3. */ pmsg = &msg2; - sprintf(str, "hello %d from t1...", val++); + snprintf(str, sizeof(str), "hello %d from t1...", val++); /* Enqueue the string. The payload points to the local 'str' buffer. */ pmsg->payload = str; /* Point payload to the string buffer. */ pmsg->size = strlen(str) + 1; /* Store string size. */ diff --git a/app/pipes_small.c b/app/pipes_small.c index bea2835d..a8cdea70 100644 --- a/app/pipes_small.c +++ b/app/pipes_small.c @@ -7,7 +7,7 @@ void task2(void) char data2[64]; while (1) { - sprintf(data2, "Hello from task 2!"); + snprintf(data2, sizeof(data2), "Hello from task 2!"); /* write pipe - write size must be less than buffer size */ mo_pipe_write(pipe2, data2, strlen((char *) data2)); } @@ -18,7 +18,7 @@ void task1(void) char data1[64]; while (1) { - sprintf(data1, "Hello from task 1!"); + snprintf(data1, sizeof(data1), "Hello from task 1!"); /* write pipe - write size must be less than buffer size */ mo_pipe_write(pipe1, data1, strlen((char *) data1)); } diff --git a/app/pipes_struct.c b/app/pipes_struct.c index dea76a65..645ae91f 100644 --- a/app/pipes_struct.c +++ b/app/pipes_struct.c @@ -18,7 +18,7 @@ void task1(void) ptr->b = -555; while (1) { - sprintf(ptr->v, "hello %ld", i++); + snprintf(ptr->v, sizeof(ptr->v), "hello %ld", i++); ptr->a++; ptr->b++; diff --git a/app/test_libc.c b/app/test_libc.c new file mode 100644 index 00000000..bf8e34ca --- /dev/null +++ b/app/test_libc.c @@ -0,0 +1,341 @@ +/* LibC Test Suite - Comprehensive tests for standard library functions. + * + * Current Coverage: + * - vsnprintf/snprintf: Buffer overflow protection + * * C99 semantics, truncation behavior, ISR safety + * * Format specifiers: %s, %d, %u, %x, %p, %c, %% + * * Edge cases: size=0, size=1, truncation, null termination + * + * Future Tests (Planned): + * - String functions: strlen, strcmp, strcpy, strncpy, memcpy, memset + * - Memory allocation: malloc, free, realloc + * - Character classification: isdigit, isalpha, isspace, etc. + */ + +#include + +#define TEST_PASS 1 +#define TEST_FAIL 0 + +/* Test result tracking */ +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +/* Simple string comparison for tests */ +static int test_strcmp(const char *s1, const char *s2) +{ + while (*s1 && (*s1 == *s2)) + s1++, s2++; + return *s1 - *s2; +} + +/* Simple string length for tests */ +static size_t test_strlen(const char *s) +{ + const char *p = s; + while (*p) + p++; + return p - s; +} + +/* Test assertion macro */ +#define ASSERT_TEST(condition, test_name) \ + do { \ + tests_run++; \ + if (condition) { \ + tests_passed++; \ + printf("[PASS] %s\n", test_name); \ + } else { \ + tests_failed++; \ + printf("[FAIL] %s\n", test_name); \ + } \ + } while (0) + +/* Test 1: Basic functionality with sufficient buffer */ +void test_basic_functionality(void) +{ + char buf[64]; + int ret; + + /* Test simple string */ + ret = snprintf(buf, sizeof(buf), "Hello World"); + ASSERT_TEST(ret == 11 && test_strcmp(buf, "Hello World") == 0, + "Basic string formatting"); + + /* Test integer formatting */ + ret = snprintf(buf, sizeof(buf), "Number: %d", 42); + ASSERT_TEST(ret == 10 && test_strcmp(buf, "Number: 42") == 0, + "Integer formatting"); + + /* Test unsigned formatting */ + ret = snprintf(buf, sizeof(buf), "Unsigned: %u", 123); + ASSERT_TEST(ret == 13 && test_strcmp(buf, "Unsigned: 123") == 0, + "Unsigned formatting"); + + /* Test hex formatting */ + ret = snprintf(buf, sizeof(buf), "Hex: %x", 0xDEAD); + ASSERT_TEST(ret == 9 && test_strcmp(buf, "Hex: dead") == 0, + "Hex formatting"); + + /* Test pointer formatting */ + void *ptr = (void *) 0x12345678; + ret = snprintf(buf, sizeof(buf), "Ptr: %p", ptr); + ASSERT_TEST(ret == 13 && test_strcmp(buf, "Ptr: 12345678") == 0, + "Pointer formatting"); + + /* Test character formatting */ + ret = snprintf(buf, sizeof(buf), "Char: %c", 'A'); + ASSERT_TEST(ret == 7 && test_strcmp(buf, "Char: A") == 0, + "Character formatting"); + + /* Test multiple format specifiers */ + ret = snprintf(buf, sizeof(buf), "%d %s %x", 42, "test", 0xFF); + ASSERT_TEST(ret == 10 && test_strcmp(buf, "42 test ff") == 0, + "Multiple format specifiers"); +} + +/* Test 2: Edge case - size = 0 (C99 semantics) */ +void test_size_zero(void) +{ + char buf[10] = "unchanged"; + int ret; + + /* C99: should return length that would be written, no buffer modification + */ + ret = snprintf(buf, 0, "Hello World"); + ASSERT_TEST(ret == 11 && test_strcmp(buf, "unchanged") == 0, + "Size=0 preserves buffer"); + + /* NULL buffer with size=0 is valid (C99) */ + ret = snprintf(NULL, 0, "Test %d", 123); + ASSERT_TEST(ret == 8, "NULL buffer with size=0"); +} + +/* Test 3: Edge case - size = 1 (only null terminator) */ +void test_size_one(void) +{ + char buf[10]; + int ret; + + buf[0] = 'X'; /* Sentinel value */ + ret = snprintf(buf, 1, "Hello"); + ASSERT_TEST(ret == 5 && buf[0] == '\0', + "Size=1 writes only null terminator"); +} + +/* Test 4: Truncation scenarios */ +void test_truncation(void) +{ + char buf[10]; + int ret; + + /* String longer than buffer */ + ret = snprintf(buf, sizeof(buf), "This is a very long string"); + ASSERT_TEST(ret == 26 && test_strlen(buf) == 9 && buf[9] == '\0', + "Truncation with long string"); + + /* Exact fit (9 chars + null in 10-byte buffer) */ + ret = snprintf(buf, sizeof(buf), "123456789"); + ASSERT_TEST( + ret == 9 && test_strcmp(buf, "123456789") == 0 && buf[9] == '\0', + "Exact fit"); + + /* One char too long */ + ret = snprintf(buf, sizeof(buf), "1234567890"); + ASSERT_TEST( + ret == 10 && test_strcmp(buf, "123456789") == 0 && buf[9] == '\0', + "One char truncation"); + + /* Format string producing truncated output */ + ret = snprintf(buf, 8, "Value: %d", 12345); + ASSERT_TEST(ret == 12 && test_strcmp(buf, "Value: ") == 0 && buf[7] == '\0', + "Format truncation"); +} + +/* Test 5: Null-termination guarantee */ +void test_null_termination(void) +{ + char buf[5]; + int ret; + + /* Fill buffer to verify null-termination */ + for (int i = 0; i < 5; i++) + buf[i] = 'X'; + + ret = snprintf(buf, 5, "1234567890"); + ASSERT_TEST(buf[4] == '\0', "Null termination guaranteed"); + + /* Verify buffer was limited */ + ASSERT_TEST(test_strcmp(buf, "1234") == 0, "Truncated content correct"); + + /* C99 return value: chars that would be written */ + ASSERT_TEST(ret == 10, "C99 return value for truncation"); +} + +/* Test 6: Format specifier edge cases */ +void test_format_specifiers(void) +{ + char buf[32]; + int ret; + + /* Null string pointer */ + ret = snprintf(buf, sizeof(buf), "String: %s", (char *) NULL); + ASSERT_TEST(test_strcmp(buf, "String: ") == 0, + "NULL string handling"); + + /* Negative numbers */ + ret = snprintf(buf, sizeof(buf), "%d", -42); + ASSERT_TEST(test_strcmp(buf, "-42") == 0, "Negative number formatting"); + + /* Zero */ + ret = snprintf(buf, sizeof(buf), "%d %u %x", 0, 0, 0); + ASSERT_TEST(test_strcmp(buf, "0 0 0") == 0, "Zero formatting"); + + /* Width padding */ + ret = snprintf(buf, sizeof(buf), "%5d", 42); + ASSERT_TEST(test_strcmp(buf, " 42") == 0, "Width padding"); + + /* Zero padding */ + ret = snprintf(buf, sizeof(buf), "%05d", 42); + ASSERT_TEST(test_strcmp(buf, "00042") == 0, "Zero padding"); + + /* Literal percent */ + ret = snprintf(buf, sizeof(buf), "100%% complete"); + ASSERT_TEST(test_strcmp(buf, "100% complete") == 0, "Literal percent sign"); + + (void) ret; /* Return values tested in test_return_values() */ +} + +/* Test 7: C99 return value semantics */ +void test_return_values(void) +{ + char buf[10]; + int ret; + + /* Return value = chars that would be written (excluding null) */ + ret = snprintf(buf, sizeof(buf), "12345"); + ASSERT_TEST(ret == 5, "Return value for normal case"); + + /* Return value for truncated string */ + ret = snprintf(buf, 5, "1234567890"); + ASSERT_TEST(ret == 10, "Return value for truncated case"); + + /* Empty string */ + ret = snprintf(buf, sizeof(buf), ""); + ASSERT_TEST(ret == 0 && buf[0] == '\0', "Empty string return value"); +} + +/* Test 8: Buffer boundary verification */ +void test_buffer_boundaries(void) +{ + char buf[16]; + char guard_before = 0xAA; + char guard_after = 0xBB; + int ret; + + /* Place guards around buffer */ + char *test_buf = &buf[1]; + buf[0] = guard_before; + buf[15] = guard_after; + + /* Write to middle buffer, verify guards intact */ + ret = snprintf(test_buf, 14, "Test boundary"); + ASSERT_TEST(buf[0] == guard_before && buf[15] == guard_after, + "No buffer overrun"); + + ASSERT_TEST(test_strcmp(test_buf, "Test boundary") == 0, + "Content correct within boundaries"); + + (void) ret; /* Return value not critical for boundary test */ +} + +/* Test 9: ISR safety verification */ +void test_isr_safety(void) +{ + char buf[32]; + int ret; + + /* Verify no dynamic allocation (manual inspection of code required) + * Verify bounded execution time (all format handlers O(1) or O(n) small n) + * Verify reentrancy (no global state modified) + */ + + /* Test can be called multiple times without side effects */ + ret = snprintf(buf, sizeof(buf), "ISR Test %d", 123); + char saved[32]; + for (int i = 0; i < 32; i++) + saved[i] = buf[i]; + + ret = snprintf(buf, sizeof(buf), "ISR Test %d", 123); + int match = 1; + for (int i = 0; i < 32; i++) { + if (buf[i] != saved[i]) { + match = 0; + break; + } + } + ASSERT_TEST(match == 1, "Reentrant behavior (no global state)"); + + (void) ret; /* Return value not critical for ISR safety test */ +} + +/* Test 10: Mixed format stress test */ +void test_mixed_formats(void) +{ + char buf[128]; + int ret; + + /* Complex format string with multiple types */ + ret = snprintf(buf, sizeof(buf), + "Task %d: ptr=%p, count=%u, hex=%x, char=%c, str=%s", 5, + (void *) 0xABCD, 100, 0xFF, 'X', "test"); + + /* Verify no crash and reasonable return value */ + ASSERT_TEST(ret > 0 && ret < 128, "Mixed format stress test"); + + /* Verify null termination */ + ASSERT_TEST(buf[test_strlen(buf)] == '\0', "Mixed format null termination"); +} + +void test_runner(void) +{ + printf("\n=== LibC Test Suite ===\n"); + printf("Testing: vsnprintf/snprintf\n\n"); + + test_basic_functionality(); + test_size_zero(); + test_size_one(); + test_truncation(); + test_null_termination(); + test_format_specifiers(); + test_return_values(); + test_buffer_boundaries(); + test_isr_safety(); + test_mixed_formats(); + + printf("\n=== Test Summary ===\n"); + printf("Tests run: %d\n", tests_run); + printf("Tests passed: %d\n", tests_passed); + printf("Tests failed: %d\n", tests_failed); + + if (tests_failed == 0) { + printf("\n[SUCCESS] All tests passed!\n"); + } else { + printf("\n[FAILURE] %d test(s) failed!\n", tests_failed); + } +} + +int32_t app_main(void) +{ + test_runner(); + + /* Shutdown QEMU cleanly after tests complete */ + *(volatile uint32_t *) 0x100000U = 0x5555U; + + /* Fallback: keep task alive if shutdown fails (non-QEMU environments) */ + while (1) + ; /* Idle loop */ + + return 0; +} diff --git a/include/lib/libc.h b/include/lib/libc.h index ee7354f2..5951c3b5 100644 --- a/include/lib/libc.h +++ b/include/lib/libc.h @@ -8,6 +8,7 @@ */ #include +#include /* For va_list in vsnprintf declaration */ /* Basic Type Definitions */ #ifndef NULL @@ -149,4 +150,5 @@ char *getline(char *s); /* Formatted output */ int32_t printf(const char *fmt, ...); -int32_t sprintf(char *out, const char *fmt, ...); +int32_t snprintf(char *str, size_t size, const char *fmt, ...); +int vsnprintf(char *str, size_t size, const char *fmt, va_list args); diff --git a/lib/stdio.c b/lib/stdio.c index a89cf8aa..7529eb75 100644 --- a/lib/stdio.c +++ b/lib/stdio.c @@ -129,11 +129,15 @@ static int toint(const char **s) return i; } -/* Emits a single character and increments the total character count. */ -static inline void printchar(char **str, int32_t c, int *len) +/* Emits a single character and increments the total character count. + * Bounds-aware version: only writes if within buffer limits. + * Always increments len to track total chars that would be written (C99). + */ +static inline void printchar_bounded(char **str, char *end, int32_t c, int *len) { if (str) { - **str = c; + if (*str < end) + **str = c; ++(*str); } else if (c) { _putchar(c); @@ -141,63 +145,76 @@ static inline void printchar(char **str, int32_t c, int *len) (*len)++; } -/* Main formatted string output function. */ -static int vsprintf(char **buf, const char *fmt, va_list args) +/* Supports: %s %d %u %x %p (no floating point). + * Returns: Number of chars that would be written (C99 semantics). + * NOTE: Does NOT include null terminator in return count. + * + * Deviations from C99: + * - Limited format specifier support (no %f, %e, %g, etc.) + * - No precision or complex width modifiers + * - Simplified %p format (basic hex without "0x" prefix handling) + * + * ISR-Safe: No malloc, no blocking, reentrant, bounded execution time. + */ +int vsnprintf(char *str, size_t size, const char *fmt, va_list args) { - char **p = buf; - const char *str; + char *end = (size > 0) ? (str + size - 1) : str; + const char *s; char pad; int width; int base; int sign; int i; long num; - int len = 0; + int len = 0; /* Total chars that would be written (excluding null) */ char tmp[32]; - /* The digits string for number conversion. */ + /* C99 semantics: allow NULL str if size is 0 (for size calculation) */ + if (!str && size != 0) + return -1; + const char *digits = "0123456789abcdef"; - /* Iterate through the format string. */ + /* Iterate through the format string */ for (; *fmt; fmt++) { if (*fmt != '%') { - printchar(p, *fmt, &len); + printchar_bounded(&str, end, *fmt, &len); continue; } /* Process format specifier: '%' */ - ++fmt; /* Move past '%'. */ + ++fmt; /* Move past '%' */ - /* Get flags: padding character. */ - pad = ' '; /* Default padding is space. */ + /* Get flags: padding character */ + pad = ' '; /* Default padding is space */ if (*fmt == '0') { pad = '0'; fmt++; } - /* Get width: minimum field width. */ + /* Get width: minimum field width */ width = -1; if (isdigit(*fmt)) width = toint(&fmt); - base = 10; /* Default base for numbers is decimal. */ - sign = 0; /* Default is unsigned. */ + base = 10; /* Default base for numbers is decimal */ + sign = 0; /* Default is unsigned */ - /* Handle format specifiers. */ + /* Handle format specifiers */ switch (*fmt) { case 'c': /* Character */ - printchar(p, (char) va_arg(args, int), &len); + printchar_bounded(&str, end, (char) va_arg(args, int), &len); continue; case 's': /* String */ - str = va_arg(args, char *); - if (str == 0) /* Handle NULL string. */ - str = ""; + s = va_arg(args, char *); + if (s == 0) /* Handle NULL string */ + s = ""; - /* Print string, respecting width. */ - for (; *str && width != 0; str++, width--) - printchar(p, *str, &len); + /* Print string, respecting width */ + for (; *s && width != 0; s++, width--) + printchar_bounded(&str, end, *s, &len); - /* Pad if necessary. */ + /* Pad if necessary */ while (width-- > 0) - printchar(p, pad, &len); + printchar_bounded(&str, end, pad, &len); continue; case 'l': /* Long integer modifier */ fmt++; @@ -217,20 +234,23 @@ static int vsprintf(char **buf, const char *fmt, va_list args) case 'p': /* Pointer address (hex) */ base = 16; num = va_arg(args, size_t); - width = sizeof(size_t); + width = sizeof(size_t) * 2; /* 2 hex digits per byte */ break; - default: /* Unknown format specifier, ignore. */ + case '%': /* Literal '%' */ + printchar_bounded(&str, end, '%', &len); + continue; + default: /* Unknown format specifier, ignore */ continue; } - /* Handle sign for signed integers. */ + /* Handle sign for signed integers */ if (sign && num < 0) { num = -num; - printchar(p, '-', &len); + printchar_bounded(&str, end, '-', &len); width--; } - /* Convert number to string (in reverse order). */ + /* Convert number to string (in reverse order) */ i = 0; if (num == 0) tmp[i++] = '0'; @@ -241,40 +261,61 @@ static int vsprintf(char **buf, const char *fmt, va_list args) tmp[i++] = digits[divide(&num, base)]; } - /* Pad with leading characters if width is specified. */ + /* Pad with leading characters if width is specified */ width -= i; while (width-- > 0) - printchar(p, pad, &len); + printchar_bounded(&str, end, pad, &len); - /* Print the number string in correct order. */ + /* Print the number string in correct order */ while (i-- > 0) - printchar(p, tmp[i], &len); + printchar_bounded(&str, end, tmp[i], &len); + } + + /* Always null-terminate within bounds (C99 requirement) */ + if (size > 0) { + if (str <= end) + *str = '\0'; + else + *end = '\0'; } - printchar(p, '\0', &len); + /* Return total chars that would be written (C99 semantics), + * NOT including the null terminator. + */ return len; } -/* Formatted output to stdout. */ +/* Formatted output to stdout. + * Uses a fixed stack buffer - very long output will be truncated. + */ int32_t printf(const char *fmt, ...) { + char buf[256]; /* Stack buffer for formatted output */ va_list args; - int32_t v; va_start(args, fmt); - v = vsprintf(0, fmt, args); + int32_t len = vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); - return v; + + /* Output the formatted string to stdout */ + char *p = buf; + while (*p) + _putchar(*p++); + + return len; } -/* Formatted output to a string. */ -int32_t sprintf(char *out, const char *fmt, ...) +/* Formatted output to a bounded string buffer (C99). + * Guarantees null termination if size > 0. + * Returns total chars that would be written (excluding null terminator). + */ +int32_t snprintf(char *str, size_t size, const char *fmt, ...) { va_list args; int32_t v; va_start(args, fmt); - v = vsprintf(&out, fmt, args); + v = vsnprintf(str, size, fmt, args); va_end(args); return v; }