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

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.