diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 35026ad3af65794180301b3cf470890b9e9b03bd..f135d322464ecbb8d9d87ea92e9068a310bad5e5 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -129,9 +129,7 @@ services:
   automatic_updates.validator.staged_database_updates:
     class: Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
     arguments:
-      - '@package_manager.path_locator'
-      - '@extension.list.module'
-      - '@extension.list.theme'
+      - '@package_manager.validator.staged_database_updates'
       - '@string_translation'
     tags:
       - { name: event_subscriber }
diff --git a/automatic_updates_extensions/src/Form/UpdateReady.php b/automatic_updates_extensions/src/Form/UpdateReady.php
index b92452ffea0aa32eedc593d2e0c009b8f2052eea..88ed08ab588214f0405a3aae38807f7b2beab308 100644
--- a/automatic_updates_extensions/src/Form/UpdateReady.php
+++ b/automatic_updates_extensions/src/Form/UpdateReady.php
@@ -4,7 +4,7 @@ namespace Drupal\automatic_updates_extensions\Form;
 
 use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\package_manager\ProjectInfo;
-use Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator;
+use Drupal\package_manager\Validator\StagedDBUpdateValidator;
 use Drupal\automatic_updates_extensions\BatchProcessor;
 use Drupal\automatic_updates\BatchProcessor as AutoUpdatesBatchProcessor;
 use Drupal\automatic_updates_extensions\ExtensionUpdater;
@@ -52,7 +52,7 @@ final class UpdateReady extends FormBase {
   /**
    * The staged database update validator service.
    *
-   * @var \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
+   * @var \Drupal\package_manager\Validator\StagedDBUpdateValidator
    */
   protected $stagedDatabaseUpdateValidator;
 
@@ -74,12 +74,12 @@ final class UpdateReady extends FormBase {
    *   The state service.
    * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
    *   The module list service.
-   * @param \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator $staged_database_update_validator
+   * @param \Drupal\package_manager\Validator\StagedDBUpdateValidator $staged_database_update_validator
    *   The staged database update validator service.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
    */
-  public function __construct(ExtensionUpdater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDatabaseUpdateValidator $staged_database_update_validator, RendererInterface $renderer) {
+  public function __construct(ExtensionUpdater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDBUpdateValidator $staged_database_update_validator, RendererInterface $renderer) {
     $this->updater = $updater;
     $this->setMessenger($messenger);
     $this->state = $state;
@@ -104,7 +104,7 @@ final class UpdateReady extends FormBase {
       $container->get('messenger'),
       $container->get('state'),
       $container->get('extension.list.module'),
-      $container->get('automatic_updates.validator.staged_database_updates'),
+      $container->get('package_manager.validator.staged_database_updates'),
       $container->get('renderer')
     );
   }
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index ad96bab6dcbef6a2ec8fec22753d0af26309b831..3db8249aacceea0fe0b3b78adeb599004eff6c7e 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -123,6 +123,14 @@ services:
       - '@package_manager.path_locator'
     tags:
       - { name: event_subscriber }
+  package_manager.validator.staged_database_updates:
+    class: Drupal\package_manager\Validator\StagedDBUpdateValidator
+    arguments:
+      - '@package_manager.path_locator'
+      - '@extension.list.module'
+      - '@extension.list.theme'
+    tags:
+      - { name: event_subscriber }
   package_manager.test_site_excluder:
     class: Drupal\package_manager\PathExcluder\TestSiteExcluder
     arguments:
diff --git a/package_manager/src/Validator/StagedDBUpdateValidator.php b/package_manager/src/Validator/StagedDBUpdateValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..d131f834bfe11df4e1752006c0ed6b9efe9e7cd1
--- /dev/null
+++ b/package_manager/src/Validator/StagedDBUpdateValidator.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Drupal\package_manager\Validator;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ModuleExtensionList;
+use Drupal\Core\Extension\ThemeExtensionList;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\package_manager\Event\StatusCheckEvent;
+use Drupal\package_manager\PathLocator;
+use Drupal\package_manager\Stage;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Flags a warning if there are database updates in a staged update.
+ *
+ * @internal
+ *   This is an internal part of Package Manager and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
+ */
+class StagedDBUpdateValidator implements EventSubscriberInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The path locator service.
+   *
+   * @var \Drupal\package_manager\PathLocator
+   */
+  protected $pathLocator;
+
+  /**
+   * The module list service.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  protected $moduleList;
+
+  /**
+   * The theme list service.
+   *
+   * @var \Drupal\Core\Extension\ThemeExtensionList
+   */
+  protected $themeList;
+
+  /**
+   * Constructs a StagedDBUpdateValidator object.
+   *
+   * @param \Drupal\package_manager\PathLocator $path_locator
+   *   The path locator service.
+   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
+   *   The module list service.
+   * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
+   *   The theme list service.
+   */
+  public function __construct(PathLocator $path_locator, ModuleExtensionList $module_list, ThemeExtensionList $theme_list) {
+    $this->pathLocator = $path_locator;
+    $this->moduleList = $module_list;
+    $this->themeList = $theme_list;
+  }
+
+  /**
+   * Checks that the staged update does not have changes to its install files.
+   *
+   * @param \Drupal\package_manager\Event\StatusCheckEvent $event
+   *   The event object.
+   */
+  public function checkForStagedDatabaseUpdates(StatusCheckEvent $event): void {
+    $stage = $event->getStage();
+    if ($stage->isAvailable()) {
+      // No staged updates exist, therefore we don't need to run this check.
+      return;
+    }
+
+    $extensions_with_updates = $this->getExtensionsWithDatabaseUpdates($stage);
+    if ($extensions_with_updates) {
+      $event->addWarning($extensions_with_updates, $this->t('Possible database updates have been detected in the following extensions.'));
+    }
+  }
+
+  /**
+   * Determines if a staged extension has changed update functions.
+   *
+   * @param \Drupal\package_manager\Stage $stage
+   *   The updater which is controlling the update process.
+   * @param \Drupal\Core\Extension\Extension $extension
+   *   The extension to check.
+   *
+   * @return bool
+   *   TRUE if the staged copy of the extension has changed update functions
+   *   compared to the active copy, FALSE otherwise.
+   *
+   * @todo Use a more sophisticated method to detect changes in the staged
+   *   extension. Right now, we just compare hashes of the .install and
+   *   .post_update.php files in both copies of the given extension, but this
+   *   will cause false positives for changes to comments, whitespace, or
+   *   runtime code like requirements checks. It would be preferable to use a
+   *   static analyzer to detect new or changed functions that are actually
+   *   executed during an update. No matter what, this method must NEVER cause
+   *   false negatives, since that could result in code which is incompatible
+   *   with the current database schema being copied to the active directory.
+   *
+   * @see https://www.drupal.org/project/automatic_updates/issues/3253828
+   */
+  public function hasStagedUpdates(Stage $stage, Extension $extension): bool {
+    $active_dir = $this->pathLocator->getProjectRoot();
+    $stage_dir = $stage->getStageDirectory();
+
+    $web_root = $this->pathLocator->getWebRoot();
+    if ($web_root) {
+      $active_dir .= DIRECTORY_SEPARATOR . $web_root;
+      $stage_dir .= DIRECTORY_SEPARATOR . $web_root;
+    }
+
+    $active_hashes = $this->getHashes($active_dir, $extension);
+    $staged_hashes = $this->getHashes($stage_dir, $extension);
+
+    return $active_hashes !== $staged_hashes;
+  }
+
+  /**
+   * Returns hashes of the .install and .post-update.php files for a module.
+   *
+   * @param string $root_dir
+   *   The root directory of the Drupal code base.
+   * @param \Drupal\Core\Extension\Extension $extension
+   *   The module to check.
+   *
+   * @return string[]
+   *   The hashes of the module's .install and .post_update.php files, in that
+   *   order, if they exist. The array will be keyed by file extension.
+   */
+  protected function getHashes(string $root_dir, Extension $extension): array {
+    $path = implode(DIRECTORY_SEPARATOR, [
+      $root_dir,
+      $extension->getPath(),
+      $extension->getName(),
+    ]);
+    $hashes = [];
+
+    foreach (['.install', '.post_update.php'] as $suffix) {
+      $file = $path . $suffix;
+
+      if (file_exists($file)) {
+        $hashes[$suffix] = hash_file('sha256', $file);
+      }
+    }
+    return $hashes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      StatusCheckEvent::class => 'checkForStagedDatabaseUpdates',
+    ];
+  }
+
+  /**
+   * Gets extensions that have database updates.
+   *
+   * @param \Drupal\package_manager\Stage $stage
+   *   The stage.
+   *
+   * @return string[]
+   *   The names of the extensions that have possible database updates.
+   */
+  public function getExtensionsWithDatabaseUpdates(Stage $stage): array {
+    $extensions_with_updates = [];
+    // Check all installed extensions for database updates.
+    $lists = [$this->moduleList, $this->themeList];
+    foreach ($lists as $list) {
+      foreach ($list->getAllInstalledInfo() as $name => $info) {
+        if ($this->hasStagedUpdates($stage, $list->get($name))) {
+          $extensions_with_updates[] = $info['name'];
+        }
+      }
+    }
+
+    return $extensions_with_updates;
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index d6d06a90e510f03642388c2697ba12985a0d2687..a46ba08a35eef8f2257fe208f411fd36118b5b0b 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -177,9 +177,12 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
    *
    * @param \Drupal\package_manager\ValidationResult[] $expected_results
    *   The expected validation results.
+   * @param \Drupal\package_manager\Stage|null $stage
+   *   (optional) The stage to use to create the status check event. If none is
+   *   provided a new stage will be created.
    */
-  protected function assertStatusCheckResults(array $expected_results): void {
-    $event = new StatusCheckEvent($this->createStage());
+  protected function assertStatusCheckResults(array $expected_results, Stage $stage = NULL): void {
+    $event = new StatusCheckEvent($stage ?? $this->createStage());
     $this->container->get('event_dispatcher')->dispatch($event);
     $this->assertValidationResultsEqual($expected_results, $event->getResults());
   }
diff --git a/package_manager/tests/src/Kernel/ReadinessValidation/StagedDBUpdateValidatorTest.php b/package_manager/tests/src/Kernel/ReadinessValidation/StagedDBUpdateValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..77a67f54ae816ae9176367122ab3aeb2228a7d67
--- /dev/null
+++ b/package_manager/tests/src/Kernel/ReadinessValidation/StagedDBUpdateValidatorTest.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel\ReadinessValidation;
+
+use Drupal\package_manager\ValidationResult;
+use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
+
+/**
+ * @covers \Drupal\package_manager\Validator\StagedDBUpdateValidator
+ *
+ * @group package_manager
+ */
+class StagedDBUpdateValidatorTest extends PackageManagerKernelTestBase {
+
+  /**
+   * The suffixes of the files that can contain database updates.
+   *
+   * @var string[]
+   */
+  private const SUFFIXES = ['install', 'post_update.php'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createVirtualProject(?string $source_dir = NULL): void {
+    parent::createVirtualProject($source_dir);
+
+    $drupal_root = $this->getDrupalRoot();
+    $virtual_active_dir = $this->container->get('package_manager.path_locator')
+      ->getProjectRoot();
+
+    // Copy the .install and .post_update.php files from all extensions used in
+    // this test class, in the *actual* Drupal code base that is running this
+    // test, into the virtual project (i.e., the active directory).
+    $module_list = $this->container->get('extension.list.module');
+    $extensions = [];
+    $extensions['system'] = $module_list->get('system');
+    $extensions['views'] = $module_list->get('views');
+    $extensions['package_manager_bypass'] = $module_list->get('package_manager_bypass');
+    $theme_list = $this->container->get('extension.list.theme');
+    // Theme with updates.
+    $extensions['olivero'] = $theme_list->get('olivero');
+    // Theme without updates.
+    $extensions['stark'] = $theme_list->get('stark');
+    foreach ($extensions as $name => $extension) {
+      $path = $extension->getPath();
+      @mkdir("$virtual_active_dir/$path", 0777, TRUE);
+
+      foreach (static::SUFFIXES as $suffix) {
+        if ($name === 'olivero') {
+          @touch("$virtual_active_dir/$path/$name.$suffix");
+          continue;
+        }
+        // If the source file doesn't exist, silence the warning it raises.
+        @copy("$drupal_root/$path/$name.$suffix", "$virtual_active_dir/$path/$name.$suffix");
+      }
+    }
+  }
+
+  /**
+   * Data provider for testFileChanged().
+   *
+   * @return mixed[]
+   *   The test cases.
+   */
+  public function providerFileChanged(): array {
+    $scenarios = [];
+    foreach (static::SUFFIXES as $suffix) {
+      $scenarios["$suffix kept"] = [$suffix, FALSE];
+      $scenarios["$suffix deleted"] = [$suffix, TRUE];
+    }
+    return $scenarios;
+  }
+
+  /**
+   * Tests that an error is raised if install or post-update files are changed.
+   *
+   * @param string $suffix
+   *   The suffix of the file to change. Can be either 'install' or
+   *   'post_update.php'.
+   * @param bool $delete
+   *   Whether or not to delete the file.
+   *
+   * @dataProvider providerFileChanged
+   */
+  public function testFileChanged(string $suffix, bool $delete): void {
+    $stage = $this->createStage();
+    $stage->create();
+    $dir = $stage->getStageDirectory();
+    $this->container->get('theme_installer')->install(['olivero']);
+    $theme = $this->container->get('theme_handler')
+      ->getTheme('olivero');
+    $module_file = "$dir/core/modules/system/system.$suffix";
+    $theme_file = "$dir/{$theme->getPath()}/{$theme->getName()}.$suffix";
+    if ($delete) {
+      unlink($module_file);
+      unlink($theme_file);
+    }
+    else {
+      file_put_contents($module_file, $this->randomString());
+      file_put_contents($theme_file, $this->randomString());
+    }
+    $error = ValidationResult::createWarning(['System', 'Olivero'], t('Possible database updates have been detected in the following extensions.'));
+    $this->assertStatusCheckResults([$error], $stage);
+
+  }
+
+  /**
+   * Tests that no errors are raised if staged files have no DB updates.
+   */
+  public function testNoUpdates(): void {
+    // Since we're testing with a modified version of 'views' and
+    // 'olivero', these should not be installed.
+    $this->assertFalse($this->container->get('module_handler')->moduleExists('views'));
+    $this->assertFalse($this->container->get('theme_handler')->themeExists('olivero'));
+
+    // Create bogus staged versions of Views' and
+    // Package Manager Theme with Updates .install and .post_update.php
+    // files. Since these extensions are not installed, the changes should not
+    // raise any validation errors.
+    $stage = $this->createStage();
+    $stage->create();
+    $dir = $stage->getStageDirectory();
+    $module_list = $this->container->get('extension.list.module')->getList();
+    $theme_list = $this->container->get('extension.list.theme')->getList();
+    $module_dir = $dir . '/' . $module_list['views']->getPath();
+    $theme_dir = $dir . '/' . $theme_list['olivero']->getPath();
+    foreach (static::SUFFIXES as $suffix) {
+      file_put_contents("$module_dir/views.$suffix", $this->randomString());
+      file_put_contents("$theme_dir/olivero.$suffix", $this->randomString());
+    }
+    // There should not have been any errors.
+    $this->assertStatusCheckResults([], $stage);
+  }
+
+  /**
+   * Tests that an error is raised if install or post-update files are added.
+   */
+  public function testUpdatesAddedInStage(): void {
+    $module = $this->container->get('module_handler')
+      ->getModule('package_manager_bypass');
+    $theme_installer = $this->container->get('theme_installer');
+    $theme_installer->install(['stark']);
+    $theme = $this->container->get('theme_handler')
+      ->getTheme('stark');
+
+    $stage = $this->createStage();
+    $stage->create();
+    $dir = $stage->getStageDirectory();
+    foreach (static::SUFFIXES as $suffix) {
+      $module_file = sprintf('%s/%s/%s.%s', $dir, $module->getPath(), $module->getName(), $suffix);
+      $theme_file = sprintf('%s/%s/%s.%s', $dir, $theme->getPath(), $theme->getName(), $suffix);
+      // The files we're creating shouldn't already exist in the staging area
+      // unless it's a file we actually ship, which is a scenario covered by
+      // ::testFileChanged().
+      $this->assertFileDoesNotExist($module_file);
+      $this->assertFileDoesNotExist($theme_file);
+      file_put_contents($module_file, $this->randomString());
+      file_put_contents($theme_file, $this->randomString());
+    }
+    $error = ValidationResult::createWarning(['Package Manager Bypass', 'Stark'], t('Possible database updates have been detected in the following extensions.'));
+
+    $this->assertStatusCheckResults([$error], $stage);
+  }
+
+}
diff --git a/src/Form/UpdateReady.php b/src/Form/UpdateReady.php
index f77b2ece8bdf574380cd4c731455aa79eb180113..0f33929e617b515e5f50bd43a011b8fde784b36a 100644
--- a/src/Form/UpdateReady.php
+++ b/src/Form/UpdateReady.php
@@ -5,7 +5,7 @@ namespace Drupal\automatic_updates\Form;
 use Drupal\automatic_updates\BatchProcessor;
 use Drupal\automatic_updates\Updater;
 use Drupal\automatic_updates\Validation\ReadinessTrait;
-use Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator;
+use Drupal\package_manager\Validator\StagedDBUpdateValidator;
 use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Form\FormBase;
@@ -55,7 +55,7 @@ final class UpdateReady extends FormBase {
   /**
    * The staged database update validator service.
    *
-   * @var \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
+   * @var \Drupal\package_manager\Validator\StagedDBUpdateValidator
    */
   protected $stagedDatabaseUpdateValidator;
 
@@ -84,14 +84,14 @@ final class UpdateReady extends FormBase {
    *   The state service.
    * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
    *   The module list service.
-   * @param \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator $staged_database_update_validator
+   * @param \Drupal\package_manager\Validator\StagedDBUpdateValidator $staged_database_update_validator
    *   The staged database update validator service.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   Event dispatcher service.
    */
-  public function __construct(Updater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDatabaseUpdateValidator $staged_database_update_validator, RendererInterface $renderer, EventDispatcherInterface $event_dispatcher) {
+  public function __construct(Updater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDBUpdateValidator $staged_database_update_validator, RendererInterface $renderer, EventDispatcherInterface $event_dispatcher) {
     $this->updater = $updater;
     $this->setMessenger($messenger);
     $this->state = $state;
@@ -117,7 +117,7 @@ final class UpdateReady extends FormBase {
       $container->get('messenger'),
       $container->get('state'),
       $container->get('extension.list.module'),
-      $container->get('automatic_updates.validator.staged_database_updates'),
+      $container->get('package_manager.validator.staged_database_updates'),
       $container->get('renderer'),
       $container->get('event_dispatcher')
     );
diff --git a/src/Validator/StagedDatabaseUpdateValidator.php b/src/Validator/StagedDatabaseUpdateValidator.php
index fb386c6b4d698b5a5374694c468d9cb20e6bd150..70949123473aec21c2729f8836bd7c0b1e07d8b8 100644
--- a/src/Validator/StagedDatabaseUpdateValidator.php
+++ b/src/Validator/StagedDatabaseUpdateValidator.php
@@ -3,14 +3,10 @@
 namespace Drupal\automatic_updates\Validator;
 
 use Drupal\automatic_updates\CronUpdater;
-use Drupal\Core\Extension\Extension;
-use Drupal\Core\Extension\ModuleExtensionList;
-use Drupal\Core\Extension\ThemeExtensionList;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\package_manager\Event\PreApplyEvent;
-use Drupal\package_manager\PathLocator;
-use Drupal\package_manager\Stage;
+use Drupal\package_manager\Validator\StagedDBUpdateValidator;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -26,42 +22,22 @@ class StagedDatabaseUpdateValidator implements EventSubscriberInterface {
   use StringTranslationTrait;
 
   /**
-   * The path locator service.
+   * The Staged DB Update Validator service.
    *
-   * @var \Drupal\package_manager\PathLocator
+   * @var \Drupal\package_manager\Validator\StagedDBUpdateValidator
    */
-  protected $pathLocator;
-
-  /**
-   * The module list service.
-   *
-   * @var \Drupal\Core\Extension\ModuleExtensionList
-   */
-  protected $moduleList;
-
-  /**
-   * The theme list service.
-   *
-   * @var \Drupal\Core\Extension\ThemeExtensionList
-   */
-  protected $themeList;
+  protected $stagedDBUpdateValidator;
 
   /**
    * Constructs a StagedDatabaseUpdateValidator object.
    *
-   * @param \Drupal\package_manager\PathLocator $path_locator
-   *   The path locator service.
-   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
-   *   The module list service.
-   * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
-   *   The theme list service.
+   * @param \Drupal\package_manager\Validator\StagedDBUpdateValidator $staged_db_update_update_validator
+   *   The Staged DB Update Validator service.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
    *   The string translation service.
    */
-  public function __construct(PathLocator $path_locator, ModuleExtensionList $module_list, ThemeExtensionList $theme_list, TranslationInterface $translation) {
-    $this->pathLocator = $path_locator;
-    $this->moduleList = $module_list;
-    $this->themeList = $theme_list;
+  public function __construct(StagedDBUpdateValidator $staged_db_update_update_validator, TranslationInterface $translation) {
+    $this->stagedDBUpdateValidator = $staged_db_update_update_validator;
     $this->setStringTranslation($translation);
   }
 
@@ -77,82 +53,12 @@ class StagedDatabaseUpdateValidator implements EventSubscriberInterface {
       return;
     }
 
-    $invalid_extensions = $this->getExtensionsWithDatabaseUpdates($stage);
+    $invalid_extensions = $this->stagedDBUpdateValidator->getExtensionsWithDatabaseUpdates($stage);
     if ($invalid_extensions) {
       $event->addError($invalid_extensions, $this->t('The update cannot proceed because possible database updates have been detected in the following extensions.'));
     }
   }
 
-  /**
-   * Determines if a staged extension has changed update functions.
-   *
-   * @param \Drupal\package_manager\Stage $stage
-   *   The updater which is controlling the update process.
-   * @param \Drupal\Core\Extension\Extension $extension
-   *   The extension to check.
-   *
-   * @return bool
-   *   TRUE if the staged copy of the extension has changed update functions
-   *   compared to the active copy, FALSE otherwise.
-   *
-   * @todo Use a more sophisticated method to detect changes in the staged
-   *   extension. Right now, we just compare hashes of the .install and
-   *   .post_update.php files in both copies of the given extension, but this
-   *   will cause false positives for changes to comments, whitespace, or
-   *   runtime code like requirements checks. It would be preferable to use a
-   *   static analyzer to detect new or changed functions that are actually
-   *   executed during an update. No matter what, this method must NEVER cause
-   *   false negatives, since that could result in code which is incompatible
-   *   with the current database schema being copied to the active directory.
-   *
-   * @see https://www.drupal.org/project/automatic_updates/issues/3253828
-   */
-  public function hasStagedUpdates(Stage $stage, Extension $extension): bool {
-    $active_dir = $this->pathLocator->getProjectRoot();
-    $stage_dir = $stage->getStageDirectory();
-
-    $web_root = $this->pathLocator->getWebRoot();
-    if ($web_root) {
-      $active_dir .= DIRECTORY_SEPARATOR . $web_root;
-      $stage_dir .= DIRECTORY_SEPARATOR . $web_root;
-    }
-
-    $active_hashes = $this->getHashes($active_dir, $extension);
-    $staged_hashes = $this->getHashes($stage_dir, $extension);
-
-    return $active_hashes !== $staged_hashes;
-  }
-
-  /**
-   * Returns hashes of the .install and .post-update.php files for a module.
-   *
-   * @param string $root_dir
-   *   The root directory of the Drupal code base.
-   * @param \Drupal\Core\Extension\Extension $extension
-   *   The module to check.
-   *
-   * @return string[]
-   *   The hashes of the module's .install and .post_update.php files, in that
-   *   order, if they exist. The array will be keyed by file extension.
-   */
-  protected function getHashes(string $root_dir, Extension $extension): array {
-    $path = implode(DIRECTORY_SEPARATOR, [
-      $root_dir,
-      $extension->getPath(),
-      $extension->getName(),
-    ]);
-    $hashes = [];
-
-    foreach (['.install', '.post_update.php'] as $suffix) {
-      $file = $path . $suffix;
-
-      if (file_exists($file)) {
-        $hashes[$suffix] = hash_file('sha256', $file);
-      }
-    }
-    return $hashes;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -162,31 +68,4 @@ class StagedDatabaseUpdateValidator implements EventSubscriberInterface {
     ];
   }
 
-  /**
-   * Get extensions that have database updates.
-   *
-   * @param \Drupal\package_manager\Stage $stage
-   *   The stage.
-   *
-   * @return string[]
-   *   The names of the extensions that have possible database updates.
-   */
-  public function getExtensionsWithDatabaseUpdates(Stage $stage): array {
-    $invalid_extensions = [];
-    // Although \Drupal\automatic_updates\Validator\StagedProjectsValidator
-    // should prevent non-core modules from being added, updated, or removed in
-    // the staging area, we check all installed extensions so as not to rely on
-    // the presence of StagedProjectsValidator.
-    $lists = [$this->moduleList, $this->themeList];
-    foreach ($lists as $list) {
-      foreach ($list->getAllInstalledInfo() as $name => $info) {
-        if ($this->hasStagedUpdates($stage, $list->get($name))) {
-          $invalid_extensions[] = $info['name'];
-        }
-      }
-    }
-
-    return $invalid_extensions;
-  }
-
 }
diff --git a/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php b/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php
index 3dfd3dd79d0d04e0b5abfcd182ceb929de5a6ae7..0d2691a94fcb312b363ab6250e00aef0a1c547ca 100644
--- a/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php
+++ b/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php
@@ -17,7 +17,7 @@ class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
   public function alter(ContainerBuilder $container) {
     parent::alter($container);
 
-    $service_id = 'automatic_updates.validator.staged_database_updates';
+    $service_id = 'package_manager.validator.staged_database_updates';
     if ($container->hasDefinition($service_id)) {
       $container->getDefinition($service_id)
         ->setClass(StagedDatabaseUpdateValidator::class)
diff --git a/tests/modules/automatic_updates_test/src/StagedDatabaseUpdateValidator.php b/tests/modules/automatic_updates_test/src/StagedDatabaseUpdateValidator.php
index e2abcea63e7a3b1936c448d5e59617e417bb56ee..d4725297070246438f055051a50ba02d375c077f 100644
--- a/tests/modules/automatic_updates_test/src/StagedDatabaseUpdateValidator.php
+++ b/tests/modules/automatic_updates_test/src/StagedDatabaseUpdateValidator.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\automatic_updates_test;
 
-use Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator as BaseValidator;
+use Drupal\package_manager\Validator\StagedDBUpdateValidator as BaseValidator;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\State\StateInterface;
 use Drupal\package_manager\Stage;