Hacker News new | past | comments | ask | show | jobs | submit login
A bug in GCC that may cause memory leaks in valid C++ programs (akrzemi1.wordpress.com)
235 points by ingve on April 27, 2017 | hide | past | favorite | 103 comments



Good stuff.

Only compiler bugs I've seen in C++ have all been around exceptions. I think this is just another reason towards force-handled error codes. You don't have a whole new control flow that needs to be verified and is much simpler from an implementation/runtime perspective.


I disagree. You are suggesting everyone should write error handling code manually rather than rely on compiler.


Not really. See Rust/Scala/Haskell's Result/Either type, which has compiler support, but that compiler support is lightweight - you use the same control structures and flows as with regular code.


That's a great approach, but needs sum types and HKT to be effective, neither of which C++ really has (std::variant exists but doesn't always behave as a proper sum type AIUI) at present.


Rust manages it without HKT quite reasonably (rather than implementing generic monads, it has the `?` operator, which is syntax-sugar for something like [0]), though sum types are a necessity.

[0] match whatever { Ok(v) => v, Err(e) => return Err(From::from(e)) }


Ah, last I knew they were relying on macros. Still, ? is an ugly special case where with HKT it could be a reusable general operator.


Absolutely - there's currently discussion around a Carrier trait which would make it generic, once we have some variety of HKT. Still, it's good enough for now, and any future development of it will be backwards-compatible.


It doesn't solve the problem because you'd have move to 2 stage construction/initialization and that creates even more problems than this.


Nope, if you have an operation that can fail then wrap it in a type that represents that(I'm partial to Rust's Result<T,E>) then you force consumers to deal with potential errors and you can do 1-call construct + init if you want to.

Exceptions cause all sorts of problems(like violating the principal of only paying for what you use). They aren't checked so you can't enforce handling via type system and in general they're just a mess in C++.


> Exceptions cause all sorts of problems(like violating the principal of only paying for what you use).

Exceptions are zero-cost in terms of performance on any sane architecture (this excludes x86, but not x64). The only cost that you're paying is the extra memory for the tables, which are all statically allocated.


Compiling with exceptions can still have performance cost because the addition of implicit control flow at every function call.

For example the following code will compile differently depending on whether goo() can throw.

struct k { void p[4]; };

void goo(k s);

k foo() { k r; k s; goo(&s); r = s; return r; }

If goo() doesn't throw the extra variable s can be eliminated.


If goo doesn't throw, why not declare it nothrow?

Although I still don't see why, in your example, one of the variables cannot be elided even if goo does throw. Since there's no point at which address of r is taken, much less escapes the scope, it should always be safe to elide r and just return s, no?


Try indenting your code section by 4 spaces. :)


you mean r? you can't eliminate s regardless of goo throwing.


They're zero(ish) cost at runtime, but focusing on just that is missing something: pervasive unchecked exceptions inhibit optimisations, both in compilers and in libraries, which have to be more defensive to preserve correctness.


If you replace exceptions with e.g. return values, it doesn't really change anything for the libraries - if they expect to be able to deal with an error, they still have to defensively preserve correctness.

OTOH the library always has the option to say "this particular thing that you give me should be nothrow; if it throws, that's U.B.", and not even try being defensive in the face of exceptions when they come out of such a thing.


I think you missed the precise qualification of my comment: this only applies when the compiler can't tell what exceptions can occur, like in C++, Rust and even Java, and so needs to be able to handle any/all of them. The most basic (and most relevant) query is "can this call throw?", but even that is impossible to answer without global information when there are unchecked exceptions.

In statically typed languages, return values, like (fully) checked exceptions, are different, as they're exactly the sort of global information the compiler needs. Functions can statically guarantee the exact set of ways in which it can interact with its caller, and the compiler can understand. This means a function that, say, sorts a list with a less-than function (that returns bool) knows that every time that function exits, it gives a value of true or false, no exceptions (hah).

In any case, just writing a library that says "UB to throw" isn't quite enough, as the compiler won't understand, and will still structure code to handle exceptions "sensibly", even thought it doesn't need to. Fixing that means being able to communicate nothrow to the compiler (which is nice too), but this starts to no longer be a pervasive unchecked error system: it's somewhat of a binary form of checking.


Well, we were talking about exceptions in C++, where there is a standard way to communicate nothrow. So yes, you're right that it's no longer a pervasively unchecked error system - but that's precisely why they're not any worse than Result<T,E> at this point. Effectively you can treat C++ as having every function return Result<T,E> by default, with nothrow being an opt-out to just return T.


The thing is, in C++ you have to explicitly write noexcept for that to happen.

If you use optional<T> you have to explicitly write that, and turning off exceptions means you don't have to write noexcept everywhere.


I guess that's why you Rust guys don't have exceptions, so you don't have to pay this cost. </s>


(Explanation of the /s: panics use the same mechanism, so the compiler and libraries unfortunately still have to be just as defensive when they're enabled. When the compiler is in -C panic=abort mode, it doesn't suffer from this, but I don't think libraries have a great way to customise/improve their behaviour in that case at the moment.)


Don't you need RTTI as well? That's not been zero-cost on all compilers I've used in the past.


You do, but C++ includes RTTI separately from exceptions (dynamic_cast and typeid), so it's going to be there regardless. If you turn it off, then we're not talking about standard C++ anymore, but some subset.


When you throw and exception, C++ compilers add RTTI for those classes even when RTTI is separately disabled. You cannot call dynamic_cast and type_id but the generated code is the same.

I don't really see this as much of an issue given you have to have this RTTI to catch the exception properly.


My point is that RTTI is required by other things in the C++ standard, not just exceptions. So it's still "free" if we're only considering exceptions separately by themselves, and leave all other features as they are in standard C++ (and if you disable RTTI entirely, then you no longer have standard C++, since those other features also stop working).

If we consider the combo of exceptions+RTTI, then yes, dropping both at the same time lets you get rid of some further overhead (although again it's memory-only, not compute cycles). But then we have to discuss the lost benefits of those other features, not just exceptions alone, to judge whether doing so is worthwhile or not.

BTW, I may well be wrong here, but isn't RTTI generation for exceptions (when it's otherwise disabled) an example of "only pay for what you use"? i.e. wouldn't the compiler then only generate RTTI entries for those types that are either thrown somewhere, or are caught somewhere? If so, any library code that doesn't throw itself, and only needs to allow exceptions to pass through it, is still zero-overhead in that regard.


> Exceptions are zero-cost in terms of performance on any sane architecture

Not when the exception is triggered


If you trigger an exception, then you're not paying for something that you don't use anymore.


C++ constructors can't return error codes or Result. That's the two-stage initialization (constructor call + initialize() call) wvenable is talking about.

(Of course you could do the same thing as Rust and expose only a static creator function and hide the actual constructor.)


Yeah, most projects I worked on was doing the static creator before Rust showed up on the scene since it sidesteps a lot of these issues.


Exceptions are typically faster than error codes when errors are rare.


In terms of memory usage for similar Result type in C++, is there any way around allocating the space for both types (as opposed to Result in Rust, which just allocates enough space for the larger of the two)? My understanding is that there is not, although I'm still not very experienced with C++, so I wouldn't be surprised if I'm wrong


You can use a union + tag(which the same as Rust uses which is why they're called Tagged Unions) however you run into issues with constructors/destructors. You can do something similar with placement new/delete but it requires some hoops.

Honestly most error codes are small enough that you can just pay the hit for it on the stack and discard it once you've handled the good/bad case.


std::variant with c++17, but on linux at least you need to install gcc from git to have access to it so far.

boost::variant otherwise, but depending on the types used (if there aren't any that are nothrow default constructible) it allocates on assignment.

If you just want a result type and not a more general "rust enum", this repository does it (while only allocating space for the larger, I think) https://github.com/oktal/result/

And I don't think there's any reason you couldn't implement an equivalent of std::variant for previous versions of C++, I'm just not aware of any.


Heh, I implemented an in-house variant at work a week ago. As it turns out, I was able to use my variant class in a python extension I developed. Also, as it turns out, python is scary in what it lets you get away with when developing extensions.


Just because a language feature is hard to implement doesn't mean we should abandon it.

I'm not going back to explicit error handling.


Come on in, the water is fine. Handling errors without using exceptions is not that bad.


It's still very bad, though. Exceptions control that badness. There's no excuse to avoid using a useful feature for reasons that actually worsen your code.


Exception handling is computationally very very expensive when triggered. a cmp op costs nearly nothng.


Exception handling costs absolutely nothing when not triggered (on most reasonably platforms these days, due to zero-cost exceptions), while that comparison instruction is still there, still taking its performance cost for every call of every function that could theoretically fail in the entire program.


If it's a hot path, branch prediction will almost certainly make the cost of that comparison negligible though. If it's not a hot path, does it matter?


The difference in cost is negligible compared to the difference in correctness. Vtables are so optimized (and often elided) and caches so efficient and comparisons are cheap and branch predictors so great that the cost of both of these is functionally 0 in all code I touch outside the tightest inner in physics sims and rendering pipelines. Even there the differences is statistically negligible.

On correctness, it is impossible to forget to deal with an exception, it is Tuesday when a C program forgets to check an error code. Did you know printf returns an error code?

There are ways to force these returns to be checked by the compiler, but a language needs to be structured around it to be sane otherwise you wind up with every printf in an if statement. Forced exception checking is almost as burdensome, anybody with a few years of java ought to agree or at least see where I am coming from.

Error handling is just plain hard. Only a few languages do it really well. I think either could be done right if baked in from the first language spec. But that is not where most code is written, most Java, C and C++ has to exist now. Clearly C has only error codes, and it has the most bugs. Java has exceptions and finally to force the dev to handle everything right there, which I think is a huge source of anti-patterns but better than error codes. C++ has exceptions and destructors which work the best of these, but still separate error handling from code execute all the time when it is only desirable most of the time.


I think this would just move the bugs to the applications.


Hence the "force-handled" part of my comment :).


But what you need is force-handled-correctly. If you think that programmers will correctly deconstruct partially constructed objects at every location where construction might fail, then you have far too much faith in our profession.


By that logic we should write hand optimized assembly. I think there was even a rant by Linus on GCC messing up the stack and he writes C. Clearly compilers shouldn't be trusted with important programs.


That makes sense. Exceptions in C++ are a mess. It's completely platform, architecture, OS and compiler version specific. That's not well defined.


The behavior of exceptions in C++ is well-defined. But implementations do have bugs about many things that are well-defined - this is one of those cases.


Where in the C++ spec are implementations of features defined?


Of course it is not defined, that is a language specification not a compiler implementation guide.


A number of years ago, I hit against a GCC bug that generated an ICE (internal compiler error). Cause? Usage of an anonymous namespace in the global namespace. It was a short-lived regression, I think in the 4.1.2 series that came to me as a result of a supposedly minor OS update.


Very interesting. This is impossible in C++03; this must be related to the very useful changes in aggregate initialisation for C++11 (particularly useful as there's one great syntax for that and uniform initialisation).


If you're curious (as I was), this bug does not occur in clang, as you can demonstrate for yourself by following the link to wandbox.org in the original story and changing the compiler.


I tested it with Visual Studio 2017 and it seems to work correctly there as well.


Nice find!

Btw: Is it correct that this won't get fixed in Debian because it isn't a security issue?


Not sure on the current Debian policy but this bug could certainly be framed as a potential security issue; exploiting the bug could result in resource exhaustion and denial of service.


For released distro, I doubt Debian will rebuild all their packages and upload them all on security.debian.org...


Exploited by who? The programmers ?


The users. Anybody who can connect to a server implemented in C++ can compiled by a gcc with this problem.


I think it's not quite that severe; anybody who can connect to a server implemented in C++ that is compiled with gcc and initializes objects in the way described here could exploit the bug.


And the initialization needs to conditionally throw an exception based on user input.


If you assign the initialized value to a variable then you get the expected behavior: https://wandbox.org/permlink/r6csgR44BZBWudLV. What exactly is the defined destruction behavior for an object that's never (necessarily) on the stack? Or is it actually required to be on the stack in this case?


In C++14, the spec specifies that a temporary object's destructor is called as the last step in evaluating the full expression that contains the point where it was created (C++14 §12.2).

I'm presuming it's the same in earlier versions of the language, but I don't have those specs handy ATM.


I'm more talking about the exception behavior looking at e.g. https://wandbox.org/permlink/S5paK3Z9NpeCv3I0 (which exhibits the same bug). I mean you're right, and the fact that the User object is temporary should presumably not keep the first Resource destructor from firing, just it seems weird. I guess in my defense, apparently it seems weird to GCC as well.


The spec also explicitly addresses that (automatic objects' destructors must be called after an exception)-- see section 15.2 in the C++14 spec.

There is no ambiguity in the spec here; this is a GCC bug.


I don't have the specs either, but I do remember that in Design and Evolution of C++, Stroustrup discusses nailing down the lifetime of temporary objects. I believe it was in the first standard (i.e. 1998).


How can it be this was reported almost two years ago and nothing has been done?


I imagine it's because those who maintain GCC have varying agendas for prioritizing their contributions.

The fact that is hasn't been fixed yet may be a signal that, in practice, this hasn't bitten anyone hard enough to motivate a patch submission. In which case, one could argue that the bugs are being tackled in a somewhat rational order.

It may be instructive to consider why you yourself haven't submitted a fix. Perhaps that reason is common across many of those capable of submitting a fix.


I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.

I hacked around in clang for a bit for a compiler project. I think it would have taken me weeks or months to get to the point where I was able to fix the equivalent bug (if it existed) in Clang. Compilers are big, hard to debug projects. And, IMHO, the Clang/LLVM code is much more approachable than GCC. Not all developers are hobbyists that can afford to detour from their work for weeks or months to ramp up on GCC to fix this.

I'd also point out that this is the kind of bug that could exist and be triggering in a lot of code as we speak but no one (besides the author of this blog post) have caught it. I think it's insane to ignore a bug like this.


>I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.

That's not my take on it. It doesn't read like an urge to fix it yourself, rather it's literally an urge to consider why you haven't, in order to understand why others also have not.

"Not all developers are hobbyists that can afford to detour from their work for weeks or months to ramp up on GCC to fix this" seems like an appropriate response and probably more or less the point that the GP wanted to make.


I think "fix yourself" in this context means implementing a workaround in your code, not fixing GCC.


I don't think it does. ambrop7, DoofusOfDeath and dsharlet are IMO quite obviously talking about patching the GCC codebase and go out of their way to talk about the priorities of GCC maintainers, how approachable its codebase is and how hard compilers are to debug in general.


> I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.

I totally agree. GCC is reputed to be a complex beast, and few people have the spare time to get to the point where they can make the fix.

To clarify my original point: The bug apparently wasn't bad enough to cause anyone like yourself to either (a) fix it, or (b) pay someone else to fix it. So I'm at peace with this at a kind of meta/process level.


Because there's actually not that many gcc developers, and very few companies support its development?


Yea, the core project of GNU has "very few companies" supporting it.


GCC has a more though, there's a few people being paid to work a good chunk of their time on it.


As others have pointed out up-thread, the bug only triggers on a very specific use case - changing the body of the sample to:

        User u{make_1(), make_2()};
        process(u);
causes the first resource to be correctly destroyed.


Show me a program that someone used in production that became unstable because of this bug, and I will give you the answer to your question ;)


A meta-comment on this: had this been a bug in VC people here would shit on Microsoft and their non-compliance to standards, not caring and so on and so on. But since it's in GCC, people are discussing a language feature.


Any C++ programmer knows that bad things happen when throwing in a ctor. Just don't do it. This is a non problem.


No. Just no.


The RAII doctrine leaves you little choice, since the architects of C++ decided that constructors wouldn't be allowed to return NULL to indicate failure, or otherwise signal it in an easily-detectable manner.

That's why I don't use RAII. It comes with a massive amount of baggage.


You can do RAII without constructors (e.g. Rust)


Yep. There are any number of ways that C++ could have supported RAII without requiring either exceptions or a lot of unnecessary hacking, but they didn't.


C++ exceptions are dangerous, even when working as expected. In my opinion, C++ without exceptions (or the minimal for e.g. logging the error -division by 0, invalid memory access, etc.-) is a fine language.


Without exceptions you can't do RAII (because you can't do nontrivial work in constructors, because you have no way to report failure) which is probably C++'s one real selling point. (There are other reasons I'd say it isn't a fine language - mainly the lack of sum types and the way templates are only checked after full expansion (that one will at least be fixed if and when concepts make it into the language)).


RAII used to be incompatible with exceptions (I'm not sure whether anything changed for the better in C++17). The reason is that you can't throw exceptions from destructors, and e.g., on Linux, destruction of the file object (wrapping a file descriptor) can result in an error because close() may well return -1 setting errno to EIO. See `man 2 close` for details.


That's really not true. It takes a little discipline to ensure your constructor will succeed prior to calling it, but it's worth it. You generally want to make your destructor noexcept, so you get free implicit move optimizations.


Note: there is no such thing as standard C++ exceptions on invalid memory accesses. I'm not sure about division by 0, might be in the same case.


Standard or not, if you use an OS with memory protection (MMU), you'll get those exceptions in C++.


This reminds me of a bug that I found in Borland C++ 14 years ago: http://qc.embarcadero.com/wc/qcmain.aspx?d=3478

Sad to know that after all that time compilers still can't get even the core semantics right.


read http://250bpm.com/blog:4 for an interesting related discussion.



I would like to point out that if you write code like this (that is, creating a temporary object only to have it instantly deleted), your code smells.

Backwards-compatibility notwithstanding, it would not be totally illogical for C++ to allow optimizing out construct-destruct chains even in the presence of side effects, in a similar manner to the existing copy elision behavior. After all, the object is unused, and the destructor should be merely undoing whatever the constructor did, so afterward it should be as if the object was never constructed.

If you rely on the side effects of normal construction, that's not so different from relying on the side effects of copy construction, which you can't already do. Hence you shouldn't do the first either.

So yes, it's a bug, and needs to be fixed, but regardless of that, you shouldn't write code like this in the first place.

EDIT: See comment below, apparently the bug comes up in more cases than those I referred to here.


I don't think it seems like a code smell at all. For example, I could be adding together multiple vectors, some of which are created as temporaries.

    auto total_offset = Vector{x_offset, y_offset} + global_offset;
Granted, in this case, there are no resources being held by such a Vector class, but having immediately-destroyed temporary objects is not necessarily a code smell.


That's definitely not "immediately destroyed", there's operator+ being called between construction and destruction.

I didn't realize this can trigger the bug though, so if it does, I should clarify I didn't mean to include it in my comment. I was only referring to cases where there is no intervening operation between construction and destruction.


It's a minimal testcase; minimal. I thought the blog post was pretty clear that it had nothing to do with how the object was used after (e.g. if it was immediately destructed or not) and even linked to a real world example that would disprove that.

Hence why your holier than thou attitude about not writing smelly code is just asinine and not helpful. It plays into a mindset that people who write "good code" don't have to worry about 'esoteric' bug reports (e.g. like security advisories).


There's probably far more reasonable cases of temporaries being immediately destructed than you'd expect.

One that springs to mind is gtest. It defines a bunch of macros like ASSERT_TRUE that return a temporary object that has operator<< overloads to let you add additional information to the assertion failure. For example:

    ASSERT_NE(-1, open("/foo", O_RDONLY)) << "failed to open file: " << strerror(errno);
It's perfectly legitimate to just go ASSERT_TRUE(1 != 2), though.


Like I said:

> That's definitely not "immediately destroyed", there's operator<< being called between construction and destruction.


The point is that you have the option of using operator<<, but if you don't exercise that option, the temporary object is destroyed immediately.


> I would like to point out that if you write code like this (that is, creating a temporary object only to have it instantly deleted), your code smells.

Not necessarily. What if all you want are the side effects of the object?

In particular I/O. Either disk or network.

Creating the object opens the connection, destroying it closes the connection. And you use the object in between to write data.

Or, don't assign the new object to anything, and simply write the data you pass in to it, and close the stream.

It's a perfectly reasonable use case.


> And you use the object in between to write data

That's not what I was talking about. See the original comment and follow-ups.


> That's not what I was talking about.

You didn't read my next sentence.

I was explaining that you could use the object in two ways. (If you only ever used it the second way then you would just make it static, so I was giving the first way as a reason why it would not be static.)


Instantly, as in next line of code? Yeah, probable human error (or auto-generated code).

Without being used? Not surprising. It is surprisingly common to allocate a variable, pass it to a function, and have the call never actually use the variable. Unwind, and the variable is destructed without use.

I've seen other nasty framework errors exposed by such (non-)usage patterns.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: