11 ms·
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
by yaakov34 15y ago
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.
- ANH 15y agoFWIW, 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.
- yaakov34 15y agoNot 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.
- ANH 15y agoI 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.
- kragen 15y agoMaybe you can set the new guy to writing code in Lua for a while, and do pre-checkin reviews on all of his code.
- yaakov34 15y agoIt 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) http://en.wikipedia.org/wiki/Psycho_(film)
- yaakov34 15y agoOne 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.
- jordanb 15y agoC++ 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.
- yaakov34 15y agoRight; 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...
- sliverstorm 15y agoC/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.
- scott_s 15y agoIt 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.
- T-hawk 15y agoyou 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.
- leif 15y agoC++ 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.
- yaakov34 15y agoI 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.
- leif 15y agogcc 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; v.push_back(&a); } int main(void) { vector<int *> v; bar(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?
- kragen 15y agoThe 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.
- yaakov34 15y agoSorry, 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.
- alok-g 15y agoThis 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?