Restic slice bounds out of range?

Could you modify

https://github.com/restic/restic/blob/7e72d638dfc4a159a8a454f16b1819d18c6c153b/chunker/chunker.go#L269

and replace line 269

if n < 0 {

with

if n > cap(buf) {

and test again?

I think that the negative length was a red herring: 0xfffffff3 would be uint32(-13). However, the read length and the slice offset are stored in a 64-bit integer on amd64. Thus it looks like the read syscall reported a read length of 0xfffffff3 (probably after messing up a -13).

Low-level details of what happens to the read length

I’ve followed the internals of io.ReadFull in order to find an explanation for the chunker crash: The file from which the chunker reads, was opened via arch.FS.OpenFile, so we’ve got a normal go file object. The loop in ReadFull -> ReadAtLeast can only leave with a negative n if the call to Read did so too. To return a negative n the loop in ReadAtLeast must also terminate early, which is only possible if Read returns an error. The actual read happens in go/internal/poll/fd_unix.go. That FD.Read can either return a non-zero length or an error. The returned length is exactly the length returned by the read syscall. To get the negative length and the early termination would require two calls of the read syscall which must return a negative length first, followed by an error on the second invocation -> that is the only way to get n < 0 is the have err != nil.

There’s another hurdle to take: The only way to prevent the chunker code from returning with an error, while at the same time having ReadFull return an error is that ReadFull returns ErrUnexpectedEOF. The complication there is that the only way to get that error, is to fulfill n > 0 && err == EOF in ReadAtLeast (I’ve grepped through the go standard library). Remember that earlier on we’ve established that n < 0 requires err != nil. Even if err == EOF, then it could not be replaced with ErrUnexpectedEOF as that only happens for n > 0 which conflicts with n < 0. So in summary it is probably impossible to get a negative length.

An alternative would be that the read syscall already returns uint32(-13) = 0xfffffff3. In that case the length would simply be backpropagated to the chunker. On 64-bit platforms Go actually uses 64-bit indices for slices. 0xfffffff3 would be -13 when interpreted as a 32-bit number, but int and uint are 64-bit for amd64 so that doesn’t happen. In case n were negative (e.g. -13), it’d have a value of 0xfffffffffffffff3 or 18446744073709551603. The latter was obviously not printed when the chunker crashed.

So to sum up: There’s only one thing that could have happened and that is that the read syscall reported a read length of 0xfffffff3 (probably after messing up a -13).

2 Likes