As many others have noted, there was a transition earlier this year with AI tools that can find bugs in code such as APSW. Before those reports used to seem plausible but on closer investigation would show they got the wrong end of the stick, and weren't useful.
Now they find real bugs. Lots of real bugs. To see examples look at issues 624, 627, and 637.
It isn't just APSW - for example see the stream of reports for SQLite.
What does the code look like?
There is a Python object that corresponds to each SQLite object. For example this is an APSW Connection and the corresponding SQLite object. For each SQLite function on the object, there is a corresponding Python function or attribute. That code is written in C and looks like:
- Check if the object is closed and exception
- Parse the function arguments so that they get converted from Python types (abstract and concrete) into C types as used by SQLite
- Check if this is an async connection, and if so forward to the corresponding async controller which sends the call to a worker thread
- Acquire the database mutex to prevent concurrent usage conflicts
- Further conversion of types, or initiate usage of more complex ones like buffers
- Make the SQLite call
- Do housekeeping saving and updating values stored in the connection
- Convert the SQLite results back to Python objects
- Release the database mutex
- Return a Python result
Every Python call in all those conversions and usages can error because Python is a dynamic language, and those callbacks can do anything such as mutating objects, closing them, raising exceptions, taking a long time, running garbage collection, releasing the interpreter lock, etc.
Adding a typical method is around:
- 6 lines describing the arguments
- 1 line handling async
- 1 line to acquire and 1 to release the mutex
- 10 lines of conversions, including trivial error handling
- 1 line to call SQLite
- 50 lines to handle all the errors that could occur
- 20 lines of documentation
- 10 lines of test cases using the functionality
- 100 lines of test cases doing things wrong and making sure they are handled
Some are far more complex than this, involving callbacks, dealing with the Python interpreter lock, multiple SQLite calls needed, etc. It gives an idea of how little code there is to do the call, and just how much extra is needed for errors - an order of magnitude or more.
But APSW shouldn't have bugs ...
All code has bugs. It is somewhat hard to measure, because bugs are found while writing code, when testing is added, by pre-release testing, by users and customers, and importantly exist but are not recorded. If a bug is never encountered, or encountered but never realised, does it exist? This doesn't even address what a bug is. Depending on what you count, studies find bugs ranging from one every 10 lines, to one every few hundred lines.
The software development process tries to get zero bugs. APSW goes further than most, and here are the things that are done:
- Attention to detail paid during decades of development
- Maximum warnings from compilers
- Maximum warnings from Python
- Use a debug configuration of SQLite that does a lot more verification
- Use a debug configuration of Python that does a lot more verification
- Several APSW tools that produce a debug configuration - for example every SQLite call is annotated if it requires a mutex, and that is checked at runtime
- Use of compiler sanitizers which check every memory access for misuse
- Test suite that aims for 100% coverage of the C and Python code. It gets around 99%.
- The test suite includes code inspection tests - for example that every method and attribute of connections, cursors, blobs etc have a check for them being closed. There is separate code that calls after close to check again.
- Runtime testing with all the configurations, such as compilers, Python versions, 32/64 bit, Windows / Linux / MacOS. This is almost 200 combinations done during development repeatedly, and another 80 or so done for the release process
- An open development process and bug tracker so anyone else finding something can find and report them
The irony is that the simplest thing is by far the hardest to test. Every C programmer learns about calling the memory allocator and getting NULL (a glorified zero) back indicating you have run out of memory. The vast majority of APSW memory consumption directly and indirectly is in small dribs and drabs - 16 bytes here, 64 bytes there, such as adding an item to a list. In practise it is highly improbable that the specific lines of code would ever get an allocation failure. And even if they did, nothing useful could happen because reacting to it such as writing a log message would need more than that amount of memory!
If you look at most projects that have error handling code, realise that you'll see the error detection then handling pattern, but that that code has never actually run in practise because the tests can't make small memory allocations fail in that code or the code it calls. Asking a Python tuple for one of its members could fail, but making that happen involves writing a modified tuple class to do that, and orchestrating it with the test code - a lot of extra work.
So APSW has a fault injection framework to make sure all the code actually runs. It has a list of APIs, the way in which they fail (some return -1, some zero etc) and a mechanism for each call site to function normally, or simulate the appropriate error. That results in several thousand failures executed during testing. It includes one of the most difficult categories - failures in failure handling code while failures are happening.
So APSW shouldn't have bugs.
APSW does have bugs
Those reports make it clear bugs exist and are plentiful. I have now fixed almost 200 individual items, and more still await.
You won't hit them
Most cases wouldn't pass through the development and test process above, and if they did someone else would have spotted them by now.
Consequences
The vast majority are in error handling paths, often when more than one error is happening. Code using APSW is unlikely to rely on repeatedly hitting failure paths for its own correct functioning. Each hit would most likely result in the wrong exception type being raised, because multiple are in play, and worst case SystemError is raised. Some cases result in incorrect reference counting, especially saying something is still in use when it isn't. The majority of those cases would have been objects that lived for the whole process anyway such as small numbers, string constants, and loaded class definitions.
A very small number would result in a process crash due to using the NULL mentioned earlier. I feel bad about those.
Why weren't the bugs already caught?
They should have been, and that is why I fixed all of them by hand. That lets me understand what the bug was, but far more importantly why it wasn't caught, and what could also be updated to detect or prevent that entire class of bug.
Runtime
The strength of the testing is code that actually runs. The validation and tooling are good at catching problems in code that actually executes. I already have to synthesize the majority of failures because getting them to happen in practise is impossible or impractical.
Human inspection aka code review tries to address what happens without running the code, but it is very difficult to see errors in what you wrote because it looks normal. It turns out the AIs do a really good job at this.
Test bugs
Just as the main code can have bugs, test code can too. For example several Python memory APIs were mis-classified in the fault injection as setting the exception when they do not. You would have got SystemError as a result. There is a small amount of test code that tests the test code, but there is only so far I can practically go.
Complexity
Two different API families with completely different semantics, usage models, error handling mechanisms, threading models, handling of dynamism, and backgrounds means there is inherent complexity that can't be escaped.
When APSW started there were several Python SQLite wrappers. Now there is one in the standard library, APSW, and some limited functionality use case focussed ones. APSW is the only one providing the complete SQLite API and common extensions. Complexity is hard work.
Incomplete
I mentioned 99% coverage earlier. It gets incrementally harder and harder to get the remaining cases. So some lines of code have not been run.
Why did I fix them?
Because I want to learn. I need to understand what happened and why. I want software to be better, and easier to develop. I have provided a lot of feedback to the folks whose tools produced the reports. And I have learned.
The fixes allow two things:
Free Threading
Free threading - doing away with the global interpreter lock - will eventually become the default Python. C extensions have long relied on the global lock to know that things couldn't change between lines of code, with the Python C API providing macros and optimisations (eg avoid reference counting updates) for simplicity and performance.
APSW would work fine with free threading, but would not survive deliberately hostile code. I will only mark it as free thread safe once hostile attack code can't cause problems. Other code bases have a lower bar, but this is a database and that matters.
Many of the reports overlap with what hostile free threaded code could do, hence fixing them.
Translation
The code could be translated into other programming languages, specifications, or even a test suite. But to be best for that, every single little detail must be right. Better input will provide better output.
Final thoughts
SQLite is a great database, and Python is a great programming language. APSW unites them, and does the best job possible.
Sorry about the bugs - they are being fixed.