In-memory database option #634
Replies: 1 comment 5 replies
-
First of all, thank you for your detailed explanation and in depth analysis @kamakazikamikaze! The results do look very promising but I also agree with you that the complexity will increase. As for the implementation, I agree with you in all but one point, which is the extra Session arguments. The idea behind this is to not clutter Looking forward to your PR! |
Beta Was this translation helpful? Give feedback.
-
The idea
While waiting to get approval to submit a PR to close out #624, I've been working on another feature that allows the user to specify the use of an in-memory database with eventual persistence to disk.
Why?
A need for this arose when we found ourselves restricted to using a station with a slower HDD and a low-IOPS NAS. Since SQLite creates files to lock the database, calls via NFS/CIFS induced a lot of overhead, and the HDD experienced high latency when we attempted to switch over to it. Tests slowed down considerably in both scenarios. Granted, the situation we were under for this very specific scenario is unlikely to be encountered by others, but even for SSDs there is a significant performance boost possible if the API is not the choke point.
Overview of the implementation
I am unable to share the source code for now but the gist of the design is as follows:
boofuzz/sessions.py -> Session
adds three new optional parametersdb_in_memory
: Boolean (defaultFalse
) to indicate that an in-memory database is desired. Documentation notes that this is discouraged except in scenarios where IOPS for a local disk is a significant choke point.db_in_memory_flush_timer
: Int (default180
) to indicate the number of seconds between flushes to disk. We will refer to this as 'X'db_in_memory_check_timer
: Int (default5
) to indicate the number of seconds the thread that does flushes should wait between checks for whether it is supposed to exit. We will refer to this as 'Y'boofuzz\fuzz_logger_db.py -> FuzzLoggerDb
adds the same three parameters (renamed) as aboveSession
passes the parameters toFuzzLoggerDb
FuzzLoggerDb
creates the in-memory database and retains the target filename where the persistent database is saved toSession._main_fuzz_loop
makes a call toself._db_logger
to begin the synchronization thread when fuzzing beginsFuzzLoggerDb
initializes a "token bucket" semaphore which only allows acquiring when the token is replenished every X seconds. The thread enters a while-loop on condition of the termination flag being set toFalse
.threading.Timer
to replenish the bucketTimer
callback essentially replaces theTimer
with a new instanceFuzzLoggerDb
tracks when the last disk flush took place and the time of the last written testcase. Flushing to disk is skipped in the loop if the latest flush has occurred after the most recent testcase to avoid unnecessary disk utilizationFuzzLoggerDb
blocks while trying to acquire the semaphore. A timeout of Y is set. If the timeout is reached the loop starts back at the top.If the semaphore is acquired, a second in-memory database is created.
.backup()
releases the GIL, so this thread is suspended to allow testcases to continue runningSession
will instructFuzzLoggerDb
to stop the sync thread when fuzzing completes (added tofinally
statement for graceful close)When fuzzing terminates,
FuzzLoggerDb
will stop the loop on the next semaphore.acquire(...)
timeout and perform a final flush to diskPerformance
Using the same TCP Echo Server from #622 and the definitions for FTP from the Quickstart section of the documentation, I've compared ~20 minutes of runtime performance between on-disk and in-memory databases for Linux. This was done on an Ubuntu 22.04 VM with 4 i7-10810U cores, 8GB RAM, and an SK Hynix PC711 SSD.
As you can see, there's nearly a 6x performance boost from using an in-memory database. We reach the same amount of testcases in 3 minutes with an in-memory database as we do after almost 20 minutes with an on-disk database. There are certainly some oddities though with performance being rather volatile. Though I've attempted to minimize blocking of the fuzzing thread, some calls may be "stealing" a little too much time before releasing the GIL. Further testing and refactoring will be needed.
I should note that memory consumption grows linearly with the number of testcases which is not ideal for long-running fuzzing sessions. You can easily surpass 1GB of RAM after 10 minutes. I will look into minimizing memory footprint by "rotating" the database like logfiles and combining them at the end of the fuzzing session.
Interest?
I hope to submit this as a separate PR once approval comes through. I'm unsure though if the maintainers would see any value from this. While the increased rate of completed testcases may seem enticing, the additional threads means more complexity to be aware of, and chunking out the database for the sake of reducing the memory footprint may make it unappealing to accept. If the maintainers aren't interested I can still submit a PR for it to be rejected so others may reference/consider its use, otherwise I may put it into a gist.
Additional performance graphics
Average performance of in-memory database without partitioning
FTP with TCP echo server, 10 runs, 15 minutes, flush to disk every 60 seconds with checks every 15 seconds to terminate
Performance obviously suffers over time as the database grows with hundreds of thousands of testcases. This would indicate that partitioning would be more ideal.
Average performance of in-memory database with partitioning to disk
FTP with TCP echo server, 10 runs, 15 minutes, flush to disk every 60 seconds with checks every 15 seconds to terminate
Performance is much more promising. It's still rather volatile with random dips to <100 testcases/sec.
Average performance of the various options, cumulative testcase view
Partitioning the database has a linear performance, whereas having it completely in-memory suffers as it grows. The drawback of partitioning is that stopping the session requires "zipping" all the partitions together, which essentially just delays our original issue to the end. On an SSD, it took 4 minutes to combine all the partitions.
How does this translate to real-world performance?
Replaced the TCP echo server with the jtpereyda/boofuzz-ftp repo, using uFTP and
ProcessMonitorLocal
(and slight changes toboofuzz/utils/dbugger_thread_simple.py
). Four erroneous responses were caught at the start which resulted in the initial delays that you see. May want to make the wait time for process spawning to be more configurable, but as you can see the performance is still significantly better using an in-memory database than it would be on-disk.Beta Was this translation helpful? Give feedback.
All reactions