stat 函数简介
stat 是 C 标准库中的一个函数,用于获取文件或文件系统的状态信息,它的原型通常在 <sys/stat.h> 中定义。

#include <sys/stat.h> #include <unistd.h> // 用于 S_IS macros int stat(const char *pathname, struct stat *buf);
pathname: 你想查询信息的文件或目录的路径。buf: 一个指向struct stat结构体的指针,函数会将文件信息填充到这个结构体中。
struct stat 结构体中包含了文件的元数据,
st_dev: 设备 ID。st_ino: inode 号。st_mode: 文件类型和权限。st_nlink: 硬链接数。st_uid: 用户 ID。st_gid: 组 ID。st_size: 文件大小(以字节为单位)。st_atime: 最后访问时间。st_mtime: 最后修改时间。st_ctime: 最后状态改变时间。- ...等等。
什么是 stat 溢出?
这里的“溢出”通常指的是 整数溢出,而不是缓冲区溢出,它发生在对 stat 结构体中的某个数值(尤其是 st_size)进行算术运算时,结果超出了该数据类型的表示范围,导致意外的、错误的行为。
核心问题:st_size 的类型
st_size 的类型在不同平台和不同编译器下可能有所不同,但最常见的是:
off_t: 这是一个有符号或无符号的整数类型,其大小取决于系统架构(32位或64位)。- 在 32位系统 上,
off_t通常是 32位 的,它能表示的最大值是2,147,483,647字节(约 2GB)。 - 在 64位系统 上,
off_t通常是 64位 的,它能表示的最大值是9,223,372,036,854,775,807字节(约 8 EB)。
- 在 32位系统 上,
溢出就发生在你对 st_size 进行计算时,结果超出了其类型的最大值。

溢出的场景和示例
整数溢出本身不会直接导致程序崩溃,但它会破坏后续计算的正确性,从而引发更严重的问题,如 缓冲区溢出。
场景1:分配内存时溢出
这是最经典和最危险的场景,程序获取文件大小,然后根据这个大小来分配内存。
错误的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
void read_file_and_process(const char *filename) {
struct stat st;
if (stat(filename, &st) == -1) {
perror("stat");
return;
}
// 假设我们要读取文件内容
// 错误:直接使用 st_size 作为 malloc 的大小
char *buffer = (char *)malloc(st.st_size);
if (buffer == NULL) {
perror("malloc");
return;
}
printf("Allocated %zu bytes for buffer.\n", st.st_size);
// ... 读取文件到 buffer ...
free(buffer);
}
int main() {
// 假设存在一个恰好比 2GB 小 1 字节的文件
// st_size = 2147483646 (在32位系统上是有效的)
read_file_and_process("large_file_1");
// 假设存在一个比 2GB 大 1 字节的文件
// st_size = 2147483648 (在32位系统上,off_t 是 32位 int)
// 这个值会被 "截断" 成一个负数,-2147483648
read_file_and_process("large_file_2");
return 0;
}
问题分析:

-
对于
large_file_1:st.st_size是2147483646。malloc(2147483646)在32位系统上可能会成功(如果内存足够),分配一个非常大的缓冲区。
-
对于
large_file_2:- 文件实际大小是
2147483648字节(2GB)。 - 在一个 32位系统 上,
off_t是int。2147483648这个数字超出了int的最大正值(2147483647),发生了整数上溢。 - 结果会变成一个负数,通常是
-2147483648(0x80000000)。 malloc(-2147483648)的行为是未定义的,在某些实现中,malloc会将负数转换为无符号整数,导致它尝试分配一个巨大的内存块(2GB),这几乎肯定会失败,返回NULL。- 如果代码没有检查
malloc的返回值,后续向buffer写入数据就会导致空指针解引用,程序崩溃。
- 文件实际大小是
更隐蔽的错误:
考虑下面的代码,问题更严重:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
void dangerous_copy(const char *src_file, const char *dest_file) {
struct stat st;
if (stat(src_file, &st) == -1) {
perror("stat");
return;
}
// 假设我们要读取一个块,1KB
size_t chunk_size = 1024;
// 错误:计算需要分配的块数
// st.st_size 很大,这个乘法会溢出
size_t num_chunks = st.st_size / chunk_size;
size_t buffer_size = num_chunks * chunk_size; // 这行也可能溢出
char *buffer = (char *)malloc(buffer_size);
if (buffer == NULL) {
perror("malloc");
return;
}
// ... 使用 buffer 读取和写入文件 ...
free(buffer);
}
st.st_size 是 2^32 - 1 (一个很大的数),num_chunks 也会很大。num_chunks * chunk_size 这个乘法很容易就溢出,如果 num_chunks 是 2^31,chunk_size 是 2,2^31 * 2 = 2^32,size_t 是32位的,结果会溢出回 0。
malloc(0) 可能会返回一个小的、非空的指针(虽然不推荐这样做),然后程序会向这个很小的缓冲区中写入海量数据,直接导致堆缓冲区溢出,这是极其危险的漏洞。
如何防止 stat 溢出?
预防的核心思想是:永远不要盲目信任来自外部的数值(如文件大小),并在进行任何算术运算前进行充分的检查。
使用更大的数据类型
在进行计算时,使用比原始数据类型更大的数据类型,可以防止中间结果溢出。
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <stdint.h> // for uint64_t
void safe_read_file(const char *filename) {
struct stat st;
if (stat(filename, &st) == -1) {
perror("stat");
return;
}
// 使用 uint64_t 来安全地存储和处理大小
uint64_t file_size = (uint64_t)st.st_size;
size_t buffer_size = (size_t)file_size; // 假设我们确实需要分配整个文件
// 检查转换是否安全
if (file_size != (uint64_t)buffer_size) {
fprintf(stderr, "Error: File size is too large to be allocated.\n");
return;
}
// 检查文件大小是否合理
if (buffer_size > 1024 * 1024 * 1024) { // 限制为 1GB
fprintf(stderr, "Error: File size exceeds the limit of 1GB.\n");
return;
}
char *buffer = (char *)malloc(buffer_size);
if (buffer == NULL) {
perror("malloc");
return;
}
printf("Successfully allocated %zu bytes.\n", buffer_size);
free(buffer);
}
在运算前进行边界检查
在执行乘法或加法之前,先检查操作数是否会导致溢出。
#include <limits.h> // for SIZE_MAX
// 检查 a * b 是否会溢出 size_t
int is_multiplication_safe(size_t a, size_t b) {
if (a == 0 || b == 0) return 1;
return (a <= SIZE_MAX / b);
}
void safe_copy_with_check(const char *src_file) {
struct stat st;
if (stat(src_file, &st) == -1) {
perror("stat");
return;
}
size_t chunk_size = 1024;
// 先进行除法,再检查乘法
size_t num_chunks = st.st_size / chunk_size;
if (!is_multiplication_safe(num_chunks, chunk_size)) {
fprintf(stderr, "Error: Calculation overflowed.\n");
return;
}
size_t buffer_size = num_chunks * chunk_size;
// ... 后续操作 ...
}
检查 malloc 的返回值
这是最基本但最重要的一步,永远不要假设 malloc 会成功。
char *buffer = (char *)malloc(buffer_size);
if (buffer == NULL) {
// 处理错误,而不是继续执行
perror("malloc failed");
return;
}
使用 64 位 API
如果你处理的文件可能非常大(超过 2GB),确保你的编译器和库使用的是 64 位的 off_t。
- 在 GCC/Clang 中,你可以使用
-D_FILE_OFFSET_BITS=64编译选项来强制使用 64 位的off_t和fseeko等函数。 - 这使得
st_size在32位程序中也能表示 64 位的文件大小,从根源上减少了溢出的可能性,但不能完全消除(因为后续的size_t可能还是32位的)。
| 问题 | 描述 | 后果 | 防护措施 |
|---|---|---|---|
stat 整数溢出 |
对 stat 结构体中的 st_size(类型为 off_t)进行算术运算时,结果超出其类型表示范围。 |
分配的内存远小于预期(malloc(0) 或 malloc(负数))。导致后续的缓冲区溢出(堆溢出)。 程序崩溃或被恶意利用。 |
检查返回值:始终检查 stat 和 malloc 的返回值。类型转换:使用更大的数据类型(如 uint64_t)进行中间计算。边界检查:在乘法/加法前检查是否会溢出。 设置上限:为文件大小或分配的内存设置合理的上限。 使用 64 位 API:编译时使用 -D_FILE_OFFSET_BITS=64。 |
理解 stat 溢出的原理对于编写健壮、安全的 C 语言程序至关重要,它提醒我们,即使是像获取文件大小这样看似无害的操作,也可能成为安全漏洞的根源。
