Skip to content

CWE-195: Signed to Unsigned Conversion Error - C

Overview

C converts a negative signed value to unsigned implicitly whenever it is assigned to an unsigned variable, passed to a parameter of unsigned type, or compared against an unsigned operand - there is no compiler error and, without -Wsign-conversion/-Wsign-compare, often no warning either. The result is the value's two's complement bit pattern reinterpreted as unsigned, which for any negative number is a huge positive one. The most common source is a function whose signed return type exists specifically to carry a negative error code (read(), recv(), snprintf(), ssize_t-returning POSIX calls). Such a return is then cast or compared as if it could only ever be a valid, non-negative size.

Common Vulnerable Patterns

Error Return Value Cast to an Unsigned Size

#include <unistd.h>
#include <stdlib.h>
#include <string.h>

// VULNERABLE - no check for a negative (error) return before the unsigned cast
void read_file_data(int fd) {
    char buffer[1024];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));

    // Attack: read() returns -1 on error
    size_t count = (size_t)bytes_read;   // -1 becomes SIZE_MAX
    if (count > 0) {                      // this check is now useless
        char *data = malloc(count);       // huge allocation, likely fails or exhausts memory
        memcpy(data, buffer, count);      // buffer overread if malloc happened to succeed
    }
}

Why this is vulnerable: read() returns -1 to signal an error. Casting that ssize_t straight to size_t before checking its sign turns the error indicator into SIZE_MAX, which then passes the count > 0 check that was meant to filter it out.

Signed Bounds Check Followed by an Unsigned Copy

#include <string.h>

// VULNERABLE - the bounds check is evaluated in int, the copy length in size_t
int copy_chunk(const char *src, int length, char *dest, size_t dest_size) {
    // Attack: length = -1
    if (length > (int)dest_size) {   // -1 is not greater than a positive size: the check passes
        return -1;
    }

    memcpy(dest, src, length);       // memcpy takes size_t: -1 becomes SIZE_MAX
    return 0;
}

Why this is vulnerable: the guard and the sink disagree about the type. length > (int)dest_size is evaluated entirely in int, where -1 is just a small number and passes any upper-bound test. memcpy's third parameter is size_t, so the same -1 converts on the call and asks for a copy of SIZE_MAX bytes. Nothing here ever compares length against 0, and the line that looks like a bounds check is what lets the negative value through to the copy.

The comparison written the other way round - if (length < dest_size), with length still int - has no such hole, but not because it validates the sign. Mixing the two operands converts length to size_t first, so -1 becomes SIZE_MAX, fails the test, and the branch is skipped. It rejects the value by accident rather than by design, and the same unchecked -1 still reaches any sink that sits outside that branch.

Negative Value Cast Directly to an Allocation Size

#include <stdlib.h>

// VULNERABLE - no negative check before the unsigned cast
void allocate_sized_buffer(int requested_size) {
    // Attack: requested_size = -1
    size_t alloc_size = (size_t)requested_size;  // becomes SIZE_MAX
    char *buffer = malloc(alloc_size);            // huge allocation request
}

Why this is vulnerable: Nothing checks requested_size for a negative value before it is cast. A caller that passes -1 (whether from a bug or deliberately) produces a malloc request for the largest possible size_t.

Secure Patterns

Check the Return Value Before Converting

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void read_file_data_safe(int fd) {
    char buffer[1024];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));

    if (bytes_read < 0) {          // catch the error while still signed
        perror("read failed");
        return;
    }

    size_t count = (size_t)bytes_read;  // safe: known non-negative
    if (count > 0 && count <= sizeof(buffer)) {
        char *data = malloc(count);
        if (data != NULL) {
            memcpy(data, buffer, count);
            free(data);
        }
    }
}

Why this works: Checking bytes_read < 0 while the value is still ssize_t catches the error case before any conversion happens. Only a confirmed non-negative value is ever cast to size_t.

Validate the Sign Before Converting or Comparing

#include <stdio.h>
#include <string.h>

int copy_chunk_safe(const char *src, int length, char *dest, size_t dest_size) {
    if (length < 0) {
        fprintf(stderr, "Negative length not allowed\n");
        return -1;
    }

    size_t count = (size_t)length;   // safe: validated non-negative
    if (count > dest_size) {
        fprintf(stderr, "Length exceeds destination buffer\n");
        return -1;
    }

    memcpy(dest, src, count);
    return 0;
}

Why this works: length < 0 is asked while the value is still int, which is the only place it can be answered - after the conversion, -1 is indistinguishable from a genuine SIZE_MAX. Converting once into a named size_t and bounding that against dest_size leaves the guard and the memcpy argument as the same value in the same type, so there is no second conversion at the call for a later reader to miss.

Centralize the Check in a Conversion Helper

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

bool safe_int_to_size_t(int value, size_t *result) {
    if (value < 0) {
        return false;
    }
    *result = (size_t)value;
    return true;
}

void allocate_buffer_safe(int requested_size) {
    size_t alloc_size;
    if (!safe_int_to_size_t(requested_size, &alloc_size)) {
        fprintf(stderr, "Invalid size: negative\n");
        return;
    }
    if (alloc_size > 10000000) {   // separate, practical upper bound
        fprintf(stderr, "Size too large\n");
        return;
    }

    char *buffer = malloc(alloc_size);
    if (buffer != NULL) {
        free(buffer);
    }
}

Why this works: A single, reusable helper makes the non-negative check consistent everywhere a signed value needs to become a size_t, instead of relying on every call site to remember it. The bool return forces the caller to handle the failure case explicitly.

Testing

  • Unit-test every conversion helper with -1, INT_MIN, 0, and INT_MAX, asserting negative inputs are rejected and non-negative ones round-trip correctly.
  • Feed functions under test a simulated error return (e.g., mock read() to return -1) and confirm the caller rejects it instead of proceeding with a wrapped value.
  • Compile with -Wsign-compare -Wsign-conversion -Wconversion -Werror and treat any warning on a security-relevant path as a build failure.
  • Run Cppcheck (--enable=warning), Clang Static Analyzer, or a commercial SAST tool (Coverity, Veracode) and confirm no signed/unsigned findings remain on the fixed paths.
  • On MSVC, the equivalent build setting is /W4 /w44365 with warnings treated as errors (/WX). C4365, the signed/unsigned conversion warning, is off by default at every warning level, so /W4 on its own does not report it.

Common Pitfalls

  • Checking count > 0 after the value is already size_t: by that point a negative source value has already become a huge positive one, so the check no longer catches the error case it was meant to.
  • Fixing the cast and leaving the guard: adding a helper for casts to size_t while an upper-bound check elsewhere is still evaluated in int - the helper never runs on that path, because the negative value satisfied the guard and went straight to the sink.
  • Assuming int parameters are always non-negative because "sizes can't be negative": many APIs use a signed parameter or return type precisely so a negative value can mean "error" - treating the type as if it were unsigned throws away that signal.
  • Relying on -Wall -Wextra alone: -Wsign-compare is included in -Wextra on some compilers but -Wsign-conversion generally is not - it must be enabled explicitly, or these conversions compile silently.

Additional Resources