diff --git a/CMakeLists.txt b/CMakeLists.txt index 273ca6aedef..e9cbd44ba88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,8 @@ if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT CMAKE_CXX_COMPILER_ID FATAL_ERROR "You can build broker with g++ or clang++. CMake will exit.") endif() +option(WITH_MALLOC_TRACE "compile centreon-malloc-trace library." OFF) + # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -stdlib=libc++") # set(CMAKE_CXX_COMPILER "clang++") add_definitions("-D_GLIBCXX_USE_CXX11_ABI=1") @@ -190,6 +192,10 @@ add_subdirectory(engine) add_subdirectory(connectors) add_subdirectory(ccc) +if (WITH_MALLOC_TRACE) + add_subdirectory(malloc-trace) +endif() + add_custom_target(test-broker COMMAND tests/ut_broker) add_custom_target(test-engine COMMAND tests/ut_engine) add_custom_target(test-clib COMMAND tests/ut_clib) diff --git a/broker/core/sql/src/mysql_connection.cc b/broker/core/sql/src/mysql_connection.cc index db18b4e50f3..23a38f455a8 100644 --- a/broker/core/sql/src/mysql_connection.cc +++ b/broker/core/sql/src/mysql_connection.cc @@ -848,6 +848,7 @@ void mysql_connection::_run() { ::mysql_error(_conn))); _state = finished; _start_condition.notify_all(); + _clear_connection(); return; } _last_access = std::time(nullptr); diff --git a/malloc-trace/CMakeLists.txt b/malloc-trace/CMakeLists.txt new file mode 100644 index 00000000000..dd1954a6c6c --- /dev/null +++ b/malloc-trace/CMakeLists.txt @@ -0,0 +1,42 @@ +# +# Copyright 2024 Centreon +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# For more information : contact@centreon.com +# + +# Global options. +project("Centreon malloc trace" C CXX) + +# Set directories. +set(INC_DIR "${PROJECT_SOURCE_DIR}/inc/com/centreon/malloc_trace") +set(SRC_DIR "src") + +add_library(centreon-malloc-trace SHARED + "src/by_thread_trace_active.cc" + "src/malloc_trace.cc" + "src/orphan_container.cc" + "src/simply_allocator.cc" +) + +target_link_libraries(centreon-malloc-trace + CONAN_PKG::fmt +) + +target_include_directories(centreon-malloc-trace PRIVATE + ${INC_DIR} + ${CMAKE_SOURCE_DIR}/common/inc +) + +target_precompile_headers(centreon-malloc-trace PRIVATE "precomp_inc/precomp.hh") \ No newline at end of file diff --git a/malloc-trace/README.md b/malloc-trace/README.md new file mode 100644 index 00000000000..78e0e7b640c --- /dev/null +++ b/malloc-trace/README.md @@ -0,0 +1,67 @@ +# malloc-trace + +## Description + +The goal of this little library is to trace each orphan malloc free call. It overrides weak **malloc**, **realloc**, **calloc** and **free** +We store in a container in memory every malloc, free calls. We remove malloc from that container each time a free with the same address is called otherwise free is also store in the container. +Every minute (by default), we flush to disk container content: + * malloc that had not be freed and that are older than one minute + * free that has not corresponding malloc in the container. + +In order to use it you have to feed LD_PRELOAD env variable +```bash +export LD_PRELOAD=malloc-trace.so +``` +Then you can launch your executable and each call will be recorded in /tmp/malloc-trace.csv with ';' as field separator + +The columns are: +* function (malloc or free) +* thread id +* address in process mem space +* size of memory allocated +* timestamp in ms +* call stack contained in a json array + ```json + [ + { + "f": "free", + "s": "", + "l": 0 + }, + { + "f": "__gnu_cxx::new_allocator, (__gnu_cxx::_Lock_policy)2> >::deallocate(std::_Sp_counted_ptr_inplace, (__gnu_cxx::_Lock_policy)2>*, unsigned long)", + "s": "", + "l": 0 + } + ] + ``` +f field is function name +s field is source file if available +l field is source line + +This library works in that manner: +We replace all malloc, realloc, calloc and free in order to trace all calls. +We store all malloc in a container. Each time free is called, if the corresponding malloc is found, it's erased from container, +otherwise orphan free is stored in the container. +Every 60s (by default), we flush the container, all malloc older than 60s and not freed are dumped to disk, all orphan freed are also dumped. + +Output file may be moved during running, in that case it's automatically recreated. + +## Environment variables +Some parameters of the library can be overriden with environment variables. +| Environment variable | default value | description | +| ------------------------ | --------------------- | ----------------------------------------------------------------------------- | +| out_file_path | /tmp/malloc-trace.csv | path of the output file | +| out_file_max_size | 0x100000000 | when the output file size exceeds this limit, the ouput file is truncated | +| malloc_second_peremption | one minute | delay between two flushes and delay after which malloc is considered orphaned | + +## Provided scripts + +### create_malloc_trace_table.sql +This script creates a table that can store an output_file +In this scripts, you will find in comments how to load output csv file in that table. + +### remove_malloc_free.py +We store in output file malloc that aren't freed in the next minute, we also store orphan free. +So if a malloc is dumped and it's corresponding free is operated two minutes later, the two are stored in output file. +The purpose of this script is to remove these malloc-free pairs. \ No newline at end of file diff --git a/malloc-trace/create_malloc_trace_table.sql b/malloc-trace/create_malloc_trace_table.sql new file mode 100644 index 00000000000..c5bdda41c48 --- /dev/null +++ b/malloc-trace/create_malloc_trace_table.sql @@ -0,0 +1,32 @@ +-- Copyright 2024 Centreon +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- For more information : contact@centreon.com +-- + + +-- this sql code lets you to create table where you can load data from /tmp/malloc-trace.csv +-- you have to connect to the bdd with this command line: +-- mysql -h 127.0.0.1 --local-infile=1 -D centreon_storage -u centreon -pcentreon +-- then you load data with +-- load data local infile '/tmp/malloc-trace.csv' into table malloc_trace fields terminated by ';'; + +CREATE TABLE `centreon_storage`.`malloc_trace` ( + `funct_name` VARCHAR(10) NOT NULL, + `thread_id` INT UNSIGNED NULL, + `address` BIGINT UNSIGNED NULL, + `size` INT UNSIGNED NULL, + `ms_timestamp` BIGINT UNSIGNED NULL, + `call_stack` TEXT(65535) NULL, + FULLTEXT INDEX `call_stack_ind` (`call_stack`) VISIBLE); diff --git a/malloc-trace/inc/com/centreon/malloc_trace/by_thread_trace_active.hh b/malloc-trace/inc/com/centreon/malloc_trace/by_thread_trace_active.hh new file mode 100644 index 00000000000..abadb5b6c7c --- /dev/null +++ b/malloc-trace/inc/com/centreon/malloc_trace/by_thread_trace_active.hh @@ -0,0 +1,99 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#ifndef CMT_BY_THREAD_TRACE_ACTIVE_HH +#define CMT_BY_THREAD_TRACE_ACTIVE_HH + +#include "intrusive_map.hh" + +namespace com::centreon::malloc_trace { + +/** + * @brief This class is used to store the tracing of a thread + * The problem is: malloc is called, we explore call stack and this research may + * do another malloc and we risk an infinite loop + * So the first malloc set the _malloc_trace_active and explore call stack + * The next malloc called by stacktrace process try to set _malloc_trace_active + * and as it's yet setted we don't try to explore call stack + * + */ +class thread_trace_active : public boost::intrusive::set_base_hook<> { + pid_t _thread_id; + mutable bool _malloc_trace_active = false; + + public: + thread_trace_active() {} + thread_trace_active(pid_t thread_id) : _thread_id(thread_id) {} + + pid_t get_thread_id() const { return _thread_id; } + + /** + * @brief Set _malloc_trace_active + * + * @return old value of _malloc_trace_active + */ + bool set_malloc_trace_active() const { + if (_malloc_trace_active) { + return true; + } + _malloc_trace_active = true; + return false; + } + + /** + * @brief reset _malloc_trace_active + * + * @return old value of _malloc_trace_active + */ + bool reset_malloc_trace_active() const { + if (!_malloc_trace_active) { + return false; + } + _malloc_trace_active = false; + return true; + } + + bool is_malloc_trace_active() const { return _malloc_trace_active; } + + struct key_extractor { + using type = pid_t; + type operator()(const thread_trace_active& node) const { + return node._thread_id; + } + }; +}; + +/** + * @brief container of thread_trace_active with zero allocation + * the drawback is that we are limited to store 4096 thread trace states + * + */ +class thread_dump_active + : protected intrusive_map { + std::mutex _protect; + + public: + bool set_dump_active(pid_t thread_id); + void reset_dump_active(pid_t thread_id); +}; + +} // namespace com::centreon::malloc_trace + +#endif diff --git a/malloc-trace/inc/com/centreon/malloc_trace/funct_info_cache.hh b/malloc-trace/inc/com/centreon/malloc_trace/funct_info_cache.hh new file mode 100644 index 00000000000..77a5eef010a --- /dev/null +++ b/malloc-trace/inc/com/centreon/malloc_trace/funct_info_cache.hh @@ -0,0 +1,50 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ +#ifndef CMT_FUNCT_INFO_CACHE_HH +#define CMT_FUNCT_INFO_CACHE_HH + +namespace com::centreon::malloc_trace { +/** + * @brief symbol information research is very expensive + * so we store function informations in a cache + * + */ +class funct_info { + const std::string _funct_name; + const std::string _source_file; + const size_t _source_line; + + public: + funct_info(std::string&& funct_name, + std::string&& source_file, + size_t source_line) + : _funct_name(funct_name), + _source_file(source_file), + _source_line(source_line) {} + + const std::string& get_funct_name() const { return _funct_name; } + const std::string& get_source_file() const { return _source_file; } + size_t get_source_line() const { return _source_line; } +}; + +using funct_cache_map = + std::map; + +} // namespace com::centreon::malloc_trace + +#endif diff --git a/malloc-trace/inc/com/centreon/malloc_trace/intrusive_map.hh b/malloc-trace/inc/com/centreon/malloc_trace/intrusive_map.hh new file mode 100644 index 00000000000..0962f8f52b7 --- /dev/null +++ b/malloc-trace/inc/com/centreon/malloc_trace/intrusive_map.hh @@ -0,0 +1,82 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ +#ifndef CMT_INTRUSIVE_MAP_HH +#define CMT_INTRUSIVE_MAP_HH + +namespace com::centreon::malloc_trace { + +/** + * @brief The goal of this class is to provide map without allocation + * + * @tparam node_type node (key and data) that must inherit from + * boost::intrusive::set_base_hook<> + * @tparam key_extractor struct with an operator that extract key from node_type + * @tparam node_arrray_size size max of the container + */ +template +class intrusive_map { + public: + using key_type = typename key_extractor::type; + + private: + node_type _nodes_array[node_array_size]; + node_type* _free_node = _nodes_array; + const node_type* _array_end = _free_node + node_array_size; + + using node_map = + boost::intrusive::set >; + + node_map _nodes; + + public: + + ~intrusive_map() { _nodes.clear(); } + + const node_type* find(const key_type& key) const { + auto found = _nodes.find(key); + if (found == _nodes.end()) { + return nullptr; + } else { + return &*found; + } + } + + const node_type* insert_and_get(const key_type& key) { + if (_free_node >= _array_end) { + return nullptr; + } + + node_type* to_insert = _free_node++; + new (to_insert) node_type(key); + _nodes.insert(*to_insert); + return to_insert; + } + + /** + * @brief sometimes method are called before object construction + * + * @return true constructor has been called + * @return false + */ + bool is_initialized() const { return _free_node; } +}; + +} // namespace com::centreon::malloc_trace + +#endif diff --git a/malloc-trace/inc/com/centreon/malloc_trace/orphan_container.hh b/malloc-trace/inc/com/centreon/malloc_trace/orphan_container.hh new file mode 100644 index 00000000000..3c0d76dc546 --- /dev/null +++ b/malloc-trace/inc/com/centreon/malloc_trace/orphan_container.hh @@ -0,0 +1,211 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#ifndef CMT_ORPHAN_CONTAINER_HH +#define CMT_ORPHAN_CONTAINER_HH + +#include + +#include "funct_info_cache.hh" + +namespace com::centreon::malloc_trace { + +constexpr size_t max_backtrace_size = 10; + +/** + * @brief information of free or malloc with stacktrace + * In this bean, we store: + * - allocated address + * - size of memory allocated (0 if free) + * - thread id + * - function name: malloc, free, realloc or freerealloc + * - length of the backtrace + * - backtrace + * - timestamp + */ +class backtrace_info { + const void* _allocated; + const size_t _allocated_size; + const pid_t _thread_id; + const std::string_view _funct_name; + size_t _backtrace_size; + boost::stacktrace::frame::native_frame_ptr_t _backtrace[max_backtrace_size]; + const std::chrono::system_clock::time_point _last_allocated; + + public: + backtrace_info(const void* allocated, + size_t allocated_size, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset); + + const void* get_allocated() const { return _allocated; } + size_t get_allocated_size() const { return _allocated_size; } + size_t get_backtrace_size() const { return _backtrace_size; } + const boost::stacktrace::frame::native_frame_ptr_t* get_backtrace() const { + return _backtrace; + } + const std::chrono::system_clock::time_point& get_last_allocated() const { + return _last_allocated; + } + + void to_file(int fd, funct_cache_map& funct_info_cache) const; +}; + +/** + * @brief infos of malloc + * as this object is stored in 2 containers, it has to set hooks: + * set_base_hook and allocated_time_hook + * + */ +class orphan_malloc : public backtrace_info, + public boost::intrusive::set_base_hook<> { + public: + boost::intrusive::set_member_hook<> allocated_time_hook; + + orphan_malloc(const void* allocated, + size_t allocated_size, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset) + : backtrace_info(allocated, + allocated_size, + thread_id, + funct_name, + backtrace, + backtrace_offset) {} + + // key extractor used to create a map addr to orphan_malloc + struct address_extractor { + using type = const void*; + type operator()(const orphan_malloc& node) const { + return node.get_allocated(); + } + }; + + // key extractor used to create a map time_alloc to orphan_malloc + struct time_allocated_extractor { + using type = std::chrono::system_clock::time_point; + const type& operator()(const orphan_malloc& node) const { + return node.get_last_allocated(); + } + }; +}; + +/** + * @brief infos of a free + * this object is stored in single link list: slist_base_hook + * + */ +class orphan_free : public backtrace_info, + public boost::intrusive::slist_base_hook<> { + public: + orphan_free(const void* allocated, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset) + : backtrace_info(allocated, + 0, + thread_id, + funct_name, + backtrace, + backtrace_offset) {} +}; + +/** + * @brief this object contains all orphan mallocs (malloc without free) and all + * orphan frees In order to limit allocation and improve performance, all + * objects are stored in intrusive container and allocated in simple node + * allocator (allocator that doesn't allow to allocate multiple object at once) + * + */ +class orphan_container { + // malloc part + // map alloc adress => orphan_malloc + using orphan_malloc_address_set = boost::intrusive::set< + orphan_malloc, + boost::intrusive::key_of_value>; + + // map time alloc => orphan_malloc + using orphan_malloc_time_hook = + boost::intrusive::member_hook, + &orphan_malloc::allocated_time_hook>; + using orphan_malloc_time_set = boost::intrusive::multiset< + orphan_malloc, + orphan_malloc_time_hook, + boost::intrusive::key_of_value>; + + // node allocator used to create orphan_malloc + using orphan_malloc_allocator = com::centreon::common:: + node_allocator, 0x100000>; + + orphan_malloc_address_set _address_to_malloc; + orphan_malloc_time_set _time_to_malloc; + orphan_malloc_allocator _malloc_allocator; + + // free part + // orphan_free are stored in single linked list + using orphan_free_list = + boost::intrusive::slist>; + + // node allocator used to create orphan_free + using orphan_free_allocator = com::centreon::common:: + node_allocator, 0x100000>; + + orphan_free_list _free; + orphan_free_allocator _free_allocator; + + funct_cache_map _funct_info_cache; + + std::chrono::system_clock::duration _malloc_peremption; + std::chrono::system_clock::time_point _last_flush; + size_t _max_file_size; + std::string_view _out_file_path; + + mutable std::mutex _protect; + + int open_file(); + + public: + orphan_container(); + + void add_malloc(const void* addr, + size_t allocated_size, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset); + + bool free(const void* addr); + + void add_free(const void* addr, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset); + + void flush_to_file(); +}; + +} // namespace com::centreon::malloc_trace + +#endif diff --git a/malloc-trace/inc/com/centreon/malloc_trace/simply_allocator.hh b/malloc-trace/inc/com/centreon/malloc_trace/simply_allocator.hh new file mode 100644 index 00000000000..23daa26b958 --- /dev/null +++ b/malloc-trace/inc/com/centreon/malloc_trace/simply_allocator.hh @@ -0,0 +1,59 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#ifndef CMT_SIMPLY_ALLOCATOR_HH +#define CMT_SIMPLY_ALLOCATOR_HH + +namespace com::centreon::malloc_trace { + +constexpr unsigned block_size = 4096; +constexpr unsigned nb_block = 256; +/** + * @brief basic allocator + * At the beginning, we don't know original malloc + * we must provide a simple malloc free for dlsym + * + */ +class simply_allocator { + class node_block { + unsigned char _buff[block_size]; + bool _free = true; + + public: + struct key_extractor { + using type = unsigned char const*; + type operator()(const node_block& block) const { return block._buff; } + }; + + bool is_free() const { return _free; } + void set_free(bool free) { _free = free; } + unsigned char* get_buff() { return _buff; } + }; + + node_block _blocks[nb_block]; + std::mutex _protect; + + public: + void* malloc(size_t size); + void* realloc(void* p, size_t size); + bool free(void* p); +}; + +} // namespace com::centreon::malloc_trace + +#endif diff --git a/malloc-trace/precomp_inc/precomp.hh b/malloc-trace/precomp_inc/precomp.hh new file mode 100644 index 00000000000..48f8663ed1a --- /dev/null +++ b/malloc-trace/precomp_inc/precomp.hh @@ -0,0 +1,34 @@ +/* +** Copyright 2024 Centreon +** +** This file is part of Centreon Engine. +** +** Centreon Engine is free software: you can redistribute it and/or +** modify it under the terms of the GNU General Public License version 2 +** as published by the Free Software Foundation. +** +** Centreon Engine is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with Centreon Engine. If not, see +** . +*/ + +#ifndef CMT_PRECOMP_HH +#define CMT_PRECOMP_HH + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#endif diff --git a/malloc-trace/remove_malloc_free.py b/malloc-trace/remove_malloc_free.py new file mode 100755 index 00000000000..551aa4651e0 --- /dev/null +++ b/malloc-trace/remove_malloc_free.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 +# +# Copyright 2024 Centreon +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For more information : contact@centreon.com +# +# This script takes a file produced by centreon-malloc-trace library and remove all malloc free pairs + +import sys +import getopt +import csv + + + + +def main(argv): + csv.field_size_limit(sys.maxsize) + inputfile = '' + outputfile = '' + opts, args = getopt.getopt(argv,"hi:o:",["in_file=","out_file="]) + for opt, arg in opts: + if opt == '-h': + print ('remove_malloc_free.py -i -o ') + return + elif opt in ("-i", "--in_file"): + inputfile = arg + elif opt in ("-o", "--out_file"): + outputfile = arg + + if inputfile == '' or outputfile == '': + print ('remove_malloc_free.py -i -o ') + return + allocated = {} + with open(inputfile) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=';') + for row in csv_reader: + if len(row) > 2 and row[2].isdigit(): + if row[0].find('free') >= 0: + if (row[2] in allocated): + allocated.pop(row[2]) + else: + allocated[row[2]] = row + with open(outputfile, 'w') as f: + for row in allocated.values(): + f.write(';'.join(row) ) + f.write('\n') + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/malloc-trace/src/by_thread_trace_active.cc b/malloc-trace/src/by_thread_trace_active.cc new file mode 100644 index 00000000000..91fbc934f8a --- /dev/null +++ b/malloc-trace/src/by_thread_trace_active.cc @@ -0,0 +1,57 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include "by_thread_trace_active.hh" + +using namespace com::centreon::malloc_trace; + +/** + * @brief Set the flag to true in _by_thread_dump_active + * + * @return true the flag was not setted before call + * @return false the flag was yet setted before call + */ +bool thread_dump_active::set_dump_active(pid_t thread_id) { + std::lock_guard l(_protect); + if (!is_initialized()) { + return false; + } + const thread_trace_active* exist = find(thread_id); + + if (!exist) { + const thread_trace_active* inserted = insert_and_get(thread_id); + if (!inserted) { + return false; + } + inserted->set_malloc_trace_active(); + return true; + } else { + return !exist->set_malloc_trace_active(); + } +} + +void thread_dump_active::reset_dump_active(pid_t thread_id) { + std::lock_guard l(_protect); + if (!is_initialized()) { + return; + } + const thread_trace_active* exist = find(thread_id); + if (exist) { + exist->reset_malloc_trace_active(); + } +} diff --git a/malloc-trace/src/malloc_trace.cc b/malloc-trace/src/malloc_trace.cc new file mode 100644 index 00000000000..3a712d76d8e --- /dev/null +++ b/malloc-trace/src/malloc_trace.cc @@ -0,0 +1,242 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include +#include + +#include "by_thread_trace_active.hh" +#include "funct_info_cache.hh" +#include "orphan_container.hh" +#include "simply_allocator.hh" + +using namespace com::centreon::malloc_trace; + +extern void* __libc_malloc(size_t size); + +pid_t gettid() __attribute__((weak)); + +/** + * @brief gettid is not available on alma8 + * + * @return pid_t + */ +pid_t m_gettid() { + if (gettid) { + return gettid(); + } else { + return syscall(__NR_gettid); + } +} + +/** + * @brief when we enter in malloc or free, we store information of stack trace, + * this will generate allocation that we don't want to store, this object allow + * us to store the active tracing on the active thread in order to avoid store + * recursion. + * + */ +static thread_dump_active _thread_dump_active; + +/** + * @brief simply allocator used by dlsym + * + */ +static simply_allocator _first_malloc; + +/** + * @brief the container that store every malloc and free + * + */ +static orphan_container* _orphans = new orphan_container; + +static void* first_malloc(size_t size) { + return _first_malloc.malloc(size); +} + +static void* first_realloc(void* p, size_t size) { + return _first_malloc.realloc(p, size); +} + +typedef void* (*malloc_signature)(size_t); + +// will be filled by original malloc +static malloc_signature original_malloc = first_malloc; + +typedef void* (*realloc_signature)(void*, size_t); + +// will be filled by original realloc +static realloc_signature original_realloc = first_realloc; + +static void first_free(void* p) { + _first_malloc.free(p); +} + +typedef void (*free_signature)(void*); +// will be filled by original free +static free_signature original_free = first_free; + +/** + * @brief there is 3 stages + * on the first alloc, we don't know malloc, realloc and free address + * So we call dlsym to get these address + * As dlsym allocates memory, we are in dlsym_running state and we provide + * allocation mechanism by simply_allocator. + * Once dlsym are done, we are in hook state + * + */ +enum class e_library_state { not_hooked, dlsym_running, hooked }; +static e_library_state _state = e_library_state::not_hooked; + +static void search_symbols() { + original_malloc = + reinterpret_cast(dlsym(RTLD_NEXT, "malloc")); + original_free = reinterpret_cast(dlsym(RTLD_NEXT, "free")); + original_realloc = + reinterpret_cast(dlsym(RTLD_NEXT, "realloc")); +} + +/** + * @brief our malloc + * + * @param size + * @param funct_name function name logged + * @return void* + */ +static void* malloc(size_t size, const char* funct_name) { + switch (_state) { + case e_library_state::not_hooked: + _state = e_library_state::dlsym_running; + search_symbols(); + _state = e_library_state::hooked; + break; + case e_library_state::dlsym_running: + return first_malloc(size); + default: + break; + } + + void* p = original_malloc(size); + + pid_t thread_id = m_gettid(); + bool have_to_dump = _thread_dump_active.set_dump_active(thread_id); + + // if this thread is not yet dumping => store it + if (have_to_dump) { + if (_orphans) { + constexpr std::string_view _funct_name("malloc"); + _orphans->add_malloc(p, size, thread_id, _funct_name, + boost::stacktrace::stacktrace(), 2); + _orphans->flush_to_file(); + } + _thread_dump_active.reset_dump_active(thread_id); + } + return p; +} + +/** + * @brief our realloc function + * + * @param p + * @param size + * @return void* + */ +void* realloc(void* p, size_t size) { + switch (_state) { + case e_library_state::not_hooked: + _state = e_library_state::dlsym_running; + search_symbols(); + _state = e_library_state::hooked; + break; + case e_library_state::dlsym_running: + return first_realloc(p, size); + default: + break; + } + void* new_p = original_realloc(p, size); + pid_t thread_id = m_gettid(); + bool have_to_dump = _thread_dump_active.set_dump_active(thread_id); + // if this thread is not yet dumping => call dump_callstack + if (have_to_dump) { + constexpr std::string_view realloc_funct_name("realloc"); + // if pointer has changed, we record a free + if (new_p != p && p) { + if (!_orphans->free(p)) { + constexpr std::string_view free_funct_name("freerealloc"); + _orphans->add_free(p, thread_id, free_funct_name, + boost::stacktrace::stacktrace(), 2); + } + } + _orphans->add_malloc(new_p, size, thread_id, realloc_funct_name, + boost::stacktrace::stacktrace(), 2); + _orphans->flush_to_file(); + _thread_dump_active.reset_dump_active(thread_id); + } + return new_p; +} + +/** + * @brief replacement of the original malloc + * + * @param size + * @return void* + */ +void* malloc(size_t size) { + return malloc(size, "malloc"); +} + +/** + * @brief our calloc function + * call to malloc + * + * @param num + * @param size + * @return void* + */ +void* calloc(size_t num, size_t size) { + size_t total_size = num * size; + void* p = malloc(total_size, "calloc"); + memset(p, 0, total_size); + return p; +} + +/** + * @brief our free + * + * @param p + */ +void free(void* p) { + if (_first_malloc.free(p)) + return; + original_free(p); + if (!p) + return; + + pid_t thread_id = m_gettid(); + bool have_to_dump = _thread_dump_active.set_dump_active(thread_id); + + // if this thread is not yet dumping => call dump_callstack + if (have_to_dump) { + if (!_orphans->free(p)) { + constexpr std::string_view free_funct_name("free"); + _orphans->add_free(p, thread_id, free_funct_name, + boost::stacktrace::stacktrace(), 2); + _orphans->flush_to_file(); + } + _thread_dump_active.reset_dump_active(thread_id); + } +} \ No newline at end of file diff --git a/malloc-trace/src/orphan_container.cc b/malloc-trace/src/orphan_container.cc new file mode 100644 index 00000000000..45f7fce6b10 --- /dev/null +++ b/malloc-trace/src/orphan_container.cc @@ -0,0 +1,304 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include +#include + +#include "orphan_container.hh" + +using namespace com::centreon::malloc_trace; + +/** + * @brief Construct a new backtrace info::backtrace info object + * + * @param allocated address allocated or freed + * @param allocated_size size allocated + * @param thread_id + * @param funct_name malloc, free or freerealloc + * @param backtrace + * @param backtrace_offset we will ignore backtrace_offset first frames + */ +backtrace_info::backtrace_info(const void* allocated, + size_t allocated_size, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset) + : _allocated(allocated), + _allocated_size(allocated_size), + _thread_id(thread_id), + _funct_name(funct_name), + _backtrace_size(0), + _last_allocated(std::chrono::system_clock::now()) { + if (backtrace_offset >= backtrace.size()) { + return; + } + + boost::stacktrace::stacktrace::const_iterator frame_iter = backtrace.begin(); + std::advance(frame_iter, backtrace_offset); + for (; frame_iter != backtrace.end() && _backtrace_size < max_backtrace_size; + ++frame_iter) { + _backtrace[_backtrace_size++] = frame_iter->address(); + } +} + +/** + * @brief add a line in output file + * + * @param fd file descriptor + * @param funct_info_cache map address=>function info where we store infos of + * functions (name, source line) + */ +void backtrace_info::to_file(int fd, funct_cache_map& funct_info_cache) const { + if (fd <= 0) { + return; + } + constexpr unsigned size_buff = 0x40000; + char buff[size_buff]; + char* end_buff = buff + size_buff - 10; + *end_buff = 0; + + char* work_pos = + fmt::format_to_n(buff, size_buff, "\"{}\";{};{};{};{};\"[", _funct_name, + _thread_id, reinterpret_cast(_allocated), + _allocated_size, + std::chrono::duration_cast( + _last_allocated.time_since_epoch()) + .count()) + .out; + + for (unsigned stack_cpt = 0; stack_cpt < _backtrace_size; ++stack_cpt) { + const boost::stacktrace::frame::native_frame_ptr_t addr = + _backtrace[stack_cpt]; + boost::stacktrace::frame frame(addr); + if (end_buff - work_pos < 1000) { + break; + } + + if (stack_cpt) { + *work_pos++ = ','; + } + funct_cache_map::const_iterator cache_entry = funct_info_cache.find(addr); + if (cache_entry == funct_info_cache.end()) { // not found => search and + // save + funct_info to_insert(frame.name(), frame.source_file(), + frame.source_line()); + cache_entry = funct_info_cache.emplace(addr, to_insert).first; + } + + if (cache_entry->second.get_source_file().empty()) { + work_pos = fmt::format_to_n(work_pos, end_buff - work_pos, + "{{\\\"f\\\":\\\"{}\\\" }}", + cache_entry->second.get_funct_name()) + .out; + } else { + work_pos = + fmt::format_to_n( + work_pos, end_buff - work_pos, + "{{\\\"f\\\":\\\"{}\\\" , \\\"s\\\":\\\"{}\\\" , \\\"l\\\":{}}}", + cache_entry->second.get_funct_name(), + cache_entry->second.get_source_file(), + cache_entry->second.get_source_line()) + .out; + } + } + + *work_pos++ = ']'; + *work_pos++ = '"'; + *work_pos++ = '\n'; + + ::write(fd, buff, work_pos - buff); +} + +/********************************************************************************* + orphan_container +*********************************************************************************/ + +/** + * @brief Construct a new orphan container::orphan container object + * + */ +orphan_container::orphan_container() + : _malloc_allocator(std::allocator()), + _free_allocator(std::allocator()) { + char* env_out_file_max_size = getenv("out_file_max_size"); + if (env_out_file_max_size && atoll(env_out_file_max_size) > 0) + _max_file_size = atoll(env_out_file_max_size); + else + _max_file_size = 0x100000000; + + char* env_out_file_path = getenv("out_file_path"); + if (env_out_file_path && strlen(env_out_file_path) > 0) + _out_file_path = env_out_file_path; + + else + _out_file_path = "/tmp/malloc-trace.csv"; + + char* malloc_second_peremption = getenv("malloc_second_peremption"); + if (malloc_second_peremption && atoi(malloc_second_peremption) > 0) + _malloc_peremption = std::chrono::seconds(atoi(malloc_second_peremption)); + else + _malloc_peremption = std::chrono::minutes(1); +} + +/** + * @brief register a malloc action, it can be a malloc or a realloc + * + * @param addr address allocated + * @param allocated_size size allocated + * @param thread_id + * @param funct_name + * @param backtrace + * @param backtrace_offset we will ignore backtrace_offset first frames + */ +void orphan_container::add_malloc( + const void* addr, + size_t allocated_size, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset) { + std::lock_guard l(_protect); + orphan_malloc* new_node = _malloc_allocator.allocate(); + new (new_node) orphan_malloc(addr, allocated_size, thread_id, funct_name, + backtrace, backtrace_offset); + + if (!_address_to_malloc.insert(*new_node).second) { + _malloc_allocator.deallocate(new_node); + } else { + _time_to_malloc.insert(*new_node); + } +} + +/** + * @brief when program call free we try to unregister previous malloc at addr + * + * @param addr address to free + * @return true malloc was found and unregistered + * @return false no malloc found for this address + */ +bool orphan_container::free(const void* addr) { + std::lock_guard l(_protect); + auto found = _address_to_malloc.find(addr); + if (found != _address_to_malloc.end()) { + orphan_malloc& to_erase = *found; + + //_time_to_malloc is only indexed by alloc timestamp (ms) + auto where_to_search = + _time_to_malloc.equal_range(to_erase.get_last_allocated()); + for (; where_to_search.first != where_to_search.second; + ++where_to_search.first) { + if (&*where_to_search.first == &to_erase) { + _time_to_malloc.erase(where_to_search.first); + break; + } + } + + _address_to_malloc.erase(found); + _malloc_allocator.deallocate(&to_erase); + return true; + } else { + return false; + } +} + +/** + * @brief in case or free has returned false, we have to add free orphan in this + * container + * + * @param addr address freed + * @param thread_id + * @param funct_name + * @param backtrace + * @param backtrace_offset we will ignore backtrace_offset first frames + */ +void orphan_container::add_free(const void* addr, + pid_t thread_id, + const std::string_view& funct_name, + const boost::stacktrace::stacktrace& backtrace, + size_t backtrace_offset) { + std::lock_guard l(_protect); + orphan_free* new_free = _free_allocator.allocate(); + new (new_free) + orphan_free(addr, thread_id, funct_name, backtrace, backtrace_offset); + _free.push_back(*new_free); +} + +/** + * @brief flush contents to disk + * all malloc older than _malloc_peremption are flushed + * more recent mallocs are not flushed because we hope a free that will + * unregister its all orphan free are flushed all data flushed are remove from + * container + * + */ +void orphan_container::flush_to_file() { + std::lock_guard l(_protect); + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + if (_last_flush + _malloc_peremption > now) { + return; + } + + int fd = open_file(); + + _last_flush = now; + + // we flush to disk oldest malloc and remove its from the container + if (!_time_to_malloc.empty()) { + orphan_malloc_time_set::iterator upper = _time_to_malloc.upper_bound( + std::chrono::system_clock::now() - _malloc_peremption); + for (orphan_malloc_time_set::iterator to_flush = _time_to_malloc.begin(); + to_flush != upper; ++to_flush) { + to_flush->to_file(fd, _funct_info_cache); + _address_to_malloc.erase(to_flush->get_allocated()); + } + _time_to_malloc.erase_and_dispose( + _time_to_malloc.begin(), upper, [this](orphan_malloc* to_dispose) { + _malloc_allocator.deallocate(to_dispose); + }); + } + + // we flush all free + for (const orphan_free& to_flush : _free) { + to_flush.to_file(fd, _funct_info_cache); + } + _free.clear_and_dispose([this](orphan_free* to_dispose) { + _free_allocator.deallocate(to_dispose); + }); + ::close(fd); +} + +/** + * @brief this function open out file if it hasn't be done + * if file size exceed 256Mo, file is truncated + * + * @return int file descriptor + */ +int orphan_container::open_file() { + int out_file_fd = open(_out_file_path.data(), O_APPEND | O_CREAT | O_RDWR, + S_IRUSR | S_IWUSR); + if (out_file_fd > 0) { + struct stat file_stat; + if (!fstat(out_file_fd, &file_stat)) { + if (file_stat.st_size > _max_file_size) { + ftruncate(out_file_fd, 0); + } + } + } + return out_file_fd; +} diff --git a/malloc-trace/src/simply_allocator.cc b/malloc-trace/src/simply_allocator.cc new file mode 100644 index 00000000000..84b6e4c7841 --- /dev/null +++ b/malloc-trace/src/simply_allocator.cc @@ -0,0 +1,78 @@ +/** + * Copyright 2024 Centreon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * For more information : contact@centreon.com + */ + +#include "simply_allocator.hh" + +using namespace com::centreon::malloc_trace; + +/** + * @brief same as malloc + * + * @param size + * @return void* + */ +void* simply_allocator::malloc(size_t size) { + if (size > block_size) { + return nullptr; + } + std::lock_guard l(_protect); + for (node_block* search = _blocks; search != _blocks + nb_block; ++search) { + if (search->is_free()) { + search->set_free(false); + return search->get_buff(); + } + } + return nullptr; +} + +/** + * @brief reallocate a pointer, + * if size > block_size or p doesn't belong to simply_allocator, it returns + * nullptr + * + * @param p + * @param size + * @return void* + */ +void* simply_allocator::realloc(void* p, size_t size) { + if (p < _blocks || p >= _blocks + block_size) + return nullptr; + if (size > block_size) { + return nullptr; + } + return p; +} + +/** + * @brief same as free + * + * @param p + * @return true if the pointer belong to this allocator + */ +bool simply_allocator::free(void* p) { + if (p < _blocks || p >= _blocks + block_size) + return false; + std::lock_guard l(_protect); + for (node_block* search = _blocks; search != _blocks + nb_block; ++search) { + if (search->get_buff() == p) { + search->set_free(true); + return true; + } + } + return false; +}