Back to Blog
high SEVERITY6 min read

NULL Pointer Dereference in ESP8266 user_interface.c wifi_station_set_default_hostname()

A critical NULL pointer dereference vulnerability in the ESP8266 firmware's `user_interface.c` allowed attackers to crash devices by exhausting the limited 80KB heap memory. The `wifi_station_set_default_hostname()` function's `os_malloc` call lacked a proper NULL guard, causing `ets_sprintf` to write to address 0 when allocation failed. The fix corrected a logic inversion in the NULL check condition.

O
By Orbis AppSec
Published June 3, 2026Reviewed June 3, 2026

Answer Summary

This is a NULL pointer dereference vulnerability (CWE-476) in C code running on the ESP8266 IoT platform. The `wifi_station_set_default_hostname()` function in `user_interface.c` had an inverted NULL check (`if(hostname == NULL)` instead of `if(hostname != NULL)`) that caused `ets_sprintf` to write to a NULL pointer when memory allocation succeeded under normal conditions, and critically failed to protect against NULL dereference during memory exhaustion. The fix corrects the conditional to `if(hostname != NULL)` ensuring `ets_sprintf` only executes when the pointer is valid.

Vulnerability at a Glance

cweCWE-476
fixChanged `if(hostname == NULL)` to `if(hostname != NULL)` before ets_sprintf call
riskDevice crash/denial of service via memory exhaustion
languageC (ESP8266 firmware)
root causeInverted NULL check after os_malloc allows write to address 0
vulnerabilityNULL Pointer Dereference

Introduction

The user_interface.c file in the ESP8266 firmware handles core device initialization, including setting the WiFi station's default hostname based on the device's MAC address. A subtle but dangerous logic error in the wifi_station_set_default_hostname() function meant that the firmware would write formatted data to a NULL pointer under memory exhaustion conditions—a trivially exploitable denial-of-service vulnerability on a device with only 80KB of available RAM.

What makes this vulnerability particularly insidious is that it stems from a single-character logic inversion: == instead of !=. This kind of bug can easily slip past code review because the surrounding code looks structurally correct. The function allocates memory, checks the result, and then uses it—but the check was backwards.

The Vulnerability Explained

Let's look at the vulnerable code in wifi_station_set_default_hostname() at line 44 of info/libs/main/user_interface.c:

void wifi_station_set_default_hostname(uint8 * mac)
{
    if(hostname != NULL) {
        os_free(hostname);
        hostname = NULL;
    }
    hostname = os_malloc(32);
    if(hostname == NULL) {  // BUG: This is inverted!
        ets_sprintf(hostname, "ESP_%02X%02X%02X", mac[3], mac[4], mac[5]);
    }
}

The condition if(hostname == NULL) means: "if memory allocation failed, go ahead and write to the pointer anyway." This is exactly backwards. When os_malloc returns NULL (because the 80KB heap is exhausted), the code proceeds to call ets_sprintf(hostname, ...) which attempts to write the formatted string "ESP_XXYYZZ" to memory address 0.

The Attack Scenario

On the ESP8266, with only ~80KB of usable RAM, an attacker with network access can exhaust the heap with minimal effort:

  1. Open many concurrent HTTP connections to the device's built-in web server. Each connection consumes heap memory for socket buffers, request parsing, and response data.
  2. Trigger a WiFi reconnection event (e.g., by briefly jamming the WiFi channel or causing the AP to deauthenticate the device). When the device reconnects, it calls wifi_station_set_default_hostname().
  3. os_malloc(32) fails because the heap is exhausted from the concurrent connections.
  4. ets_sprintf writes to address 0, causing an immediate hardware exception and device reset.

This creates a reliable, repeatable denial-of-service attack. The attacker can keep the device in a crash loop by maintaining the memory pressure during each reboot cycle.

Why This Is Worse Than a Typical NULL Check

In most NULL pointer bugs, the issue is a missing check. Here, the check exists but is logically inverted. This means:

  • Under normal operation (when os_malloc succeeds and returns a valid pointer), the ets_sprintf call is skipped because hostname != NULL. The device's hostname is never actually set.
  • Under memory exhaustion (when os_malloc returns NULL), the ets_sprintf call executes and writes to address 0.

Both paths are broken—the function never works correctly under normal conditions AND crashes under adversarial conditions.

The Fix

The fix is elegantly simple—a single character change that corrects the logic inversion:

Before (Vulnerable)

hostname = os_malloc(32);
if(hostname == NULL) {
    ets_sprintf(hostname, "ESP_%02X%02X%02X", mac[3], mac[4], mac[5]);
}

After (Fixed)

hostname = os_malloc(32);
if(hostname != NULL) {
    ets_sprintf(hostname, "ESP_%02X%02X%02X", mac[3], mac[4], mac[5]);
}

By changing == to !=, the code now correctly:
1. Only calls ets_sprintf when hostname points to valid allocated memory (32 bytes).
2. Gracefully handles allocation failure by simply not setting the hostname—the device continues operating without a custom hostname rather than crashing.

Regression Test

The PR also includes a comprehensive regression test (tests/test_invariant_user_interface.c) that mocks os_malloc to simulate memory exhaustion at different points:

START_TEST(test_malloc_null_check_boundary)
{
    struct {
        int fail_at_call;
        const char *description;
    } test_cases[] = {
        {1, "First malloc fails (hostname allocation)"},
        {2, "Second malloc fails (subsequent allocation)"},
        {0, "All mallocs succeed (valid case)"},
    };

    for (int i = 0; i < num_cases; i++) {
        malloc_call_count = 0;
        malloc_fail_count = test_cases[i].fail_at_call;

        /* Call the function under test - it must handle NULL gracefully */
        user_interface_init();

        /* If we reach here without segfault, the invariant holds */
        ck_assert_msg(1, "No crash on malloc failure at call %d", 
                      test_cases[i].fail_at_call);
    }
}

This test ensures that even if os_malloc returns NULL at any allocation point, the function never crashes—establishing the security invariant that "the security boundary is maintained under adversarial input."

Prevention & Best Practices

1. Use Consistent NULL Check Patterns

Establish a team convention for NULL checks after allocation. Many teams prefer the "early return" pattern which is harder to get wrong:

hostname = os_malloc(32);
if (hostname == NULL) {
    return;  // or handle error
}
// Only reachable if hostname is valid
ets_sprintf(hostname, "ESP_%02X%02X%02X", mac[3], mac[4], mac[5]);

This pattern makes the logic flow clearer and eliminates the risk of inverting the condition.

2. Static Analysis for Embedded Code

Use tools that specifically understand embedded patterns:
- Coverity can detect NULL dereferences including inverted checks
- PVS-Studio has specific ESP8266/ESP32 analysis profiles
- clang-tidy with nullability checks catches many of these patterns

3. Bounded Operations

Even with the correct NULL check, use bounded string operations. Replace ets_sprintf with ets_snprintf where available:

if (hostname != NULL) {
    ets_snprintf(hostname, 32, "ESP_%02X%02X%02X", mac[3], mac[4], mac[5]);
}

4. Memory Exhaustion Testing

On resource-constrained devices, always test under memory pressure:
- Mock allocation functions to simulate failure at every call site
- Use heap monitoring to detect when available memory drops below safety thresholds
- Implement connection limits to prevent external memory exhaustion

References

  • CWE-476: NULL Pointer Dereference
  • OWASP Embedded Security: Memory management guidelines for IoT devices
  • CERT C Coding Standard: MEM32-C — Detect and handle memory allocation errors

Key Takeaways

  • A single inverted operator (== vs !=) turned a safety check into a crash trigger in wifi_station_set_default_hostname(). Always double-check conditional logic around allocation results.
  • The ESP8266's 80KB RAM makes memory exhaustion trivially achievable from the network. Every os_malloc call in embedded firmware must handle NULL gracefully.
  • This bug broke both paths: normal operation skipped hostname setting, and memory exhaustion caused a crash. The inverted logic meant the function was never correct.
  • Regression tests that mock allocation failures (like the included test_malloc_null_check_boundary) are essential for embedded code and should be part of CI.
  • Early-return patterns after NULL checks are less error-prone than wrapping code in if(ptr != NULL) blocks because they eliminate the possibility of condition inversion.

Conclusion

This vulnerability demonstrates how a one-character typo in a NULL check can create a remotely exploitable denial-of-service condition on IoT devices. The wifi_station_set_default_hostname() function's inverted conditional meant that ets_sprintf would write to address 0 precisely when memory was exhausted—the exact condition an attacker would create.

The fix—changing == to !=—is minimal but critical. Combined with the regression test that verifies crash-free behavior under simulated memory exhaustion, this ensures the ESP8266 firmware maintains stability even under adversarial network conditions.

For developers working on embedded systems: treat every allocation as potentially failing, prefer early-return patterns that make logic flow unambiguous, and always include memory exhaustion in your test scenarios. On devices with kilobytes of RAM, your attacker doesn't need a sophisticated exploit—they just need to open enough connections.

Frequently Asked Questions

What is a NULL pointer dereference?

A NULL pointer dereference occurs when code attempts to read from or write to memory address 0 (NULL), which is not a valid memory location. This typically causes a crash or segmentation fault, and on embedded systems like the ESP8266, can cause a complete device reset.

How do you prevent NULL pointer dereference in C?

Always check the return value of memory allocation functions (malloc, os_malloc, calloc) before using the returned pointer. Ensure the check logic is correct—verify the pointer is NOT NULL before dereferencing, not the inverse. Use static analysis tools to catch missing or incorrect NULL checks.

What CWE is NULL pointer dereference?

NULL pointer dereference is classified as CWE-476 (NULL Pointer Dereference). It is a common weakness in C and C++ programs where pointers are used without proper validation, leading to crashes or potentially exploitable conditions.

Is checking malloc return values enough to prevent NULL pointer dereference?

Checking malloc return values is necessary but not sufficient. You must also ensure the check logic is correct (as this vulnerability demonstrates), handle the failure path gracefully, and verify that pointers remain valid throughout their lifetime. Defensive programming practices like initializing pointers to NULL and checking before every dereference provide defense in depth.

Can static analysis detect NULL pointer dereference?

Yes, static analysis tools like Coverity, PVS-Studio, and clang-analyzer can detect many NULL pointer dereference patterns, including missing NULL checks after malloc and inverted conditional logic. In this case, the multi_agent_ai scanner flagged the pattern, demonstrating that automated tooling can catch even subtle logic inversions.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

high

How Spring Boot EndpointRequest.to() security bypass happens in Java Spring Boot and how to fix it

CVE-2025-22235 is a high-severity vulnerability in Spring Boot where `EndpointRequest.to()` creates an incorrect request matcher when an actuator endpoint is not exposed, potentially allowing unauthorized access to protected endpoints. The fix upgrades Spring Boot from 3.4.4 to 3.4.5 in the anti-corruption-layer service's `pom.xml`. This is particularly dangerous because actuator endpoints can expose sensitive operational data and administrative functions.

medium

How path traversal happens in C file extraction and how to fix it

A path traversal vulnerability in the borpak archive extraction tool allowed attackers to write files to arbitrary locations on the filesystem by crafting malicious .pak archives with `../` sequences in filenames. This medium-severity issue in `tools/borpak/source/borpak.c` could enable system compromise through overwriting critical files like `.bashrc` or cron jobs. The fix implements path validation to ensure extracted files never escape the intended extraction directory.

critical

How SQL injection happens in PostgreSQL dictionary synchronization and how to fix it

A critical SQL injection vulnerability in `zhparser--2.1.sql` allowed attackers to execute arbitrary SQL commands by crafting malicious database names. The vulnerability existed because the dictionary synchronization function constructed COPY commands using string concatenation without proper escaping. This fix implements parameterized queries to safely handle database identifiers.

critical

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to

critical

How heap buffer overflow happens in C++ JPEG2000 decoding and how to fix it

A critical heap buffer overflow vulnerability was discovered in the OpenJPEG wrapper for Android (jp2forandroid). The `opj_read_from_byte_array()` function performed memcpy operations without validating that the source offset hadn't exceeded the buffer length, allowing maliciously crafted JPEG2000 images to trigger arbitrary code execution. A simple bounds check before the copy operation now prevents this exploitation path.

critical

How heap buffer overflow happens in C UART response handling and how to fix it

A critical heap buffer overflow vulnerability was discovered in the AT client response handler (`sm_at_client.c`) where incoming UART data was copied into a fixed-size buffer without verifying available capacity. A compromised modem or malicious UART data could trigger arbitrary heap corruption. The fix replaces an assertion-only guard with proper bounds clamping using `MIN()` to ensure writes never exceed the `at_cmd_resp` buffer allocation.