Seeking in very large files on 32-bit systems

Viewed 834

I'm currently having a small fight involving seek with a C program running on a 32-bit (x86) box.

Specifically, I don't seem to be able to seek beyond a seemingly rather arbitrary file offset.

If I do:

unsigned long long pos = 15032385535LLU;
int r = fseek(fd, pos, SEEK_SET);

then I'll get

fstat64(3, {st_mode=S_IFREG|0644, st_size=1000000000000, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77c3000
_llseek(3, 2147479552, [2147479552], SEEK_SET) = 0
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4095) = 4095

TL;DR it works.

However, if I increment pos by just 1...

unsigned long long pos = 15032385536LLU;
int r = fseek(fd, pos, SEEK_SET);

...then everything falls apart spectacularly:

fstat64(3, {st_mode=S_IFREG|0644, st_size=1000000000000, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb771e000
_llseek(3, 18446744071562067968, 0xbfd0f5f8, SEEK_SET) = -1 EINVAL (Invalid argument)

I'm completely lost as to why. What am I doing wrong?

The only significant tidbits I can come up with is that 15032385535 is 37FFFFFFF which seems interesting-ish, along with the fact that the number seems to be related to time wraparound.

The program in question is being compiled with -D_FILE_OFFSET_BITS=64, which proved helpful with actually getting the large files I'm working on open in the first place, but doesn't seem to be making a useful difference here. I stumbled on -DLARGEFILES -D_LARGEFILE_SOURCE and tried adding that but that doesn't seem to have had any discernible effect.

 

For context (because trivia is fun): I created a large sparse file with truncate to reproduce the issue on a separate 32-bit machine (perfectly) with; and the program in question is a small webserver - I'm trying to copy some data off a spare computer, and I've discovered that it's surprisingly hard to find a compact web server that can handle Range: requests and simultaneous downloads. nginx is throwing Perl errors I'm not going to investigate (Slackware packaging issues - nope), Python's SimpleHTTPServer is uselessly simple, and thttpd dissolved into a puddle of mmap errors. Fun day...

1 Answers

It's easier to follow if you write the numbers in hexadecimal (or binary).

15032385535 = 0x37fffffff
15032385536 = 0x380000000

On your system, unsigned long is a 32-bit type, and unsigned long long is a 64-bit type.

The second argument of fseek has the type long. When you write

unsigned long long pos = …;
int r = fseek(fd, pos, SEEK_SET);

the value of the second argument is converted to the requisite type. Converting an unsigned integer type to a smaller integer type — here unsigned long long to unsigned long — has undefined behavior when the value overflows the smaller type, but on most platforms, including yours, it truncates the most significant bits of the value. It's equivalent to

fseek(fd, pos & 0xffffffff, SEEK_SET)

since 0xffffffff is the maximum value of unsigned long. When pos = 0x37fffffff, the resulting value is 0x7fffffff = 2147483647. Note that the call to fseek actually doesn't work! It doesn't seek to the position you thought you requested.

When pos = 0x380000000, another phenomenon happens: the most significant bit in the truncated value is set, and it's used as a sign bit, since your machine (like most machines) uses two's complement representation for negative numbers. Thus the resulting value is negative; it's -0x80000000 = -2147483648. This negative value is then passed to the system call _llseek, which takes a 64-bit value (even on 32-bit systems). The value it receives is -0x80000000, which strace shows as its two's complement unsigned counterpart as a 64-bit number — 18446744071562067968 = 0xffffffff80000000.

With standard C, you can't seek in a file using positions that go beyond the range of long. If you're willing to rely on POSIX functions, there's fseeko, which is simular to fseek but takes a second argument of type off_t instead of long. Under -D_FILE_OFFSET_BITS=64, off_t is a 64-bit type.

Related