Skip to content

[Defect Avoidance Guideline for] CERT C, INT31-C: Ensure that integer conversions do not result in lost or misinterpreted data #185

@felix91gr

Description

@felix91gr

Chapter

Expressions

Guideline Title

Ensure that all Integer Conversions are Lossless

Category

Mandatory

Status

Draft

Release Begin

1.0.0

Release End

latest

FLS Paragraph ID

fls_1qhsun1vyarz

Decidability

Undecidable

Scope

Module

Tags

numerics, surprising-behavior, defect

Amplification

Whenever using the as keyword to convert between integer types, the user must ensure that no information is lost.

The recommended way to do this is to forgo the usage of as altogether, and instead:

  • To use into and from whenever available - these guarantee lossless conversion.
  • To use try_into and try_from everywhere else - these ensure the programmer handles the cases where the intended conversion might lose information.

If the intent is to truncate values, the remainder operator % is to be used instead of a type cast.

If the intent is to reinterpret an unsigned (or signed) integer as its signed (or unsigned) counterpart, this should not be done through a type cast either. Instead, this conversion may be performed through the functions cast_signed and cast_unsigned, respectively.

Exception(s)

No response

Rationale

This is directly inspired by INT31-C. Ensure that integer conversions do not result in lost or misinterpreted data.

In Rust, casts (as conversions) are well-defined, regardless of hardware and compiler implementation. However:

  • Casts can implicitly truncate, round or otherwise lose information.
  • They can also subtly change the value when going from eg uN to iN:
    • The bits in this case are reinterpreted,
    • And signed integers always follow 2's complement in Rust,
    • So any uN greater in value than iN::MAX will be cast to a negative iN.

Since implicitly losing information is undesirable, specially when it happens in such a drastic way as a change of sign, this behavior must be avoided.

Non-Compliant Example - Prose

The first example implicitly converts the u8 values to their bit-identical i8 ones. The output of this program shows how the counter gets implicitly wrapped down to the negative domain:

Sent: 125. Received: 125
Sent: 126. Received: 126
Sent: 127. Received: 127
Sent: 128. Received: -128
Sent: 129. Received: -127

The second example implicitly converts the large u16 values to truncated u8 ones:

Sent: 254. Received: 254
Sent: 255. Received: 255
Sent: 256. Received: 0
Sent: 257. Received: 1
Sent: 258. Received: 2
Sent: 259. Received: 3

Non-Compliant Example - Code

First Example

fn i_require_signed_ints(input: i8) {
    println!("Received: {input}");
}

fn i_count_up(from: u8, to: u8) {
    let mut counter: u8 = from;
    
    while counter < to  {
        print!("Sent: {counter}. ");
        i_require_signed_ints(counter as i8);
        counter += 1;
    }
}
i_count_up(125, 130);

Second Example

fn i_require_small_ints(input: u8) {
    println!("Received: {input}");
}

fn i_count_up(from: u16, to: u16) {
    let mut counter: u16 = from;
    
    while counter < to {
        print!("Sent: {counter}. ");
        i_require_small_ints(counter as u8);
        counter += 1;
    }
}
i_count_up(254, 260);

Compliant Example - Prose

The first example demonstrates how to avoid implicit conversions by guarding against the known limit of the type being cast to. Here, we avoid sending a potentially breaking input to the receiver:

Sent: 125. Received: 125
Sent: 126. Received: 126
Sent: 127. Received: 127
Counter not sent. Exceeded limit of receiver (LIMIT = 127, counter = 128)
Counter not sent. Exceeded limit of receiver (LIMIT = 127, counter = 129)

The second example showcases the usage of try_into to guard against loss of information. Here, we avoid truncating unintentionally, and are also able to recover a string representation of the error:

Sent: 254. Received: 254
Sent: 255. Received: 255
Didn't send 256: out of range integral type conversion attempted.
Didn't send 257: out of range integral type conversion attempted.
Didn't send 258: out of range integral type conversion attempted.
Didn't send 259: out of range integral type conversion attempted.

Compliant Example - Code

First Example

fn i_require_signed_ints(input: i8) {
    println!("Received: {input}");
}

fn i_count_up(from: u8, to: u8) {
    let mut counter: u8 = from;
    
    const LIMIT : u8 = i8::MAX as u8;
    
    while counter < to {
        if counter <= LIMIT {
            print!("Sent: {counter}. ");
            i_require_signed_ints(counter as i8);
        } else {
            println!("Counter not sent. Exceeded limit of receiver (LIMIT = {LIMIT}, counter = {counter})");
        }
        counter += 1;
    }
}
i_count_up(125, 130);

Second Example

fn i_require_small_ints(input: u8) {
    println!("Received: {input}");
}

fn i_count_up(from: u16, to: u16) {
    let mut counter: u16 = from;
    
    while counter < to {
        match counter.try_into() {
            Ok(small_int) => {
                print!("Sent: {small_int}. ");
                i_require_small_ints(small_int);
            },
            Err(error) => {
                println!("Didn't send {counter}: {error}.")
            }
        }
        counter += 1;
    }
}
i_count_up(254, 260);

Metadata

Metadata

Labels

category: mandatoryA coding guideline with category mandatorychapter: expressionscoding guidelineAn issue related to a suggestion for a coding guidelinedecidability: undecidableA coding guideline which cannot be checked automaticallyscope: moduleA coding guideline that can be determined applied at the module levelstatus: draft

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions