-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
libkmod: Refactor builtin modinfo parser #136
Conversation
d6112c0
to
885821b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improves overall performance as well.
Please provide some (any really) non-scientific numbers for posterity sake. Fwiw I'm already loving this from loc POV,
Implementation wise it looks spot-on. Left a few requests for inline comments and some pedantic nits 😅
libkmod/libkmod-builtin.c
Outdated
} | ||
|
||
snprintf(path, PATH_MAX, "%s/%s", dirname, MODULES_BUILTIN_MODINFO); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: we could do snprintf(path, PATH_MAX, MODULES_BUILTIN_MODINFO "/%s", dirname);
since the compiler isn't smart enough to do it :-\
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the other way around, i.e. "%s/" MODULES_BUILTIN_MODINFO
, but I can do it. Feels like we should refactor this "resolve a path" thing next, after all the snprintf handling is the other pending PR of mine.
libkmod/libkmod-builtin.c
Outdated
return count; | ||
} | ||
count++; | ||
} while (n != -1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will always be true afaict. Personally, I'd make this a for loop for (count = 0;....)
with the INTPTR_MAX
handling just outside of the loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dislike this do-while as well because it's really just a for(;;)-loop without triggering static analyzers, I guess. I didn't think about putting count into the for-loop because it implies that count is somehow relevant for further iterations. But then again, it still looks nicer than this one and for the unlikely overflow-event, it's actually relevant. Applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for(;;)-loop without triggering static analyzers
Remember seeing that in the past, didn't know it was still a thing. Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was not meant that I tried to obscure an endless loop, but trying to say through code that we loop here until no more lines are left (or calling break for optimization). So I've tried to bend my mind around "how to state that we are iterating through lines here?" but the for-loop with count is still nice, since it completely avoids this obscure endless loop and implies that we would eventually find an end if there are WAY too many strings around.
libkmod/libkmod-builtin.c
Outdated
modlen = len; | ||
} else if (modlen != len || strncmp(modname, line, len)) { | ||
if (strncmp(line, modname, modlen) || | ||
line[modlen] != '.') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you can keep this + ERR() on a single line.
The clang-format series will reformat this though, once it lands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks!
libkmod/libkmod-builtin.c
Outdated
pos = offset; | ||
if (n == -1 && !feof(info->fp)) { | ||
count = -errno; | ||
ERR(info->ctx, "get_string: %s\n", strerror(errno)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feof()
does not set errno
- is the error here correct? Is it effectively the "ENOMEM" from getdelimit()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We check that feof
is false, i.e. we did not reach the end. So yes, we effectively print the error of getdelim
, which sets errno.
if (strncmp(line, modname, modlen) || | ||
line[modlen] != '.') { | ||
if (count == 0) | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an inline comment why we continue here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. If we did not get a single string yet, it implies that no string so far matched our modname. This means that we are still looking for the correct block. If we have read at least one string and the modname mismatches again, it means that we are done.
free(iter->buf); | ||
free(iter); | ||
fclose(info->fp); | ||
free(info->buf); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: flip the order and add a note that info->buf
is allocated in get_string()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flipped, added a note to comment in struct declaration.
libkmod/libkmod-builtin.c
Outdated
ERR(iter->ctx, "kmod_builtin_iter_get_modname: unexpected string without modname prefix\n"); | ||
goto fail; | ||
if (SIZE_MAX / sizeof(char *) - 1 < count || | ||
SIZE_MAX - buf->used < sizeof(char *) * (count + 1)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's throw in a const fooo = sizeof(char *) * (count + 1)
and a small comment above this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meant a comment just above the if check if vecsz has overflown or it won't cause an overflow in realloc below
.
libkmod/libkmod-builtin.c
Outdated
} | ||
|
||
len = dot - line; | ||
vector = realloc(buf->bytes, buf->used + sizeof(char *) * (count + 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been somewhat tempted to change the strbuf helpers to:
- allocate +1 for the null byte + always set it
- steal -> disown, which returns a pointer and effectively invalidates the strbuf
Those two will a) remove the explicit NULL handling further up and b) simplify this function.
What do you think about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably won't hurt but this file will have a "special" strbuf handling anyway, because it puts NUL bytes into it on purpose. If at all, I would expect the most negative feedback on this PR for doing exactly that. ;)
libkmod/libkmod-builtin.c
Outdated
fail: | ||
kmod_builtin_iter_free(iter); | ||
kmod_builtin_info_release(&info); | ||
strbuf_release(&buf); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: let's teardown in reverse order of things being setup. strbuf first, then modinfo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this one applied as well. :)
4c0aed5
to
a90ea5a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would replace the Improves overall performance as well.
line in the commit message with:
This converts the 1000+ pread64 syscalls, to a dozen read(s). Effectively
reducing the syscalls we make by ~90%
$ strace before/modinfo drm 2>&1 >/dev/null | wc -l
1480
$ strace -e read before/modinfo drm 2>&1 >/dev/null | wc -l
23
$ strace -e pread64 before/modinfo drm 2>&1 >/dev/null | wc -l
1375
$ strace after/modinfo drm 2>&1 >/dev/null | wc -l
119
$ strace -e read after/modinfo drm 2>&1 >/dev/null | wc -l
34
$ strace -e pread64 after/modinfo drm 2>&1 >/dev/null | wc -l
3
Awesome work 👍
I have added a comment and adjusted the commit message. I have mentioned the system call reduction but also pointed out that we most likely do allocate memory more often, even though basically the same amount. This means that I won't argue that every program which uses less system calls is automatically faster, but that the reduction of system calls definitely outweights the added memory handling. Also, this is not the main motivation, but a very nice additional benefit. |
In case you're wondering why strace numbers. I've seen it used a handful of times in As you mentioned memory curiosity got the best of me, so here are some numbers for that. command: Valgrind/massif peak memory used command:
after:
So overall, ignoring the syscall side, instead of repeatedly allocating 256-512 byte chunks it allocates one 4K and keeping them for longer. Resulting in 2-3 times shorter runtime. But as you said - performance side is a nice benefit. Security, correctness and code clarity are the main goals. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some comments. Overall LGTM. Nice improvement.
} | ||
|
||
static bool kmod_builtin_iter_get_modname(struct kmod_builtin_iter *iter, | ||
char modname[static PATH_MAX]) | ||
static char **strbuf_to_vector(struct strbuf *buf, size_t count) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this reminds me of a similar functions systemd has with the name "strv". I think we may eventually migrate this function to strbuf.h, but I'm fine with keep it where it is for now.
} | ||
|
||
len = dot - line; | ||
vector = realloc(buf->bytes, vecsz + buf->used); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of forcing a realloc, should we rather grow strbuf? It may be that we still have room and don't actually need the extra alloc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used the same realloc
which would occur in strbuf_steal
, i.e. even if we have enough memory, don't waste additional unneeded one.
if (count == 0) | ||
*modinfo = NULL; | ||
else if (count > 0) { | ||
*modinfo = strbuf_to_vector(&buf, (size_t)count); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider strbuf_to_vector() as a function stealing/disowning the buffer. As such it's odd to still call strbuf_release() below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If everything works as expected, then yes: strbuf_to_vector
would steal/disown the buffer. But if realloc
fails, then the strbuf
is considered "left alone for caller to clean up who passed reference to us."
Going the Rust way, we could argue that we pass ownership to strbuf_to_vector
. But on the other hand, from a C perspective, we construct the strbuf
in kmod_builtin_get_modinfo
so it should be its responsibility to clean up any resources associated with it when leaving.
So I went with the second (C) approach. Let me know what you think, I don't have a hard opinion either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the cleanup into error handling. I think it actually looks nicer this way, like "I expect this function to convert the buffer and only if it fails, I release the whole thing on my own."
The modinfo command can show information about builtin modules. Make sure that it functions correctly. Signed-off-by: Tobias Stoeckmann <[email protected]>
Remove arbitrary limits due to file sizes (INTPR_MAX check). Reduce amount of system calls by up to 90 % utilizing stream functions. Also make sure that no TOCTOU could ever happen by not iterating through the file twice: First to figure out amount of strings, then parsing them. If the file changes in between, this can lead to memory corruption. Even though more memory allocations might occur due to strbuf usage, performance generally increased by heavy reduction of system calls. Signed-off-by: Tobias Stoeckmann <[email protected]>
The modinfo command can show information about builtin modules. Make sure that it functions correctly. Signed-off-by: Tobias Stoeckmann <[email protected]> Reviewed-by: Emil Velikov <[email protected]> Link: #136 Signed-off-by: Lucas De Marchi <[email protected]>
Remove arbitrary limits due to file sizes (INTPR_MAX check). Reduce amount of system calls by up to 90 % utilizing stream functions. Also make sure that no TOCTOU could ever happen by not iterating through the file twice: First to figure out amount of strings, then parsing them. If the file changes in between, this can lead to memory corruption. Even though more memory allocations might occur due to strbuf usage, performance generally increased by heavy reduction of system calls. Signed-off-by: Tobias Stoeckmann <[email protected]> Reviewed-by: Emil Velikov <[email protected]> Link: #136 Signed-off-by: Lucas De Marchi <[email protected]>
Applied, thanks. |
I have refactored the builtin modinfo parser to utilize stream functions, i.e. getdelim. This should make it much easier to see what's going on and heavily reduces the amount of system calls. In my tests, it's thrice as fast (you can check this best with one of the last entries in your builtin.modinfo file). Also you might want to use strace to see how many system calls are currently performed.
This removes the arbitrary INTPTR_MAX limit because the size of the file is of no interest, because the parser does not jump back and forth anymore.
Also, the explicit memory management is greatly reduced by using getdelim and kmod's strbuf.
Since the file is parsed from beginning to end, there is no TOCTOU left either. Right now, the file is parsed to retrieve the amount of strings we are interested in, then it's parsed again to retrieve the strings. If the file changes in between, this can lead to memory corruption. I would argue that this does not happen for system files, but with this
-b
option, we should be a bit more careful.What this parser does:
Last but not least: I've added a test. Please let me know if there is some kind of "standard module set" you use, because right now I've taken a small sample of my live system.