This is an automated email from the ASF dual-hosted git repository. szaszm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
commit 73668eb1ad939ab561474ca3682b47162e9caf39 Author: Adam Debreceni <[email protected]> AuthorDate: Fri Jun 30 07:33:14 2023 +0200 MINIFICPP-2020 Protect MINIFI_HOME from mutual access Closes #1577 Signed-off-by: Marton Szasz <[email protected]> --- libminifi/include/utils/Error.h | 26 +++++ libminifi/include/utils/FileMutex.h | 59 ++++++++++++ libminifi/include/utils/OsUtils.h | 2 + libminifi/src/utils/Error.cpp | 37 +++++++ libminifi/src/utils/FileMutex.cpp | 171 +++++++++++++++++++++++++++++++++ libminifi/src/utils/OsUtils.cpp | 8 ++ libminifi/test/unit/FileMutexTests.cpp | 59 ++++++++++++ minifi_main/MiNiFiMain.cpp | 9 ++ 8 files changed, 371 insertions(+) diff --git a/libminifi/include/utils/Error.h b/libminifi/include/utils/Error.h new file mode 100644 index 000000000..77ef4a6ac --- /dev/null +++ b/libminifi/include/utils/Error.h @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +#pragma once + +#include <system_error> + +namespace org::apache::nifi::minifi::utils { + +std::error_code getLastError(); + +} // namespace org::apache::nifi::minifi::utils diff --git a/libminifi/include/utils/FileMutex.h b/libminifi/include/utils/FileMutex.h new file mode 100644 index 000000000..54596ead2 --- /dev/null +++ b/libminifi/include/utils/FileMutex.h @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +#pragma once + +#include <filesystem> +#include <array> +#include <mutex> +#include <optional> +#include "utils/gsl.h" + +#ifdef WIN32 +#include <windows.h> +#endif + +namespace org::apache::nifi::minifi::utils { + +// Warning: this will write the pid of the current process into the file +class FileMutex { + public: + explicit FileMutex(std::filesystem::path path); + ~FileMutex() { + gsl_Expects(!file_handle_.has_value()); + } + + FileMutex(const FileMutex&) = delete; + FileMutex(FileMutex&&) = delete; + FileMutex& operator=(const FileMutex&) = delete; + FileMutex& operator=(FileMutex&&) = delete; + + void lock(); + void unlock(); + + private: + std::filesystem::path path_; + + std::mutex mtx_; +#ifdef WIN32 + std::optional<HANDLE> file_handle_; +#else + std::optional<int> file_handle_; +#endif +}; + +} // namespace org::apache::nifi::minifi::utils diff --git a/libminifi/include/utils/OsUtils.h b/libminifi/include/utils/OsUtils.h index a6ef83d58..a71172333 100644 --- a/libminifi/include/utils/OsUtils.h +++ b/libminifi/include/utils/OsUtils.h @@ -30,6 +30,8 @@ extern std::string userIdToUsername(const std::string &uid); /// Returns physical memory usage by the current process in bytes int64_t getCurrentProcessPhysicalMemoryUsage(); +int64_t getCurrentProcessId(); + /// Returns physical memory usage by the system in bytes int64_t getSystemPhysicalMemoryUsage(); diff --git a/libminifi/src/utils/Error.cpp b/libminifi/src/utils/Error.cpp new file mode 100644 index 000000000..ba6f15e9a --- /dev/null +++ b/libminifi/src/utils/Error.cpp @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +#include "utils/Error.h" + +#include <cerrno> +#include "utils/gsl.h" + +#ifdef WIN32 +#include <windows.h> +#endif + +namespace org::apache::nifi::minifi::utils { + +std::error_code getLastError() { +#ifdef WIN32 + return {gsl::narrow<int>(GetLastError()), std::system_category()}; +#else + return {gsl::narrow<int>(errno), std::generic_category()}; +#endif +} + +} // namespace org::apache::nifi::minifi::utils diff --git a/libminifi/src/utils/FileMutex.cpp b/libminifi/src/utils/FileMutex.cpp new file mode 100644 index 000000000..0ae1eb213 --- /dev/null +++ b/libminifi/src/utils/FileMutex.cpp @@ -0,0 +1,171 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +#include "utils/FileMutex.h" + +#include <span> +#include <iostream> +#include "utils/gsl.h" +#include "utils/OsUtils.h" +#include "utils/Error.h" + +#ifdef WIN32 + +namespace org::apache::nifi::minifi::utils { + +FileMutex::FileMutex(std::filesystem::path path): path_(std::move(path)) {} + +// we cannot assume the logging system to be initialized + +void FileMutex::lock() { + std::lock_guard guard(mtx_); + gsl_Expects(!file_handle_.has_value()); + HANDLE handle = CreateFileA(path_.string().c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + + if (handle == INVALID_HANDLE_VALUE) { + const auto err = utils::getLastError(); + std::string pid_str = "unknown"; + handle = CreateFileA(path_.string().c_str(), GENERIC_READ, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (handle == INVALID_HANDLE_VALUE) { + std::cerr << "Failed to open file to read pid: " << utils::getLastError().message() << std::endl; + } else { + std::array<char, 16> buffer; + size_t pid_str_size = 0; + DWORD read_size; + while (ReadFile(handle, buffer.data() + pid_str_size, buffer.size() - pid_str_size, &read_size, NULL) && read_size != 0) { + pid_str_size += read_size; + } + pid_str = "'" + std::string(buffer.data(), pid_str_size) + "'"; + if (!CloseHandle(handle)) { + std::cerr << "Failed to close file after unsuccessful locking attempt: " << utils::getLastError().message() << std::endl; + } + } + + throw std::system_error{err, "Failed to open file '" + path_.string() + "' to be locked, previous pid: " + pid_str}; + } + + const std::string pidstr = std::to_string(utils::OsUtils::getCurrentProcessId()); + std::span<const char> buffer = pidstr; + while (!buffer.empty()) { + DWORD written; + if (!WriteFile(handle, buffer.data(), buffer.size(), &written, NULL)) { + const auto err = utils::getLastError(); + if (!CloseHandle(file_handle_.value())) { + std::cerr << "Failed to close file: " << utils::getLastError().message() << std::endl; + } + throw std::system_error(err, "Failed to write pid to lock file '" + path_.string() + "'"); + } + buffer = buffer.subspan(written); + } + + file_handle_ = handle; +} + +void FileMutex::unlock() { + std::lock_guard guard(mtx_); + gsl_Expects(file_handle_.has_value()); + if (!CloseHandle(file_handle_.value())) { + std::cerr << "Failed to close file: " << utils::getLastError().message() << std::endl; + } + file_handle_.reset(); +} + +} // namespace org::apache::nifi::minifi::utils + +#else + +#include <fcntl.h> +#include <unistd.h> +#include <cstring> + +namespace org::apache::nifi::minifi::utils { + +FileMutex::FileMutex(std::filesystem::path path): path_(std::move(path)) {} + +void FileMutex::lock() { + std::lock_guard guard(mtx_); + gsl_Expects(!file_handle_.has_value()); + int flags = O_RDWR | O_CREAT; +#ifdef O_CLOEXEC + flags |= O_CLOEXEC; +#endif + int fd = open(path_.string().c_str(), flags, 0644); + if (fd < 0) { + throw std::system_error{utils::getLastError(), "Failed to open file '" + path_.string() + "' to be locked"}; + } + + struct flock file_lock_info{}; + file_lock_info.l_type = F_WRLCK; + int value = fcntl(fd, F_SETLK, &file_lock_info); + if (value == -1) { + const auto err = utils::getLastError(); + std::string pid_str = "unknown"; + std::array<char, 16> buffer; + size_t pid_str_size = 0; + ssize_t ret; + while ((ret = read(fd, buffer.data() + pid_str_size, buffer.size() - pid_str_size)) > 0) { + pid_str_size += ret; + } + if (ret < 0) { + std::cerr << "Failed to read file content: " << utils::getLastError().message() << std::endl; + } else { + pid_str = "'" + std::string(buffer.data(), pid_str_size) + "'"; + } + + if (close(fd) == -1) { + std::cerr << "Failed to close file after unsuccessful locking attempt: " << utils::getLastError().message() << std::endl; + } + throw std::system_error{err, "Failed to lock file '" + path_.string() + "', previous pid: " + pid_str}; + } + + const std::string pidstr = std::to_string(utils::OsUtils::getCurrentProcessId()); + std::span<const char> buffer = pidstr; + while (!buffer.empty()) { + ssize_t ret = write(fd, buffer.data(), buffer.size()); + if (ret < 0) { + const auto err = utils::getLastError(); + if (close(fd) == -1) { + std::cerr << "Failed to close file after unsuccessful pid write attempt: " << utils::getLastError().message() << std::endl; + } + throw std::system_error{err, "Failed to write pid to lock file '" + path_.string() + "'"}; + } + buffer = buffer.subspan(ret); + } + + file_handle_ = fd; +} + +void FileMutex::unlock() { + std::lock_guard guard(mtx_); + gsl_Expects(file_handle_.has_value()); + auto file_guard = gsl::finally([&] { + if (close(file_handle_.value()) == -1) { + std::cerr << "Failed to close file after unlock: " << utils::getLastError().message() << std::endl; + } + file_handle_.reset(); + }); + struct flock file_lock_info{}; + file_lock_info.l_type = F_UNLCK; + int value = fcntl(file_handle_.value(), F_SETLK, &file_lock_info); + if (value == -1) { + throw std::system_error{utils::getLastError(), "Failed to unlock file '" + path_.string() + "'"}; + } +} + +} // namespace org::apache::nifi::minifi::utils + +#endif diff --git a/libminifi/src/utils/OsUtils.cpp b/libminifi/src/utils/OsUtils.cpp index 708ce426f..4c2fc9a00 100644 --- a/libminifi/src/utils/OsUtils.cpp +++ b/libminifi/src/utils/OsUtils.cpp @@ -200,6 +200,14 @@ int64_t OsUtils::getCurrentProcessPhysicalMemoryUsage() { #endif } +int64_t OsUtils::getCurrentProcessId() { +#ifdef WIN32 + return int64_t{GetCurrentProcessId()}; +#else + return int64_t{getpid()}; +#endif +} + int64_t OsUtils::getSystemPhysicalMemoryUsage() { #if defined(__linux__) const std::string available_memory_prefix = "MemAvailable:"; diff --git a/libminifi/test/unit/FileMutexTests.cpp b/libminifi/test/unit/FileMutexTests.cpp new file mode 100644 index 000000000..c00ad4086 --- /dev/null +++ b/libminifi/test/unit/FileMutexTests.cpp @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +#undef NDEBUG +#include <cassert> +#include <cstdlib> +#include "utils/FileMutex.h" +#include "utils/file/FileUtils.h" +#include "utils/OsUtils.h" +#include "../TestBase.h" + +namespace minifi = org::apache::nifi::minifi; + +int main(int argc, char* argv[]) { + if (argc == 2) { + std::cout << "Trying to lock file a second time '" << argv[1] << "', from pid: " << utils::OsUtils::getCurrentProcessId() << std::endl; + minifi::utils::FileMutex mtx{argv[1]}; + try { + std::unique_lock lock{mtx}; + } catch (const std::exception& ex) { + std::cerr << ex.what() << std::endl; + throw; + } + return 0; + } + + TestController controller; + auto lock_file = controller.createTempDirectory() / "LOCK"; + + minifi::utils::FileMutex mtx{lock_file}; + { + std::cout << "Locking file the first time '" << lock_file << "', from pid: " << utils::OsUtils::getCurrentProcessId() << std::endl; + std::unique_lock lock{mtx}; + + int second_lock = std::system((utils::file::get_executable_path().string() + " " + lock_file.string()).c_str()); // NOLINT + + assert(second_lock != 0); + } + // unlocked file the other process can lock now + { + int second_lock = std::system((utils::file::get_executable_path().string() + " " + lock_file.string()).c_str()); // NOLINT + + assert(second_lock == 0); + } +} diff --git a/minifi_main/MiNiFiMain.cpp b/minifi_main/MiNiFiMain.cpp index 36b4205ae..1d596f4a4 100644 --- a/minifi_main/MiNiFiMain.cpp +++ b/minifi_main/MiNiFiMain.cpp @@ -62,6 +62,7 @@ #include "utils/file/PathUtils.h" #include "utils/file/FileUtils.h" #include "utils/Environment.h" +#include "utils/FileMutex.h" #include "FlowController.h" #include "AgentDocs.h" #include "MainHelper.h" @@ -195,6 +196,14 @@ int main(int argc, char **argv) { // determineMinifiHome already logged everything we need return -1; } + utils::FileMutex minifi_home_mtx(minifiHome / "LOCK"); + std::unique_lock minifi_home_lock(minifi_home_mtx, std::defer_lock); + try { + minifi_home_lock.lock(); + } catch (const std::exception& ex) { + logger->log_error("Could not acquire LOCK for minifi home '%s', maybe another minifi instance is running: %s", minifiHome.string(), ex.what()); + std::exit(1); + } // chdir to MINIFI_HOME std::error_code current_path_error; std::filesystem::current_path(minifiHome, current_path_error);
