Búsqueda de errores con AddressSanitizer:patrones de proyectos de código abierto

Búsqueda de errores con AddressSanitizer:patrones de proyectos de código abierto

AddressSanitizer (ASan) se lanzó oficialmente en Visual Studio 2019, versión 16.9. Recientemente usamos esta función para encontrar y corregir un error en el propio compilador de MSVC. Para validar aún más la utilidad de nuestra implementación ASan, también la usamos en una colección de proyectos de código abierto ampliamente utilizados donde encontró errores en Boost, Azure IoT C SDK y OpenSSL. En este artículo, presentamos nuestros hallazgos describiendo el tipo de errores que encontramos y cómo se presentaron en estos proyectos. Proporcionamos enlaces a las confirmaciones de GitHub donde se corrigieron estos errores para que pueda obtener una visión útil de los cambios de código involucrados. Si no está familiarizado con lo que es ASan y cómo usarlo, puede consultar la documentación de AddressSanitizer antes de profundizar en este artículo.

Boost y el iterador ansioso

Un iterador ansioso es uno que apunta a un elemento fuera de los límites de un contenedor y luego se le quita la referencia. El siguiente ejemplo de código muestra un ejemplo de este patrón de acceso a la memoria con errores:

template <typename Iter>
int ComputeSum(Iter b, Iter e)
{
    int sum = 0;

    for (; b <= e; ++b) {
        // ERROR: will dereference the 'end' iterator
        // due to the use of the '<=' operator above.
        sum += *b;
    }

    return sum;
}

A veces, los iteradores ansiosos pueden aparecer por error en bucles que son más complejos, como en el do_length función de la implementación de la faceta de conversión UTF-8 de Boost, que se muestra a continuación:

int utf8_codecvt_facet::do_length(
    std::mbstate_t &,
    const char * from,
    const char * from_end, 
    std::size_t max_limit
) const
#if BOOST_WORKAROUND(__IBMCPP__, BOOST_TESTED_AT(600))
        throw()
#endif
{ 
    int last_octet_count=0;
    std::size_t char_count = 0;
    const char* from_next = from;

    while (from_next+last_octet_count <= from_end && char_count <= max_limit) {
        from_next += last_octet_count;
        last_octet_count = (get_octet_count(*from_next));
        ++char_count;
    }
    return static_cast<int>(from_next-from);
}

Aquí, el operador menor o igual se usa para establecer correctamente from_next a from_end cuando este último apunta a un límite de caracteres UTF-8. Sin embargo, esto también provoca un error en el que se elimina la referencia al iterador final. La compilación de este código con ASan y su depuración en Visual Studio da como resultado una interrupción de ASan en la ubicación esperada:

Le informamos al equipo de Boost acerca de este problema y rápidamente confirmaron una solución en GitHub.

Azure IoT C SDK:una matriz y su longitud constante no están de acuerdo

Un desacuerdo entre una matriz y su constante de longitud ocurre cuando se usa una constante para realizar un seguimiento de la longitud de una matriz, pero tiene una longitud incorrecta. Esto puede resultar en errores de acceso a la memoria cuando se usa la constante de longitud en operaciones de copia de memoria. El sencillo ejemplo siguiente ilustra el problema:

#include <cstring>

unsigned char GLOBAL_BUFFER[] = { 1,2,3,4,5 };
constexpr size_t BUF_SIZE = 6;

void CopyGlobalBuffer(unsigned char* dst)
{
    // ERROR: AddressSanitizer: global-buffer-overflow
    std::memcpy(dst, GLOBAL_BUFFER, BUF_SIZE);
}

Encontramos una instancia de este error en el SDK de Azure IoT C, donde la constante de longitud de una cadena no coincidía con la longitud real:

static const unsigned char* TWIN_REPORTED_PROPERTIES = 
    (const unsigned char*)
    "{ \"reportedStateProperty0\": \"reportedStateProperty0\", "
    "\"reportedStateProperty1\": \"reportedStateProperty1\" }";

static int TWIN_REPORTED_PROPERTIES_LENGTH = 117;

El valor del TWIN_REPORTED_PROPERTIES_LENGTH constante es 117 mientras que el tamaño real del TWIN_REPORTED_PROPERTIES cadena es 107, lo que resulta en un desbordamiento de búfer global al copiar la cadena con memcpy . La compilación de este código con ASan y la depuración con Visual Studio muestra un error durante una llamada a memcpy , en una función interna profunda llamada CONSTBUFFER_Create_Internal :

Esto no nos dijo de inmediato cuál era el origen del error, pero gracias a la integración de ASan dentro de Visual Studio, fue posible usar la ventana Pila de llamadas para recorrer la pila y encontrar la función que pasó el valor de tamaño incorrecto:

El culpable en este caso fue el send_one_report_patch función, que pasó TWIN_REPORTED_PROPERTIES y TWIN_REPORTED_PROPERTIES_LENGTH a una función que llama indirectamente a CONSTBUFFER_Create_Internal :

static void send_one_report_patch(TWIN_MESSENGER_HANDLE handle, time_t current_time)
{
    const unsigned char* buffer = (unsigned char*)TWIN_REPORTED_PROPERTIES;
    size_t size = TWIN_REPORTED_PROPERTIES_LENGTH;
    CONSTBUFFER_HANDLE report = real_CONSTBUFFER_Create(buffer, size);

    umock_c_reset_all_calls();
    set_twin_messenger_report_state_async_expected_calls(report, current_time);
    (void)twin_messenger_report_state_async(handle, report, 
        TEST_on_report_state_complete_callback, NULL);

    real_CONSTBUFFER_DecRef(report);
}

Solucionamos este problema usando el sizeof operador para establecer la constante de longitud en un valor que siempre refleje el tamaño real de la cadena. Puede encontrar nuestra confirmación de corrección de errores en GitHub.

OpenSSL y el tipo de cambio de forma

Un tipo que cambia de forma nace cuando el tamaño de un tipo varía según la definición del preprocesador. Si se supone que el tipo tiene un tamaño específico, pueden ocurrir errores de acceso a la memoria. A continuación se muestra un ejemplo sencillo:

#include <cstdint>
#include <cstring>
#include <array>

#ifdef BIGGER_INT
typedef int64_t MyInt;
#else
typedef int32_t MyInt;
#endif

MyInt GLOBAL_BUFFER[] = { 1,2,3,4,5 };

void SizeTypeExample()
{
    int localBuffer[std::size(GLOBAL_BUFFER)];

    // ERROR: AddressSanitizer: stack-buffer-overflow
    std::memcpy(localBuffer, GLOBAL_BUFFER, sizeof(GLOBAL_BUFFER));
}

Si BIGGER_INT está definido, el memcpy la operación podría desencadenar un desbordamiento del búfer de pila debido al localBuffer variable asumiendo MyInt tiene un tamaño idéntico a int . Se encontró una instancia de este error en el test_param_time_t Prueba OpenSSL:

static int test_param_time_t(int n)
{
    time_t in, out;
    unsigned char buf[MAX_LEN], cmp[sizeof(size_t)];
    const size_t len = raw_values[n].len >= sizeof(size_t)
                       ? sizeof(time_t) : raw_values[n].len;
    OSSL_PARAM param = OSSL_PARAM_time_t("a", NULL);

    memset(buf, 0, sizeof(buf));
    le_copy(buf, raw_values[n].value, sizeof(in));
    memcpy(&in, buf, sizeof(in));
    param.data = &out;
    if (!TEST_true(OSSL_PARAM_set_time_t(&param, in)))
        return 0;
    le_copy(cmp, &out, sizeof(out));
    if (!TEST_mem_eq(cmp, len, raw_values[n].value, len))
        return 0;
    in = 0;
    if (!TEST_true(OSSL_PARAM_get_time_t(&param, &in)))
        return 0;
    le_copy(cmp, &in, sizeof(in));
    if (!TEST_mem_eq(cmp, sizeof(in), raw_values[n].value, sizeof(in)))
        return 0;
    param.data = &out;
    return test_param_type_extra(&param, raw_values[n].value, sizeof(size_t));
}

Aquí, size_t se supone que es del mismo tipo que time_t , pero este no es siempre el caso dependiendo de la arquitectura para la que se compila. Al copiar out a cmp utilizando el le_copy función, el tamaño de la operación de copia es sizeof(time_t) pero el cmp el búfer se inicializó con el tamaño size_t . Al compilar las pruebas de OpenSSL con ASan y depurar con Visual Studio, el depurador falla con un error de ASan dentro de le_copy :

Nuevamente, gracias a la integración de ASan en VS, pudimos usar la ventana de la pila de llamadas para llegar al origen real del error:el test_param_time_t función:

Informamos al equipo de OpenSSL sobre este error y se comprometió una corrección en GitHub.

¡Pruebe AddressSanitizer hoy!

En este artículo, compartimos cómo pudimos usar AddressSanitizer para encontrar errores en varios proyectos de código abierto. Esperamos que esto lo motive a probar esta función en su propia base de código. ¿Ha encontrado iteradores entusiastas, tipos que cambian de forma o desacuerdos constantes de matriz/longitud en sus proyectos? Háganos saber en los comentarios a continuación, en Twitter (@VisualC) o por correo electrónico a [email protected].

Este artículo contiene fragmentos de código de las siguientes fuentes:

archivo utf8_codecvt_facet.ipp, bibliotecas Boost C++, Copyright (c) 2001 Ronald García y Andrew Lumsdaine, distribuido bajo la licencia de software Boost, versión 1.0.

SDK y bibliotecas de Azure IoT C, Copyright (c) Microsoft Corporation, distribuido bajo la licencia MIT.

Utilidad compartida de Azure C, Copyright (c) Microsoft Corporation, distribuida bajo la licencia MIT.

archivo params_api_test.c, OpenSSL, Copyright 2019-2021 The OpenSSL Project Authors, Copyright (c) 2019 Oracle y/o sus afiliados, distribuido bajo la Licencia Apache 2.0.