kern/117010: [linux] linux_getdents() get somethinng like buffer overflow

Chagin Dmitry chagin.dmitry at gmail.com
Mon Jul 28 10:23:22 UTC 2008


On Mon, 28 Jul 2008, Alexander Leidinger wrote:

> Quoting "Chagin Dmitry" <chagin.dmitry at gmail.com> (from Sun, 27 Jul 2008 
> 21:05:03 +0400 (MSD)):
>
>> On Sat, 26 Jul 2008, Alexander Leidinger wrote:
>> 
>>> Quoting Chagin Dmitry <chagin.dmitry at gmail.com> (Fri, 25 Jul 2008 07:00:15 
>>> GMT):
>>> 
>>>> The following reply was made to PR kern/117010; it has been noted by 
>>>> GNATS.
>>>> 
>>>> From: Chagin Dmitry <chagin.dmitry at gmail.com>
>>>> To: bug-followup at freebsd.org, samflanker at gmail.com
>>>> Cc:
>>>> Subject: Re: kern/117010: [linux] linux_getdents() get somethinng like 
>>>> buffer
>>>> overflow
>>>> Date: Fri, 25 Jul 2008 10:22:46 +0400 (MSD)
>>>> 
>>>> Please, try a patch below:
>>>> 
>>>> diff --git a/src/sys/compat/linux/linux_file.c 
>>>> b/src/sys/compat/linux/linux_file
>>>> index 303bc3f..d88f95f 100644
>>>> --- a/src/sys/compat/linux/linux_file.c
>>>> +++ b/src/sys/compat/linux/linux_file.c
>>>> @@ -303,8 +303,8 @@ struct l_dirent64 {
>>>> 	char            d_name[LINUX_NAME_MAX + 1];
>>>>  };
>>>> 
>>>> -#define LINUX_RECLEN(de,namlen) \
>>>> -    ALIGN((((char *)&(de)->d_name - (char *)de) + (namlen) + 1))
>>>> +#define LINUX_RECLEN(de,namlen,trail) \
>>>> +    ALIGN((((char *)&(de)->d_name - (char *)de) + (namlen) + trail))
>>> 
>>> 
>>> The start of de->d_name minus the start of de should be the same as the 
>>> offset of d_name in de, so I would expect that this is expressed with the 
>>> offsetof maro instead of handmade.
>>> 
>>> So the result of this is the offset plus a len + something.
>>> 
>> 
>> well... agree.
>>
>>>>  #define        LINUX_DIRBLKSIZ         512
>>>> 
>>>> @@ -436,8 +436,8 @@ again:
>>>> 		}
>>> 
>>> I try to understand the code before this. There's "if (reclen & 3)" error 
>>> out. Does it mean it has to be a multiple of 4? If yes it should be 
>>> changed to some modulo calculation to make it obvious (the compiler should 
>>> be able to do such micro optimisations, but I doubt the error case needs 
>>> to be micro optimized).
>>> 
>> 
>> this code looks as a workaround... exists since v1.1, I don't understand 
>> what is it.
>
> When you look at the FreeBSD manpage of dirent, it's not that surprising:
> ---snip---
>   /*
>     * The dirent structure defines the format of directory entries returned 
> by
>     * the getdirentries(2) system call.
>     *
>     * A directory entry has a struct dirent at the front of it, containing 
> its
>     * inode number, the length of the entry, and the length of the name
>     * contained in the entry.  These are followed by the name padded to a 4
>     * byte boundary with null bytes.  All names are guaranteed null 
> terminated.
>     * The maximum length of a name in a directory is MAXNAMLEN.
>     */
> ---snip---
>

ups... tnhx!, but what for we do here this check? IMO, it is excessive.


>>>> 		linuxreclen = (is64bit)
>>>> -                   ? LINUX_RECLEN(&linux_dirent64, bdp->d_namlen)
>>>> -                   : LINUX_RECLEN(&linux_dirent, bdp->d_namlen);
>>>> +                   ? LINUX_RECLEN(&linux_dirent64, bdp->d_namlen, 1)
>>>> +                   : LINUX_RECLEN(&linux_dirent, bdp->d_namlen, 2);
>>> 
>>> Translated: The length of the linux record is the offset plus the FreeBSD 
>>> size plus something. Doesn't make sense to me. sizeof(linux_dirent) sound 
>>> more suitable for this variable name. From the code it can not be the 
>>> length of the linux record, but the size of a linux dirent struct which 
>>> would be required to put all info inside (+ some more space... very 
>>> suspicious).
>>>
>>>> 		if (reclen > len || resid < linuxreclen) {
>>>> 			outp++;
>>> 
>>> First part: if the length of the current record is larger than the 
>>> remaining free space (if it does not fit) go out of the loop... ok.
>>> 
>> 
>> no, here reclen is the length of FreeBSD record and len is the remaining 
>> space in FreeBSD records buffer.
>
> len is the memory region where you construct the linux response, isn't it?
>

you are mistaken here, len points to FreeBSD buffer filled by 
vop_readdir

>>> Second part: if the length (in bytes?) is smaller than the theoretical 
>>> size of the linux struct, go out of the loop. Ouch. Please tell me this is 
>>> wrong (I didn't had breakfast yet, I really hope I misanalysed this 
>>> because of this fact).
>>> 
>> 
>> no, resid is the free space in Linux records buffer, linuxreclen is the 
>> length of the Linux record.
>
> Seems there was a part missing above... "lenght in bytes" = remaining length 
> in bytes. The important part is the use of the macro. The linux reclen macro 
> calculates some linux stuff + some freebsd stuff without any limit checks. 
> What happens if the size of the name member of the struct changes in 
> FreeBSD!?! Even if they _may_ be the same currently, this is dangerous.
>

agree, we should do check before calculating linuxreclen, like:

 	if (bdp->d_namlen > LINUX_NAME_MAX) {
 		error = ENAMETOOLONG;
 		goto out;
 	}


>>> I smell buffer mismanagement because of the strange 1 or 2 being added to 
>>> the size, and I smell some convoluted logic there. Instead of trying to 
>>> poke the thing until it works, I suggest to step back and have a look at 
>>> the big picture if the entire part of the function can be improved.
>> 
>> This is the Linux, as Roman says - linux is a really strange :)
>> See linux source fs/readdir.c implementation of filldir functions
>
> When I look at this, I even see more dragons in our code. In linux (2.6 
> kernel) linux_dirent is playing the ARRAY[1] + size trick, in FreeBSD it 
> isn't. Some things are handled like in linux, but because the trick is not 
> done in FreeBSD, those can not be handled like in linux.
>
> When I look at the patch you proposed, I also see a pitfall. In linux in the 
> 64bit case, it's "int reclen = ALIGN(NAME_OFFSET(dirent) + namlen + 1, 
> sizeof(u64));", in the 32bit case it's "int reclen = 
> ALIGN(NAME_OFFSET(dirent) + namlen + 2, sizeof(long));". This means the 
> length is aligned to 64bit for the 64bit case and 32bit in the 32bit case.
>

ah.., getdents64 on all $arch's uses 64bit alignment, we should follow 
this behaviour.

>>>> it solves getdents() problem (at least at x86_64 emulation with
>>>> linux_base-f8)
>>>> 
>>>> ps, be not bared, linux really has such features...
>>> 
>>> What I would expect is to compare the strlen of the FreeBSD record with 
>>> the size of the place in linux_dirent. If the FreeBSD record does not fit, 
>>> fail (ENAMETOOLONG?). Compare the remaining space with the size of 
>>> linux_dirent, if it is '<=' fill in the data into the fixed size struct.
>>> 
>> 
>> It's done in the 'Second part'
>
> There should be a check before data is copied to the place. As the size is 
> already available, it doesn't cost much.
>
> I try to get some time this week to produce a patch which addresses my 
> concerns.
>

ok, I shall test on amd64 :)

thnx!

-- 
Have fun!
chd


More information about the freebsd-emulation mailing list