4

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...

i336_
  • 1,813
  • 1
  • 20
  • 41
  • It's not entirely arbitrary, its one byte less than 14GB. That said, yeah. Not sure why this would happen. – hnefatl Nov 25 '17 at 00:05
  • 1
    The second argument of `fseek()` is a `long int`, which means a specific (implemented-defined) limit on the size that can be passed. Simply passing an `unsigned long long` is not the way to get it to accept a larger value. – Peter Nov 25 '17 at 00:05
  • Switching the `unsigned long long` to `size_t` or `off_t` also produces `Invalid argument`. – i336_ Nov 25 '17 at 00:06
  • [try a `long`](https://linux.die.net/man/3/fseek). If your `long` is 4 bytes.. 15032385535 is 0x37FFFFFFF. That's too big for 4 bytes, but I bet it's picking up on the 0x7FFFFFFF, so it works. Add one to that, and and you get 0x380000000, again, too big, but I bet it's getting 0x80000000, which is what? Some negative value. – yano Nov 25 '17 at 00:12
  • 1
    See the duplicate. Use `fseeko`. – Antti Haapala -- Слава Україні Nov 25 '17 at 00:12
  • @AnttiHaapala: **Thanks, that fixed it**. Copy this to an answer and I'll accept it! – i336_ Nov 25 '17 at 00:15
  • 1
    @i336_ no, go see the duplicate question and answers if there is something to upvote. – Antti Haapala -- Слава Україні Nov 25 '17 at 00:16
  • Also regarding the other info in the dupe, I was getting -1 and errno=EOVERFLOW when I was using fopen() on the file without `-DFILE_OFFSET_BITS=64`. – i336_ Nov 25 '17 at 00:16
  • @AnttiHaapala: well, alright then! Thanks heaps again! – i336_ Nov 25 '17 at 00:16

1 Answers1

5

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.

Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254