読者です 読者をやめる 読者になる 読者になる

整数オーバーフローと符号エラー

整数に関係するバグについてのメモ。

環境

Ubuntu 14.04.4 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.4 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4

整数オーバーフロー(Integer Overflow)

整数オーバーフローは、演算の結果、整数が表現できる最大値を越えてしまうことにより発生するバグである。

/* integer_overflow.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char buf[40];
    int n;

    scanf("%d", &n);
    printf("n+1 == %d\n", n+1);
    if (n+1 > sizeof(buf)) {
        puts("too long!");
        exit(1);
    }

    read(0, buf, n);

    return 0;
}

上のコードではバッファサイズのチェックが行われているが、次のように4294967295(=232-1)を与えたときチェックを通過してしまう。

$ gcc integer_overflow.c

$ ./a.out
10
n+1 == 11
AAAA

$ ./a.out
80
n+1 == 81
too long!

$ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> (1<<32)-1
4294967295
>>>

$ ./a.out
4294967295
n+1 == 0
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)

これを防ぐには、入力に依存しない箇所で計算を行うようにすればよい。

--- integer_overflow.c  2016-09-27 15:23:56.920312000 +0900
+++ patched/integer_overflow.c  2016-09-27 15:24:30.728312000 +0900
@@ -9,7 +9,7 @@

     scanf("%d", &n);
     printf("n+1 == %d\n", n+1);
-    if (n+1 > sizeof(buf)) {
+    if (n > sizeof(buf)-1) {
         puts("too long!");
         exit(1);
     }

符号エラー(Signedness Error)

符号エラーは、符号付き整数が符号なし整数として扱われる、あるいはその逆が起こることにより発生するバグである。

/* signedness_error.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char buf[40];
    int n;

    scanf("%d", &n);
    if (n > 40) {
        puts("too long!");
        exit(1);
    }

    read(0, buf, n);

    return 0;
}

上のコードではバッファサイズのチェックが行われているが、次のように-1を与えたときチェックを通過してしまう。

$ gcc signedness_error.c

$ ./a.out
10
AAAA

$ ./a.out
80
too long!

$ ./a.out
-1
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)

$ strace ./a.out
execve("./a.out", ["./a.out"], [/* 21 vars */]) = 0
brk(0)                                  = 0x1178000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5d72941000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=37010, ...}) = 0
mmap(NULL, 37010, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5d72937000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P \2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0
mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5d7235c000
mprotect(0x7f5d72516000, 2097152, PROT_NONE) = 0
mmap(0x7f5d72716000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f5d72716000
mmap(0x7f5d7271c000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f5d7271c000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5d72936000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5d72934000
arch_prctl(ARCH_SET_FS, 0x7f5d72934740) = 0
mprotect(0x7f5d72716000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7f5d72943000, 4096, PROT_READ) = 0
munmap(0x7f5d72937000, 37010)           = 0
fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5d72940000
read(0, -1
"-1\n", 1024)                   = 3
read(0, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4294967295) = 49
open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = 3
writev(3, [{"*** ", 4}, {"stack smashing detected", 23}, {" ***: ", 6}, {"./a.out", 7}, {" terminated\n", 12}], 5*** stack smashing detected ***: ./a.out terminated
) = 52
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5d7293f000
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
gettid()                                = 4558
tgkill(4558, 4558, SIGABRT)             = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=4558, si_uid=1000} ---
+++ killed by SIGABRT (core dumped) +++
Aborted (core dumped)

straceコマンドの結果から、チェックを通過した-1がread関数では符号なし整数4294967295として扱われていることがわかる。

これを防ぐには、最初から符号なし整数として宣言すればよい。

--- signedness_error.c  2016-09-27 15:50:22.524312000 +0900
+++ patched/signedness_error.c  2016-09-27 15:50:51.888312000 +0900
@@ -5,7 +5,7 @@
 int main()
 {
     char buf[40];
-    int n;
+    unsigned int n;

     scanf("%d", &n);
     if (n > 40) {

関連リンク