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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/core/log_bridge/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ add_library(log_bridge_core STATIC
inc/log_bridge/logger.hpp
)

if(NOT WIN32)
target_sources(log_bridge_core PRIVATE
posix_secure_directory.cpp
posix_secure_directory.hpp
)
endif()

target_include_directories(log_bridge_core
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
Expand Down
151 changes: 151 additions & 0 deletions src/core/log_bridge/cpp/posix_secure_directory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include "posix_secure_directory.hpp"

#include <sys/stat.h>

#include <cerrno>
#include <fcntl.h>
#include <filesystem>
#include <string>
#include <unistd.h>
#include <utility>
#include <vector>

namespace isaaccapture::detail
{
namespace
{

class FileDescriptor
{
public:
explicit FileDescriptor(int fd) : fd_(fd)
{
}

~FileDescriptor()
{
if (fd_ >= 0)
{
::close(fd_);
}
}

FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;

FileDescriptor(FileDescriptor&& other) noexcept : fd_(std::exchange(other.fd_, -1))
{
}

FileDescriptor& operator=(FileDescriptor&& other) noexcept
{
if (this != &other)
{
if (fd_ >= 0)
{
::close(fd_);
}
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}

int get() const
{
return fd_;
}

private:
int fd_;
};

bool has_secure_permissions(const struct ::stat& info, bool target)
{
if (!S_ISDIR(info.st_mode) || (target && info.st_uid != ::getuid()))
{
return false;
}
const bool publicly_writable = (info.st_mode & (S_IWGRP | S_IWOTH)) != 0;
return !publicly_writable || (!target && (info.st_mode & S_ISVTX) != 0);
}

std::vector<std::string> path_components(const std::filesystem::path& path)
{
std::vector<std::string> components;
for (const auto& component : path.relative_path())
{
if (component != ".")
{
components.push_back(component.string());
}
}
return components;
}

bool open_or_create_child(FileDescriptor& parent, const std::string& name, bool target)
{
struct ::stat entry
{
};
if (::fstatat(parent.get(), name.c_str(), &entry, AT_SYMLINK_NOFOLLOW) != 0)
{
if (errno != ENOENT || (::mkdirat(parent.get(), name.c_str(), 0700) != 0 && errno != EEXIST))
{
return false;
}
}

// Traverse relative to the vetted parent. O_NOFOLLOW rejects a symlink
// even if the entry changed after fstatat.
const int child = ::openat(parent.get(), name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (child < 0)
{
return false;
}
FileDescriptor opened(child);
if (::fstat(opened.get(), &entry) != 0 || !has_secure_permissions(entry, target))
{
return false;
}
parent = std::move(opened);
return true;
}

} // namespace

bool ensure_secure_directory(const std::filesystem::path& directory)
{
std::error_code absolute_ec;
const auto absolute = std::filesystem::absolute(directory, absolute_ec).lexically_normal();
if (absolute_ec || !absolute.is_absolute())
{
return false;
}

const auto components = path_components(absolute);
if (components.empty())
{
return false;
}

FileDescriptor current(::open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
if (current.get() < 0)
{
return false;
}

for (std::size_t index = 0; index < components.size(); ++index)
{
const bool target = index + 1 == components.size();
if (components[index] == ".." || !open_or_create_child(current, components[index], target))
{
return false;
}
}
return true;
}

} // namespace isaaccapture::detail
16 changes: 16 additions & 0 deletions src/core/log_bridge/cpp/posix_secure_directory.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <filesystem>

namespace isaaccapture::detail
{

// std::filesystem does not expose POSIX ownership, sticky bits, or no-follow
// traversal. Keep this policy in one testable helper instead of open-coding it
// at each sink setup site.
bool ensure_secure_directory(const std::filesystem::path& directory);

} // namespace isaaccapture::detail
25 changes: 6 additions & 19 deletions src/core/log_bridge/cpp/sink_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include "sink_config.hpp"

#include "inc/log_bridge/logger.hpp"
#ifndef _WIN32
# include "posix_secure_directory.hpp"
#endif
#include "socket_sink.hpp"

#include <spdlog/common.h>
Expand All @@ -33,7 +36,6 @@

# include <fcntl.h>
# include <unistd.h>
# include <vector>
#else
# include <process.h>
#endif
Expand Down Expand Up @@ -358,28 +360,13 @@ const std::vector<spdlog::sink_ptr>& local_sinks()

auto dir = log_dir();
#ifndef _WIN32
// Track newly created components for owner-only permissions.
std::vector<std::filesystem::path> missing;
if (!ensure_secure_directory(dir))
{
std::error_code exists_ec;
std::filesystem::path probe = dir;
while (!probe.empty() && probe != probe.parent_path() && !std::filesystem::exists(probe, exists_ec))
{
missing.push_back(probe);
probe = probe.parent_path();
}
return std::vector<spdlog::sink_ptr>{ console };
}
#endif
#else
std::error_code dir_ec;
std::filesystem::create_directories(dir, dir_ec);
#ifndef _WIN32
// Preserve permissions on existing operator-provided directories.
for (const auto& component : missing)
{
std::error_code perms_ec;
std::filesystem::permissions(
component, std::filesystem::perms::owner_all, std::filesystem::perm_options::replace, perms_ec);
}
#endif

// Give each C++ writer a timestamped, PID-qualified path.
Expand Down
5 changes: 2 additions & 3 deletions src/plugins/vive_se3_tracker/vive_se3_tracker_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ bool is_secure_dir(const std::string& dir)
// 1. VIVE_SE3_COLLECTIONS_FILE, if set, wins verbatim (caller's responsibility).
// 2. else $XDG_RUNTIME_DIR (systemd gives a per-user, mode-0700 dir).
// 3. else a private /tmp/vive_se3_tracker-<uid> we create 0700 and verify we own.
// Falls through to the plain /tmp file only if the private dir can't be secured,
// and warns — never silently write to a world-writable fixed path.
// Disables the advertisement if the private directory cannot be secured.
// Keep in sync with record_se3_vive.py.
std::string resolve_collections_file()
{
Expand All @@ -83,7 +82,7 @@ std::string resolve_collections_file()
->warn(
"could not secure a per-user directory for the collections file; set VIVE_SE3_COLLECTIONS_FILE to a "
"private path.");
return dir + "/" + kCollectionsFileName; // best effort; open still O_TRUNC-guarded below
return {};
}

core::SchemaPusherConfig make_pusher_config(const std::string& collection_id)
Expand Down
9 changes: 9 additions & 0 deletions tests/cpp/core/log_bridge/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ add_executable(log_bridge_tests
test_logger.cpp
)

if(NOT WIN32)
target_sources(log_bridge_tests PRIVATE
test_posix_secure_directory.cpp
)
target_include_directories(log_bridge_tests PRIVATE
${PROJECT_SOURCE_DIR}/src/core/log_bridge/cpp
)
endif()

target_link_libraries(log_bridge_tests PRIVATE
log_bridge::log_bridge_core
Catch2::Catch2WithMain
Expand Down
83 changes: 83 additions & 0 deletions tests/cpp/core/log_bridge/test_posix_secure_directory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include "posix_secure_directory.hpp"

#include <catch2/catch_test_macros.hpp>
#include <sys/stat.h>

#include <algorithm>
#include <array>
#include <filesystem>
#include <iterator>
#include <unistd.h>

namespace
{

class TemporaryDirectory
{
public:
TemporaryDirectory()
{
std::array<char, 32> pattern{};
constexpr char kPattern[] = "/tmp/isaaccapture-log-test-XXXXXX";
std::copy(std::begin(kPattern), std::end(kPattern), pattern.begin());
char* created = ::mkdtemp(pattern.data());
REQUIRE(created != nullptr);
path_ = created;
}

~TemporaryDirectory()
{
std::error_code ec;
std::filesystem::remove_all(path_, ec);
}

const std::filesystem::path& path() const
{
return path_;
}

private:
std::filesystem::path path_;
};

} // namespace

TEST_CASE("secure directory creation uses owner-only permissions", "[unit]")
{
TemporaryDirectory temporary;
const auto directory = temporary.path() / "nested" / "logs";

REQUIRE(isaaccapture::detail::ensure_secure_directory(directory));

struct ::stat info
{
};
REQUIRE(::lstat(directory.c_str(), &info) == 0);
REQUIRE((info.st_mode & 0777) == 0700);
REQUIRE(info.st_uid == ::getuid());
}

TEST_CASE("secure directory creation rejects symlinked parents", "[unit]")
{
TemporaryDirectory temporary;
const auto destination = temporary.path() / "destination";
const auto link = temporary.path() / "runtime";
REQUIRE(std::filesystem::create_directory(destination));
REQUIRE(::symlink(destination.c_str(), link.c_str()) == 0);

REQUIRE_FALSE(isaaccapture::detail::ensure_secure_directory(link / "logs"));
REQUIRE_FALSE(std::filesystem::exists(destination / "logs"));
}

TEST_CASE("secure directory creation rejects writable parents", "[unit]")
{
TemporaryDirectory temporary;
const auto shared = temporary.path() / "shared";
REQUIRE(std::filesystem::create_directory(shared));
REQUIRE(::chmod(shared.c_str(), 0777) == 0);

REQUIRE_FALSE(isaaccapture::detail::ensure_secure_directory(shared / "logs"));
}
Loading
Loading