]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3605] Prepare existing code for fuzzing
authorAndrei Pavel <andrei@isc.org>
Mon, 14 Oct 2024 07:05:31 +0000 (10:05 +0300)
committerAndrei Pavel <andrei@isc.org>
Wed, 23 Oct 2024 13:37:31 +0000 (16:37 +0300)
- Separate ENABLE_AFL into ENABLE_FUZZING and HAVE_AFL.
- Add the --disable-unicode flag required in the oss-fuzz container.
- Add checking of support for C++17.
- Make Kea compile with afl++.
- Rotate ports in `getServerPort()` functions under an env var.
- Fix some destruction issues that would result in crashes when fuzzing.
- Add some checks in the UnixControlClient that prevent some crashes when fuzzing.
- Add `isc::util::isSocket()` function.
- Change `isc::util::file::Path` to not append a trailing slash to allow
  chained calls of `parentPath()`.
- Add `isc::util::file::TemporaryDirectory` useful when fuzzing.

20 files changed:
configure.ac
m4macros/ax_cpp14.m4
m4macros/ax_cpp17.m4 [new file with mode: 0644]
m4macros/ax_cpp20.m4
src/bin/dhcp4/dhcp4_srv.cc
src/bin/dhcp4/dhcp4_srv.h
src/bin/dhcp6/dhcp6_srv.cc
src/bin/dhcp6/dhcp6_srv.h
src/lib/dhcpsrv/Makefile.am
src/lib/dhcpsrv/allocator.cc
src/lib/dhcpsrv/lease_mgr_factory.cc
src/lib/dhcpsrv/lease_mgr_factory.h
src/lib/dhcpsrv/packet-fuzzer.cc [moved from src/lib/dhcpsrv/fuzz.cc with 75% similarity]
src/lib/dhcpsrv/packet-fuzzer.h [moved from src/lib/dhcpsrv/fuzz.h with 84% similarity]
src/lib/testutils/Makefile.am
src/lib/testutils/unix_control_client.cc
src/lib/testutils/unix_control_client.h
src/lib/util/filesystem.cc
src/lib/util/filesystem.h
src/lib/util/tests/filesystem_unittests.cc

index 5bafc2cb63576fc4cadfbd2596cc4cbf33be4e0b..59dbdee819af9aeadc0adf91d39e967322e96ade 100644 (file)
@@ -204,6 +204,9 @@ AM_CONDITIONAL(USE_CLANGPP, test "X${CLANGPP}" = "Xyes")
 # Check for C++14 features support
 AX_ISC_CPP14
 
+# Check for C++17 features support
+AX_ISC_CPP17
+
 # Check for C++20 compiler support.
 AX_ISC_CPP20
 
@@ -346,6 +349,15 @@ if less_than "5" "$CXX_DUMP_VERSION"; then
        CPPP="$CPPP -P"
 fi
 
+# Kea does not support unicode aka wide character strings. Some systems force it
+# by default in headers. Provide a way to explicitly disable it.
+AC_ARG_ENABLE(unicode,
+  [AS_HELP_STRING([--disable-unicode], [Explicitly disable unicode])],
+  [case "${enableval}" in
+   yes) AC_MSG_ERROR(["You are trying to explicitly enable unicode. Kea does not support unicode."]) ;;
+   no) KEA_CXXFLAGS="${KEA_CXXFLAGS} -U_UNICODE -UUNICODE" ;;
+   esac])
+
 case "$host" in
 *-solaris*)
         MULTITHREADING_FLAG=-pthreads
@@ -676,7 +688,7 @@ int main() {
 # usable or not.
 # Let's be optimistic and assume it is by testing only the negative case.
 if test "${usable_regex}" = 'no'; then
-  AC_MSG_ERROR([Need proper regex functionality.])]
+  AC_MSG_ERROR([Need proper regex functionality.])
 fi
 
 # Check for NETCONF support. If NETCONF was enabled in the build, and this check
@@ -1405,25 +1417,48 @@ if test "x$VALGRIND" != "xno"; then
    found_valgrind="found"
 fi
 
+AC_MSG_CHECKING([for fuzzing])
 AC_ARG_ENABLE([fuzzing],
-  [AS_HELP_STRING([--enable-fuzzing],
-  [indicates that the code will be built with AFL (American Fuzzy Lop) support.
-   Code built this way is unusable as a regular server. [default=no]])],
-  [enable_fuzzing=$enableval], [enable_fuzzing=no])
-AM_CONDITIONAL([ENABLE_AFL], [test x$enable_fuzzing != xno])
-
-if test "x$enable_fuzzing" != "xno" ; then
-    AC_DEFINE([ENABLE_AFL], [1], [AFL fuzzing was enabled.])
-    AC_MSG_CHECKING([for AFL enabled compiler])
-    AC_COMPILE_IFELSE([AC_LANG_PROGRAM([],
-                                          [#ifndef __AFL_COMPILER
-                                           #error AFL compiler required
-                                           #endif
-                                          ])],
-                         [AC_MSG_RESULT([yes])],
-                         [AC_MSG_ERROR([set CXX to afl-clang-fast++ when --enable-fuzzing is used])])
+  [AS_HELP_STRING(
+    [--enable-fuzzing[[=mode]]],
+    [indicates that the code will be built for fuzzing purposes.
+     Code built this way is unusable as a regular server.
+     Mode can be ci or standalone. [default=no]])],
+  [if test ! "${CPP17_SUPPORTED}"; then
+     AC_MSG_RESULT("no. Fuzzing requires C++17 support.")
+     AC_MSG_ERROR("Fuzzing requires C++17 support.")
+   fi
+   enable_fuzzing=${enableval}],
+  [enable_fuzzing=no]
+)
+AM_CONDITIONAL([FUZZING], [test "${enable_fuzzing}" != 'no'])
+AM_CONDITIONAL([FUZZING_IN_CI], [test "${enable_fuzzing}" = 'ci'])
+fuzzing_enabled='no'
+if test "${enable_fuzzing}" != 'no' ; then
+  fuzzing_enabled='yes'
+  AC_DEFINE([FUZZING], [true], [Fuzzing enabled.])
+
+  if test "${enable_fuzzing}" = 'ci'; then
+    fuzzing_enabled='yes, running in CI'
+    AC_DEFINE([FUZZING_IN_CI], [true], [Fuzzing running in CI.])
+  fi
+fi
+AC_MSG_RESULT(${fuzzing_enabled})
+
+# Check for AFL.
+AC_MSG_CHECKING([for AFL compiler])
+AC_COMPILE_IFELSE([AC_LANG_PROGRAM([],
+                                   [#ifndef __AFL_COMPILER
+                                      #error AFL compiler required
+                                    #endif
+                                   ])],
+                  [have_afl='yes'],
+                  [have_afl='no'])
+AC_MSG_RESULT([${have_afl}])
+AM_CONDITIONAL([HAVE_AFL], [test "${have_afl}" = 'yes'])
+if test "${have_afl}" = 'yes'; then
+  AC_DEFINE([HAVE_AFL], [true], [AFL compiler enabled.])
 fi
-
 
 # Check for optreset in unistd.h. On BSD systems the optreset is
 # used to reset the state of getopt() function. Resetting its state
@@ -2148,7 +2183,8 @@ Developer:
   Generate Messages Files:   $enable_generate_messages
   Perfdhcp:                  $enable_perfdhcp
   Kea-shell:                 $shell_report
-  Enable fuzzing:            $enable_fuzzing
+  Fuzzing:                   $fuzzing_enabled
+  AFL:                       $have_afl
 
 END
 
index f061c57bc6efae68f243bc5f42b91491fc599e3b..12cc00efc4dcc860c66026ba3b63a9b9bd98d4dc 100644 (file)
@@ -4,6 +4,8 @@ CXX_SAVED=$CXX
 feature=
 for retry in "none" "--std=c++14" "--std=c++1y" "fail"; do
         if test "$retry" = "fail"; then
+                AC_MSG_CHECKING([c++14 support])
+                AC_MSG_RESULT([no])
                 AC_MSG_ERROR([$feature (a C++14 feature) is not supported])
         fi
         if test "$retry" != "none"; then
@@ -250,4 +252,7 @@ for retry in "none" "--std=c++14" "--std=c++1y" "fail"; do
         break
 done
 
+AC_MSG_CHECKING([c++14 support])
+AC_MSG_RESULT([yes])
+
 ])
diff --git a/m4macros/ax_cpp17.m4 b/m4macros/ax_cpp17.m4
new file mode 100644 (file)
index 0000000..b7d6dc5
--- /dev/null
@@ -0,0 +1,23 @@
+AC_DEFUN([AX_ISC_CPP17], [
+    AC_MSG_CHECKING([c++17 support])
+
+    # Save flags.
+    CPPFLAGS_SAVED="${CPPFLAGS}"
+
+    # Provide -std=c++17 flag temporarily.
+    CPPFLAGS="${CPPFLAGS} -std=c++17"
+
+    # Check that the filesystem library is supported.
+    AC_LINK_IFELSE(
+      [AC_LANG_PROGRAM(
+        [#include <filesystem>],
+        [std::filesystem::path cwd = std::filesystem::current_path();]
+      )],
+      [AC_MSG_RESULT([yes])
+       CPP17_SUPPORTED=true],
+      [AC_MSG_RESULT([no])
+       CPP17_SUPPORTED=false])
+
+    # Restore flags.
+    CPPFLAGS="${CPPFLAGS_SAVED}"
+])
index da116c9dd4777c7f5a2dad809cd3d0aa573b00df..80f8ede9531d7f210b0575770631ee396ecdb8ff 100644 (file)
@@ -1,9 +1,8 @@
 AC_DEFUN([AX_ISC_CPP20], [
-    AC_MSG_CHECKING(c++20 support)
+    AC_MSG_CHECKING([c++20 support])
 
     # Save flags.
     CPPFLAGS_SAVED="${CPPFLAGS}"
-    LIBS_SAVED="${LIBS}"
 
     # Provide -std=c++20 flag temporarily.
     CPPFLAGS="${CPPFLAGS} -std=c++20"
index 3aeb22a83c81f45a49648eb85b730c3737f67f38..5904d49a76fd888417a4900bd843c9738528a377 100644 (file)
 #include <dhcpsrv/cfg_shared_networks.h>
 #include <dhcpsrv/cfg_subnets4.h>
 #include <dhcpsrv/dhcpsrv_exceptions.h>
-#include <dhcpsrv/fuzz.h>
 #include <dhcpsrv/host_data_source_factory.h>
 #include <dhcpsrv/host_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/ncr_generator.h>
+#include <dhcpsrv/packet-fuzzer.h>
 #include <dhcpsrv/resource_handler.h>
 #include <dhcpsrv/shared_network.h>
 #include <dhcpsrv/subnet.h>
@@ -55,6 +55,7 @@
 #include <stats/stats_mgr.h>
 #include <util/encode/encode.h>
 #include <util/str.h>
+#include <log/interprocess/interprocess_sync_file.h>
 #include <log/logger.h>
 #include <cryptolink/cryptolink.h>
 #include <process/cfgrpt/config_report.h>
@@ -65,6 +66,8 @@
 #include <boost/pointer_cast.hpp>
 #include <boost/shared_ptr.hpp>
 
+
+#include <chrono>
 #include <functional>
 #include <iomanip>
 #include <set>
@@ -78,9 +81,11 @@ using namespace isc::dhcp;
 using namespace isc::dhcp_ddns;
 using namespace isc::hooks;
 using namespace isc::log;
+using namespace isc::log::interprocess;
 using namespace isc::stats;
 using namespace isc::util;
 using namespace std;
+using namespace std::chrono_literals;
 namespace ph = std::placeholders;
 
 namespace {
@@ -1122,10 +1127,25 @@ Dhcpv4Srv::earlyGHRLookup(const Pkt4Ptr& query,
 
 int
 Dhcpv4Srv::run() {
-#ifdef ENABLE_AFL
+#ifdef HAVE_AFL
+    // Get the values of the environment variables used to control the
+    // fuzzing.
+
+    // Specfies the interface to be used to pass packets from AFL to Kea.
+    const char* interface = getenv("KEA_AFL_INTERFACE");
+    if (!interface) {
+        isc_throw(FuzzInitFail, "no fuzzing interface has been set");
+    }
+
+    // The address on the interface to be used.
+    const char* address = getenv("KEA_AFL_ADDRESS");
+    if (!address) {
+        isc_throw(FuzzInitFail, "no fuzzing address has been set");
+    }
+
     // Set up structures needed for fuzzing.
-    Fuzz fuzzer(4, server_port_);
-    //
+    PacketFuzzer fuzzer(4, server_port_, interface, address);
+
     // The next line is needed as a signature for AFL to recognize that we are
     // running persistent fuzzing.  This has to be in the main image file.
     while (__AFL_LOOP(fuzzer.maxLoopCount())) {
@@ -1134,7 +1154,7 @@ Dhcpv4Srv::run() {
         fuzzer.transfer();
 #else
     while (!shutdown_) {
-#endif // ENABLE_AFL
+#endif // HAVE_AFL
         try {
             runOne();
             // Handle events registered by hooks using external IOService objects.
@@ -5159,6 +5179,39 @@ void Dhcpv4Srv::discardPackets() {
     HooksManager::clearParkingLots();
 }
 
+uint16_t Dhcpv4Srv::getServerPort() const {
+    char const* const randomize(getenv("KEA_DHCP4_FUZZING_RANDOMIZE_PORT"));
+    if (randomize) {
+        InterprocessSyncFile file("kea-dhcp4-fuzzing-randomize-port");
+        InterprocessSyncLocker locker(file);
+        while (!locker.lock()) {
+            this_thread::sleep_for(1s);
+        }
+        fstream port_file;
+        port_file.open("/tmp/port4.txt", ios::in);
+        string line;
+        int port;
+        getline(port_file, line);
+        port_file.close();
+        if (line.empty()) {
+            port = 2000;
+        } else {
+            port = stoi(line);
+            if (port < 3000) {
+                ++port;
+            } else {
+                port = 2000;
+            }
+        }
+        port_file.open("/tmp/port4.txt", ios::out | ios::trunc);
+        port_file << to_string(port) << endl;
+        port_file.close();
+        locker.unlock();
+        return port;
+    }
+    return server_port_;
+}
+
 std::list<std::list<std::string>> Dhcpv4Srv::jsonPathsToRedact() const {
     static std::list<std::list<std::string>> const list({
         {"config-control", "config-databases", "[]"},
index 895ce10f0bd2a86f727d064ec54b206eb0b7bcce..23ea4fbddddbea56b15ccf0deb8b8c8e649fae82 100644 (file)
@@ -454,9 +454,7 @@ public:
     /// for testing purposes only.
     ///
     /// @return UDP port on which server should listen.
-    uint16_t getServerPort() const {
-        return (server_port_);
-    }
+    uint16_t getServerPort() const;
 
     /// @brief Return bool value indicating that broadcast flags should be set
     /// on sockets.
index 62590a2a5ea27aa2143537c6025177967a56d7af..fe1617e9980b8074ebe9941dbfd3bb01b85a7d5c 100644 (file)
@@ -15,7 +15,6 @@
 #include <dhcp/docsis3_option_defs.h>
 #include <dhcp/duid.h>
 #include <dhcp/duid_factory.h>
-#include <dhcpsrv/fuzz.h>
 #include <dhcp/iface_mgr.h>
 #include <dhcp/libdhcp++.h>
 #include <dhcp/option6_addrlst.h>
@@ -41,6 +40,7 @@
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/ncr_generator.h>
+#include <dhcpsrv/packet-fuzzer.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/subnet_selector.h>
 #include <dhcpsrv/utils.h>
@@ -54,6 +54,7 @@
 #include <util/encode/encode.h>
 #include <util/pointer_util.h>
 #include <util/range_utilities.h>
+#include <log/interprocess/interprocess_sync_file.h>
 #include <log/logger.h>
 #include <cryptolink/cryptolink.h>
 #include <process/cfgrpt/config_report.h>
@@ -82,6 +83,7 @@ using namespace isc::dhcp;
 using namespace isc::dhcp_ddns;
 using namespace isc::hooks;
 using namespace isc::log;
+using namespace isc::log::interprocess;
 using namespace isc::stats;
 using namespace isc::util;
 using namespace std;
@@ -597,10 +599,25 @@ Dhcpv6Srv::initContext(AllocEngine::ClientContext6& ctx, bool& drop) {
 
 int
 Dhcpv6Srv::run() {
-#ifdef ENABLE_AFL
+#ifdef HAVE_AFL
+    // Get the values of the environment variables used to control the
+    // fuzzing.
+
+    // Specfies the interface to be used to pass packets from AFL to Kea.
+    const char* interface = getenv("KEA_AFL_INTERFACE");
+    if (!interface) {
+        isc_throw(FuzzInitFail, "no fuzzing interface has been set");
+    }
+
+    // The address on the interface to be used.
+    const char* address = getenv("KEA_AFL_ADDRESS");
+    if (!address) {
+        isc_throw(FuzzInitFail, "no fuzzing address has been set");
+    }
+
     // Set up structures needed for fuzzing.
-    Fuzz fuzzer(6, server_port_);
-    //
+    PacketFuzzer fuzzer(6, server_port_, interface, address);
+
     // The next line is needed as a signature for AFL to recognize that we are
     // running persistent fuzzing.  This has to be in the main image file.
     while (__AFL_LOOP(fuzzer.maxLoopCount())) {
@@ -609,7 +626,7 @@ Dhcpv6Srv::run() {
         fuzzer.transfer();
 #else
     while (!shutdown_) {
-#endif // ENABLE_AFL
+#endif // HAVE_AFL
         try {
             runOne();
             // Handle events registered by hooks using external IOService objects.
@@ -4902,6 +4919,39 @@ void Dhcpv6Srv::discardPackets() {
     HooksManager::clearParkingLots();
 }
 
+uint16_t Dhcpv6Srv::getServerPort() const {
+    char const* const randomize(getenv("KEA_DHCP6_FUZZING_RANDOMIZE_PORT"));
+    if (randomize) {
+        InterprocessSyncFile file("kea-dhcp6-fuzzing-randomize-port");
+        InterprocessSyncLocker locker(file);
+        while (!locker.lock()) {
+            this_thread::sleep_for(1s);
+        }
+        fstream port_file;
+        port_file.open("/tmp/port6.txt", ios::in);
+        string line;
+        int port;
+        getline(port_file, line);
+        port_file.close();
+        if (line.empty()) {
+            port = 2000;
+        } else {
+            port = stoi(line);
+            if (port < 3000) {
+                ++port;
+            } else {
+                port = 2000;
+            }
+        }
+        port_file.open("/tmp/port6.txt", ios::out | ios::trunc);
+        port_file << to_string(port) << endl;
+        port_file.close();
+        locker.unlock();
+        return port;
+    }
+    return server_port_;
+}
+
 /// @todo This logic to be modified if we decide to support infinite lease times.
 void
 Dhcpv6Srv::setTeeTimes(uint32_t preferred_lft, const Subnet6Ptr& subnet, Option6IAPtr& resp) {
index 62363c0983b6fed643a87300cae64bc6c3f3da8b..489ce8aad9ce195ce24580d4afc6813d067e3e8d 100644 (file)
@@ -240,9 +240,7 @@ public:
     /// for testing purposes only.
     ///
     /// @return UDP port on which server should listen.
-    uint16_t getServerPort() const {
-        return (server_port_);
-    }
+    uint16_t getServerPort() const;
     //@}
 
     /// @brief Starts DHCP_DDNS client IO if DDNS updates are enabled.
index 7d42abeb2a17f1ee5d59fc496733801abb0da436..55781747295ca42e9a2adc8dc37b1812a586217a 100644 (file)
@@ -172,11 +172,11 @@ libkea_dhcpsrv_la_SOURCES += parsers/simple_parser4.h
 libkea_dhcpsrv_la_SOURCES += parsers/simple_parser6.cc
 libkea_dhcpsrv_la_SOURCES += parsers/simple_parser6.h
 
-if ENABLE_AFL
-libkea_dhcpsrv_la_SOURCES += fuzz.cc fuzz.h
+if FUZZING
+libkea_dhcpsrv_la_SOURCES += packet-fuzzer.cc packet-fuzzer.h
 libkea_dhcpsrv_la_SOURCES += fuzz_log.cc fuzz_log.h
 libkea_dhcpsrv_la_SOURCES += fuzz_messages.cc fuzz_messages.h
-endif
+endif  # FUZZING
 
 libkea_dhcpsrv_la_CXXFLAGS = $(AM_CXXFLAGS)
 libkea_dhcpsrv_la_CPPFLAGS = $(AM_CPPFLAGS)
@@ -350,9 +350,9 @@ libkea_dhcpsrv_include_HEADERS = \
        utils.h \
        writable_host_data_source.h
 
-if ENABLE_AFL
+if FUZZING
 libkea_dhcpsrv_include_HEADERS += \
-       fuzz.h \
+       packet-fuzzer.h \
        fuzz_log.h \
        fuzz_messages.h
 endif
index 5925689d20aa25e67e297af3c8bf99503aab4a65..5175ff985da9ca535c8280df1405780e7750753a 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2022-2023 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2022-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -31,8 +31,7 @@ Allocator::~Allocator() {
         return;
     }
     // Remove the callbacks.
-    auto& lease_mgr = LeaseMgrFactory::instance();
-    lease_mgr.unregisterCallbacks(subnet_id_, pool_type_);
+    LeaseMgrFactory::instance().unregisterCallbacks(subnet_id_, pool_type_);
 }
 
 bool
index 40f3388d45f24045fc95d0be9243663982c3d121..44ab45d49a4974627d03412e33a6546c6c5dc7ad 100644 (file)
@@ -92,6 +92,7 @@ LeaseMgrFactory::destroy() {
     if (getLeaseMgrPtr()) {
         LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CLOSE_DB)
             .arg(getLeaseMgrPtr()->getType());
+        getLeaseMgrPtr().reset();
     }
     getLeaseMgrPtr().reset();
 }
index ba8ea7bede841e683161f4a5e79dee7c9c8106e1..d71707700bd35b7f8da58836730fa8790153a63a 100644 (file)
@@ -42,6 +42,10 @@ public:
 ///        user-supplied backends (so that there is no need to modify the code).
 class LeaseMgrFactory {
 public:
+    ~LeaseMgrFactory() {
+        destroy();
+    }
+
     /// @brief Create an instance of a lease manager.
     ///
     /// Each database backend has its own lease manager type.  This static
similarity index 75%
rename from src/lib/dhcpsrv/fuzz.cc
rename to src/lib/dhcpsrv/packet-fuzzer.cc
index a62d7a6798b17f2f9ad645bd96308188ec71c36b..573b527e72bd25a98c63a389f09ba6bd43e6b81b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2016-2019  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -6,14 +6,10 @@
 
 #include <config.h>
 
-#ifdef ENABLE_AFL
-
-#ifndef __AFL_LOOP
-#error To use American Fuzzy Lop you have to set CXX to afl-clang-fast++
-#endif
+#ifdef FUZZING
 
 #include <dhcp/dhcp6.h>
-#include <dhcpsrv/fuzz.h>
+#include <dhcpsrv/packet-fuzzer.h>
 #include <dhcpsrv/fuzz_log.h>
 
 #include <boost/lexical_cast.hpp>
@@ -23,6 +19,7 @@
 #include <string.h>
 #include <signal.h>
 
+#include <algorithm>
 #include <iostream>
 #include <sstream>
 #include <fstream>
@@ -32,39 +29,29 @@ using namespace isc;
 using namespace isc::dhcp;
 using namespace std;
 
-// Constants defined in the Fuzz class definition.
-constexpr size_t        Fuzz::BUFFER_SIZE;
-constexpr size_t        Fuzz::MAX_SEND_SIZE;
-constexpr long          Fuzz::MAX_LOOP_COUNT;
+// Constants defined in the PacketFuzzer class definition.
+constexpr size_t        PacketFuzzer::BUFFER_SIZE;
+constexpr size_t        PacketFuzzer::MAX_SEND_SIZE;
+constexpr long          PacketFuzzer::MAX_LOOP_COUNT;
 
 // Constructor
-Fuzz::Fuzz(int ipversion, uint16_t port) :
-    loop_max_(MAX_LOOP_COUNT), sockaddr_len_(0), sockaddr_ptr_(nullptr),
-    sockfd_(-1) {
+PacketFuzzer::PacketFuzzer(int const ipversion,
+                           uint16_t const port,
+                           string const interface,
+                           string const address)
+    : loop_max_(MAX_LOOP_COUNT), sockaddr_len_(0), sockaddr_ptr_(nullptr), sockfd_(-1) {
 
     try {
         stringstream reason;    // Used to construct exception messages
 
-        // Get the values of the environment variables used to control the
-        // fuzzing.
-
-        // Specfies the interface to be used to pass packets from AFL to Kea.
-        const char* interface = getenv("KEA_AFL_INTERFACE");
-        if (! interface) {
-            isc_throw(FuzzInitFail, "no fuzzing interface has been set");
-        }
-
-        // The address on the interface to be used.
-        const char* address = getenv("KEA_AFL_ADDRESS");
-        if (address == 0) {
-            isc_throw(FuzzInitFail, "no fuzzing address has been set");
-        }
-
         // Number of Kea packet-read loops before Kea exits and AFL starts a
         // new instance.  This is optional: the default is set by the constant
         // MAX_LOOP_COUNT.
-        const char *loop_max_ptr = getenv("KEA_AFL_LOOP_MAX");
-        if (loop_max_ptr != 0) {
+        const char *loop_max_ptr(nullptr);
+#ifdef HAVE_AFL
+        loop_max_ptr = getenv("KEA_AFL_LOOP_MAX");
+#endif
+        if (loop_max_ptr) {
             try {
                 loop_max_ = boost::lexical_cast<long>(loop_max_ptr);
             } catch (const boost::bad_lexical_cast&) {
@@ -81,7 +68,7 @@ Fuzz::Fuzz(int ipversion, uint16_t port) :
         }
 
         // Set up address structures used to route the packets from AFL to Kea.
-        createAddressStructures(ipversion, interface, address, port);
+        createAddressStructures(ipversion, port, interface, address);
 
         // Create the socket through which packets read from stdin will be sent
         // to the port on which Kea is listening.  This is closed in the
@@ -105,24 +92,26 @@ Fuzz::Fuzz(int ipversion, uint16_t port) :
 }
 
 // Destructor
-Fuzz::~Fuzz() {
+PacketFuzzer::~PacketFuzzer() {
     static_cast<void>(close(sockfd_));
 }
 
 // Set up address structures.
 void
-Fuzz::createAddressStructures(int ipversion, const char* interface,
-                              const char* address, uint16_t port) {
+PacketFuzzer::createAddressStructures(int const ipversion,
+                                      uint16_t const port,
+                                      string const interface,
+                                      string const address) {
     stringstream reason;    // Used in error messages
 
     // Set up the appropriate data structure depending on the address given.
-    if ((ipversion == 6) && (strstr(address, ":") != NULL)) {
+    if (ipversion == 6 && address.find(":") != string::npos) {
         // Expecting IPv6 and the address contains a colon, so assume it is an
         // an IPv6 address.
         memset(&servaddr6_, 0, sizeof (servaddr6_));
 
         servaddr6_.sin6_family = AF_INET6;
-        if (inet_pton(AF_INET6, address, &servaddr6_.sin6_addr) != 1) {
+        if (inet_pton(AF_INET6, address.c_str(), &servaddr6_.sin6_addr) != 1) {
             reason << "inet_pton() failed: can't convert "
                    << address << " to an IPv6 address" << endl;
             isc_throw(FuzzInitFail, reason.str());
@@ -130,7 +119,7 @@ Fuzz::createAddressStructures(int ipversion, const char* interface,
         servaddr6_.sin6_port = htons(port);
 
         // Interface ID is needed for IPv6 address structures.
-        servaddr6_.sin6_scope_id = if_nametoindex(interface);
+        servaddr6_.sin6_scope_id = if_nametoindex(interface.c_str());
         if (servaddr6_.sin6_scope_id == 0) {
             reason << "error retrieving interface ID for "
                    << interface << ": " << strerror(errno);
@@ -140,14 +129,14 @@ Fuzz::createAddressStructures(int ipversion, const char* interface,
         sockaddr_ptr_ = reinterpret_cast<sockaddr*>(&servaddr6_);
         sockaddr_len_ = sizeof(servaddr6_);
 
-    } else if ((ipversion == 4) && (strstr(address, ".") != NULL)) {
+    } else if (ipversion == 4 && address.find(".") != string::npos) {
         // Expecting an IPv4 address and it contains a dot, so assume it is.
         // This check is done after the IPv6 check, as it is possible for an
         // IPv4 address to be embedded in an IPv6 one.
         memset(&servaddr4_, 0, sizeof(servaddr4_));
 
         servaddr4_.sin_family = AF_INET;
-        if (inet_pton(AF_INET, address, &servaddr4_.sin_addr) != 1) {
+        if (inet_pton(AF_INET, address.c_str(), &servaddr4_.sin_addr) != 1) {
             reason << "inet_pton() failed: can't convert "
                    << address << " to an IPv6 address" << endl;
             isc_throw(FuzzInitFail, reason.str());
@@ -166,16 +155,26 @@ Fuzz::createAddressStructures(int ipversion, const char* interface,
 
 }
 
+void
+PacketFuzzer::transfer() const {
+    // Read from stdin.  Just return if nothing is read (or there is an error)
+    // and hope that this does not cause a hang.
+    uint8_t buf[BUFFER_SIZE];
+    ssize_t const length(read(0, buf, sizeof(buf)));
+
+    transfer(&buf[0], length);
+}
 
 // This is the main fuzzing function. It receives data from fuzzing engine over
 // stdin and then sends it to the configured UDP socket.
 void
-Fuzz::transfer(void) const {
-
-    // Read from stdin.  Just return if nothing is read (or there is an error)
-    // and hope that this does not cause a hang.
+PacketFuzzer::transfer(uint8_t const* data, size_t size) const {
     char buf[BUFFER_SIZE];
-    ssize_t length = read(0, buf, sizeof(buf));
+    ssize_t const length(size);
+
+    if (data) {
+        memcpy(&buf[0], data, min(BUFFER_SIZE, size));
+    }
 
     // Save the errno in case there was an error because if debugging is
     // enabled, the following LOG_DEBUG call may destroy its value.
@@ -189,12 +188,12 @@ Fuzz::transfer(void) const {
         size_t send_len = (length < MAX_SEND_SIZE) ? length : MAX_SEND_SIZE;
         ssize_t sent = sendto(sockfd_, buf, send_len, 0, sockaddr_ptr_,
                               sockaddr_len_);
-        if (sent > 0) {
-            LOG_DEBUG(fuzz_logger, FUZZ_DBG_TRACE_DETAIL, FUZZ_SEND).arg(sent);
+        if (sent < 0) {
+            LOG_ERROR(fuzz_logger, FUZZ_SEND_ERROR).arg(strerror(errno));
         } else if (sent != length) {
             LOG_WARN(fuzz_logger, FUZZ_SHORT_SEND).arg(length).arg(sent);
         } else {
-            LOG_ERROR(fuzz_logger, FUZZ_SEND_ERROR).arg(strerror(errno));
+            LOG_DEBUG(fuzz_logger, FUZZ_DBG_TRACE_DETAIL, FUZZ_SEND).arg(sent);
         }
     } else {
         // Read did not get any bytes.  A zero-length read (EOF) may have been
@@ -203,7 +202,6 @@ Fuzz::transfer(void) const {
             LOG_ERROR(fuzz_logger, FUZZ_READ_FAIL).arg(strerror(errnum));
         }
     }
-
 }
 
-#endif  // ENABLE_AFL
+#endif  // FUZZING
similarity index 84%
rename from src/lib/dhcpsrv/fuzz.h
rename to src/lib/dhcpsrv/packet-fuzzer.h
index c71e6c5387e8ea4842d05b07e21e4a5186106713..428e325cdab2fc724d091e862d28a956c6de406b 100644 (file)
@@ -1,13 +1,13 @@
-// Copyright (C) 2016-2019  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2016-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-#ifndef FUZZ_H
-#define FUZZ_H
+#ifndef DHCPSRV_PACKET_FUZZER_H
+#define DHCPSRV_PACKET_FUZZER_H
 
-#ifdef ENABLE_AFL
+#ifdef FUZZING
 
 #include <exceptions/exceptions.h>
 
@@ -22,7 +22,6 @@
 #include <thread>
 
 namespace isc {
-    
 
 /// @brief AFL Fuzzing
 ///
@@ -35,12 +34,12 @@ namespace isc {
 /// is listening.  Kea then reads the data from that port and processes it
 /// in the usual way.
 ///
-/// The Fuzz class handles the transfer of data between AFL and Kea.  After
+/// The PacketFuzzer class handles the transfer of data between AFL and Kea.  After
 /// suitable initialization, its transfer() method is called in the main
 /// processing loop, right before Kea waits for input. The method handles the
 /// read from stdin and the write to the selected address port.
 
-class Fuzz {
+class PacketFuzzer {
 public:
     /// @brief size of the buffer used to transfer data between AFL and Kea.
     ///
@@ -65,7 +64,6 @@ public:
     /// environment variable KEA_AFL_LOOP_MAX.
     static constexpr long MAX_LOOP_COUNT = 1000;
 
-
     /// @brief Constructor
     ///
     /// Sets up data structures to access the address/port being used to
@@ -75,20 +73,24 @@ public:
     ///                  server responds to.
     /// @param port      Port on which the server is listening, and hence the
     ///                  port to which the fuzzer will send input from AFL.
-    Fuzz(int ipversion, uint16_t port);
+    PacketFuzzer(int const ipversion,
+                 uint16_t const port,
+                 std::string const interface,
+                 std::string const address);
 
     /// @brief Destructor
     ///
     /// Closes the socket used for transferring data from stdin to the selected
     /// interface.
-    ~Fuzz();
+    ~PacketFuzzer();
 
     /// @brief Transfer Data
     ///
     /// Called immediately prior to Kea reading data, this reads stdin (where
     /// AFL will have sent the packet being tested) and copies the data to the
     /// interface on which Kea is listening.
-    void transfer(void) const;
+    void transfer() const;
+    void transfer(uint8_t const* data, size_t size) const;
 
     /// @brief Return Max Loop Count
     ///
@@ -117,8 +119,10 @@ private:
     ///
     /// @throws FuzzInitFail Thrown if the address is not in the expected
     ///                      format.
-    void createAddressStructures(int ipversion, const char* interface,
-                                 const char* address, uint16_t port);
+    void createAddressStructures(int const ipversion,
+                                 uint16_t const port,
+                                 std::string const interface,
+                                 std::string const address);
 
     // Other member variables.
     long                loop_max_;      //< Maximum number of loop iterations
@@ -127,18 +131,17 @@ private:
     struct sockaddr_in  servaddr4_;     //< IPv6 address information
     struct sockaddr_in6 servaddr6_;     //< IPv6 address information
     int                 sockfd_;        //< Socket used to transfer data
-};
-
+};  // class PacketFuzzer
 
 /// @brief Exception thrown if fuzzing initialization fails.
 class FuzzInitFail : public Exception {
 public:
     FuzzInitFail(const char* file, size_t line, const char* what) :
         isc::Exception(file, line, what) { };
-};
+};  // class FuzzInitFail
 
-}
+}  // namespace isc
 
-#endif // ENABLE_AFL
+#endif  // FUZZING
 
-#endif // FUZZ_H
+#endif  // DHCPSRV_PACKET_FUZZER_H
index 30332da54810bd7c83ec99291ba48c83605d7f9b..34942b5158ed746b3497a5ca0e519f0dbdb00238 100644 (file)
@@ -17,8 +17,11 @@ libkea_testutils_la_SOURCES += user_context_utils.cc user_context_utils.h
 libkea_testutils_la_SOURCES += gtest_utils.h
 libkea_testutils_la_SOURCES += multi_threading_utils.h
 libkea_testutils_la_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+libkea_testutils_la_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+
 libkea_testutils_la_LIBADD  = $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 libkea_testutils_la_LIBADD += $(top_builddir)/src/lib/dns/libkea-dns++.la
+libkea_testutils_la_LIBADD += $(GTEST_LDADD)
 endif
 
 # Include common libraries being used by shell-based tests.
index f0f8dfaaab3fd2a53629b8077617c8f7ac88e4b5..65cca400084960d1b685eebf3fa2d10d4f8d0d37 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2019 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -73,6 +73,10 @@ bool UnixControlClient::connectToServer(const std::string& socket_path) {
 }
 
 bool UnixControlClient::sendCommand(const std::string& command) {
+    if (socket_fd_ < 0) {
+        ADD_FAILURE() << "send command with closed socket";
+        return (false);
+    }
     // Send command
     int bytes_sent = send(socket_fd_, command.c_str(), command.length(), 0);
     if (bytes_sent < command.length()) {
@@ -118,13 +122,21 @@ bool UnixControlClient::getResponse(std::string& response,
 }
 
 int UnixControlClient::selectCheck(const unsigned int timeout_sec) {
+    if (socket_fd_ < 0) {
+        ADD_FAILURE() << "select check with closed socket";
+        return -1;
+    }
+    if (socket_fd_ > 1023) {
+        ADD_FAILURE() << "select check with out of bound socket";
+        return -1;
+    }
     int maxfd = 0;
 
     fd_set read_fds;
     FD_ZERO(&read_fds);
 
     // Add this socket to listening set
-    FD_SET(socket_fd_,  &read_fds);
+    FD_SET(socket_fd_, &read_fds);
     maxfd = socket_fd_;
 
     struct timeval select_timeout;
@@ -134,6 +146,6 @@ int UnixControlClient::selectCheck(const unsigned int timeout_sec) {
     return (select(maxfd + 1, &read_fds, NULL, NULL, &select_timeout));
 }
 
-};
-};
-};
+}
+}
+}
index 225c9491bfd596b2266cf888d8981294dbaeb73c..2c7e665e3bcfc52e1ab9ed26fbe4d620272c67f2 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -59,8 +59,8 @@ public:
     int socket_fd_;
 };
 
-}; // end of isc::dhcp::test namespace
-}; // end of isc::dhcp namespace
-}; // end of isc namespace
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
 
 #endif // UNIX_CONTROL_CLIENT_H
index cc0cc03d0c1014a2f54d6c87c096e83c34938c40..4c0c3ecd27507358edca390ebaa2fb8fe9e25d5c 100644 (file)
 #include <util/filesystem.h>
 #include <util/str.h>
 
-#include <algorithm>
-#include <cctype>
-#include <cerrno>
-#include <cstring>
+#include <cstdio>
+#include <cstdlib>
 #include <fstream>
-#include <iostream>
 #include <string>
 
+#include <dirent.h>
 #include <fcntl.h>
 
 using namespace isc::util::str;
@@ -76,6 +74,15 @@ Umask::~Umask() {
     umask(orig_umask_);
 }
 
+bool
+isSocket(string const& path) {
+    struct stat statbuf;
+    if (::stat(path.c_str(), &statbuf) < 0) {
+        return (false);
+    }
+    return ((statbuf.st_mode & S_IFMT) == S_IFSOCK);
+}
+
 Path::Path(string const& full_name) {
     if (!full_name.empty()) {
         bool dir_present = false;
@@ -84,9 +91,9 @@ Path::Path(string const& full_name) {
         if (last_slash != string::npos) {
             // Found the last slash, so extract directory component and
             // set where the scan for the last_dot should terminate.
-            parent_path_ = full_name.substr(0, last_slash + 1);
+            parent_path_ = full_name.substr(0, last_slash);
             if (last_slash == full_name.size()) {
-                // The entire string was a directory, so exit not and don't
+                // The entire string was a directory, so exit and don't
                 // do any more searching.
                 return;
             }
@@ -119,7 +126,7 @@ Path::Path(string const& full_name) {
 
 string
 Path::str() const {
-    return (parent_path_ + stem_ + extension_);
+    return (parent_path_ + ((parent_path_.empty() || parent_path_ == "/") ? string() : "/") + stem_ + extension_);
 }
 
 string
@@ -163,14 +170,47 @@ Path::replaceParentPath(string const& replacement) {
     string const trimmed_replacement(trim(replacement));
     if (trimmed_replacement.empty()) {
         parent_path_ = string();
-    } else if (trimmed_replacement.at(trimmed_replacement.size() - 1) == '/') {
+    } else if (trimmed_replacement == "/") {
         parent_path_ = trimmed_replacement;
+    } else if (trimmed_replacement.at(trimmed_replacement.size() - 1) == '/') {
+        parent_path_ = trimmed_replacement.substr(0, trimmed_replacement.size() - 1);
     } else {
-        parent_path_ = trimmed_replacement + '/';
+        parent_path_ = trimmed_replacement;
     }
     return (*this);
 }
 
+TemporaryDirectory::TemporaryDirectory() {
+    char dir[]("/tmp/kea-tmpdir-XXXXXX");
+    char const* dir_name = mkdtemp(dir);
+    if(!dir_name) {
+        isc_throw(Unexpected, "mkdtemp failed " << dir << ": " << strerror(errno));
+    }
+    dir_name_ = string(dir_name);
+}
+
+TemporaryDirectory::~TemporaryDirectory() {
+    rmdir(dir_name_.c_str());
+    DIR *dir(opendir(dir_name_.c_str()));
+    struct dirent *i;
+    string filepath;
+
+    while ((i = readdir(dir))) {
+        if (strcmp(i->d_name, ".") == 0 || strcmp(i->d_name, "..") == 0) {
+            continue;
+        }
+
+        filepath = dir_name_ + '/' + i->d_name;
+        remove(filepath.c_str());
+    }
+    closedir(dir);
+    rmdir(dir_name_.c_str());
+}
+
+string TemporaryDirectory::dirName() {
+    return dir_name_;
+}
+
 }  // namespace file
 }  // namespace util
 }  // namespace isc
index 0305fa4d1c9dfc25d52f7b18530f88a0e8017f4d..2f8a681917f7e0cb288629976c1e4119428d5a79 100644 (file)
@@ -14,7 +14,7 @@ namespace isc {
 namespace util {
 namespace file {
 
-/// \brief Get the content of a regular file.
+/// @brief Get the content of a regular file.
 ///
 /// \param file_name The file name.
 ///
@@ -23,7 +23,7 @@ namespace file {
 std::string
 getContent(const std::string& file_name);
 
-/// \brief Check if there is a file or directory at the given path.
+/// @brief Check if there is a file or directory at the given path.
 ///
 /// \param path The path being checked.
 ///
@@ -31,7 +31,7 @@ getContent(const std::string& file_name);
 bool
 exists(const std::string& path);
 
-/// \brief Check if there is a directory at the given path.
+/// @brief Check if there is a directory at the given path.
 ///
 /// \param path The path being checked.
 ///
@@ -40,7 +40,7 @@ exists(const std::string& path);
 bool
 isDir(const std::string& path);
 
-/// \brief Check if there is a file at the given path.
+/// @brief Check if there is a file at the given path.
 ///
 /// \param path The path being checked.
 ///
@@ -49,66 +49,69 @@ isDir(const std::string& path);
 bool
 isFile(const std::string& path);
 
-/// \brief RAII device to limit access of created files.
+/// @brief RAII device to limit access of created files.
 struct Umask {
-    /// \brief Constructor
+    /// @brief Constructor
     ///
     /// Set wanted bits in umask.
     Umask(mode_t mask);
 
-    /// \brief Destructor.
+    /// @brief Destructor.
     ///
     /// Restore umask.
     ~Umask();
 
 private:
-    /// \brief Original umask.
+    /// @brief Original umask.
     mode_t orig_umask_;
 };
 
-/// \brief Paths on a filesystem
+bool
+isSocket(const std::string& path);
+
+/// @brief Paths on a filesystem
 struct Path {
-    /// \brief Constructor
+    /// @brief Constructor
     ///
     /// Splits the full name into components.
     Path(std::string const& path);
 
-    /// \brief Get the path in textual format.
+    /// @brief Get the path in textual format.
     ///
     /// Counterpart for std::filesystem::path::string.
     ///
     /// \return stored filename.
     std::string str() const;
 
-    /// \brief Get the parent path.
+    /// @brief Get the parent path.
     ///
     /// Counterpart for std::filesystem::path::parent_path.
     ///
     /// \return parent path of current path.
     std::string parentPath() const;
 
-    /// \brief Get the base name of the file without the extension.
+    /// @brief Get the base name of the file without the extension.
     ///
     /// Counterpart for std::filesystem::path::stem.
     ///
     /// \return the base name of current path without the extension.
     std::string stem() const;
 
-    /// \brief Get the extension of the file.
+    /// @brief Get the extension of the file.
     ///
     /// Counterpart for std::filesystem::path::extension.
     ///
     /// \return extension of current path.
     std::string extension() const;
 
-    /// \brief Get the name of the file, extension included.
+    /// @brief Get the name of the file, extension included.
     ///
     /// Counterpart for std::filesystem::path::filename.
     ///
     /// \return name + extension of current path.
     std::string filename() const;
 
-    /// \brief Identifies the extension in {replacement}, trims it, and
+    /// @brief Identifies the extension in {replacement}, trims it, and
     /// replaces this instance's extension with it.
     ///
     /// Counterpart for std::filesystem::path::replace_extension.
@@ -121,7 +124,7 @@ struct Path {
     /// \return The current instance after the replacement was done.
     Path& replaceExtension(std::string const& replacement = std::string());
 
-    /// \brief Trims {replacement} and replaces this instance's parent path with
+    /// @brief Trims {replacement} and replaces this instance's parent path with
     /// it.
     ///
     /// The change is done in the members and {this} is returned to allow call
@@ -133,16 +136,24 @@ struct Path {
     Path& replaceParentPath(std::string const& replacement = std::string());
 
 private:
-    /// \brief Parent path.
+    /// @brief Parent path.
     std::string parent_path_;
 
-    /// \brief Stem.
+    /// @brief Stem.
     std::string stem_;
 
-    /// \brief File name extension.
+    /// @brief File name extension.
     std::string extension_;
 };
 
+struct TemporaryDirectory {
+    TemporaryDirectory();
+    ~TemporaryDirectory();
+    std::string dirName();
+private:
+    std::string dir_name_;
+};
+
 }  // namespace file
 }  // namespace util
 }  // namespace isc
index 28d9513f1ccd6569fb9cae0a83f8537826dbbb5e..9a5020c6d72dc139fca7c8a24d469d114ef637fd 100644 (file)
@@ -85,7 +85,8 @@ TEST_F(FileUtilTest, umask) {
 TEST(PathTest, components) {
     // Complete name
     Path fname("/alpha/beta/gamma.delta");
-    EXPECT_EQ("/alpha/beta/", fname.parentPath());
+    EXPECT_EQ("/alpha/beta/gamma.delta", fname.str());
+    EXPECT_EQ("/alpha/beta", fname.parentPath());
     EXPECT_EQ("gamma", fname.stem());
     EXPECT_EQ(".delta", fname.extension());
     EXPECT_EQ("gamma.delta", fname.filename());
@@ -94,6 +95,7 @@ TEST(PathTest, components) {
 /// @brief Check replaceExtension.
 TEST(PathTest, replaceExtension) {
     Path fname("a.b");
+    EXPECT_EQ("a.b", fname.str());
 
     EXPECT_EQ("a", fname.replaceExtension("").str());
     EXPECT_EQ("a.f", fname.replaceExtension(".f").str());
@@ -110,11 +112,11 @@ TEST(PathTest, replaceParentPath) {
     EXPECT_EQ("a.b", fname.str());
 
     fname.replaceParentPath("/just/some/dir/");
-    EXPECT_EQ("/just/some/dir/", fname.parentPath());
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
     EXPECT_EQ("/just/some/dir/a.b", fname.str());
 
     fname.replaceParentPath("/just/some/dir");
-    EXPECT_EQ("/just/some/dir/", fname.parentPath());
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
     EXPECT_EQ("/just/some/dir/a.b", fname.str());
 
     fname.replaceParentPath("/");
@@ -126,11 +128,11 @@ TEST(PathTest, replaceParentPath) {
     EXPECT_EQ("a.b", fname.str());
 
     fname = Path("/first/a.b");
-    EXPECT_EQ("/first/", fname.parentPath());
+    EXPECT_EQ("/first", fname.parentPath());
     EXPECT_EQ("/first/a.b", fname.str());
 
     fname.replaceParentPath("/just/some/dir");
-    EXPECT_EQ("/just/some/dir/", fname.parentPath());
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
     EXPECT_EQ("/just/some/dir/a.b", fname.str());
 }