Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/website-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_PARALLEL:BOOL=OFF
-DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF
-DSOURCEMETA_CORE_LANG_MEMORY:BOOL=OFF
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/website-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_PARALLEL:BOOL=OFF
-DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF
-DSOURCEMETA_CORE_LANG_MEMORY:BOOL=OFF
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ option(SOURCEMETA_CORE_LANG_IO "Build the Sourcemeta Core language I/O library"
option(SOURCEMETA_CORE_LANG_PROCESS "Build the Sourcemeta Core language Process library" ON)
option(SOURCEMETA_CORE_LANG_PARALLEL "Build the Sourcemeta Core language parallel library" ON)
option(SOURCEMETA_CORE_LANG_NUMERIC "Build the Sourcemeta Core language numeric library" ON)
option(SOURCEMETA_CORE_LANG_MEMORY "Build the Sourcemeta Core language memory library" ON)
option(SOURCEMETA_CORE_LANG_ERROR "Build the Sourcemeta Core language error library" ON)
option(SOURCEMETA_CORE_LANG_OPTIONS "Build the Sourcemeta Core Options library" ON)
option(SOURCEMETA_CORE_LANG_TEXT "Build the Sourcemeta Core language text library" ON)
Expand Down Expand Up @@ -116,6 +117,10 @@ if(SOURCEMETA_CORE_LANG_NUMERIC)
add_subdirectory(src/lang/numeric)
endif()

if(SOURCEMETA_CORE_LANG_MEMORY)
add_subdirectory(src/lang/memory)
endif()

if(SOURCEMETA_CORE_LANG_ERROR)
add_subdirectory(src/lang/error)
endif()
Expand Down Expand Up @@ -299,6 +304,10 @@ if(SOURCEMETA_CORE_TESTS)
add_subdirectory(test/numeric)
endif()

if(SOURCEMETA_CORE_LANG_MEMORY)
add_subdirectory(test/memory)
endif()

if(SOURCEMETA_CORE_LANG_ERROR)
add_subdirectory(test/error)
endif()
Expand Down
3 changes: 3 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS)
list(APPEND SOURCEMETA_CORE_COMPONENTS process)
list(APPEND SOURCEMETA_CORE_COMPONENTS parallel)
list(APPEND SOURCEMETA_CORE_COMPONENTS numeric)
list(APPEND SOURCEMETA_CORE_COMPONENTS memory)
list(APPEND SOURCEMETA_CORE_COMPONENTS unicode)
list(APPEND SOURCEMETA_CORE_COMPONENTS punycode)
list(APPEND SOURCEMETA_CORE_COMPONENTS time)
Expand Down Expand Up @@ -68,6 +69,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
elseif(component STREQUAL "numeric")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake")
elseif(component STREQUAL "memory")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_memory.cmake")
elseif(component STREQUAL "unicode")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake")
elseif(component STREQUAL "punycode")
Expand Down
6 changes: 6 additions & 0 deletions src/lang/memory/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME memory
PRIVATE_HEADERS owned_or_reference.h)

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME memory)
endif()
17 changes: 17 additions & 0 deletions src/lang/memory/include/sourcemeta/core/memory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef SOURCEMETA_CORE_MEMORY_H_
#define SOURCEMETA_CORE_MEMORY_H_

// NOLINTBEGIN(misc-include-cleaner)
#include <sourcemeta/core/memory_owned_or_reference.h>
// NOLINTEND(misc-include-cleaner)

/// @defgroup memory Memory
/// @brief Growing collection of utilities for value ownership and lifetime
///
/// This functionality is included as follows:
///
/// ```cpp
/// #include <sourcemeta/core/memory.h>
/// ```

#endif
124 changes: 124 additions & 0 deletions src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#ifndef SOURCEMETA_CORE_MEMORY_OWNED_OR_REFERENCE_H_
#define SOURCEMETA_CORE_MEMORY_OWNED_OR_REFERENCE_H_

#include <cassert> // assert
#include <concepts> // std::move_constructible, std::copy_constructible
#include <memory> // std::addressof
#include <optional> // std::optional, std::nullopt_t
#include <type_traits> // std::is_object_v
#include <utility> // std::move

namespace sourcemeta::core {

/// @ingroup memory
/// What a value that can be either owned or referred to must satisfy
template <typename T>
concept Ownable = std::is_object_v<T> && std::move_constructible<T>;

/// @ingroup memory
/// Either a value this holds itself, a reference to one that outlives it, or
/// nothing at all. For example:
///
/// ```cpp
/// #include <sourcemeta/core/memory.h>
/// #include <cassert>
/// #include <string>
///
/// static const std::string CACHED{"foo"};
///
/// const sourcemeta::core::OwnedOrReference<std::string> reference{CACHED};
/// assert(&reference.value() == &CACHED);
///
/// const sourcemeta::core::OwnedOrReference<std::string> owned{
/// std::string{"bar"}};
/// assert(owned.value() == "bar");
/// ```
///
/// Reach for this when a function sometimes materialises its result and
/// sometimes hands back something it already has. Producers that build a
/// value, by reading a file, performing a network request, or computing it,
/// return it as they always would. Producers backed by storage that outlives
/// the call, such as a long lived cache, return a reference instead and skip
/// the copy.
///
/// A reference must stay put and stay alive for as long as the consumer reads
/// it. Anything temporary binds to the owning constructor, so a temporary can
/// never be captured by reference here. A value that another one of these owns
/// does not qualify either, as assigning to that one destroys what it holds.
template <Ownable T> class OwnedOrReference {
public:
/// Hold nothing
OwnedOrReference() = default;

/// Hold nothing
// NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
OwnedOrReference(std::nullopt_t) {}

/// Take ownership of a value that may or may not be there
// NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
OwnedOrReference(std::optional<T> &&value) : owned_{std::move(value)} {}

/// Take ownership of a value
// NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
OwnedOrReference(T &&value) : owned_{std::move(value)} {}

/// Refer to a value that outlives this. Anything temporary binds to the
/// owning constructor above instead, so this never refers to a dead value
// NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions)
OwnedOrReference(const T &value) : referenced_{std::addressof(value)} {}

// A constant temporary would otherwise bind to the referencing constructor
// and leave a dangling pointer behind
OwnedOrReference(const T &&value) = delete;

// Prevent accidental copies, as copying is the very thing this type exists
// to avoid. Take ownership through `to_owned` instead
OwnedOrReference(const OwnedOrReference &) = delete;
auto operator=(const OwnedOrReference &) -> OwnedOrReference & = delete;
/// Move
OwnedOrReference(OwnedOrReference &&) = default;
/// Move
auto operator=(OwnedOrReference &&) -> OwnedOrReference & = default;

@augmentcode augmentcode Bot Aug 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h:76, when the source borrows the target’s owned value (for example, OwnedOrReference<T> borrowed{target.value()}; target = std::move(borrowed);), memberwise assignment clears target.owned_ before copying borrowed.referenced_. The target then selects a dangling pointer in value(), so this publicly constructible aliasing case has use-after-free behavior.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a borrowed OwnedOrReference points at the destination’s current owned_ value, the defaulted move assignment destroys that value before copying the borrowed pointer, leaving referenced_ dangling. Implement a custom move assignment that detects this aliasing case before replacing owned_.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h, line 79:

<comment>When a borrowed `OwnedOrReference` points at the destination’s current `owned_` value, the defaulted move assignment destroys that value before copying the borrowed pointer, leaving `referenced_` dangling. Implement a custom move assignment that detects this aliasing case before replacing `owned_`.</comment>

<file context>
@@ -0,0 +1,120 @@
+  /// Move
+  OwnedOrReference(OwnedOrReference &&) = default;
+  /// Move
+  auto operator=(OwnedOrReference &&) -> OwnedOrReference & = default;
+  ~OwnedOrReference() = default;
+
</file context>
Suggested change
auto operator=(OwnedOrReference &&) -> OwnedOrReference & = default;
auto operator=(OwnedOrReference &&other) -> OwnedOrReference & {
if (this == &other) {
return *this;
}
if (this->owned_.has_value() &&
other.referenced_ == &this->owned_.value()) {
return *this;
}
this->owned_ = std::move(other.owned_);
this->referenced_ = other.referenced_;
return *this;
}

~OwnedOrReference() = default;

/// Whether there is anything to read
[[nodiscard]] auto has_value() const noexcept -> bool {
return this->referenced_ != nullptr || this->owned_.has_value();
}

/// Read the value, however it is held
[[nodiscard]] auto value() const -> const T & {
assert(this->has_value());
return this->referenced_ != nullptr ? *this->referenced_
: this->owned_.value();
}

/// Read the value, however it is held
[[nodiscard]] auto operator*() const -> const T & { return this->value(); }

/// Read the value, however it is held
[[nodiscard]] auto operator->() const -> const T * {
return std::addressof(this->value());
}

/// Get a value the caller owns, moving out of this one when it owns it and
/// copying only when it holds a reference
[[nodiscard]] auto to_owned() && -> T
requires std::copy_constructible<T>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: to_owned() && is disabled for the move-only types that this type explicitly supports, so an owned move-only value can never be extracted even though the owned branch only needs a move. After construction, Ownable accepts any std::move_constructible object (and the tests target a MoveOnly type and a holds_a_move_only_value case), but calling to_owned() on such an OwnedOrReference fails to compile because the whole overload is gated on std::copy_constructible<T>. Only the referenced branch (return *this->referenced_;) actually needs copyability; the owned branch (std::move(this->owned_).value()) needs only a move. This contradicts the method's stated contract ("moving out of this one when it owns it"). Requiring copyability should be scoped to the referenced-copy path, not the whole function, so owned move-only values can be moved out.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/memory/include/sourcemeta/core/memory_owned_or_reference.h, line 103:

<comment>`to_owned() &&` is disabled for the move-only types that this type explicitly supports, so an owned move-only value can never be extracted even though the owned branch only needs a move. After construction, `Ownable` accepts any `std::move_constructible` object (and the tests target a `MoveOnly` type and a `holds_a_move_only_value` case), but calling `to_owned()` on such an `OwnedOrReference` fails to compile because the whole overload is gated on `std::copy_constructible<T>`. Only the referenced branch (`return *this->referenced_;`) actually needs copyability; the owned branch (`std::move(this->owned_).value()`) needs only a move. This contradicts the method's stated contract ("moving out of this one when it owns it"). Requiring copyability should be scoped to the referenced-copy path, not the whole function, so owned move-only values can be moved out.</comment>

<file context>
@@ -0,0 +1,120 @@
+  /// Get a value the caller owns, moving out of this one when it owns it and
+  /// copying only when it holds a reference
+  [[nodiscard]] auto to_owned() && -> T
+    requires std::copy_constructible<T>
+  {
+    assert(this->has_value());
</file context>

{
assert(this->has_value());
if (this->referenced_ != nullptr) {
return *this->referenced_;
}

return std::move(this->owned_).value();
}

private:
std::optional<T> owned_;
const T *referenced_{nullptr};
};

} // namespace sourcemeta::core

#endif
4 changes: 4 additions & 0 deletions test/memory/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME memory
SOURCES memory_owned_or_reference_test.cc)

target_link_libraries(sourcemeta_core_memory_unit PRIVATE sourcemeta::core::memory)
Loading
Loading