From 65961d5a75e28aa04a90ee65106f71da177fd4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85gren?= Date: Mon, 21 Aug 2017 19:43:47 +0200 Subject: strbuf_setlen: don't write to strbuf_slopbuf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit strbuf_setlen(., 0) writes '\0' to sb.buf[0], where buf is either allocated and unique to sb, or the global slopbuf. The slopbuf is meant to provide a guarantee that buf is not NULL and that a freshly initialized buffer contains the empty string, but it is not supposed to be written to. That strbuf_setlen writes to slopbuf has at least two implications: First, it's wrong in principle. Second, it might be hiding misuses which are just waiting to wreak havoc. Third, ThreadSanitizer detects a race when multiple threads write to slopbuf at roughly the same time, thus potentially making any more critical races harder to spot. Avoid writing to strbuf_slopbuf in strbuf_setlen. Let's instead assert on the first byte of slopbuf being '\0', since it helps ensure the promised invariant of buf[len] == '\0'. (We know that "len" was already 0, or someone has messed with "alloc". If someone has fiddled with the fields that much beyond the correct interface, they're on their own.) This is a function which is used in many places, possibly also in hot code paths. There are two branches in strbuf_setlen already, and we are adding a third and possibly a fourth (in the assert). In hot code paths, we hopefully reuse the buffer in order to avoid continous reallocations. Thus, after a start-up phase, we should always take the same path, which might help branch prediction, and we would never make the assert. If a hot code path continuously reallocates, we probably have bigger performance problems than this new safety-check. Simple measurements do not contradict this reasoning. 100000000 times resetting a buffer and adding the empty string takes 5.29/5.26 seconds with/without this patch (best of three). Releasing at every iteration yields 18.01/17.87. Adding a 30-character string instead of the empty string yields 5.61/5.58 and 17.28/17.28(!). This patch causes the git binary emitted by gcc 5.4.0 -O2 on my machine to grow from 11389848 bytes to 11497184 bytes, an increase of 0.9%. I also tried to piggy-back on the fact that we already check alloc, which should already tell us whether we are using the slopbuf: if (sb->alloc) { if (len > sb->alloc - 1) die("BUG: strbuf_setlen() beyond buffer"); sb->buf[len] = '\0'; } else { if (len) die("BUG: strbuf_setlen() beyond buffer"); assert(!strbuf_slopbuf[0]); } sb->len = len; That didn't seem to be much slower (5.38, 18.02, 5.70, 17.32 seconds), but it does introduce some minor code duplication. The resulting git binary was 11510528 bytes large (another 0.1% increase). Signed-off-by: Martin Ă…gren Signed-off-by: Junio C Hamano --- strbuf.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/strbuf.h b/strbuf.h index 2075384e0b..1a77fe146a 100644 --- a/strbuf.h +++ b/strbuf.h @@ -147,7 +147,10 @@ static inline void strbuf_setlen(struct strbuf *sb, size_t len) if (len > (sb->alloc ? sb->alloc - 1 : 0)) die("BUG: strbuf_setlen() beyond buffer"); sb->len = len; - sb->buf[len] = '\0'; + if (sb->buf != strbuf_slopbuf) + sb->buf[len] = '\0'; + else + assert(!strbuf_slopbuf[0]); } /** -- cgit v1.2.3