diff --git a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2baceedb7cadedb36b8d765fb9f7b523dbc152a
--- /dev/null
+++ b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Exception\MissingOptionsException;
+
+/**
+ * Checks if a plugin exists and optionally implements a particular interface.
+ *
+ * @Constraint(
+ *   id = "PluginExists",
+ *   label = @Translation("Plugin exists", context = "Validation"),
+ * )
+ */
+class PluginExistsConstraint extends Constraint implements ContainerFactoryPluginInterface {
+
+  /**
+   * The error message if a plugin does not exist.
+   *
+   * @var string
+   */
+  public string $unknownPluginMessage = "The '@plugin_id' plugin does not exist.";
+
+  /**
+   * The error message if a plugin does not implement the expected interface.
+   *
+   * @var string
+   */
+  public string $invalidInterfaceMessage = "The '@plugin_id' plugin must implement or extend @interface.";
+
+  /**
+   * The ID of the plugin manager service.
+   *
+   * @var string
+   */
+  protected string $manager;
+
+  /**
+   * Optional name of the interface that the plugin must implement.
+   *
+   * @var string|null
+   */
+  public ?string $interface = NULL;
+
+  /**
+   * Constructs a PluginExistsConstraint.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $pluginManager
+   *   The plugin manager associated with the constraint.
+   * @param mixed|null $options
+   *   The options (as associative array) or the value for the default option
+   *   (any other type).
+   * @param array|null $groups
+   *   An array of validation groups.
+   * @param mixed|null $payload
+   *   Domain-specific data attached to a constraint.
+   */
+  public function __construct(public readonly PluginManagerInterface $pluginManager, mixed $options = NULL, array $groups = NULL, mixed $payload = NULL) {
+    parent::__construct($options, $groups, $payload);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $plugin_manager_id = $configuration['manager'] ?? $configuration['value'] ?? NULL;
+    if ($plugin_manager_id === NULL) {
+      throw new MissingOptionsException(sprintf('The option "manager" must be set for constraint "%s".', static::class), ['manager']);
+    }
+    return new static($container->get($plugin_manager_id), $configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption(): ?string {
+    return 'manager';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions(): array {
+    return ['manager'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e09bd229f75e2d9789ec3a8a8a98af60fa7cdeb
--- /dev/null
+++ b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
+
+use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the PluginExists constraint.
+ */
+class PluginExistsConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $plugin_id, Constraint $constraint) {
+    assert($constraint instanceof PluginExistsConstraint);
+
+    $definition = $constraint->pluginManager->getDefinition($plugin_id, FALSE);
+    if (empty($definition)) {
+      $this->context->addViolation($constraint->unknownPluginMessage, [
+        '@plugin_id' => $plugin_id,
+      ]);
+      return;
+    }
+
+    // If we don't need to validate the plugin class's interface, we're done.
+    if (empty($constraint->interface)) {
+      return;
+    }
+
+    if (!is_a(DefaultFactory::getPluginClass($plugin_id, $definition), $constraint->interface, TRUE)) {
+      $this->context->addViolation($constraint->invalidInterfaceMessage, [
+        '@plugin_id' => $plugin_id,
+        '@interface' => $constraint->interface,
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/block/config/schema/block.schema.yml b/core/modules/block/config/schema/block.schema.yml
index 7dc4f53d1ccf5ccb30dcb585ae17fc426981f1ea..d768a9619503220ad3f28260bfb04466023823e8 100644
--- a/core/modules/block/config/schema/block.schema.yml
+++ b/core/modules/block/config/schema/block.schema.yml
@@ -22,6 +22,10 @@ block.block.*:
     plugin:
       type: string
       label: 'Plugin'
+      constraints:
+        PluginExists:
+          manager: plugin.manager.block
+          interface: Drupal\Core\Block\BlockPluginInterface
     settings:
       type: block.settings.[%parent.plugin]
     visibility:
diff --git a/core/modules/block/tests/src/Kernel/BlockValidationTest.php b/core/modules/block/tests/src/Kernel/BlockValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8d9e07ae1afc3a7f7f033a826466241ae0fa0280
--- /dev/null
+++ b/core/modules/block/tests/src/Kernel/BlockValidationTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Tests\block\Kernel;
+
+use Drupal\block\Entity\Block;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of block entities.
+ *
+ * @group block
+ */
+class BlockValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['block'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Block::create([
+      'id' => 'test_block',
+      'theme' => 'stark',
+      'plugin' => 'system_powered_by_block',
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * Tests validating a block with an unknown plugin ID.
+   */
+  public function testInvalidPluginId(): void {
+    $this->entity->set('plugin', 'non_existent');
+    $this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
+  }
+
+}
diff --git a/core/modules/editor/config/schema/editor.schema.yml b/core/modules/editor/config/schema/editor.schema.yml
index f68cb82056af4711467f788987373e779c46545d..0c0278b71551b5ad793255abc1619009e0771f0b 100644
--- a/core/modules/editor/config/schema/editor.schema.yml
+++ b/core/modules/editor/config/schema/editor.schema.yml
@@ -10,6 +10,10 @@ editor.editor.*:
     editor:
       type: string
       label: 'Text editor'
+      constraints:
+        PluginExists:
+          manager: plugin.manager.editor
+          interface: Drupal\editor\Plugin\EditorPluginInterface
     settings:
       type: editor.settings.[%parent.editor]
     image_upload:
diff --git a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php
index 696060ffbf6a99f2ac99347d97217f181c4e2cb6..c4f0562c4fdd492a9459e6a83c763b22f69ee072 100644
--- a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php
+++ b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php
@@ -62,4 +62,12 @@ public function testInvalidDependencies(): void {
     ]);
   }
 
+  /**
+   * Tests validating an editor with an unknown plugin ID.
+   */
+  public function testInvalidPluginId(): void {
+    $this->entity->setEditor('non_existent');
+    $this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c34c8f033806140b8b1451e1bcadea998a225b5
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Plugin;
+
+use Drupal\Core\Action\ActionInterface;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\system\MenuInterface;
+
+/**
+ * @group Plugin
+ * @group Validation
+ *
+ * @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
+ * @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraintValidator
+ */
+class PluginExistsConstraintValidatorTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['action_test', 'system'];
+
+  /**
+   * Tests validation of plugin existence.
+   */
+  public function testValidation(): void {
+    $definition = DataDefinition::create('string')
+      ->addConstraint('PluginExists', 'plugin.manager.action');
+
+    // An existing action plugin should pass validation.
+    $data = $this->container->get('typed_data_manager')->create($definition);
+    $data->setValue('action_test_save_entity');
+    $this->assertCount(0, $data->validate());
+
+    // It should also pass validation if we check for an interface it actually
+    // implements.
+    $definition->setConstraints([
+      'PluginExists' => [
+        'manager' => 'plugin.manager.action',
+        'interface' => ActionInterface::class,
+      ],
+    ]);
+    $this->assertCount(0, $data->validate());
+
+    // A non-existent plugin should be invalid, regardless of interface.
+    $data->setValue('non_existent_plugin');
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'non_existent_plugin' plugin does not exist.", (string) $violations->get(0)->getMessage());
+
+    // An existing plugin that doesn't implement the specified interface should
+    // raise an error.
+    $definition->setConstraints([
+      'PluginExists' => [
+        'manager' => 'plugin.manager.action',
+        'interface' => MenuInterface::class,
+      ],
+    ]);
+    $data->setValue('action_test_save_entity');
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'action_test_save_entity' plugin must implement or extend " . MenuInterface::class . '.', (string) $violations->get(0)->getMessage());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/PluginExistsConstraintTest.php b/core/tests/Drupal/Tests/Core/Plugin/PluginExistsConstraintTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d5210d2e0963aafae5b18ff76e1f7ac336619daa
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/PluginExistsConstraintTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Tests\Core\Plugin;
+
+use Drupal\Component\DependencyInjection\ContainerInterface;
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Validator\Exception\MissingOptionsException;
+
+/**
+ * @group Plugin
+ * @group Validation
+ *
+ * @coversDefaultClass \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
+ */
+class PluginExistsConstraintTest extends UnitTestCase {
+
+  /**
+   * Tests missing option.
+   *
+   * @covers ::create
+   */
+  public function testMissingOption(): void {
+    $this->expectException(MissingOptionsException::class);
+    $this->expectExceptionMessage('The option "manager" must be set for constraint "Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint".');
+    $container = $this->createMock(ContainerInterface::class);
+    PluginExistsConstraint::create($container, [], 'test_plugin_id', []);
+  }
+
+  /**
+   * Tests with different option keys.
+   *
+   * @testWith ["value"]
+   *           ["manager"]
+   *
+   * @covers ::create
+   * @covers ::__construct
+   */
+  public function testOption(string $option_key): void {
+    $container = $this->createMock(ContainerInterface::class);
+    $manager = $this->createMock(PluginManagerInterface::class);
+    $container->expects($this->any())
+      ->method('get')
+      ->with('plugin.manager.mock')
+      ->willReturn($manager);
+    $constraint = PluginExistsConstraint::create($container, [$option_key => 'plugin.manager.mock'], 'test_plugin_id', []);
+    $this->assertSame($manager, $constraint->pluginManager);
+  }
+
+}