Common Mistakes in Embedded C Development - Buffer Overflows and Memory Leaks

Table of contents

Article photo by Jorge Ramirez on Unsplash
📘 Introduction
In this final article of our 4-part series related to memory management, we wrap up the most common memory pitfalls in Embedded C. In Part 3, we looked at real-time allocation issues and how to avoid them.
Today, we tackle buffer overflows, memory leaks in long-running systems, and tie everything together with a set of practical takeaways to write safer C code for embedded and IoT projects.
🧠 Buffer Overflows
🐞 The Problem
A buffer overflow happens when your code writes past the end of an allocated buffer, overwriting memory it shouldn’t. In embedded systems, this often leads to corrupted variables, erratic behaviour, crashes, or exploitable vulnerabilities.
Why it’s a problem:
No OS-level memory protection or segmentation
Easy to silently corrupt the stack, registers, function pointers, or config
Often triggered by unexpected input from the cloud or users
Hard to detect without runtime protection
Let’s say your device receives OTA (Over-The-Air) configuration updates over MQTT. A common approach is to copy the payload into a local buffer before parsing:
void onCloudMessageReceived(const char* topic, const char* payload) {
char configBuffer[128];
// ❌ Unsafe: no size check
strcpy(configBuffer, payload);
applyDeviceConfig(configBuffer); // parses JSON
}
If the payload exceeds 128 bytes, strcpy() will overwrite the stack. The system might:
Randomly reboot
Apply corrupted config
Crash later during unrelated operations
✅ Solution 1: Use strncpy or snprintf instead of strcpy
Update the cloud message handler with size checks:
void onCloudMessageReceived(const char* topic, const char* payload) {
char configBuffer[128];
// ✅ Safe: guarantees no overflow
snprintf(configBuffer, sizeof(configBuffer), "%s", payload);
applyDeviceConfig(configBuffer);
}
snprintf() ensures that the string is always null-terminated and never exceeds the buffer size. This is especially useful when working with formatted data (e.g. JSON, commands) from untrusted sources like cloud platforms.
✅ Solution 2: Pre-Validate Input Length Before Copying
Prevent unsafe data from ever entering your stack buffer:
void onCloudMessageReceived(const char* topic, const char* payload) {
if (strlen(payload) >= 128) {
log_error("Rejected config: payload too long");
return;
}
char configBuffer[128];
strcpy(configBuffer, payload); // ✅ Safe now
applyDeviceConfig(configBuffer);
}
This adds a gatekeeper to reject overly large MQTT messages before they corrupt your device’s memory.
✅ Solution 3: Use Static Buffers and Minimal JSON Parsers
Instead of copying the entire payload, use in-place parsers:
#include "jsmn.h"
void onCloudMessageReceived(const char* topic, const char* payload) {
// ✅ Parse directly from input (no copy!)
jsmn_parser parser;
jsmn_init(&parser);
jsmntok_t tokens[16];
int tokenCount = jsmn_parse(&parser, payload, strlen(payload), tokens, 16);
if (tokenCount < 1 || tokens[0].type != JSMN_OBJECT) {
log_error("Invalid config format");
return;
}
applyJsonTokens(payload, tokens, tokenCount);
}
Libraries like JSMN and Tiny-JSON parse structured data without allocating memory or copying buffers. This avoids overflows and saves RAM.
✅ Solution 4: Enable Compiler Protections
Detect overflows during development with compiler flags:
-Wall -Wextra -Wformat -Wstringop-overflow -fstack-protector-strong
These options catch common bugs like string overflows, stack smashing, and unsafe memory use — even before flashing to a device.
💡 Takeaway
When dealing with cloud or network data, never assume it’s safe. Whether it’s JSON configs or sensor commands, one unchecked strcpy() can brick your device or open a remote attack vector.
🏁 Conclusion
Memory safety in embedded C isn’t about complex tools; it’s about disciplined use of simple techniques. Avoid dynamic memory in real-time paths. Validate input lengths before copying. Use safe string functions like snprintf() instead of strcpy(). Prefer static allocation and lightweight parsers that work in-place. Enable compiler warnings and stack protection to catch issues early.
These practices are straightforward, effective, and require no language switch, just modern C done right.
At Itransition, we build IoT solutions with all these challenges in mind, ensuring our clients receive reliable, scalable systems with minimal maintenance overhead. Learn more about our approach at https://www.itransition.com/iot.
Subscribe to my newsletter
Read articles from Ilya Katlinski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
