Skip to content

Commit 40d37b0

Browse files
committed
🐛 avoid potential invalid memory access via lib60870 callback parameter
1 parent 38f3072 commit 40d37b0

File tree

12 files changed

+288
-124
lines changed

12 files changed

+288
-124
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dist
77
*.so.*
88
~*
99
callgrind.*
10+
asan.log
1011
valgrind.log
1112
*.prof
1213
bin/upload*

bin/profiler.sh

+41-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
echo ""
44
echo "+----------------------------------------------------------+"
5-
echo "| PROFILER |"
5+
echo "| PREPARE: VALGRIND |"
66
echo "+----------------------------------------------------------+"
77

88
# change working directory to repository directory
@@ -14,17 +14,22 @@ if [ $# -eq 1 ] ; then
1414
ARGS=$1
1515
fi
1616

17+
if [ -d cmake-build-valgrind ]; then
18+
rm -rf cmake-build-valgrind
19+
fi
20+
1721
# generate make scripts, build application and install to bin folder
18-
cmake . -Bcmake-build-debug -DCMAKE_BUILD_TYPE=Debug "$ARGS"
19-
cmake . -Bcmake-build-debug -DCMAKE_BUILD_TYPE=Debug -DC104_VERSION_INFO=2.2.2 "$ARGS"
20-
cd cmake-build-debug || exit 1
22+
export CFLAGS="-O0"
23+
export CXXFLAGS="-O0"
24+
cmake . -Bcmake-build-valgrind -DCMAKE_BUILD_TYPE=Debug -DC104_VERSION_INFO=2.2.2 "$ARGS"
25+
cd cmake-build-valgrind || exit 1
2126
make -j"$(nproc+1)" _c104
2227
cd .. || exit 1
2328

2429
rm ./c104/_c104*.so
2530

2631
# copy libraries for python package
27-
cp ./cmake-build-debug/_c104*.so ./c104/
32+
cp ./cmake-build-valgrind/_c104*.so ./c104/
2833

2934
echo ""
3035
echo "BUILD DONE!"
@@ -40,17 +45,43 @@ echo "+----------------------------------------------------------+"
4045
#valgrind --leak-check=full --leak-resolution=med --track-origins=yes --read-inline-info=yes --suppressions=valgrind-python.supp python3 -E -tt ./test.py
4146
valgrind --tool=memcheck --dsymutil=yes --leak-check=full --track-origins=yes --show-leak-kinds=all --leak-resolution=high --suppressions=./tests/valgrind-python.supp python3 -X showrefcount ./tests/test.py > valgrind.log 2>&1
4247

48+
# clean-up
49+
rm ./c104/_c104*.so
50+
4351
echo ""
4452
echo "+----------------------------------------------------------+"
45-
echo "| PROFILER: GOOGLE-PERFTOOLS |"
53+
echo "| PREPARE: GCC AddressSanitizer (ASan) |"
4654
echo "+----------------------------------------------------------+"
47-
python3 -m yep -- test.py
48-
pprof --callgrind c104.so test.py.prof > callgrind.c104
4955

50-
cd ..
56+
if [ -d cmake-build-asan ]; then
57+
rm -rf cmake-build-asan
58+
fi
59+
60+
# generate make scripts, build application and install to bin folder
61+
export CFLAGS="-fsanitize=address,leak -O0"
62+
export CXXFLAGS="-fsanitize=address,leak -O0"
63+
cmake . -Bcmake-build-asan -DCMAKE_BUILD_TYPE=Debug -DC104_VERSION_INFO=2.2.2 "$ARGS"
64+
cd cmake-build-asan || exit 1
65+
make -j"$(nproc+1)" _c104
66+
cd .. || exit 1
67+
68+
# copy libraries for python package
69+
cp ./cmake-build-asan/_c104*.so ./c104/
70+
71+
echo ""
72+
echo "BUILD DONE!"
73+
echo ""
5174

5275
echo ""
53-
echo "Open file tests/callgrind.c104 in KCachegrind or QCachegrind to visualize metrics."
76+
echo "+----------------------------------------------------------+"
77+
echo "| PROFILER: GCC AddressSanitizer (ASan) |"
78+
echo "+----------------------------------------------------------+"
79+
80+
export ASAN_OPTIONS=verify_asan_link_order=0
81+
LD_PRELOAD=$(gcc -print-file-name=libasan.so) python3 tests/test.py > asan.log 2>&1
82+
83+
# clean-up
84+
rm ./c104/_c104*.so
5485

5586
echo ""
5687
echo "+----------------------------------------------------------+"

c104/__init__.pyi

+5
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,11 @@ class Information:
13451345
This class represents all specialized kind of information a specific point may have
13461346
"""
13471347
@property
1348+
def is_readonly(self) -> bool:
1349+
"""
1350+
test if the information is read-only
1351+
"""
1352+
@property
13481353
def processed_at(self) -> datetime.datetime:
13491354
"""
13501355
timestamp with milliseconds of last local information processing

docs/source/changelog.rst

+9
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ Change log
44
v2.2.1 (draft)
55
-------
66

7+
Features
8+
^^^^^^^^
9+
10+
- Add **Information.is_readonly**- property to test, if a new value can be assigned to this information.
11+
712
Fixes
813
^^^^^^
914

1015
- fix docstring for c104.ProtocolParameters timings: unit is seconds not milliseconds
16+
- support try-except blocks in callbacks instead of catching all errors and removing the callback
17+
- prevent invalid pointer access from lib60870 callbacks
18+
- setting **Point.value** will raise ValueError if info is readonly
19+
- setting **Point.quality** will raise ValueError if info is readonly
1120

1221
v2.2.0
1322
-------

src/Server.cpp

+43-44
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ bool areKeysSequential(const std::map<uint_fast16_t, T> &inputMap) {
6363
return true; // All keys are sequential
6464
}
6565

66+
// Define static members
67+
std::unordered_map<void *, std::weak_ptr<Server>> Server::instanceMap;
68+
std::mutex Server::instanceMapMutex;
69+
6670
Server::Server(const std::string &bind_ip, const std::uint_fast16_t tcp_port,
6771
const std::uint_fast16_t tick_rate_ms,
6872
const std::uint_fast16_t select_timeout_ms,
@@ -103,35 +107,51 @@ Server::Server(const std::string &bind_ip, const std::uint_fast16_t tcp_port,
103107
param->t2 = 1; // acknowledgement interval
104108

105109
// Function pointers for custom handler functions
110+
void *key = static_cast<void *>(this);
106111
CS104_Slave_setConnectionRequestHandler(
107-
slave, &Server::connectionRequestHandler, this);
112+
slave, &Server::connectionRequestHandler, key);
108113
CS104_Slave_setConnectionEventHandler(slave, &Server::connectionEventHandler,
109-
this);
110-
CS104_Slave_setRawMessageHandler(slave, &Server::rawMessageHandler, this);
114+
key);
115+
CS104_Slave_setRawMessageHandler(slave, &Server::rawMessageHandler, key);
111116
CS104_Slave_setInterrogationHandler(slave, &Server::interrogationHandler,
112-
this);
117+
key);
113118
CS104_Slave_setCounterInterrogationHandler(
114-
slave, &Server::counterInterrogationHandler, this);
115-
// CS104_Slave_setClockSyncHandler(slave, &Server::clockSyncHandler, this);
116-
CS104_Slave_setReadHandler(slave, &Server::readHandler, this);
117-
CS104_Slave_setASDUHandler(slave, &Server::asduHandler, this);
119+
slave, &Server::counterInterrogationHandler, key);
120+
CS104_Slave_setReadHandler(slave, &Server::readHandler, key);
121+
CS104_Slave_setASDUHandler(slave, &Server::asduHandler, key);
118122

119123
DEBUG_PRINT(Debug::Server, "Created");
120124
}
121125

122126
Server::~Server() {
123127
// stops and destroys the slave
124128
stop();
125-
CS104_Slave_destroy(slave);
126129

127130
{
128131
std::lock_guard<Module::GilAwareMutex> const st_lock(station_mutex);
129132
stations.clear();
130133
}
131134

135+
{
136+
std::lock_guard<std::mutex> lock(instanceMapMutex);
137+
instanceMap.erase(
138+
static_cast<void *>(this)); // Remove from map on destruction
139+
}
140+
141+
CS104_Slave_destroy(slave);
142+
132143
DEBUG_PRINT(Debug::Server, "Removed");
133144
}
134145

146+
std::shared_ptr<Server> Server::getInstance(void *key) {
147+
std::lock_guard<std::mutex> lock(instanceMapMutex);
148+
auto it = instanceMap.find(key);
149+
if (it != instanceMap.end()) {
150+
return it->second.lock();
151+
}
152+
return {nullptr};
153+
}
154+
135155
std::string Server::getIP() const { return ip; }
136156

137157
std::uint_fast16_t Server::getPort() const { return port; }
@@ -782,11 +802,8 @@ void Server::setOnConnectCallback(py::object &callable) {
782802
std::uint_fast16_t Server::getTickRate_ms() const { return tickRate_ms; }
783803

784804
bool Server::connectionRequestHandler(void *parameter, const char *ipAddress) {
785-
std::shared_ptr<Server> instance{};
786-
787-
try {
788-
instance = static_cast<Server *>(parameter)->shared_from_this();
789-
} catch (const std::bad_weak_ptr &e) {
805+
const auto instance = getInstance(parameter);
806+
if (!instance) {
790807
DEBUG_PRINT(Debug::Server, "Reject connection request in shutdown");
791808
return false;
792809
}
@@ -818,11 +835,8 @@ void Server::connectionEventHandler(void *parameter,
818835
begin = std::chrono::steady_clock::now();
819836
}
820837

821-
std::shared_ptr<Server> instance{};
822-
823-
try {
824-
instance = static_cast<Server *>(parameter)->shared_from_this();
825-
} catch (const std::bad_weak_ptr &e) {
838+
const auto instance = getInstance(parameter);
839+
if (!instance) {
826840
DEBUG_PRINT(Debug::Server, "Ignore connection event " +
827841
PeerConnectionEvent_toString(event) +
828842
" in shutdown");
@@ -1259,11 +1273,8 @@ Server::getValidMessage(IMasterConnection connection, CS101_ASDU asdu) {
12591273

12601274
void Server::rawMessageHandler(void *parameter, IMasterConnection connection,
12611275
uint_fast8_t *msg, int msgSize, bool sent) {
1262-
std::shared_ptr<Server> instance{};
1263-
1264-
try {
1265-
instance = static_cast<Server *>(parameter)->shared_from_this();
1266-
} catch (const std::bad_weak_ptr &e) {
1276+
const auto instance = getInstance(parameter);
1277+
if (!instance) {
12671278
DEBUG_PRINT(Debug::Server, "Ignore raw message in shutdown");
12681279
return;
12691280
}
@@ -1284,11 +1295,8 @@ bool Server::interrogationHandler(void *parameter, IMasterConnection connection,
12841295
begin = std::chrono::steady_clock::now();
12851296
}
12861297

1287-
std::shared_ptr<Server> instance{};
1288-
1289-
try {
1290-
instance = static_cast<Server *>(parameter)->shared_from_this();
1291-
} catch (const std::bad_weak_ptr &e) {
1298+
const auto instance = getInstance(parameter);
1299+
if (!instance) {
12921300
DEBUG_PRINT(Debug::Server, "Reject interrogation command in shutdown");
12931301
return false;
12941302
}
@@ -1381,11 +1389,8 @@ bool Server::counterInterrogationHandler(void *parameter,
13811389
begin = std::chrono::steady_clock::now();
13821390
}
13831391

1384-
std::shared_ptr<Server> instance{};
1385-
1386-
try {
1387-
instance = static_cast<Server *>(parameter)->shared_from_this();
1388-
} catch (const std::bad_weak_ptr &e) {
1392+
const auto instance = getInstance(parameter);
1393+
if (!instance) {
13891394
DEBUG_PRINT(Debug::Server,
13901395
"Reject counter interrogation command in shutdown");
13911396
return false;
@@ -1482,11 +1487,8 @@ bool Server::readHandler(void *parameter, IMasterConnection connection,
14821487
begin = std::chrono::steady_clock::now();
14831488
}
14841489

1485-
std::shared_ptr<Server> instance{};
1486-
1487-
try {
1488-
instance = static_cast<Server *>(parameter)->shared_from_this();
1489-
} catch (const std::bad_weak_ptr &e) {
1490+
const auto instance = getInstance(parameter);
1491+
if (!instance) {
14901492
DEBUG_PRINT(Debug::Server, "Reject read command in shutdown");
14911493
return false;
14921494
}
@@ -1562,11 +1564,8 @@ bool Server::asduHandler(void *parameter, IMasterConnection connection,
15621564
begin = std::chrono::steady_clock::now();
15631565
}
15641566

1565-
std::shared_ptr<Server> instance{};
1566-
1567-
try {
1568-
instance = static_cast<Server *>(parameter)->shared_from_this();
1569-
} catch (const std::bad_weak_ptr &e) {
1567+
const auto instance = getInstance(parameter);
1568+
if (!instance) {
15701569
DEBUG_PRINT(Debug::Server, "Reject asdu in shutdown");
15711570
return false;
15721571
}

src/Server.h

+56-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,15 @@ struct Selection {
6262
};
6363

6464
/**
65-
* @brief service model for IEC60870-5-104 communication as server
65+
* @brief Represents a server capable of handling connections, stations,
66+
* and transport security for a network-based application.
67+
*
68+
* The `Server` class is designed to manage server-client communication,
69+
* handle multiple connections, and interact with stations defined on this
70+
* server. It provides robust features for managing connection limits,
71+
* executing protocol-specific parameters, and integrating externally defined
72+
* callback execution. The server also supports transport security options
73+
* and schedule-based periodic tasks.
6674
*/
6775
class Server : public std::enable_shared_from_this<Server> {
6876

@@ -103,9 +111,16 @@ class Server : public std::enable_shared_from_this<Server> {
103111
std::uint_fast8_t max_open_connections = 0,
104112
std::shared_ptr<Remote::TransportSecurity> transport_security = nullptr) {
105113
// Not using std::make_shared because the constructor is private.
106-
return std::shared_ptr<Server>(
114+
auto server = std::shared_ptr<Server>(
107115
new Server(bind_ip, tcp_port, tick_rate_ms, select_timeout_ms,
108116
max_open_connections, std::move(transport_security)));
117+
118+
// track reference as weak pointer for safe static callbacks
119+
void *key = static_cast<void *>(server.get());
120+
std::lock_guard<std::mutex> lock(instanceMapMutex);
121+
instanceMap[key] = server;
122+
123+
return server;
109124
}
110125

111126
// DESTRUCTOR
@@ -675,6 +690,12 @@ class Server : public std::enable_shared_from_this<Server> {
675690
/// another loop
676691
mutable std::condition_variable_any runThread_wait{};
677692

693+
/// @brief instance weak pointer list for safe static callbacks
694+
static std::unordered_map<void *, std::weak_ptr<Server>> instanceMap;
695+
696+
/// @brief mutex to lock instanceMap read/write access
697+
static std::mutex instanceMapMutex;
698+
678699
/// @brief python callback function pointer
679700
Module::Callback<void> py_onReceiveRaw{
680701
"Server.on_receive_raw", "(server: c104.Server, data: bytes) -> None"};
@@ -706,8 +727,41 @@ class Server : public std::enable_shared_from_this<Server> {
706727
// void thread_callback();
707728

708729
public:
730+
/**
731+
* @brief Retrieves the selector associated with a given common address (ca)
732+
* and information object address (ioa) from the selection vector.
733+
*
734+
* This function searches for a matching selector in the selection vector
735+
* based on the provided parameters. If a selection is found
736+
* and the time elapsed since its creation does not exceed
737+
* the configured selection timeout, the function returns the selector;
738+
* otherwise, it returns an empty optional.
739+
*
740+
* @param ca The common address to search for.
741+
* @param ioa The information object address to search for.
742+
* @return An optional containing the selector if found and valid;
743+
* otherwise, an empty optional.
744+
*/
709745
std::optional<uint8_t> getSelector(uint16_t ca, uint32_t ioa);
710746

747+
/**
748+
* @brief Retrieves the shared instance of the Server associated with the
749+
* given key.
750+
*
751+
* This function is a thread-safe method to access Server instances stored in
752+
* an internal instance map. A weak reference is used for storage, and the
753+
* function returns a shared pointer to the associated Server instance. If the
754+
* key is not found in the map or the associated weak reference has expired, a
755+
* null shared pointer is returned.
756+
*
757+
* @param key A pointer serving as the unique identifier for the desired
758+
* Server instance.
759+
* @return A shared pointer to the Server instance associated with the given
760+
* key, or a null shared pointer if the key is not found or the instance has
761+
* been deallocated.
762+
*/
763+
static std::shared_ptr<Server> getInstance(void *key);
764+
711765
/**
712766
* @brief Callback to accept or decline incoming client connections
713767
* @param parameter reference to custom bound connection data

0 commit comments

Comments
 (0)