Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/fuzz/Makefile.am
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) $(CODE_COVERAGE_CXXFLAGS) $(FUZZING_CFLAGS)

if ENABLE_FUZZING
noinst_PROGRAMS = terminal_parser_fuzzer terminal_fuzzer
noinst_PROGRAMS = terminal_parser_fuzzer terminal_fuzzer terminal_host_fuzzer statesync_fuzzer fragment_fuzzer crypto_fuzzer
endif

terminal_parser_fuzzer_CPPFLAGS = -I$(top_srcdir)/
Expand All @@ -11,3 +11,24 @@ terminal_parser_fuzzer_SOURCES = terminal_parser_fuzzer.cc
terminal_fuzzer_CPPFLAGS = -I$(top_srcdir)/
terminal_fuzzer_LDADD = ../terminal/libmoshterminal.a ../util/libmoshutil.a ../statesync/libmoshstatesync.a ../protobufs/libmoshprotos.a $(TINFO_LIBS) $(protobuf_LIBS)
terminal_fuzzer_SOURCES = terminal_fuzzer.cc

# Host-output escape-sequence emulator + renderer (the path a server's output
# actually drives; the older terminal_fuzzer only covers the keystroke path).
terminal_host_fuzzer_CPPFLAGS = -I$(top_srcdir)/
terminal_host_fuzzer_LDADD = ../terminal/libmoshterminal.a ../util/libmoshutil.a ../statesync/libmoshstatesync.a ../protobufs/libmoshprotos.a $(TINFO_LIBS) $(protobuf_LIBS)
terminal_host_fuzzer_SOURCES = terminal_host_fuzzer.cc

# Post-decryption protobuf state-sync apply path (client and server halves).
statesync_fuzzer_CPPFLAGS = -I$(top_srcdir)/ $(CRYPTO_CFLAGS)
statesync_fuzzer_LDADD = ../statesync/libmoshstatesync.a ../terminal/libmoshterminal.a ../crypto/libmoshcrypto.a ../protobufs/libmoshprotos.a ../util/libmoshutil.a $(TINFO_LIBS) $(protobuf_LIBS) $(CRYPTO_LIBS)
statesync_fuzzer_SOURCES = statesync_fuzzer.cc

# Fragment header parse + FragmentAssembly reassembly state machine.
fragment_fuzzer_CPPFLAGS = -I$(top_srcdir)/ $(CRYPTO_CFLAGS)
fragment_fuzzer_LDADD = ../network/libmoshnetwork.a ../crypto/libmoshcrypto.a ../protobufs/libmoshprotos.a ../util/libmoshutil.a $(protobuf_LIBS) $(CRYPTO_LIBS)
fragment_fuzzer_SOURCES = fragment_fuzzer.cc

# Keyless pre-auth surface: raw datagram -> Session::decrypt (AES-OCB).
crypto_fuzzer_CPPFLAGS = -I$(top_srcdir)/ $(CRYPTO_CFLAGS)
crypto_fuzzer_LDADD = ../crypto/libmoshcrypto.a ../util/libmoshutil.a $(CRYPTO_LIBS)
crypto_fuzzer_SOURCES = crypto_fuzzer.cc
32 changes: 32 additions & 0 deletions src/fuzz/crypto_fuzzer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Fuzzes the KEYLESS, pre-authentication surface: a raw UDP datagram fed
straight into Session::decrypt() with a fixed key the "attacker" does not
know. This is exactly what an internet attacker who cannot forge the OCB tag
can reach. In practice no fuzz input will pass the AES-OCB tag (~2^-128), so
this exercises the reject path and every bit of buffer handling that runs
before and around the tag check: the length arithmetic, the nonce parse, the
memcpy into the fixed receive buffers, and ae_decrypt() over the bundled
AES-OCB implementation. Each such datagram must be rejected with a
Crypto::CryptoException and must never read or write out of bounds -- any
AddressSanitizer report here is a remote, keyless memory-safety bug. */

#include <cstddef>
#include <cstdint>
#include <string>

#include "src/crypto/crypto.h"

extern "C" int LLVMFuzzerTestOneInput( const uint8_t* data, size_t size )
{
/* A fixed all-zero key (canonical 22-char base64). The point is not the key's
secrecy but that decrypt() handles arbitrary ciphertext safely. */
static const std::string key_b64( 22, 'A' );
static Crypto::Session session( ( Crypto::Base64Key( key_b64 ) ) );

try {
session.decrypt( reinterpret_cast<const char*>( data ), size );
} catch ( const Crypto::CryptoException& ) {
/* forged / short / oversized ciphertext: correctly rejected */
}

return 0;
}
58 changes: 58 additions & 0 deletions src/fuzz/fragment_fuzzer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* Fuzzes fragment reassembly: the wire-format Fragment header parse and the
FragmentAssembly state machine that reassembles an Instruction from a
counterparty's datagrams (parse header -> collect fragments -> uncompress
-> parse protobuf). This is reached right after OCB authentication.

The input is used two ways: (1) parsed directly as a single wire Fragment,
and (2) chopped into a stream of (fragment_num, final, contents) records fed
to one FragmentAssembly to exercise the dedup / out-of-order-final /
completion logic. Malformed input must be dropped via a
Crypto::CryptoException, not abort. */

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>

#include "src/crypto/crypto.h"
#include "src/network/transportfragment.h"

extern "C" int LLVMFuzzerTestOneInput( const uint8_t* data, size_t size )
{
const char* p = reinterpret_cast<const char*>( data );

/* 1. parse the raw bytes as a single wire Fragment */
try {
Network::Fragment frag( std::string( p, size ) );
Network::FragmentAssembly single;
if ( single.add_fragment( frag ) ) {
single.get_assembly();
}
} catch ( const Crypto::CryptoException& ) {
}

/* 2. drive one FragmentAssembly with a stream of structured fragments that
all share an id (i.e. one logical packet) */
try {
Network::FragmentAssembly assembly;
size_t pos = 0;
while ( pos + 4 <= size ) {
const uint16_t num = ( static_cast<uint8_t>( data[pos] ) | ( static_cast<uint8_t>( data[pos + 1] ) << 8 ) )
& 0x7fff;
const bool final = data[pos + 2] & 1;
const size_t want = data[pos + 3];
pos += 4;
const size_t take = std::min( want, size - pos );
std::string contents( p + pos, take );
pos += take;

Network::Fragment frag( 0 /* shared id */, num, final, contents );
if ( assembly.add_fragment( frag ) ) {
assembly.get_assembly();
}
}
} catch ( const Crypto::CryptoException& ) {
}

return 0;
}
38 changes: 38 additions & 0 deletions src/fuzz/statesync_fuzzer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Fuzzes the post-decryption state-sync input path: the serialized protobuf
"diff" a counterparty sends, applied by the client (Complete::apply_string,
host state) and the server (UserStream::apply_string, user state). This is
the exact attacker-controlled surface reached after a datagram is OCB-
authenticated, reassembled and decompressed. Malformed input is expected to
be dropped via a Crypto::CryptoException; anything else (an abort or another
uncaught exception) is a bug. */

#include <cstddef>
#include <cstdint>
#include <string>

#include "src/crypto/crypto.h"
#include "src/statesync/completeterminal.h"
#include "src/statesync/user.h"

extern "C" int LLVMFuzzerTestOneInput( const uint8_t* data, size_t size )
{
const std::string diff( reinterpret_cast<const char*>( data ), size );

/* client applying host state from the server */
try {
Terminal::Complete complete( 80, 24 );
complete.apply_string( diff );
} catch ( const Crypto::CryptoException& ) {
/* malformed counterparty input, dropped */
}

/* server applying user state from the client */
try {
Network::UserStream user;
user.apply_string( diff );
} catch ( const Crypto::CryptoException& ) {
/* malformed counterparty input, dropped */
}

return 0;
}
33 changes: 33 additions & 0 deletions src/fuzz/terminal_host_fuzzer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* Fuzzes the host-output terminal emulator: the escape/control-sequence byte
stream a server sends to a client, driven through Complete::act(std::string)
-- the path that actually reaches terminaldispatcher, terminalfunctions
(CUP/DCH/ICH/scroll/ECH/...), OSC dispatch (including OSC 8 hyperlinks) and
the framebuffer mutators, and then the new_frame() diff/render.

The existing terminal_fuzzer feeds bytes as Parser::UserByte, which exercises
the keystroke-translation path instead; this harness covers the emulator and
renderer that a malicious server's output actually drives. Any crash is a
bug. */

#include <cstddef>
#include <cstdint>
#include <string>

#include "src/statesync/completeterminal.h"
#include "src/terminal/terminaldisplay.h"
#include "src/terminal/terminalframebuffer.h"

extern "C" int LLVMFuzzerTestOneInput( const uint8_t* data, size_t size )
{
Terminal::Complete complete( 80, 24 );

/* drive the emulator with the fuzzed host byte stream */
complete.act( std::string( reinterpret_cast<const char*>( data ), size ) );

/* exercise the frame diff/render against a blank reference framebuffer */
Terminal::Display display( false );
Terminal::Framebuffer blank( 80, 24 );
display.new_frame( true, blank, complete.get_fb() );

return 0;
}