Hacker News new | past | comments | ask | show | jobs | submit login
Q: Why can I access an out-of-scope C++ var? A: So you rent a hotel room... (stackoverflow.com)
418 points by sutro on June 23, 2011 | hide | past | favorite | 98 comments

I say this as a C++ programmer (C++ is essentially the only choice in the domain in which I currently work), but this really underscores how unreasonable C++ is for writing secure/high reliability applications. On the one hand, the compiler will curse you out for trying to use a non-const iterator in a const function (sheesh, what kind of an idiot are you, anyway)? On the other hand, you can read and write to every bit of memory allocated to your application, and it will stand by and do nothing. I think the number of applications which had buffer overflows at least at some point is statistically indistinguishable from 100%.

Try this hotel analogy: you go to a hotel in which you once stayed, and tell them that you're going to dump every bit of possessions and furnishings in all the rooms outside on the street, rifle through them, set them on fire, then photograph all their guests in the nude and distribute the pictures. Most hotels will object. C++ won't.

It seems from some of the comments down the thread that the idea of writing over memory in C++ is new to many people. Let me introduce another couple of concepts.

You don't have to stop with writing over the variables. You can write over the return address of a function, which is stored on the same stack as the variables. If you write some user-supplied buffer to memory, and you didn't carefully make sure that the memory is allocated for this purpose, the (malicious) user can supply you with data that will set the function's return address to something in his buffer, and the function will go on executing his code. That's called a buffer overflow vulnerability, and it's been well known and exploited for decades; half the patches you see coming down the pipeline used to be for this.

Now, there are good ways to protect against buffer overflow vulnerabilities. If you make the stack and data non-executable, and the pages containing code non-modifiable, then seemingly there is no place for the attacker to place his code and get it to run. Modern processors support this in hardware.

Except that some people came up with something called return-oriented programming. This works by finding little pieces in your code that do something simple like increment a register or write a byte of data, followed by a "return" instruction. These tiny pieces of code are called "gadgets". You can usually find enough gadgets to make a Turing-complete language. Then you write a compiler which transforms your code into a stream of calls to the gadgets. Now all you need is one overwrite of the return address on the stack, and you can start playing the application like a piano, without ever executing data or modifying code. There are now tools to automate all this.

I don't want to reveal anything about where I work, but we write applications which need to be reliable and secure. We have regular meetings to discuss vulnerabilities and security techniques. At the meeting where we discussed return-oriented programming, when we asked what we could do to protect from it, the consensus answer was "You can't. Don't make any buffer overruns." The problem is it's not clear that anyone ever wrote a program free from those.

Since hotel analogies seem to be so popular, here you go: http://en.wikipedia.org/wiki/Psycho_(film)

One last addition - I hope I'm not overstaying my welcome with all these little lectures on security. It used to be that a buffer overrun typically resulted from carelessness, like

   char name[100]; //Who has a name longer than 100 characters?
   printf(“What is your name?”);
   scanf(name, ”%s”);
It turns out that the same kind of people who have names like "Robert'); DROP TABLE Students; --" sometimes have 150-character names with a return address and a few bytes of malicious code. That's something that you handle kind of like you sanitize your inputs. You don't pour user input into a limited space. Scanf is deprecated now, and this kind of a vuln is becoming rare.

But as I explain a couple of comments down, a buffer overflow doesn't have to be this simple. You can get a bad pointer in a structure anywhere in your program, and when you use the structure, you will overwrite the stack. It's very hard to catch that kind of vuln - basically, it's a race with you (and white hats) vs. black hats. This is why I welcome the switch from C/C++ to managed code which is happening now, but very, very slowly.

I don't think you are overstaying your welcome. Actually, I learned alot from your posts. Thanks.

Off topic/meta: In the days when comment scores used to be shown, I could do the HN-equivalent of saying "hear, hear!" [or as the Internet says it: "here, here" :)] by upvoting a "thank you" note like the above. Now, if I really want to let you know that I too liked your comment, I have to post a "me too!" comment like this one. Anyway, thanks. TIL about "return oriented programming".

You're not overstaying your welcome, but you did get the arguments to scanf backwards.

Thanks, I'll edit that, since this seems to be a popular thread. I guess I just provided the visual proof of how people make mistakes even when they should know better.

On second thought - let it stay as a reminder.

Your post is indeed very useful, but for the sake of completeness I'll add some remarks to it.

The technique you call Return Oriented Programming is not as new as you seem to beleive. It evolved from ideas and techniques that have been arround since late 90s, mainly what is called "return to lib". Surely it was not as popular and hyped as it is today, but then again neither was a non-executable stack popular back then.

Also an effective implementation of Address Space Layout Randomization serves as a mitigation to ROP shellcode. I'm no expert on exploit writing but I beleive that modern ASLR on 64 bits system will be very difficult to bypass except for the most skilled exploit writers out there, and they would most surely need to pinpoint exact versions of your OS, libraries and layout of your binary.

I don't know any specifics about your work, but though "You can't. Don't make any buffer overruns" is a very good advice, its rather simple-minded... I firmly believe any bug can be leveraged into code execution until someone shows the contrary for said bug, but that doesn't mean that a single technique is the silver bullet of exploit writing. To be able to code in a security-minded way, as one learns about were each protection might fail, one must also learn about how each exploit writing technique might fail.

A good introduction to ROP in the form of a somewhat self oriented self contained blogpost can be found here:http://eticanicomana.blogspot.com/2010/06/so-called-return-o... by a guy from Immunity Inc. I'm mentioning his post because I think one of the automated gadget finding tools you may be talking about is their Immunity Debugger/DEPLib.

I agree with you, and I didn't mean to imply that ROP is all that new. It's been on my personal radar since 2008, which is when it became really popular. I think this was also when the automated tools for exploits started to appear. Of course, the basic techniques go back before then. This happens with computer science - we had to give up writing a certain patent [not security-connected] because we found the basic idea in a paper from 1956.

We discussed address space randomization, which of course we use, and our conclusion was that for us, this doesn't provide enough protection. Not every location can be randomized with equal effectiveness, and sometimes just having an offset is enough, and we're a high-value target. Thus, the advice to "not make buffer overruns." I am also not an exploit writer, or even specifically a security guy (although I need to be aware of it). Obviously, return-oriented programming is not a "we're all going to die!!!" thing. But it's an extremely dangerous technique, which really illustrates the danger of writing user-facing code in a language with no memory safety.

> The problem is it's not clear that anyone ever wrote a program free from [buffer overruns].

There are lots of programs in languages like COBOL that don't have buffer overruns, because they don't have pointer arithmetic.

Nobody has ever found a buffer overrun in qmail, despite looking very hard for many years.

It is very likely that my toy compiler http://canonical.org/~kragen/sw/urscheme/ doesn't have any buffer overruns. It's written in Scheme, generating x86 assembly. The only places that do pointer arithmetic under the control of user code are string-ref and string-set!, which call the same check-array-bounds routine to verify that the index is within the bounds of the string. Unless I fucked up that check, which is seven lines of assembly, there's no way to get a buffer overrun in code compiled with the compiler.

(Technically there's also code having to do with closures that could potentially have a buffer-overrun bug, but it's much less likely. Indeed, virtually any of the assembly language output of the compiler could conceivably have a bug that smashes memory. But it probably doesn't.)

There's also no way to efficiently move data in and out of a string, because every access is individually bounds-checked. But reasonably-efficiently-bounds-checked array access, while it's more than seven lines of assembly, isn't an unboundedly-large problem either. It's just that there's always a tradeoff between safety and efficiency.

I'm obviously not talking about programs written in memory-safe languages - my point is that they don't have buffer overruns.

About the programs written in unsafe languages, I think it's clear that I don't literally mean that every single one has a buffer overrun; you can ensure that simple programs don't, and I am also assuming that well-designed language runtimes and compilers do not. Otherwise, you'd gain nothing by going to a memory-safe language. In the real world, something like a JIT compiler or a JVM is far less likely to be unsafe than an average app, although it does have bugs and there were a couple of patches for buffer overruns.

Also, programs which essentially transform strings to strings, like compilers, are easier to make formally correct, or at least correct enough that you have confidence in them, than many other classes of programs. If you have to deal with timings, polling, complicated interactions with the environment - it all becomes much harder. Also, compilers can have tight specifications of what they do - I don't think it's possible to specify the behavior of a word processor to the same extent. For many C/C++ apps, it's just hard to ever say they don't have a buffer overrun.

So you can imagine our feeling when learning about the ROP attacks. They're going to write a compiler for their malicious code which targets a virtual machine made up of tiny pieces of our app. What kind of a world is this? And they only need one buffer overrun. A scary feeling, although, as someone already pointed out, the idea of running a little piece of code from somewhere is not new, and the attack is not the end of the world.

> I am also assuming that well-designed language runtimes and compilers do not.

I guess the big point I was trying to make in other parts of the thread is that this is sort of begging the question. If you can assume that runtimes and compilers are flawless, then of course you should use the biggest, hairiest ones you can get your hands on, and furthermore you should argue for every possible piece of functionality to be put into the runtime, so that it will work correctly and not have any bugs.

In practice, though, that is not the best possible factoring of the problem. If it were, you wouldn't be using C++! So I don't think it provides a very good guideline to use for questions like "would moving from C++ to Java make our code more reliable?" I think that answering that question properly involves a lot more reasoning about the particular runtimes and compilers involved, as well as the structure of your application — or lack thereof, if you really have an unbounded number of places you might have written a buffer overflow!

What you say about compilers' formal correctness might be true in theory, but in practice, all compilers contain a large number of bugs. I don't think it is true in theory, though. Optimizing a piece of code is AI-complete, and verifying the equivalence of the source and target programs is known to be uncomputable, since it subsumes the halting problem — in theory, it's impossible to even determine whether the compiler has inserted an infinite loop into your program, let alone whether the rest of its semantics are preserved.

The string → string nature of compilers does mean that they can be highly portable and not spend much time interfacing with the rest of the universe, though.

> I don't think it's possible to specify the behavior of a word processor to the same extent.

Maybe not, but you can probably specify the behavior of its string class to have no buffer overruns. I mean, you can write a word processor in Python, right?

I'd heard of Return-Oriented Programming, but it just occurred to me that it's the same idea as 'Threaded Code': http://en.wikipedia.org/wiki/Threaded_code

You laughed at FORTH, ignored it, well now you'll pay. In the all new blockbuster, FORTH is back, and it's wearing the black hat now.

ROP, revenge of the TIL, coming to a computer near you.

On 32-bit Windows, don't forget SEH put exception handlers on the stack too.

FWIW, the Clang compiler does warn about this situation:

  undefined.cpp:4:13: warning: address of stack memory associated with local variable 'a' returned return &a;
Edit: So does gcc.

Not nearly enough.

Note that C++ combines C's vulnerabilities with non-transparency. Suppose you have a data structure like "set of maps of strings to pointers to vectors." That's actually not a complicated structure for a modern application. Now suppose the new guy on the team, who is working on a function which is not taking user input and is not security-sensitive in itself, puts a pointer to a local variable in this structure. Now you, writing a security-sensitive function, write something to this structure. You're overwriting the stack, and NOBODY will warn you or do anything about it. You're certainly not going to get a compiler warning, and I don't think the "new guy" got one either when he did this - he was not directly returning the pointer. This means that you need to treat every line of the application as security-critical. Some people do, but for most, it's not realistic. There are some tools and techniques to help you, but it's not 100%. And it needs to be 100%, since the attacker needs 1 entry point.

I agree with you, but I just wanted to provide a minor counterpoint to the doom and gloom. There are innumerable ways to screw up, but we are not left entirely out in the cold (standing naked in a parking lot) by the compilers.

Maybe you can set the new guy to writing code in Lua for a while, and do pre-checkin reviews on all of his code.

C++ compilers will actually warn you about plenty of memory issues if you ask them to, by writing type-safe code and turning on lots of warnings. Static analyzers today are very good, and will catch a surprisingly good amount of unsafe memory problems that even the compilers miss. The rope it gives you is there for people that need it, but if you don't want it lying around because you're afraid you'll hang yourself, you can always ask the compiler to take it away.

I might not be quite up to date, and I don't usually write extremely buggy code which compilers yell at, but have compilers really started warning on something like "vector_reference.push_back(&local_object);"? There are lots of ways to go wrong with unsafe memory access, and yes, there are tools to help you (things like valgrind help more than compiler warnings), but I think that the saying "hey, they gave you enough rope" is really unsatisfying. We've lived through 20 years of constant vulnerabilities because of these issues, and this only now starting to get sort of addressed. If virtually every team on the planet finds itself unable to use a certain tool "properly", then it's the tool which is being wrongly deployed, it's not the fault of everyone on the planet.

And I agree that the tool has a lot of usefulness in many places; obviously, you can't write system software without the ability to manipulate chunks of memory and the stack and everything else. But C++ is used as a universal programming language, and has tons of features which were not made for system programming, but for application programming. So on the one hand C++ has this extreme safety-oriented pedantry in which you have to decide if your design will have a reference to a const iterator or a const pointer or what, and on the other hand, you can cast that object to (void *) and write Stroustrup's picture to it.

Personally, I'd like to see a world in which the core system is written in a systems language like C/C++, and most libraries and all applications are managed code. Microsoft was working on something like this a few years ago, and I guess Android is sort of a step in that direction, but I think we won't get there for a long time. But it has to happen eventually, since we can't live with the vulnerabilities.

gcc 4.6 warns about returning the address of a local variable by default (you don't even need -Wall):

    % cat foo.c
    #include <stdio.h>
    int *bad(void) { int a = 42; return &a; }
    int main(void) { int *p = bad(); printf("%d\n", *p); return 0; }
    % gcc foo.c
    foo.c: In function ‘bad’:
    foo.c:2:30: warning: function returns address of local variable [enabled by default]
g++ 4.6 doesn't catch the error you mentioned, but valgrind does:

    % cat bar.cc
    #include <iostream>
    #include <vector>
    using std::vector;
    using std::cout;

    void bar(vector<int *> &v) {
        int a = 42;

    int main(void) {
        vector<int *> v;
        cout << *v[0];
        return 0;
    % g++ bar.cc
    % valgrind --track-origins=yes ./a.out
    ==8210== Conditional jump or move depends on uninitialised value(s)
    ==8210==    at 0x4EBAFE4: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/libstdc++.so.6.0.16)
    ==8210==    by 0x4EBB305: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/libstdc++.so.6.0.16)
    ==8210==    by 0x4EC62BC: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/libstdc++.so.6.0.16)
    ==8210==    by 0x400B98: main (in /home/leif/a.out)
    ==8210==  Uninitialised value was created by a stack allocation
    ==8210==    at 0x400B78: main (in /home/leif/a.out)
I don't know how hard it is to use valgrind "properly". I ran it with no arguments and it gave me the error, and even told me to add --track-origins=yes. If you're writing code that needs to be even remotely reliable and you aren't using valgrind, what the hell are you using?

Believe me, on a lot of examples of real-world code, valgrind &co. will find false positives and miss errors. I've been there and done that. And then you can't pick out the signal from the noise. Again, if just about every team on the planet has trouble with this, then it's not a question of just running some tool.

And of course we use valgrind and a bunch of other standard and custom tools. I said myself that valgrind will help here, so it's not like you need to read me the whole kindergarten primer on this.

Sorry, I don't mean to be patronizing. You asked whether the compiler catches it, it didn't, but valgrind did so I showed that.

Of course valgrind and friends have false positives, but in other languages where these errors are compile errors, you can't suppress them when you need to, instead, you just can't write that code, even if you've been alerted to the issue and know it's ok.

Of the software teams I do know, every one that was writing C/C++ (including my current one) incorporated valgrind and other tools into their build/test framework, and had no problems separating signal from noise. I don't know that many though, I'm still young. Do you know of software teams developing critical software that are unable to check their C code for memory errors automatically?

I apologize that I got testy, I just don't think there is a need to paste the whole valgrind run into the message.

Anyway, problem #1 is that valgrind is not a code-coverage tool - it only sees the execution path that just happened. You can, of course, combine it with a code-coverage tool, and people do, but code coverage is an incredibly difficult thing, since a huge number of code paths just don't happen in any normal execution. And those are the dangerous ones, both for malware attacks and for reliability.

Problem #2, which is smaller, is that valgrind is just not that perfect - for real-life code with foreign-language interfaces, real-time driver accesses, and so on and so on, it will give false positives and false negatives. This is why it has a feature where it takes a humongous error-suppression file to stop it from showing you things that you're not interested in. That's not a perfect solution.

I'm not saying there is nothing you can do - of course you can, and for well-defined projects with strong security goals and very large resources, you can get to excellent security. But even the Sun JVM had buffer overflows, I seem to remember. Those guys have heard of valgrind.

If there was a button that someone could push to make C/C++ lack of memory safety stop mattering, it would have been pushed by now.

Leif, we've reached the maximum reply depth here, which I guess means that we need to finish this up. I don't think there are people who can't use valgrind, but it just doesn't give you all that much. It lets you know that there aren't memory problems in the execution paths that you hit with your test files. But you can't test your program with every possible input; say you wrote some server - are you going to test it with every possible internal state and every combination of connections and all possible data coming over those connections? You can't. It's a gigantic search space. You can try to guess the likely problem inputs, but that's an art, not a science.

Now, malware teams use their experience/intelligence and tools (fuzzers) to find combinations of inputs that will fail your program. You won't always win a race with them - finding vulns is their only focus and main expertise. I don't see how valgrind will stop them. They'll find configurations that valgrind never saw.

The real world will also find inputs that break your program; in this case, there is just no malice.

If you run valgrind and see a clean report and say "great, no memory problems then", then you've been lulled into a false sense of security. Part of my background is in software verification, and it's never this simple even for stricter and simpler languages than C/C++.

Also, I've been involved in projects in which valgrind specifically provided tons of frustration by reporting false errors - in particular, it "didn't like" the implementation of STL which we were using, and gave us errors on half the STL allocations. This was very many years ago, and it's been improved since then, but I guess I don't have a personal experience which makes me want to trust everything valgrind tells me. But that's completely secondary to the issue of not seeing all possible execution paths.

There's just a timeout on replies that grows with depth.

Why not fuzz your own code then?

Got it, thanks, though I need to be going anyway.

Yes, you need to fuzz your code, but that's not a simple process; you need to give fuzzers very complicated information about the possible problem input for your program. And there is no reason to think that your team is better at searching for vulnerabilities than a malware team; you're in a race.

Just like valgrind and compiler warnings fuzzying is only useful in decreasing the probability of unsafe memory access. It does so at a cost and still does not solve the fundamental problem.

don't worry, I understand, I pasted it for others

Yeah, it's not perfect, but nothing we have is. I don't know what more to say. It works pretty well for most things I've seen. Do you have any examples of projects which can't use something like valgrind for some reason, not just those for which it wasn't magic pixie dust?

The valgrind comment may not have been useful to you, but it was useful to me; I didn't know valgrind could do that!

That was my point. valgrind is very user friendly. Keep using it!

C++ is perhaps the only language with a strict typing regime that doesn't have any memory safety.

This is ridiculous because safe memory access is more easy to implement, places fewer restrictions on the programmer, and is much more important for writing secure software than strict typing. Unfortunately, it does proscribe pointer manipulation patterns which are popular in C programming, so it was excluded from C++ to align with the "C with classes" marketing.

Right; we're just so used to C/C++ that this seems normal. Here's Eric Lippert telling someone "look, if you check out of a hotel room, your stuff might still be there for a while, right?" Right. Makes sense. (Not blaming Eric Lippert for anything - he didn't design the language, and his explanation is, of course, 100% correct).

What I'd be telling him is this: "You've discovered an EXTREMELY dangerous feature present in C/C++ and almost no other common language. The whole security industry has spent DECADES plugging holes caused by unsafe memory accesses, and it didn't win the battle. The very first internet worm in 1988 used a buffer overrun, and the patch you downloaded yesterday was probably for one of those, too. Of course, unsafe memory access may make a program unreliable, as well as insecure. Be VERY careful."

Of course, I might just need to become more laid-back and mellow...

C/C++ exists for a purpose. There are places where features like that are powerful and useful.

If you chose C/C++ for a project that requires security, and you accidentally left a vulnerability, it's not C/C++'s fault.

It wasn't marketing. When C++ took off as a language, such memory safety was a luxury. I've read Stroustrup's Design and Evolution of C++, and I don't think strict memory safety was even considered. Saying it was "excluded" implies that it was considered, when I think the kind of programming they wanted to support - systems programming with minimal performance overhead - precluded it from consideration.

Ada was a contemporary of C++ which had such memory safety. In fact Ada was first standardized long before C++, so the issue wasn't unknown or even obscure when C++ was being developed.

Moreover, there are no run-time performance issues. Memory safety can be verified at compile time provided things like pointer arithmetic are forbidden.

The problem for C++ is that memory safety and C-style pointer manipulation are incompatible.

C++ was marketed as "C with classes." This was a hugely successful marketing move (it helped C++ beat Ada, for one). It wouldn't have worked if all the C programmers keen on picking up "object orientation" had flipped open their C++ book and found that it had "neutered" their pointers though. They wanted to be "OO compliant" by writing "class" instead of "struct" and change nothing else and C++ promised to deliver that.

C++ was based on Stroustrup's early-1970s experience with Simula, where he was able to write a beautiful program (for his thesis research, I think?) that did exactly what he needed, but then discovered that doing the compute run he wanted to do would consume his department's entire computation-time budget on the mainframe. (Apparently 80% of the compute time was being spent in the garbage collector, even after he stopped allocating new objects, which sounds like a straight-up bug in the Simula runtime.) So he rewrote the program in BCPL, which is about like Forth except that it has infix notation and doesn't have metaprogramming.

So, some years later, in 1983, his key consideration in the design of C++ was efficiency — avoiding imposing any unnecessary overhead.


In the interim, although he didn't know this, generational garbage collection and inline method caches had been invented, which probably would have provided enough efficiency that he never would have had to switch to BCPL. (Actually, even a decently implemented early-70s Simula would have been five times faster, and that might have sufficed.)

C/C++ doesn't have this restriction because the hardware doesn't have it. In assembly you can do whatever you want with memory. C was created to give as much access as possible to the hardware, while still providing many of the features of high level languages. It is not the language that is betraying you, it is you that is not clear about what the language can do.

The thing is, all of the things you complain about in C++ are also true of machine code. Does that mean that machine code is unreasonable for writing secure applications or high-reliability applications? No. It just means you need to take extreme measures to verify that the machine code in your application doesn't have those problems. For example, you can use machine code emitted by a Scheme compiler that upholds type (and thus memory) safety. Or you can write a machine-checked proof that your machine code doesn't violate memory safety.

Stepanov points out that all the modern "safe" languages do nothing to prevent you from putting an infinite loop into your "high-reliability" application, which is every bit as fatal in an antilock brake control program as a buffer overflow would be.

He argues that the solution is to isolate unsafe operations — including, among other things, pointer arithmetic, recursion, and while loops — in a very small part of your code base, so that they have a chance of all being correct. And he argues that it doesn't matter whether the unsafe code is in a library or in the code generator of your compiler.

This seems plausible to me.

Unfortunately, I don't know of a practical programming language that allows you to factor all of your while loops and recursion into a small, non-expanding library. By contrast, the memory-safety problem is easily solvable at a substantial performance cost, and solvable at a minimal performance cost.

Sorry, but this sounds exactly like one of these comp-sci things which are perfectly true in theory and not usually true in practice. A compiler is usually written by a very strong team with an understanding of code correctness and techniques for ensuring it. Applications are written by average teams with a typically shaky understanding of software verification. A compiler usually has a much tighter specification. In the real world, it makes perfect sense to move as much of the burden of ensuring correctness as possible from the application to the compiler. Theoretically, it doesn't change anything, but practically, it helps a lot.

There are exceptions - embedded applications like anti-lock brake control systems are often written very close to the metal by strong teams which understand exactly how to ensure their correctness. They also have super-precise specifications. A compiler is often unnecessary or harmful here, and only introduces complexity. But this is not the case case for most of the millions of lines of code which keep being written in C/C++. I really think they would benefit from being written in a safer language. That's hard to justify rigorously, but I think that's part of the reason we saw C/C++ move from an absolutely dominant to merely a very important market share.

One hopes that libraries are also written by a very strong team with an understanding of code correctness and techniques for ensuring it.

I thought it was clear that I agreed that C and C++ (and other memory-unsafe languages) are generally worse than other existing alternatives. I'd just like to see Stepanov's reasoning followed a bit further; maybe it will lead somewhere interesting?

Actually, I hadn't realized that we agreed on that. I will think more about Stepanov's approach, although I still think he is ignoring the different efforts likely to be put into securing different type of programs.

you go to a hotel in which you once stayed, and tell them that you're going to dump every bit of possessions and furnishings in all the rooms outside on the street, rifle through them, set them on fire, then photograph all their guests in the nude and distribute the pictures. Most hotels will object. C++ won't.

Of course, then when the assigned task is to ransack a hotel (what if you're filming Spinal Tap or conducting a riot response police drill?), the other hotels won't even speak to you and you can't perform the task at all. Hotel C++ does not object so it becomes the resource by default.

This may be a very naive question. Is having a separate segment for return addresses and stack data a useful option or an option at all?

Eric Lippert is a prolific writer and is amazing at explaining anything related to computer programming. His blog is a MUST read for every .NET developer: http://blogs.msdn.com/b/ericlippert/

I also follow Eric's activity on SO here:


Some of his answers which I liked:




He is on HN as well:


Heads up: the two links at the top go to the same source:

Eric Lippert is a prolific writer and is amazing at explaining anything related to computer programming. His blog is a MUST read for every .NET developer: http://blogs.msdn.com/b/ericlippert/

I also follow Eric's activity on SO here:


I don't know who downvoted you, you are correct. I fixed up the links.

this one is an eye opener and very humble too http://stackoverflow.com/questions/2186595/c-is-there-a-diff...

edit: also thanks for making me spend probably next several hours reading through all that stuff :)

edit2: oooh this one is good as well http://stackoverflow.com/questions/3110154/why-is-the-explic...

edit3: Not from Eric but check out this C# code:

    sbyte[] foo = new sbyte[] { -1 };
    var x = foo as byte[];    // doesn't compile
    object bar = foo;
    var f = bar as byte[];    // succeeds
    var g = f[0];             // g = 255
edit4: oh and check out this description of c# compiler actions http://stackoverflow.com/questions/1917935/how-does-c-compil...

Is twos compliment behavior guaranteed in C#?

Yes, check out the Partition I 12.1 table 6 of ECMA-335 standard

I'll probably get downvoted, but I think elaborate real-life analogies like this are more likely to confuse novices than help them.

A better explanation in my opinion is to draw a diagram of a stack and showing that returning from a function just decreases a pointer. C++ programs don't scrub the top of the stack once you finish a function because it is a waste of time.

If another function was called before the 2nd call to foo(), then the variable on the stack would be overwritten.

The analogy is supposed to explain the concept of undefined behavior, not the details of memory management.

If you are used to safe languages, the concept of undefined behavior might be confusing. In those languages, an operation is either allowed or not allowed, and if it is allowed it has well-defined behavior. In C a number of operations does not have well-defined results, but is still technically possible to perform, or might be possible depending on circumstances. But you shouldn't use them. The analogy is supposed to explain that.

undefined behavior is fun. All this stuff gets a lot more interesting in microcontrollers, where security is not an issue and you sometimes have to manage memory directly and store data between power cycles.

Let me preface this by saying I am not attacking you just stating an opinion about the sentiment

Thats the exact type of thinking that keeps so many people away from programming and computer science.

A nice accessible answer that helps a person make a logical leap in the right direction is so much more important to a novice than pedantry.

Its not a perfect answer but to a person who has no clue whats going its a lot more helpful to get some idea

While that is more accurate and I think that programmers should understand how computers work, as well as C calling conventions, in this case the analogy helps. People who know what the stack is (other than something that can be overflowed) as well as how functions are implemented would likely never think to ask this question. By the time you give the satisfactory "real" answer, you've written the better part of an intro architecture lecture (at my school, I think we probably had 2 or 3 lectures and a lab to get to this point, with other digressions).

The real explanation would also be less complete than the analogy as going into the "construction of a football stadium", etc. would have to consider optimizing compilers, virtual memory, and the other things that could get really mucked up. The analogy is accessible even to people who don't program, rather than to just those who have seen %ebp and %esp.

Sorry for the off-topic rant, but is it only me or are people, who start with "I'll probably get downvoted, but ...", never actually downvoted? Maybe it's some kind of reverse psychology, like parents who tell their children the opposite of what they actually want them to do. Or maybe the downvoting behavior on HN is actually better then people think and different options are actually valued.

I've noticed that when people start their comments with "sorry for X," or "is it only me . . ." they tend to get upvoted. This could be another instance of the same reverse psychology you posit.

Some people (I'm one) make a point of always downvoting people who say "I know I'll get downvoted for this, but ...". (I'll make an exception if what they write is so good I'd have upvoted it; in that case I'll just not upvote it.) The less useful that sort of sympathy-fishing is, the better HN will be.

Similarly, I will often downvote comments that complain about being downvoted, either as a reply or an edit. I find whining about votes detracts from the discussion and has no place here and therefore is worth a downvote. People need to become less concerned about their scores - if people find a comment is valuable, it will be upvoted. If not, it may be downvoted and complaining about being downvoted makes it less valuable, not more.

Of course, by my own logic, this comment should also be downvoted purely because it is a meta comment, is off-topic and detracts from the actual discussion. Hell, if I could, I'd do it myself ;-)

I'm curious if you considered that the reason people preamble their posts with this is because they have gotten multiple downvotes in the past for something they said which was reasonable? My understanding is that downvotes should not be cast for dissenting opinions or unpopular but logical statements. However, my experience with the site is that this happens from time to time. Thankfully, there is sufficient traffic so that the situation often (but not always) rectifies it self .. i.e. people tend to upvote good comments. I'm relatively new to the site so am still trying to figure it out.

To this end, I feel the cap of -10 on downvotes was a great idea. It lets someone post an unpopular (but hopefully logically sound) argument with the knowledge that they will lose a capped amount of karma.

"I know I'll get downvoted" is also sometimes code for "this post is so awesome you probably won't understand it, but go ahead and downvote because I don't care about little minded people like you."

It certainly can happen that something reasonable gets a lot of downvotes. But not nearly as often as it happens that something unreasonable (or stupid or irrelevant or otherwise unhelpful) gets a lot of downvotes even though its author thinks it's a useful contribution.

I think "I know I'll get downvoted ..." usually just indicates (not a long tragic history of getting downvoted for insightful comments, but) roughly what it says: the author expects to get downvotes -- and probably hopes to get fewer by saying s/he expects them. HN is better off without those preambles because (1) they're a waste of space -- learning that the author of something expected downvotes tells you nothing useful -- and (2) they distort the scoring system, which (for all its flaws) does help to order comments well and identify ones that aren't going to be worth reading.

Also, the only reason the author would expect their comment to be downvoted is because 1) its insightful, but unpopular and 2) because the author knows right well that the post adds little value, is off-topic or detracts from the conversation.

If its #1, then you must clearly state your argument. A well thought out, but unpopular, opinion normally doesn't get as many downvotes as a not so well thought out opinion.

If its #2, then maybe the author should simply refrain from posting.

> To this end, I feel the cap of -10 on downvotes was a great idea.

Is there really such a cap? Back when comment scores were displayed, there was a minimum display score, but actual score could go far below it, with no apparent minimum. Such a cap would be/is a very good idea, of course.

I agree, in particular, that the image of how the stack works is critical for really understanding the problem. The hotel analogy is nice, but doesn't help much in generalizing or extrapolating to other issues.

Yeah, the hotel room analogy breaks as soon as you realize that visiting some completely other hotel room will already overwrite that variable in the first one.

But it was a fun story, though.

Analogies like this are stepping stones, not the destination. I try to both use an analogy and give the fullest explanation that I think they can handle. The analogy tells them the structure of the complete answer. The complete answer often has concepts that are new to them, and the more unknowns in something, the harder it is to learn. The analogy allows them to understand the structure of the reason, which removes an unknown.

I had a roommate in college who would write code like this. I (and several other people) tried to explain to him that he couldn't necessarily count on the pattern working all the time. His response was to say, "But it works here!" and continue abusing C++'s undefined behavior.

I stopped giving him help with his CS coursework in fairly short order.

Eric Lippert was one of the highlights of working at Microsoft. He was (and I'm sure, still is) very active on the internal C# mailing list. His answers are always very entertaining to read. He has a smugness that is deserved and not irritating (mostly :)), and inside the walls of MS he turns that smugness up quite a bit higher.

I'm glad you enjoyed my answers; however, I am never intending to come across as smug. It is difficult to get across subtleties in an impoverished text-only medium; it has certainly been my experience that instructive and constructive criticism can easily be read as smugness. It's also been my experience that trying to deliberately make it sound less smug often just turns it into sounding like condescention. Text-based communication of technical issues is a hard problem and I do struggle with finding the right tone. Thanks for the feedback.

I don't think "smug" is quite the correct term. I meant nothing negative with my comment (and was teasing with the "mostly"). You do have an "air of authority" about your answers, but that authority is clearly earned. I enjoyed your answers on the C# mailing list quite a bit.

Great analogy and well written. We often see attempts at this style of explanation, but we usually fail at one or both of those qualities.

While I wish I saw more of this style of teaching in technical publications, it's admittedly rarely done well enough to convey the point.

I'm not sure I agree entirely with this explanation. He emphasises that this is unsafe behaviour that is protected against in safer languages - I've never heard it said that the use of pointers should be considered unsafe and there are also pointers in other languages that exclude things like multiple-inheritance and have garbage collection (which could be considered safer languages).

I like the room key analogy (almost) but I don't think it needs to be stolen and I don't think all the talk about policies and contracts is necessary at all.

It could simply say the thing you're returning is not the local variable. It is a pointer.

I have a box and instead of giving you the box I give you a key to the box. Anytime you want to use the contents of the box you must come over to the box and open it, but you can't take the box with you. If you want to put something in the box you just walk over and put it in - but here's the rub: you have no guarantees that you're the only one with a key. I could give out the key to a bunch of people so you don't know what will be in the box and who owns it.

Use with caution!

I think he means that pointers are unsafe in the same way that dynamite is unsafe. They both have legitimate uses, but you need to be very careful when you are using them. (Rather than unsafe in the sense of don't use them at all).

I like your box analogy, but I don't think it goes far enough - with C++ pointers it is not always ok to put something in the box. If the pointer is out of date or invalid there is a chance that writing to it will make the OS kill the entire process.

Languages with garbage collection don't tend to have that kind of problem, since whatever is at the memory location will stay at that location until everyone surrenders their pointers.

I agree with you for the most part. C/C++ lets you do what you want, presumably because you know what you're doing. But I don't agree that pointers are evil, and should be avoided. If you want to do any practical programming with C/C++ at all, you have to learn how to dynamically allocate memory and use pointers. Generally, pointers work as advertised. It's the cases where they work when they shouldn't that is the problem. In these cases a compiler usually warns you, so you are still covered. So in C/C++, just because it works, doesn't mean your code is right.

But I don't agree that pointers are evil, and should be avoided.

No one is saying that at all. Pointers aren't evil, but they are dangerous.

Perhaps a better analogy is a sharp chef's knife. In the right hands it's an effective and efficient tool that lets you do things quicker and just as safely as any other tool.

In the wrong hands it is dangerous to the person using it and to those around them.

There are numerous other examples: welding torches, motorbikes, explosives etc etc.

Exactly. I didn't mean to imply that pointers are evil or should be avoided. That was supposed to be the point of my dynamite analogy, but I guess the comment made elsewhere on this topic about the inadequacies of analogies holds true here.

So, for the avoidance of doubt, I believe: pointers are awesome, powerful tools and you can do some great things in C/C++ using them and I sometimes miss them (a little bit) when using other languages. But you can also do some terrible things with them - and I have done some spectacularly bad things with them in the past. But that doesn't mean they are bad - it just means that I am reckless.

^ Ditto. I thought pointers were being shown in a negative light at first, but now I see the point. In my opinion too, manual memory management is a very important part of developing highly scalable applications, but should only be done if absolutely needed - which is fortunately rarely the case nowadays as most people just develop for the Web, and can afford to throw money at the problem. But for embedded systems where resources are limited and inputs are very limited, it is still very useful. In the early years, it was C + inline assembly for further optimization - where you try your best to avoid assembly. I guess now, it's a dynamic/interpreted language, plus C/C++ for further optimization, and avoid C/C++ like the plague as much as possible.

> C/C++ lets you do what you want, presumably because you know what you're doing.

Frankly, C++ is kinda schizophrenic about it. First, it is really anal about class membership, to the extent they had to introduce friend qualifier to get around its impracticality. And then you get this.

"I've never heard it said that the use of pointers should be considered unsafe..."

Please pardon the bluntness, but that sounds like a fundamental failing of whomever you've been learning. Use of pointers in the C family of languages is unsafe. That's "unsafe" in the context of computer languages.

As for the analogy, the talk about policies and contracts is important. There's nothing in the hotel preventing you from using the key to gain unauthorized access to the room just like there's nothing in C++ to prevent you access memory you have no business trying to access.

Local variables in C and C++ are just put on a stack (i.e. the stack) and its behavior is pretty predictable. For example:

  #include <stdio.h>
  static int *foo(void)
      int i = 42;
      return &i;
  static int *bar(void)
      int i = 43;
      return &i;
  int main(void)
      int *f = foo();
      /* with next line commented out prints 42, with it, 43 */
      int i = *f;
      printf("%i\n", i);
      return 0;
Until you've made another function call that reuses that space on the stack you'll almost certainly still have valid values when accessing those local variables by address. Things are a bit vaguer in C++ where those variables may be objects which have destructors which have already been called.

Edit: Made it predictable, per caf's (correct) comment by adding:

  int i = *f;

Also compiling with heavy optimization (such as -O3) can blow a variable out of the stack and into a register that can quickly get reused, or can also inline functions. Both of this can break the perceived predictability.

In fact, here on my computer, your code example behaves differently when compiled with gcc default optimizations and when using -O3.

  [peter@laptop ~]$ gcc test.c -o test
  [...] warnings
  [peter@laptop ~]$ ./test
  [peter@laptop ~]$ gcc -O3 test.c -o test
  [...] warnings
  [peter@laptop ~]$ ./test
I'm inclined to believe in this case this is happening due to function inlining, but I haven't checked the assembly.

Edit: My post refers to the last version of your code, with the int i = *f; line in it.

It's not entirely predictable, because, for example, the compiler may have already stored the first parameter of printf (the address of "%i\n") before it gets around to dereferencing f, and it may have stored it in the same location on the stack that was used for `i` in foo and bar.

It's also possible that foo() got inlined, so that the space used for `i` in foo() ends up being used for `f` in main.

In embedded systems where interrupts are handled from user space (or the code you show is in the kernel), then it is guaranteed that your stack will be obliterated if an interrupt occurs at the exact right time, so your value will not be there.

This is not academic: I have debugged code that relied on that behavior, and would fail once in a million runs. Very frustrating to figure out.

Same in userland if you're using POSIX signals (and you haven't explicitly configured a separate sigstack)

All it takes is the user resizing their xterm and JUST the right moment and the SIGWINCH handler will happily step on your out-of-scope stack objects.

Very true.

Of course, the exact details depend on the architecture. For example, I've been doing a bunch of PIC24 microcontroller programming recently, in both C and assembly. On these, an interrupt will store the return address on the stack, obliterating the variable. However, if more dead variables are on the stack, they may be left intact, since PIC24 uses shadow-registers to stash away the users register values temporarily so that the interrupt may reuse them. That is, they are not temporarily pushed onto the stack (obliterating more dead variables), but rather stashed in shadow registers.

Actually, interrupts store two 16bit values from the stack - a 24bit instruction pointer and the lower 8bit of the STATUS register, so would destroy 32 bits worth of dead variables' values.

Standarians will point out that what you've said is implementation dependent, for the standard makes no claims as to what you are talking about other than defining the lifetime of objects. Your edit does not make it predictable beyond the specific implementation you tested it on.

It's not that predictable - try changing bar() to

  static int *bar(int dummy)
and call it with


A good analogy no doubt! But you should be careful when using analogies. Every behavior of the analogy might not be applicable to the original problem.

Analogies can only help make people "understand" something. Normal brains try to relate things to what it has learnt so far. So analogies seem to help. Analogies in themselves are not scientific explanations obviously.

A scientific/rational explanation to the question posed by the user would be - C++ makes no promise about the behavior of the program when out-of scope C++ variable is used. Obviously this explanation -a simple one is quite less dramatic.

Title is a bit misleading... but still a great answer.

200 upvotes in nine hours?! Is that a Stack Overflow record?

no, I guess many people started to understand this behavior just now. (something you don't meet when all you do is javascript and ruby/python)

Maybe I'm an elitist but I find it rather scary that people write code for a living without understanding concepts such as this. It's easy to forget what a huge service StackOverflow provides to the developer community by spreading knowledge in an easily accessible and entertaining fashion.

If those people who write code for a living never program in C++ or other languages with pointers, instead using languages like Java and Python, I wouldn't expect them to know what happens when you mess around with pointers in C++. If you don't use pointers, and your language doesn't even support pointers, why care about the behavior of pointers? Understanding the simpler concept of "references" is good enough.

> I find it rather scary that people write code for a living without understanding concepts such as this

It's scary but the industry is full of such people. In germany here it's really bad as we have 2 ways to become a (official) professional programmer:

1) Studying CS at the University 2) A 3 year apprenticeship where you learn to be a state approved "computer scientist with a specialization".

Way 2 is still pretty popular and though not all people going way 2 are bad there's not much fundamental CS theory you learn there. You work for 3 years in a company where they teach you their way of software development (and you have blocks of school lessons every few weeks). The education is pretty lacking in fundamental theory. The people only learn either .NET or Java (depending on the shop they are working in).

It's not uncommon that people can't tell you the difference between long and double (other than that one can has a decimal point). Don't start about pointers, manual memory management or things like stack frames/stack layouts. And they don't want to know that stuff. "Why should I learn that? The compiler/runtime does it for me!" is often an answer you hear.

It's great that you don't meet this since Python/Ruby were designed not to have the same problems C/C++ had.

if you can always legally access the book, which will for sure be available to you in that drawer, then the hotel key you stole must be a closure.

Applications are open for YC Winter 2023

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