This is not likely to be a popular opinion among professional programmers, but I feel it needs to be said.
The excuse that computers are complex and that testing to remove all of these flaws would take a prohibitive amount of time just doesn’t hold water. I understand that security vulnerabilities are different from outright bugs: security vulnerabilities are only problems because people deliberately manipulate the system in unanticipated ways. Bugs happen when people inadvertently manipulate the system in unanticipated ways. Some of these ways are incredibly sophisticated and may be infeasible to anticipate. However, having supported computers for the past few years, I’ve seen bugs that should have been anticipated, and zero testing would be required in order to do so.
The problem with testing is that the people testing usually understand the software well enough to know how it is supposed to work, or they are given a few basic things to try, but they don’t have time to test a program with heavy use. Luckily, testing is not the problem.
The problem is that in many cases I’ve seen (and I’ve come to suspect most cases across the software industry) the input and output footprints of code modules are not documented (and if your code contains comments laying out the pseudocode structure, I consider you very lucky). From an engineering standpoint, the input footprint of a system or subsystem describes the conditions the system assumes to be true in order to work effectively. The output footprint describes what effects (including side-effects) the system has or could have on its environment, including if the input footprint is not fulfilled. Those aren’t the official names; I’ve just been calling them that.
The thing about input and output footprints is that you don’t have to know everything that could possibly happen, and you don’t have to test everything. The input footprint will tell you what could go wrong with a code, and the output footprint will tell you what problems the code could cause. As a simple example, if the input footprint is “the computer is intact”, you don’t have to make a list of all the things that could physically break the computer. At most, all you’d need is the range of temperatures, pressures, and other environmental conditions which interact with the material properties of the computer to cause its structure to break down. If your output footprint is “returns the sum of two numbers, and leaves the memory it used for the computation locked,” then you don’t need to test it to know that it will result in a memory leak, eat up your RAM, and ultimately crash your computer if you use it too much without restarting. When you put multiple modules together, you can check a module’s output footprint against the input footprint of another module, and make sure they don’t interfere. It’s much easier than testing, and it simplifies complex interactions enough to avoid problems.
Failure modes are also important. All programs should have error messages that should at least describe what step or module failed, if not why it failed. That’s what input footprints are for.
I know that optimized code often calls for modularity to be sacrificed, and I appreciate that. Even though it’s far more robust to have programs that keep track of each step of the way and let you go back to any previous step, that’s not always possible while maintaining efficiency. However, that is still no excuse for not documenting the code properly. If a flaw isn’t going to be fixed, fine, but it absolutely must be documented in such a way that the end user can avoid it, recognize it, or know what to do now that they’ve run into it.
All programmers should be starting with pseudocode, making things as modular as possible (while maintaining efficiency), commenting each section of code not just by what it does but by how it works, and by its input and output footprints (bonus points for commenting each line), and putting in clarifying error messages. Quality code contains its own documentation.
“But you don’t understand, software is really complicated,” software developers say.
“Yes,” I reply. “That’s why code hygiene is very important. If you practice it up front, it makes everything easier down the line. It saves a great deal of time and effort troubleshooting and patching problems, overhauling code bases, and having to tell customers that a use case they considered obvious would break the system because nobody bothered to clean up the variable in between uses. Then you can deal with the really complicated problems and not have to worry about the simple, stupid ones.” This applies to user-friendliness in addition to simple code functionality. Clarification mindset is indispensable.
Preventing unauthorized access and control is much tougher because people can be very skilled in the use of hacking mindset to exploit the overlooked properties of systems. Security mindset entails paying attention to those overlooked properties, and documenting their input and output footprints to predict what conditions could allow systems to be hacked. While the extra effectiveness of security mindset is indeed more expensive in terms of effort and resources, it becomes much easier if systems are documented clearly and comprehensively. Then people can make informed decisions about what efforts they are willing to go through to obtain greater security, instead of periodically having to patch flaws as they are discovered (and then patch the patches, and watch as the system gets more and more tangled and difficult to effectively support).
The world of software could be so much better than it is, but first we have to change the way people think. That’s why I decided to teach myself how to write and support paradigms: mental software is the most effective tool humanity will ever have.