Skip to content

[Runtime] Fix a false metadata cycle diagnostic when threads race to instantiate cyclical metadata. #80505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 7, 2025
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
68 changes: 37 additions & 31 deletions stdlib/public/runtime/Metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7811,44 +7811,50 @@ checkMetadataDependency(MetadataDependency dependency) {
void swift::blockOnMetadataDependency(MetadataDependency root,
MetadataDependency firstLink) {
std::vector<MetadataDependency> links;
auto checkNewLink = [&](MetadataDependency newLink) {
links.push_back(newLink);
for (auto i = links.begin(), e = links.end() - 1; i != e; ++i) {
if (i->Value == newLink.Value) {
diagnoseMetadataDependencyCycle(
llvm::makeArrayRef(&*i, links.end() - i));
}
}
};

links.push_back(root);

// Iteratively add each link, checking for a cycle, until we reach
// something without a known dependency.
checkNewLink(firstLink);
while (true) {

// Start out with firstLink. The initial NewState value won't be
// used, so just initialize it to an arbitrary value.
MetadataStateWithDependency currentCheckResult{
PrivateMetadataState::Allocating, firstLink};

// If there isn't a known dependency, we can't do any more checking.
while (currentCheckResult.Dependency) {
// Add this dependency to our links.
links.push_back(currentCheckResult.Dependency);

// Try to get a dependency for the metadata in the last link we added.
auto checkResult = checkMetadataDependency(links.back());

// If there isn't a known dependency, we can't do any more checking.
if (!checkResult.Dependency) {
// In the special case where it's the first link that doesn't have
// a known dependency and its current metadata state now satisfies
// the dependency leading to it, we can skip waiting.
if (links.size() == 2 &&
satisfies(checkResult.NewState, links.back().Requirement))
return;

// Otherwise, just make a blocking request for the first link in
// the chain.
auto request = MetadataRequest(firstLink.Requirement);
swift_checkMetadataState(request, firstLink.Value);
return;
}
currentCheckResult = checkMetadataDependency(links.back());

// Check the new link.
checkNewLink(checkResult.Dependency);
// Check the last link against the rest of the list.
for (auto i = links.begin(), e = links.end() - 1; i != e; ++i) {
if (i->Value == links.back().Value) {
// If there's a cycle but the new link's current state is now satisfied,
// then this is a stale dependency, not a cycle. This can happen when
// threads race to build a type in a fulfillable cycle.
if (!satisfies(currentCheckResult.NewState, links.back().Requirement))
diagnoseMetadataDependencyCycle(
llvm::makeArrayRef(&*i, links.end() - i));
}
}
}

// We didn't find any cycles. Make a blocking request if appropriate.

// In the special case where it's the first link that doesn't have
// a known dependency and its current metadata state now satisfies
// the dependency leading to it, we can skip waiting.
if (links.size() == 2 &&
satisfies(currentCheckResult.NewState, links.back().Requirement))
return;

// Otherwise, just make a blocking request for the first link in
// the chain.
auto request = MetadataRequest(firstLink.Requirement);
swift_checkMetadataState(request, firstLink.Value);
}

/***************************************************************************/
Expand Down
46 changes: 46 additions & 0 deletions test/Interpreter/metadata_cycles_threaded.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// RUN: %target-run-simple-swift(%import-libdispatch)

// REQUIRES: executable_test
// REQUIRES: libdispatch
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: back_deployment_runtime

import Dispatch

@_optimize(none) @inline(never) func forceTypeInstantiation(_: Any.Type) {}

struct AnyFoo<T, U> {
var thing: U
}

struct S<T> {
var thing: T
var next: AnyFoo<S, T>?
}

// We want to ensure that the runtime handles legal metadata cycles when threads
// race to instantiate the cycle. We have a cycle between S and AnyFoo, but it's
// resolvable because AnyFoo doesn't depend on S's layout. This tests a fix for
// a bug where the runtime's cycle detection could be overeager when multiple
// threads raced, and flag a legal.
//
// Since this is a multithreading test, failures are probabilistic and each type
// can only be tested once. The recursiveTry construct generates a large number
// of distinct types so we can do many tests.
func tryWithType<T>(_ t: T.Type) {
DispatchQueue.concurrentPerform(iterations: 5) { n in
forceTypeInstantiation(AnyFoo<S<T>, T>?.self)
}
}

struct One<T> {}
struct Two<T> {}

func recursiveTry<T>(_ t: T.Type, depth: Int = 0) {
if depth > 10 { return }
tryWithType(T.self)
recursiveTry(One<T>.self, depth: depth + 1)
recursiveTry(Two<T>.self, depth: depth + 1)
}

recursiveTry(Int.self)