Errores comunes en C

Incluso en entornos profesionales/empresariales, no deja de sorprender la facilidad con la que uno topa con código en C que no es completamente correcto/seguro. No estoy hablando ya de gente que hace en treinta líneas tareas que puedes hacer en cinco, si no de usos incorrectos de funciones. Voy a repasar algunas de las cosas que he visto recientemente, para aportar mi granito de arena y tratar de que se repitan lo menos posible (que se repetirán).

Lectura de datos

Existen muchas formas en C de leer datos de la entrada estándar (fgetc, getc, getchar, fgets, fread, getw, scanf…), pero desde luego hay una que todo el mundo debería tener claro que no se puede utilizar: gets(). ¿Por qué? Pues porque no tiene ningún control sobre la cantidad de datos leídos, es decir, que es la compañera ideal de un buffer overflow. Aún así hay que tener ojo, porque con funciones como fgets(char* s, int n, FILE* stream), si la longitud del dato introducido es menor que el tamaño (n) especificado, la cadena resultado tendrá como final un salto de línea (\n). No es mayor problema si se tiene en cuenta y se elimina después.

Cuando se intenta leer un dato caracter a caracter de la entrada estándar con, por ejemplo, getc(), los programadores con menos experiencia se encuentran frente a un problema inesperado. Un código de ejemplo:

#include <stdio.h>

int main(void){
 int exit=0;
 char option;
 while(!exit){
 printf("\n¿[S]eguir o [P]arar?: ");
 option = getchar();
 if (toupper(option) == 'P')
 exit = 1;
}
return 0;
}

El problema aquí es que la primera vez que nos pregunte leerá nuestra respuesta (S/P), pero la segunda vez que entre en el bucle, leerá el salto de línea que hemos introducido tras la letra (\n). En este ejemplo el único efecto colateral es que nos mostrará dos veces la pregunta. Pero si el if estuviera planteado de otro modo, podrían suceder cosas más extrañas.

Orion:~ Adrian$ ./a.out

¿[S]eguir o [P]arar?: s

¿[S]eguir o [P]arar?:
¿[S]eguir o [P]arar?: p

Para solucionar esto, se puede tomar la decisión de limpiar el buffer de entrada tras leer la información que nos interesa (siempre es buena política mantener los buffers de entrada limpios, por lo que pueda suceder). Y aunque la gente tiene esta idea más o menos clara, el método que utilizan para ponerla en práctica es equivocado. He tenido la desgracia de ver en más de una ocasión fflush(stdin) para limpiar el buffer de entrada. Sin embargo, la función fflush(FILE* stream) sólo está definida para flujos de salida, lo cual puede provocar comportamientos inesperados. En Windows funciona adecuadamente, pero en Linux no. Una posible alternativa pasa por limpiar el buffer de entrada, leyendo hasta el salto de línea. El código de antes quedaría tal que así:

#include <stdio.h>

int main(void){
 int exit=0;
 char option;
 while(!exit){
 printf("\n¿[S]eguir o [P]arar?: ");
 option = getchar();
 while(getchar()!='\n');
 if (toupper(option) == 'P')
 exit = 1;
 }
}

void main

Este error es tan común, y los casos en los que puede detectarse suelen ser tan escasos, que hasta aparece en libros sobre C. Recordemos que la definición del estándar dice que main se define como sigue:

int main(int argc, char** argv);

Cuando a la gente le dices que no debe marcar void como valor de retorno de main, te miran raro y te dicen que funciona bien. Bueno, funciona bien, a veces. Los motivos por los que no debe usarse void como retorno son varios, a saber:

  • Porque el estándar dice que está mal (debería bastar).
  • Porque las rutinas que invoquen tu programa, pueden asumir que main retornará 0 si acaba de manera correcta, y cualquier otro valor en caso de error. Con void, el valor que retorna es indefinido, por lo que no se podrá saber con seguridad si tu programa ha fallado o no.
  • Porque el compilador/las rutinas de inicio, pueden asumir que main colocará en la pila el valor de retorno. Si no lo hace, ir a buscarlo allí puede provocar corrupción de la pila o malfuncionamiento del programa que invoca void main.

Manejo de cadenas

A estas alturas de la película, debería ser evidente que no controlar el tamaño o el tipo de los datos de entrada a tu programa puede dar lugar a numerosas vulnerabilidades. Por lo tanto, de todas las funciones de string.h, es siempre recomendable utilizar las que tienen un control de la longitud de los datos que almacenan: strncpy en lugar de strcpy, strncat en vez de strcat…

Además hay que echar un ojo a la descripción de las funciones, porque cada una tiene sus particularidades, así por ejemplo la página de manual de strncpy dice:

The strncpy() function copies at most n characters from s2 into s1.  If s2 is less than n characters long, the remainder of s1 is filled with `’ characters.  Otherwise, s1 is not terminated.

Esto quiere decir que tras usar strncpy, es necesario comprobar si el resultado concluye con el  “\x00” de rigor, y de no ser así añadirlo manualmente.

Quizá algún día todos estos errores dejen de aparecer de manera tan frecuente, y con ellos otros errores de concepto o de aritmética de punteros (también muy comunes).

Anuncios
Tagged with: ,
Publicado en Programación
2 comments on “Errores comunes en C
  1. Mensa13 dice:

    Muy interesante. También hay que recalcar que en las funciones que copian o leen por tamaño hay que verificar que el tamaño sea el adecuado. Cuando este tamaño lo introduce el usuario o está en los metadatos de un archivo (TGA por ejemplo) sigue siendo un aliado de bof.

    Un saludo y felicidades por el blog.

  2. Adrián dice:

    Tienes razón Mensa13, los formatos de archivo en los que se indica un tamaño en la cabecera dan lugar a errores en los que el programador se fía de esa información, crea un buffer de ese tamaño y luego copia todo el contenido del archivo (que puede ser mayor que lo que indica la cabecera), dando lugar a BoF.

    Además, hay que tener cuidado con el uso de sizeof sobre memoria dinámica o estructuras, ya que puede no retornar el valor esperado.

    Me alegro de que te guste el blog.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: