lightningd: don't crash when truncating large log messages#9331
lightningd: don't crash when truncating large log messages#9331whitslack wants to merge 5 commits into
Conversation
It's not okay to call free() on the pointer to a truncated log message that was allocated by tal_fmt() in cap_header(). Let's call tal_free() instead, and rather than calling vasprintf() to malloc the log message in the first place, let's call tal_vfmt(). Also, since we're now always using a tallocated string for the log message, let's have cap_header() take ownership of it and either free it if it truncated the message (and is returning a different pointer to the truncated message) or else return the taken original pointer without freeing it. Also, log_io()'s str and data parameters are marked TAKES, but the function was not actually taking them, so fix that up too. Also, don't call strlen() on a string returned by tal_fmt(). The returned pointer is guaranteed to have tal_count() == strlen() + 1, so there's no sense in scanning through the string to find its length. Changelog-None
| @@ -685,8 +684,8 @@ void logv(struct logger *log, enum log_level level, | |||
| l.time, | |||
| l.prefix->prefix, | |||
| logmsg); | |||
| free(logmsg); | |||
There was a problem hiding this comment.
This is where the crash was occurring. The crash happens if cap_header() tallocates and returns a new string to hold the truncated message. Then we'll be trying to free() a tal pointer. Boom.
| if (hdr->msglen > max) { | ||
| msg = tal_fmt(ctx, "[TRUNCATED message from %zu bytes]: %.*s", | ||
| hdr->msglen, (int)max, msg); | ||
| hdr->msglen = strlen(msg); | ||
| hdr->msglen = tal_count(msg) - 1; |
There was a problem hiding this comment.
Why not use strlen(msg)?
There was a problem hiding this comment.
strlen has to scan over the entire string, which we've already determined is "giant." tal_count is just an integer load at an offset from the given pointer.
| bool str_taken = is_taken(str); | ||
| str = cap_header(tmpctx, &l, str/*TAKES*/); | ||
| add_entry(log->log_book, &l, str, data); | ||
| if (str_taken) | ||
| tal_free(str); | ||
| if (taken(data)) | ||
| tal_free(data); |
There was a problem hiding this comment.
This is very hard to read and understand.
Since str is a TAKES argument it will be consumed already by cap_header if marked as take.
No further action is needed. After the line str = cap_header(...); you only need to worry about the
new pointer, that one is allocated with tmpctx therefore this is just fine.
| bool str_taken = is_taken(str); | |
| str = cap_header(tmpctx, &l, str/*TAKES*/); | |
| add_entry(log->log_book, &l, str, data); | |
| if (str_taken) | |
| tal_free(str); | |
| if (taken(data)) | |
| tal_free(data); | |
| str = cap_header(tmpctx, &l, str/*TAKES*/); | |
| add_entry(log->log_book, &l, str, data); | |
| if (taken(data)) | |
| tal_free(data); |
There was a problem hiding this comment.
In your suggested change, no one frees the str returned by cap_header. Memory leak.
There was a problem hiding this comment.
I suppose cap_header could return take(str) in the case that it took ownership of the passed str and does not truncate it, but I didn't know whether take is intended to be used on return values or only on arguments, and I think the assumption already should be that the caller of an allocating function takes ownership of the returned allocation (if one occurred).
We have the following four cases:
strpassed tolog_iowas markedtake, andcap_headerdoes not truncate.cap_headermust not freestrsince it's returning it directly. (cap_headertakes ownership ofstrand then hands ownership back to the caller.)log_ioownsstrreturned fromcap_headerand must free it.
strpassed tolog_iowas not markedtake, andcap_headerdoes not truncate.cap_headermust not freestrsince the caller did not cede ownership.log_iodoes not ownstrreturned fromcap_headerand must not free it.
strpassed tolog_iowas markedtake, andcap_headerdoes truncate.cap_headermust freestrsince it has taken ownership of it and is returning a different pointer.log_ioownsstrreturned fromcap_headerand must free it.
strpassed tolog_iowas not markedtake, andcap_headerdoes truncate.cap_headermust not freestrsince the caller did not cede ownership.log_ioownsstrreturned fromcap_headerand must free it. (My code leaks in this case, although it is known that the leaked pointed was allocated fromtmpctx.)
I'll make the obligatory snide comment again that all of this would be so much simpler if you folks would just use C++ like sane people.
| if (taken(origmsg) && msg != origmsg) | ||
| tal_free(origmsg); |
There was a problem hiding this comment.
When the caller marks msg as take you don't consume the pointer if msg==origmsg.
This breaks the caller assumption that you will consume it and return a pointer allocated
in the given ctx.
I think this whole function should be left the way it is now.
There is a bunch of sloopy pointer reassignment without freeing but
it is because the caller knows that everything here is under the tmpctx.
But if we want to be pedantic I think the function should follow the take and tal allocation rules
(as for my understanding).
/* We truncate genuinely giant messages */
-static char *cap_header(const tal_t *ctx, struct log_hdr *hdr, const char *msg)
+static char *cap_header(const tal_t *ctx, struct log_hdr *hdr, const char *msg TAKES)
{
const size_t max = sizeof(((struct log_book *)0)->ringbuf) / 64;
+ msg = tal_strdup(ctx, msg); // already consumes msg
if (hdr->msglen > max) {
msg = tal_fmt(ctx, "[TRUNCATED message from %zu bytes]: %.*s",
hdr->msglen, (int)max, msg);
There was a problem hiding this comment.
NAK. There is no sense in duplicating a string and then immediately freeing the original. You're just burning CPU cycles for no reason.
There was a problem hiding this comment.
Incidentally, you can re-parent a tal pointer, but I think 30f80d2 has a cleaner answer.
|
Also this is a duplicate of PR #9215. |
Indeed. I didn't see your PR. 😞 |
No problem. I'll close mine. I am glad that you took the opportunity to fix things around. |
This relies on it being okay to return a pointer that is take().
|
It can alternatively be done as in f3a9f38, which results in more conventional EDIT: If |
|
BTW. I am still wondering why Rusty used |
Changelog-None Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
We were using vasprintf to generate the log line and then using free to deallocate the string. However, in the case of a very long log line a new pointer was created with tal_fmt and then tried to use free on it. This was introduced in commit: 4d8f923 ``` free(): invalid pointer lightningd: FATAL SIGNAL 6 (version v26.06-21-gebc5dc2) 0x563dab20be43 send_backtrace common/daemon.c:38 0x563dab20becd crashdump common/daemon.c:83 0x7f0cf0c96def ??? ./signal/../sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c:0 0x7f0cf0ceb95c __pthread_kill_implementation ./nptl/pthread_kill.c:44 0x7f0cf0c96cc1 __GI_raise ../sysdeps/posix/raise.c:26 0x7f0cf0c7f4ab __GI_abort ./stdlib/abort.c:77 0x7f0cf0c80290 __libc_message_impl ../sysdeps/posix/libc_fatal.c:134 0x7f0cf0cf5464 malloc_printerr ./malloc/malloc.c:5832 0x7f0cf0cfa41b _int_free_check ./malloc/malloc.c:4560 0x7f0cf0cfa41b _int_free ./malloc/malloc.c:4692 0x7f0cf0cfa41b __GI___libc_free ./malloc/malloc.c:3476 0x563dab19be60 logv lightningd/log.c:688 0x563dab19c0bc log_ lightningd/log.c:728 0x563dab1c1047 plugin_log_handle lightningd/plugin.c:530 0x563dab1c5801 plugin_notification_handle lightningd/plugin.c:609 0x563dab1c5a9a plugin_read_json lightningd/plugin.c:753 0x563dab2388a5 next_plan ccan/ccan/io/io.c:60 0x563dab238c83 do_plan ccan/ccan/io/io.c:422 0x563dab238d3c io_ready ccan/ccan/io/io.c:439 0x563dab239e5b io_loop ccan/ccan/io/poll.c:470 0x563dab194399 io_loop_with_timers lightningd/io_loop_with_timers.c:22 0x563dab199c29 main lightningd/lightningd.c:1480 0x7f0cf0c80ca7 __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 0x7f0cf0c80d64 __libc_start_main_impl ../csu/libc-start.c:360 0x563dab169020 ??? _start+0x20:0 0xffffffffffffffff ??? ???:0 ``` Changelog-None Signed-off-by: Lagrang3 <lagrang3@protonmail.com>
Done. Obviously this resulting sequence of commits will need to be rearranged and partially squashed before being merged, but I left it exploded apart for now for easier review. |
I expect it was because he was keeping the log message pointers around under the old logbook implementation. Now that the log message contents are simply copied into a ring buffer, the tal overhead of the original log message pointer does not matter, as that allocation is now short-lived. |
It's not okay to call
free()on the pointer to a truncated log message that was allocated bytal_fmt()incap_header(). Let's calltal_free()instead, and rather than callingvasprintf()to malloc the log message in the first place, let's calltal_vfmt().Also, since we're now always using a tallocated string for the log message, let's have
cap_header()take ownership of it and either free it if it truncated the message (and is returning a different pointer to the truncated message) or else return the taken original pointer without freeing it.Also,
log_io()'sstranddataparameters are markedTAKES, but the function was not actually taking them, so fix that up too.Also, don't call
strlen()on a string returned bytal_fmt(). The returned pointer is guaranteed to havetal_count() == strlen() + 1, so there's no sense in scanning through the string to find its length.Checklist
Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:
tools/lightning-downgradeN/A