CWE-252: Unchecked Return Value
Overview
Unchecked return value occurs when code invokes an operation that signals failure through its return value - a status code, a null/negative sentinel, an error object - and continues without inspecting it, proceeding as though the operation succeeded. It differs from CWE-248 (Uncaught Exception): the failure here is visible in the return channel, but nothing reads it.
Relationship to Other CWEs
- CWE-252 (this page) - a return value that is never inspected, across allocation, I/O and other fallible operations
- CWE-754 (Improper Check for Unusual or Exceptional Conditions) - the parent. No page here
- CWE-273 (Improper Check for Dropped Privileges) - a peer, covering the specific high-severity case of an unchecked privilege-drop call in depth. This page covers the general pattern
- CWE-476 (NULL Pointer Dereference) - where an unchecked return value leads once a null result is later dereferenced
- CWE-390 (Detection of Error Condition Without Action) - distinct from this page: an error that is detected but not acted on, where CWE-252 is a return value never inspected at all. No page here
- CWE-391 (Unchecked Error Condition) - planned for deprecation. MITRE directs its content here, to CWE-248, or to CWE-1069 (Empty Exception Block). A tool still reporting CWE-391 for an ignored return value is reporting this weakness under a retiring number, and CWE-252 is what to file it as
- CWE-703 (Improper Check or Handling of Exceptional Conditions) - the pillar above this page, by way of CWE-754. Not CWE-691 (Insufficient Control Flow Management), which is a separate pillar; that page says the same thing in its own routing section, because an unchecked return value is the shape most often filed under the wrong one of the two
OWASP Classification
A10:2025 - Mishandling of Exceptional Conditions
Risk
High: An ignored return value can leave a security control silently disabled - a privilege drop or authorization check that reported failure and was never read. It can also let code continue with an invalid or partially initialized resource, such as a null pointer from a failed allocation or a partial read treated as complete, or leave the application in an inconsistent state. An attacker can trigger any of these deliberately by forcing the underlying call to fail.
Remediation Steps
Core Principle: Check the return value or error signal of every operation that can fail, and define what happens on failure before writing the success path.
Trace the Data Path
- Source: Any call whose contract includes a failure signal - allocation functions, I/O calls, privilege-change calls, parsing/conversion functions, external API clients
- Sink: The code immediately after the call that assumes success - dereferences an allocated pointer, proceeds as an unprivileged (or still-privileged) user, treats a partial read as complete
- Missing control: No check of the return value or error signal between the call and the code that depends on its success
Check Every Fallible Call, Especially Security-Critical Ones (Primary Defense)
- Treat privilege-change operations (drop or elevate), resource allocation, and any operation gating a security decision as mandatory to check - a silent failure here means the program continues in the wrong security state
- Fail closed: if a security-critical operation's return value indicates failure, stop rather than continue with a best-effort fallback
- For operations that can return a partial result (a read/write that transfers fewer bytes than requested), loop or re-check rather than treating one call as complete
Use Language-Level Enforcement Where Available
- Prefer languages or constructs that make an unchecked result a compile-time signal rather than a silent bug: a
Result/Option-style return type the caller must unwrap, or a compiler attribute that turns an ignored return value into a warning or error - Where the language has no such enforcement, wrap fallible calls in a helper that checks internally and raises or aborts on failure, so callers cannot forget the check by construction
- Enable the compiler or linter warning for ignored return values project-wide and treat it as a build failure in CI, not just a local suggestion. Know what the warning actually covers before relying on it: GCC and Clang's
-Wunused-resultfires only for functions carrying[[nodiscard]]or__attribute__((warn_unused_result)), so it says nothing about an unannotated function of your own - annotating the fallible ones is the work, and the warning is what enforces it afterwards
Test Failure Conditions Directly
- Force each checked call to fail (invalid handle, exhausted memory, denied permission, closed connection) and confirm the caller detects it and stops rather than continuing
- For privilege-drop code specifically, verify the resulting privilege level rather than only the return code - see CWE-273 for the full verification sequence
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - return value ignored, success assumed
dropPrivileges(unprivilegedUser) // return value discarded
runUntrustedCode()
// Attack: privilege drop fails silently (e.g. permission denied)
// Result: untrusted code still executes with elevated privileges
buffer = allocate(size) // can fail
write(buffer, data) // dereferences an unchecked result
Why this is vulnerable: the operation reported its failure and the code did not ask. Nothing is thrown, nothing is logged, and execution continues into lines whose correctness depended on the answer - so the program is now running in a state it believes is impossible, and every later decision is made on that belief.
The two examples show the two ways that ends badly. In the first the failure is silent and the consequence is a security control that did not happen: runUntrustedCode executes exactly as intended, at exactly the wrong privilege level, and nothing anywhere records that the drop was refused. In the second the failure surfaces immediately as a crash on a null result, which is the better outcome of the two because someone finds out.
The reason this shape recurs is that the calls involved almost never fail during development. A privilege drop works on the developer's machine, an allocation succeeds, a close returns cleanly - so the unchecked path has no test that exercises it and no symptom until the conditions that make it fail arrive in production. Where a failure must not be ignorable, prefer an API that raises: an unhandled exception is loud, whereas an unread return value is indistinguishable from a deliberate choice.
Secure Patterns
// SECURE - failure is checked and handled before continuing
if not dropPrivileges(unprivilegedUser):
log.critical("privilege drop failed")
abort()
if currentPrivilegeLevel() != unprivilegedUser:
abort() // verify the resulting state, don't just trust the return code
runUntrustedCode()
buffer = allocate(size)
if buffer is null:
abort("allocation failed")
write(buffer, data)
Why this works: The program never proceeds past a fallible operation without knowing whether it succeeded, and for the highest-stakes case (privilege drop) the result is independently verified rather than trusted on the return code alone - an attacker cannot rely on a silently-failed security control still being in effect.
Common Pitfalls
- Checking the return value but not verifying the resulting state: Code checks that a privilege-drop call returned success but never re-queries the actual privilege level afterward - a call that misreports its result, or a partial drop that leaves a saved/effective identity unchanged, still leaves the process privileged.
- Logging the failure and continuing anyway: An
if (result == error) log(...)with noreturn/abortafter it records that something went wrong but still executes the following code as if it hadn't - functionally identical to not checking at all. - Checking only whether a read succeeded, not how much it read: Code checks that a read call didn't return an error but doesn't check the byte count against what was requested - a short read is treated as a complete one, corrupting or truncating the data silently.
- Relying on a wrapper without confirming it actually stops execution: A "safe" wrapper function is introduced but its failure path only logs or returns a sentinel instead of aborting, so callers that don't re-check the wrapper's own result inherit the original bug one layer down.
Additional Resources
- CWE-252: Unchecked Return Value
- CWE-273 (Improper Check for Dropped Privileges)
- OWASP Top 10 2025 A10: Mishandling of Exceptional Conditions
- SEI CERT C - ERR33-C: Detect and handle standard library errors
- SEI CERT C - EXP12-C: Do not ignore values returned by functions
- SEI CERT Java - EXP00-J: Do not ignore values returned by methods