In C you could have made some 'safe buffer' struct together with some safe set of functions. But people didn't do that.
In C you could have made some 'safe buffer' struct together with some safe set of functions. But people didn't do that.
Exactly.
struct buffer
{
void* data;
size_t len;
};
void smemcpy(buffer* dst, buffer* src, size_t n)
{
if(dst->len < n || src->len < n)
abort();
memcpy(dst->data, src->data, n);
}
I mean how hard is that, honestly?Buffer bugs in C don't come from the language, they come from the tendency of C programmers to value performance highly. "I just checked that the buffer is big enough, I don't need memcpy to waste cycles checking it again. I don't need that length variable sucking up memory when I know how big the buffer is." Except when you do.
Sure they do. For example, nothing in the Turing machine specification tells you how fast they are. Whether a given program will run on a Turing machine in a given amount of wall clock time is undefined.
Here's a real example with real machines. Suppose you have a program with two threads. The first thread computes data and puts it on a queue for the second thread, the second thread writes it to the filesystem. When the compute thread has computed all of the data, it sleeps for 10 seconds and then terminates the program without checking whether the write thread has actually completed writing all the data to the filesystem.
That results in undefined behavior in every real language I've ever heard of. Whether the data is all written depends on how fast the CPU is, how fast the filesystem is, what other programs are scheduled on the machine that compete for CPU and filesystem access at what priority, etc. Languages don't define any of those things so you have undefined behavior.
It reads the default symbol (see e.g. the description of a TM on Wikipedia). It's semantics are well-defined.
The semantics of TMs are well-defined. The semantics of C's "undefined behavior" is not well-defined.
This is false. All Turing Complete languages allow you to compute anything other Turing Complete languages allow you to compute, but that says nothing about encoding, or about interaction with the world. For an obvious encoding example, imagine you have a turning machine that can write only the symbols 0 and 2 to its tape. This is obviously Turing complete - it's isomorphic to a TM that writes 0 and 1, the TM doesn't care how the symbols are labelled. Now, if we ask it to encode 7 it has no trouble doing that in some encoding, but that can't ever be "111" on the tape.
When does it even make sense to partially mix byte-level representations of some data?
The buffer abstraction is necessary, but far from sufficient to build security-critical code. To do so, you also need to code in semantics somewhere, and buffers know zilch about the meaning of the data.
Incidentally, this is another reason that the language can't save you from yourself. Program termination is sometimes totally unreasonable, e.g. in real-time settings. And it's unreasonable regardless of the language. When an airplane falls out of the sky, you don't get to declare victory just because the reason it did was an uncaught out of bounds exception rather than a buffer overrun. In that context the programmer must explicitly handle the condition that the buffer is too small, regardless of the language, because "abnormal program termination" is not an acceptable outcome.
Moreover, the purpose of the above is to achieve the level of bounds checking that you get with the likes of Java. If you want to go beyond that then you need something more complicated -- maybe add separate variables to the buffer struct for buffer_len and data_len and then have different memcpy functions based on whether you want existing data in the buffer to be truncated. But there comes a point at which further complexity produces more confusion than safety.
> But there comes a point at which further complexity produces more confusion than safety.
Buffer is simply not the right abstraction for complex protocols, regardless of how "complex" operations you define.
The parsing and validation routine is where you need the buffer abstraction. If you make a mistake there then a copy function that won't let you read or write past the end of the buffer will mitigate the damage.
if(obj != null) {
obj.stuff = ...
}
I mean how hard is that exactly?
yet Java applications have null pointer exceptions all timeThere are also tools to help with it in the form of @NotNull, @Nullable, etc.
It's a completely different issue compared to actually accessing memory in an undefined way as you can in C. The bounds check you are replying to is manually creating something like that exception through the use of abort() while in java it is done always at the cost of some performance in exchange for safety.
Null is just a built-in final/sealed type(enum?) with a single value and no operations defined, like boolean is a type(enum?) with two values.
That, combined with proper type inference, would alleviate a large chunk of the boilerplate associated with statically typed languages in general, and Java in particular. (For how much Java is pushed as moving errors to compile time, there are an awful lot of things that fail only at run time that could be checked at compile time...)
Same thing with checked exceptions. A checked exception is really just an option - return foo or failure.
So while I love strong and static type systems, the issue at play here is memory safety - the very reason for why people still use C/C++ is the same reason for why Heartbleed happened.
Although I'd argue that a truly strong type system should also catch problems with pointers. The compiler should try to prove that any pointer access/arithmetic can or cannot cause undefined behavior - if it could cause undefined behavior, emit a compiler error, if it cannot prove that it won't cause undefined behavior, emit a warning and insert a runtime check.
(Still have the underlying "unsafe" functions. But group them into part of the standard library as an "unsafe" package.)
You have to use other people's code / language constructs which are underspecified and/or you don't have time to fully understand. Throw in versioning to the equation and now the problem is exponentially worse. This is why C code will always be full of memory vulns. Heartbleed is a very oversimplified example of the problems in C.
In C having such a 'safe buffer' struct would require discipline from you, in Haskell that'd be the natural order of things.
I understand that many C programmers don't do it, mind you, but how is that different from a Haskell programmer not using the proper type for a certain operation?
> value :: ConstantTime Int
But how do I obtain an "Int" from that, since I know it somehow relates to an integer type? The answer is: you must ask the implementation of ConstantTime for the value, and if it doesn't want to give it to you, it doesn't have to. Constructors can be one way.
The library author could define a number of additional operations. These operations are privileged, being written as part of the library, they can actually inspect the inner data at any time. This is the "small core" that needs to be verified.
> addInt :: ConstantTime Int -> ConstantTime Int -> ConstantTime Int > subInt :: ConstantTime Int -> ConstantTime Int -> ConstantTime Int > mulInt :: ConstantTime Int -> ConstantTime Int -> ConstantTime Int > divInt :: ConstantTime Int -> ConstantTime Int -> ConstantTime Int
Then to compute something like Euler's formula one could write a function in terms of those functions.
> newtonsMethod :: ConstantTime Int -> ConstantTime Int > newtonsMethod = ...
And one can be certain that the laws that apply to `addInt` apply to `newtonsMethod`.
This composition has wonderful benefits because if there is no branching operation defined by the library, users cannot use branches inside ConstantTime. To serially apply addition, multiplication, etc, one can write in a sort of reverse polish notation:
> square :: ConstantTime Int -> ConstantTime Int > square input = mulInt input input
Using the multiplication operator, (*), would be illegal there. And that function, square, cannot inspect the value of input. Because it can't inspect the value, it can't make choices based on that value.
Whether that merits dumping the incredible amount of man hours that have gone into OpenSSL is debateable. I'd probably say it's worth fixing OpenSSL and trying to refactor it into a more provably sound system.
I still fail to see how this is fundamentally different from a similar approach in C (but maybe that's because I don't understand Haskell at all). In C you would define your ConstantTimeInt as a struct, and define a bunch of functions that operate on this struct. Using a + or * operator is also not defined for this struct. You'd have to use
csiAdd(const struct ConstantTimeInt *addend1, const struct ConstantTimeInt *addend2, struct ConstantTimeInt *res)
In all cases it requires the developer of the library to be conscious of timing attacks, and not just use a regular int with variable-time addition. void square(const struct ConstantTimeInt *value,
struct ConstantTimeInt *result)
{
sleep(rand());
return multiply(value,value);
}
This is legal C! The ConstantTimeInt structure does not allow the composition of operations to produce safe operations from their composition!This is illegal Haskell:
square :: ConstantTime Int
-> ConstantTime Int
square = do
randomRIO (0,100) >>= threadDelay
return $ multiply value value
In Haskell, tight, small libraries can be written using impure constructs (as these ConstantTime things likely would be) and verified logically. Then, using the type system, that kernel can be used to create an amazing variety of strongly typed systems safely from a compact representation.In your envisioning of a C ConstantTime library, you would never finish writing the library. If someone wanted to perform date arithmetic, you would have to implement that in your library and check it by hand and monitor it and maintain it. You could never let your guard down on any commit that would touch an ever-expanding body of code that would have to be rigorously checked to handle increasingly complex scenarios.
For example, to implement a constant time big (but fixed size) integer multiplication operation, you would not be able to trust that some other author's composition of your functions is safe purely by its type.
For example, C is full of operations (pointer arithmetic, read from an unbounded array) that may read the inside of that struct without that code mentioning that struct in any way.
No you couldn't've. C is fundamentally unsafe language and people like you should stop pretending it isn't.
Discounting bad PHP, a huge majority of bugs are in C/C++ code. And "past popularity" isn't a good explanation, because a lot of the software is fairly recent.
So, the following isn't an opinion. It's simply a matter of observable fact. If you write in C/C++, you are far more likely to introduce security vulnerabilities than in other languages; therefore, unless there's a pressing reason to use these languages, don't.
And in the case of crypto code, one could argue that there is a very good reason to use C/C++: to prevent timing attacks.
Timing attacks work when code tries to be fast, taking shortcuts where possible. There's a reason we talk big-O instead of big-Theta: we gladly accept when an algorithm finishes early. This is, in this case, highly undesirable. Shortcuts are great, everywhere but crypto; not unlike recursion being great, unless it's embedded.
A language where every expression takes constant-time to compute seems like a solution. No short-circuiting, no variable runtimes. Don't use sorting algorithms like insertion/quicksort where best-case & worst-case are very different; use selection/merge where best-case = worst-case = average-case. Use constant-time hash functions. Caching is a very prevalent (and somewhat invisible) shortcut that needs to be solved (how can a language disable caching?).
Obviously this isn't fast but you can't have both.
Merge sort is still pretty fast. It isn't the fastest. But safe > fast when it comes to reliability and security.
That is, I don't think this exercise really shows what you think it does. Consider, if 90% of the software out there is in c/c++, and you had equal representation of language to vulnerability, then you would expect 90% of the vulnerabilities to be in c/c++. This would not mean that you are more likely to write bugs in those languages. In fact, unless I misunderstand, it would simply mean you are just as likely to have bugs there as otherwise.
Right?
1. A huge number of vulnerabilities affect C/C++ programs, almost all of which are memory based.
2. Memory-managed languages take care of this for you.
3. Therefore, C/C++ shouldn't be a default choice in domains where a managed language does just as well (a separate question).
Everything else is tangential.
But here are some reasons why your second paragraph is dangerously wrong (and why my claim was not as you characterize):
1. It only applies if we assume a uniform distribution of security effort across languages.
2. It only applies if we assume that c/c++ is being used for the same class of applications programs written in other languages, or that the applications have similar attack surfaces.
3. It only applies if we assume a uniform distribution of security effort over all code regardless of age.
and so on...
And also, 90% of code -- especially relatively recent code -- is not written in c/c++.
Especially if you follow that with the claim of "If you write in C/C++, you are far more likely to introduce security vulnerabilities than in other languages." In order for that claim to stick, you have to show not just that there are more CVEs against c/c++ than other languages, but that there is the same effort spent in attacking non c/c++ programs. Right? (Or, am I misreading a claim on that?)
Sure, if you reduce that to "memory vulnerabilities", it is a true statement. However, you did not make that reduction. As you point out in your counter #2, there are plenty of other vulnerabilities out there. What makes you think people are more likely to avoid those than they are memory vulnerabilities in c/c++?
As for the 90% of code not being c/c++, what is the point? Unless you can show that they receive the same level of attack as the c/c++, you can not really use that to claim that they are inherently more secure. Worse, your throwing of php under the bus just shows that recent languages don't do enough to prevent security mistakes.
Heaven help you if you throw in XSS and friends. Suddenly one of the darlings of the tech industry at the moment, javascript, is rocketing to the top of the list for security blunders.