This is the mail archive of the
libc-alpha@sourceware.org
mailing list for the glibc project.
Re: [PATCH v2 1/5] mips: Do not malloc on getdents64 fallback
- From: Florian Weimer <fweimer at redhat dot com>
- To: Adhemerval Zanella <adhemerval dot zanella at linaro dot org>
- Cc: libc-alpha at sourceware dot org
- Date: Mon, 02 Sep 2019 14:59:43 +0200
- Subject: Re: [PATCH v2 1/5] mips: Do not malloc on getdents64 fallback
- References: <20190731183136.21545-1-adhemerval.zanella@linaro.org> <87k1av6kuh.fsf@oldenburg2.str.redhat.com> <49dc9440-d9a5-112f-9ec6-b62f4c0bcdf2@linaro.org>
* Adhemerval Zanella:
>> The choice of size needs a comment. I think the largest possible
>> practical length of the d_name member are 255 Unicode characters in the
>> BMP, in UTF-8 encoding, so d_name is 766 bytes long, plus 10 bytes from
>> the header, for 776 bytes total. (NAME_MAX is not a constant on Linux
>> in reality.)
>
> I picked the buffer as an arbitrary value, what about:
>
> /* The largest possible practical length of the d_name member are 255
> Unicode characters in UTF-8 encoding, so d_name is 766 bytes long, plus
> 18 (mips64) / 10 (mips64n32) bytes from header, for total of 784 (mips64)
> / 776 (mips64n32) bytes total. Ensure that the minimum size hold at
> least one entry. */
> enum { KBUF_SIZE = 1024 };
“holds”
Looks good.
>>> struct kernel_dirent
>>> + {
>>> + unsigned long d_ino;
>>> + unsigned long d_off;
>>> + unsigned short int d_reclen;
>>> + char d_name[1];
>>> + } kbuf[KBUF_SIZE / sizeof (struct kernel_dirent)];
>>> + size_t kbuf_size = nbytes < KBUF_SIZE ? nbytes : KBUF_SIZE;
>>
>> I would define kbuf as a char array, and perhaps leave out the d_name
>> member in struct kernel_dirent. You can copy out the struct
>> kernel_dirent using memcpy, which GCC should optimize away.
>
> I defined the buffer as 'struct kernel_dirent' to make it easier to align
> for the required fields. It allows simplify the access on the loop to
> avoid memcpy calls.
But the code is invalid C as a result of this. We do not compile glibc
with -fno-strict-aliasing, after all.
>>> struct dirent64 *dp = (struct dirent64 *) buf;
>>> +
>>> + size_t nb = 0;
>>> + off64_t last_offset = -1;
>>> +
>>> + ssize_t r;
>>> + while ((r = INLINE_SYSCALL_CALL (getdents, fd, kbuf, kbuf_size)) > 0)
>>> {
>>
>> Sorry, I don't see how the outer loop is exited. I think we should
>> remove it because it does not seem necessary.
>
> We still need to handle the cases where NBYTES are larger than the temporary
> buffer, because it might require multiple getdents calls.
Why? The application (or readdir) will call us again to get more entries.
>> Is the length really correct, though? I'd expect it to grow by the
>> additional size of the d_ino and d_off members. I think it would be
>> best recompute it from scratch, using the actual length of d_name.
>
> It it because you are referencing to an older patch version, checking on
> mips64-n32 I adjusted to:
>
> const size_t size_diff = (offsetof (struct dirent64, d_name)
> - offsetof (struct kernel_dirent, d_name));
> [...]
> size_t new_reclen = ALIGN_UP (kdp->d_reclen + size_diff,
> _Alignof (struct dirent64));
> [...]
Okay, this needs a comment that this is a conservative approximation
(some of size_diff might fit into the existing padding for alignment).
>>> + if (nb + new_reclen > nbytes)
>>> {
>>> + /* The new entry will overflow the input buffer, rewind to
>>> + last obtained entry and return. */
>>> + __lseek64 (fd, last_offset, SEEK_SET);
>>
>> I don't think last_offset is guaranteed to have been set with a proper
>> offset at this point. Given that d_name is essentially of unbounded
>> length, even expanding the first entry can cause failure.
>>
>> Maybe it's possible to avoid this corner case by limiting the amount of
>> data being read so that we know that the application-supplied buffer is
>> always large enough for any possible expansion. I think the worse-case
>> growth is for lengths 5 to 8, from 20 bytes to 32 bytes. So perhaps we
>> should divide the buffer size by 1.6 and use that?
>
> For this case I really think we just need to return an error to user:
>
> if (nb + new_reclen > nbytes)
> {
> /* Entry is too large for the static buffer. */
Fixed-size buffer, it's not static. 8-)
> if (last_offset == -1)
> {
> __set_errno (EINVAL);
> return -1;
> }
> /* The new entry will overflow the input buffer, rewind to
> last obtained entry and return. */
> __lseek64 (fd, last_offset, SEEK_SET);
> goto out;
> }
>
> Again I see this fallback code as a best-effort since we are emulating the
> syscall with additional restraints. Most of time glibc tries to play smart
> emulating a syscall ended in a lot of headaches...
I don't disagree.
Which error code does the kernel return if no entry can be read at all?
We should mirror that.
>>> + goto out;
>>> }
>>> + nb += new_reclen;
>>>
>>> + dp->d_ino = kdp->d_ino;
>>> + dp->d_off = last_offset = kdp->d_off;
>>> + dp->d_reclen = new_reclen;
>>> + dp->d_type = *((char *) kdp + kdp->d_reclen - 1);
>>
>> I think instead of reading through kdp, you should use char *s and
>> memcpy, to avoid the aliasing violation, as discussed above. Likewise
>> for writing to dp.
>
> I think if we proper setting the buffer alignment there is no need to do it.
> Also the problem of using memcpy here is for mips64n32 the size is *not*
> equal for dp and kdp, each would require an extra step as:
>
> {
> typeof (kdp->d_ino) kino;
> memcpy (&kino, &kdp_d->ino, sizeof (kino));
> typeof (dp->d_ino) dino = kino;
> memcpy (&dp->d_ino, &kino, sizeof (dino));
> }
I think that's just the price of writing correct C. It's also what the
kernel does.
I don't even think there's a requirement that the byte buffer passed to
getdents64 has any kind of alignment.
Thanks,
Florian