diff --git a/CHANGES-3.3 b/CHANGES-3.3
index 6378c7be678827dad61218dafadc6ac92903774e..de7c712a9a7df1ef4d6f2c0b21c29ce6f22fa0a9 100644
--- a/CHANGES-3.3
+++ b/CHANGES-3.3
@@ -7,10 +7,23 @@ contributors are listed. Note that Calamares does not have a historical
 changelog -- this log starts with version 3.3.0. See CHANGES-3.2 for
 the history of the 3.2 series (2018-05 - 2022-08).
 
-# 3.3.14 (unreleased)
+# 3.3.15 (unreleased)
+
+This release contains contributions from (alphabetically by given name):
+ - Nobody yet!
+
+## Core ##
+ - Nothing yet!
+
+## Modules ##
+ - Nothing yet!
+
+
+# 3.3.14 (2024-02-20)
 
 This release contains contributions from (alphabetically by given name):
  - Adriaan de Groot
+ - Evan James
  - TNE
  - vincent PENVERN
 
@@ -19,13 +32,21 @@ This release contains contributions from (alphabetically by given name):
    consistent. At least one valid Python program would work with
    the Boost::Python bindings, but not the pybind11 bindings. A
    memory-corruption problem in the Boost::Python bindings was resolved.
+ - Steps in the UI now have a hook to undo any changes they have made
+   to the live system, if the user cancels the installation.
 
 ## Modules ##
+ - *keyboard* module undoes changes to the keyboard layout if the
+   user cancels the installation (returning the system to whatever
+   layout was in use when Calamares started). (#2377, #2431)
+ - *locale* module undoes changes to the timezone. (#2377, #2431)
  - *partition* module stores a global storage value in luksPassphrase,
    for later modules that need to manipulate the encrypted partition.
    (thanks vincent, #2424)
  - *partition* module no longer clear (unmounts) a Ventoy device.
    (thanks TNE, #2427)
+ - *partition* module has configurable exceptions for the clear-mounts
+   (unmount) job, which always includes Ventoy.
 
 
 # 3.3.13 (2024-12-31)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4c534af0409847bbb366cfc62643b8810dd43911..1f3d3e02ff3fbc1739b40ef64e261803c5c50d08 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -48,7 +48,7 @@
 
 cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
 
-set(CALAMARES_VERSION 3.3.14)
+set(CALAMARES_VERSION 3.3.15)
 set(CALAMARES_RELEASE_MODE OFF) # Set to ON during a release
 
 if(CMAKE_SCRIPT_MODE_FILE)
diff --git a/src/calamares/CalamaresWindow.cpp b/src/calamares/CalamaresWindow.cpp
index e1892e7642c000abb8328c42dfd052257de5340b..bad04809ebc2865ed1e31546e921b3ab837f6ee5 100644
--- a/src/calamares/CalamaresWindow.cpp
+++ b/src/calamares/CalamaresWindow.cpp
@@ -534,7 +534,13 @@ CalamaresWindow::ensureSize( QSize size )
 void
 CalamaresWindow::closeEvent( QCloseEvent* event )
 {
-    if ( ( !m_viewManager ) || m_viewManager->confirmCancelInstallation() )
+    if ( m_viewManager )
+    {
+        m_viewManager->quit();
+        // If it didn't actually exit, eat the event to ignore close
+        event->ignore();
+    }
+    else
     {
         event->accept();
 #if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
@@ -543,8 +549,4 @@ CalamaresWindow::closeEvent( QCloseEvent* event )
         QApplication::exit( EXIT_SUCCESS );
 #endif
     }
-    else
-    {
-        event->ignore();
-    }
 }
diff --git a/src/libcalamares/geoip/Interface.h b/src/libcalamares/geoip/Interface.h
index 09c28c1f0317bfcf4f522a005227524a91bedddc..c144cc3eeb9874233170de08e12f02dcf4e452d3 100644
--- a/src/libcalamares/geoip/Interface.h
+++ b/src/libcalamares/geoip/Interface.h
@@ -60,6 +60,8 @@ public:
         return std::tie( lhs.m_region, lhs.m_zone ) == std::tie( rhs.m_region, rhs.m_zone );
     }
 
+    QString asString() const { return isValid() ? region() + QChar( '/' ) + zone() : QString(); }
+
 private:
     QString m_region;
     QString m_zone;
@@ -68,13 +70,13 @@ private:
 inline QDebug&
 operator<<( QDebug&& s, const RegionZonePair& tz )
 {
-    return s << tz.region() << '/' << tz.zone();
+    return s << tz.asString();
 }
 
 inline QDebug&
 operator<<( QDebug& s, const RegionZonePair& tz )
 {
-    return s << tz.region() << '/' << tz.zone();
+    return s << tz.asString();
 }
 
 /** @brief Splits a region/zone string into a pair.
diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp
index 2fd7f45c1418feb81fc954bb49d676fe6e036f79..0e8c4531c6229a6dd3c93e706b552cd5696a419a 100644
--- a/src/libcalamaresui/ViewManager.cpp
+++ b/src/libcalamaresui/ViewManager.cpp
@@ -489,17 +489,31 @@ ViewManager::back()
 void
 ViewManager::quit()
 {
-    if ( confirmCancelInstallation() )
+    const auto r = confirmCancelInstallation();
+    if ( r == Confirmation::Continue )
     {
+        return;
+    }
+
+    if ( r == Confirmation::CancelInstallation )
+    {
+        // Cancel view steps in reverse
+        for ( int i = m_currentStep; i >= 0; --i )
+        {
+            auto* step = m_steps.at( i );
+            cDebug() << "Cancelling view step" << step->moduleInstanceKey();
+            step->onCancel();
+        }
+    }
+
 #if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
-        QApplication::quit();
+    QApplication::quit();
 #else
-        QApplication::exit( EXIT_SUCCESS );
+    QApplication::exit( EXIT_SUCCESS );
 #endif
-    }
 }
 
-bool
+ViewManager::Confirmation
 ViewManager::confirmCancelInstallation()
 {
     const auto* const settings = Calamares::Settings::instance();
@@ -507,17 +521,17 @@ ViewManager::confirmCancelInstallation()
     // When we're at the very end, then it's always OK to exit.
     if ( isAtVeryEnd( m_steps, m_currentStep ) )
     {
-        return true;
+        return Confirmation::EndOfInstallation;
     }
 
     // Not at the very end, cancel/quit might be disabled
     if ( settings->disableCancel() )
     {
-        return false;
+        return Confirmation::Continue;
     }
     if ( settings->disableCancelDuringExec() && stepIsExecute( m_steps, m_currentStep ) )
     {
-        return false;
+        return Confirmation::Continue;
     }
 
     // Otherwise, confirm cancel/quit.
@@ -530,7 +544,7 @@ ViewManager::confirmCancelInstallation()
     mb.setDefaultButton( QMessageBox::No );
     Calamares::fixButtonLabels( &mb );
     int response = mb.exec();
-    return response == QMessageBox::Yes;
+    return ( response == QMessageBox::Yes ) ? Confirmation::CancelInstallation : Confirmation::Continue;
 }
 
 void
diff --git a/src/libcalamaresui/ViewManager.h b/src/libcalamaresui/ViewManager.h
index 5a449a1536a67dac3f20be9a908ae286d8085a49..62d2b9f032d8bbfa17de470be50beb6cbecebf89 100644
--- a/src/libcalamaresui/ViewManager.h
+++ b/src/libcalamaresui/ViewManager.h
@@ -101,14 +101,21 @@ public:
      */
     int currentStepIndex() const;
 
+    enum class Confirmation
+    {
+        Continue,  // User rejects cancel / close question
+        CancelInstallation,  // User accepts cancel / close question
+        EndOfInstallation,  // There was no question because the installation was already done
+    };
+
     /**
      * @brief Called when "Cancel" is clicked; asks for confirmation.
      * Other means of closing Calamares also call this method, e.g. alt-F4.
      * At the end of installation, no confirmation is asked.
      *
-     * @return @c true if the user confirms closing the window.
+     * @return a Confirmation value, @c Unasked if the installation is complete
      */
-    bool confirmCancelInstallation();
+    Confirmation confirmCancelInstallation();
 
     Qt::Orientations panelSides() const { return m_panelSides; }
     void setPanelSides( Qt::Orientations panelSides ) { m_panelSides = panelSides; }
diff --git a/src/libcalamaresui/viewpages/ViewStep.cpp b/src/libcalamaresui/viewpages/ViewStep.cpp
index 417f5413c9a11ba57814ff74ae033d23fa877ab8..4a172a990503aa98cc4fd84e6bde87dfafb83d17 100644
--- a/src/libcalamaresui/viewpages/ViewStep.cpp
+++ b/src/libcalamaresui/viewpages/ViewStep.cpp
@@ -45,6 +45,11 @@ ViewStep::onLeave()
 {
 }
 
+void
+ViewStep::onCancel()
+{
+}
+
 void
 ViewStep::next()
 {
diff --git a/src/libcalamaresui/viewpages/ViewStep.h b/src/libcalamaresui/viewpages/ViewStep.h
index 22e72e5fd53ef17d874d6595fa156d981c42ef30..1a52ef6304887fe3f2fc86ef248b6bbee29c392b 100644
--- a/src/libcalamaresui/viewpages/ViewStep.h
+++ b/src/libcalamaresui/viewpages/ViewStep.h
@@ -168,6 +168,15 @@ public:
      */
     virtual RequirementsList checkRequirements();
 
+    /**
+     * @brief Called when the user cancels the installation
+     *
+     * View steps are expected to leave the system unchanged when
+     * the installation is cancelled. The default implementation
+     * does nothing.
+     */
+    virtual void onCancel();
+
 signals:
     /// @brief Tells the viewmanager to enable the *next* button according to @p status
     void nextStatusChanged( bool status );
diff --git a/src/modules/keyboard/AdditionalLayoutInfo.h b/src/modules/keyboard/AdditionalLayoutInfo.h
index 61e854d3b9ad2ea35430d9918827d6a9232b591c..f606bfec9f518b141a97406b20dffedfd540b875 100644
--- a/src/modules/keyboard/AdditionalLayoutInfo.h
+++ b/src/modules/keyboard/AdditionalLayoutInfo.h
@@ -12,6 +12,14 @@
 
 #include <QString>
 
+struct BasicLayoutInfo
+{
+    QString selectedLayout;
+    QString selectedModel;
+    QString selectedVariant;
+    QString selectedGroup;
+};
+
 struct AdditionalLayoutInfo
 {
     QString additionalLayout;
diff --git a/src/modules/keyboard/Config.cpp b/src/modules/keyboard/Config.cpp
index 54ee7649cd4a6bf6770717075081c47b6f30f799..47c81a347b2483f80f8f8bbb8c3168510f2c8527 100644
--- a/src/modules/keyboard/Config.cpp
+++ b/src/modules/keyboard/Config.cpp
@@ -169,7 +169,7 @@ Config::Config( QObject* parent )
              &KeyboardModelsModel::currentIndexChanged,
              [ & ]( int index )
              {
-                 m_selectedModel = m_keyboardModelsModel->key( index );
+                 m_current.selectedModel = m_keyboardModelsModel->key( index );
                  somethingChanged();
              } );
 
@@ -177,7 +177,7 @@ Config::Config( QObject* parent )
              &KeyboardLayoutModel::currentIndexChanged,
              [ & ]( int index )
              {
-                 m_selectedLayout = m_keyboardLayoutsModel->item( index ).first;
+                 m_current.selectedLayout = m_keyboardLayoutsModel->item( index ).first;
                  updateVariants( QPersistentModelIndex( m_keyboardLayoutsModel->index( index ) ) );
                  emit prettyStatusChanged();
              } );
@@ -186,14 +186,14 @@ Config::Config( QObject* parent )
              &KeyboardVariantsModel::currentIndexChanged,
              [ & ]( int index )
              {
-                 m_selectedVariant = m_keyboardVariantsModel->key( index );
+                 m_current.selectedVariant = m_keyboardVariantsModel->key( index );
                  somethingChanged();
              } );
     connect( m_KeyboardGroupSwitcherModel,
              &KeyboardGroupsSwitchersModel::currentIndexChanged,
              [ & ]( int index )
              {
-                 m_selectedGroup = m_KeyboardGroupSwitcherModel->key( index );
+                 m_current.selectedGroup = m_KeyboardGroupSwitcherModel->key( index );
                  somethingChanged();
              } );
 
@@ -207,10 +207,10 @@ Config::Config( QObject* parent )
              this,
              &Config::selectionChange );
 
-    m_selectedModel = m_keyboardModelsModel->key( m_keyboardModelsModel->currentIndex() );
-    m_selectedLayout = m_keyboardLayoutsModel->item( m_keyboardLayoutsModel->currentIndex() ).first;
-    m_selectedVariant = m_keyboardVariantsModel->key( m_keyboardVariantsModel->currentIndex() );
-    m_selectedGroup = m_KeyboardGroupSwitcherModel->key( m_KeyboardGroupSwitcherModel->currentIndex() );
+    m_current.selectedModel = m_keyboardModelsModel->key( m_keyboardModelsModel->currentIndex() );
+    m_current.selectedLayout = m_keyboardLayoutsModel->item( m_keyboardLayoutsModel->currentIndex() ).first;
+    m_current.selectedVariant = m_keyboardVariantsModel->key( m_keyboardVariantsModel->currentIndex() );
+    m_current.selectedGroup = m_KeyboardGroupSwitcherModel->key( m_KeyboardGroupSwitcherModel->currentIndex() );
 }
 
 void
@@ -224,38 +224,56 @@ Config::somethingChanged()
     emit prettyStatusChanged();
 }
 
-void
-Config::apply()
+static void
+applyXkb( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra )
 {
-    if ( m_configureXkb )
-    {
-        applyXkb();
-    }
-    if ( m_configureLocale1 )
+    QStringList basicArguments = xkbmap_model_args( settings.selectedModel );
+    if ( !extra.additionalLayout.isEmpty() )
     {
-        applyLocale1();
+        if ( !settings.selectedGroup.isEmpty() )
+        {
+            extra.groupSwitcher = "grp:" + settings.selectedGroup;
+        }
+
+        if ( extra.groupSwitcher.isEmpty() )
+        {
+            extra.groupSwitcher = xkbmap_query_grp_option();
+        }
+        if ( extra.groupSwitcher.isEmpty() )
+        {
+            extra.groupSwitcher = "grp:alt_shift_toggle";
+        }
+
+        basicArguments.append(
+            xkbmap_layout_args_with_group_switch( { extra.additionalLayout, settings.selectedLayout },
+                                                  { extra.additionalVariant, settings.selectedVariant },
+                                                  extra.groupSwitcher ) );
+        QProcess::execute( "setxkbmap", basicArguments );
+
+        cDebug() << "xkbmap selection changed to: " << settings.selectedLayout << '-' << settings.selectedVariant
+                 << "(added " << extra.additionalLayout << "-" << extra.additionalVariant
+                 << " since current layout is not ASCII-capable)";
     }
-    if ( m_configureKWin )
+    else
     {
-        applyKWin();
+        basicArguments.append( xkbmap_layout_args( settings.selectedLayout, settings.selectedVariant ) );
+        QProcess::execute( "setxkbmap", basicArguments );
+        cDebug() << "xkbmap selection changed to: " << settings.selectedLayout << '-' << settings.selectedVariant;
     }
-    // Writing /etc/ files is not needed "live"
 }
 
-void
-Config::applyLocale1()
+static void
+applyLocale1( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra )
 {
-    m_additionalLayoutInfo = getAdditionalLayoutInfo( m_selectedLayout );
-
-    QString layout = m_selectedLayout;
-    QString variant = m_selectedVariant;
+    QString layout = settings.selectedLayout;
+    QString variant = settings.selectedVariant;
     QString option;
 
-    if ( !m_additionalLayoutInfo.additionalLayout.isEmpty() )
+    if ( !extra.additionalLayout.isEmpty() )
     {
-        layout = m_additionalLayoutInfo.additionalLayout + "," + layout;
-        variant = m_additionalLayoutInfo.additionalVariant + "," + variant;
-        option = m_additionalLayoutInfo.groupSwitcher;
+        layout = extra.additionalLayout + "," + layout;
+        variant = extra.additionalVariant + "," + variant;
+        option = extra.groupSwitcher;
     }
 
     QDBusInterface locale1( "org.freedesktop.locale1",
@@ -270,7 +288,8 @@ Config::applyLocale1()
 
     // Using convert=true, this also updates the VConsole config
     {
-        QDBusReply< void > r = locale1.call( "SetX11Keyboard", layout, m_selectedModel, variant, option, true, false );
+        QDBusReply< void > r
+            = locale1.call( "SetX11Keyboard", layout, settings.selectedModel, variant, option, true, false );
         if ( !r.isValid() )
         {
             cWarning() << "Could not set keyboard config through org.freedesktop.locale1.X11Keyboard." << r.error();
@@ -278,47 +297,6 @@ Config::applyLocale1()
     }
 }
 
-void
-Config::applyXkb()
-{
-    m_additionalLayoutInfo = getAdditionalLayoutInfo( m_selectedLayout );
-
-    QStringList basicArguments = xkbmap_model_args( m_selectedModel );
-    if ( !m_additionalLayoutInfo.additionalLayout.isEmpty() )
-    {
-        if ( !m_selectedGroup.isEmpty() )
-        {
-            m_additionalLayoutInfo.groupSwitcher = "grp:" + m_selectedGroup;
-        }
-
-        if ( m_additionalLayoutInfo.groupSwitcher.isEmpty() )
-        {
-            m_additionalLayoutInfo.groupSwitcher = xkbmap_query_grp_option();
-        }
-        if ( m_additionalLayoutInfo.groupSwitcher.isEmpty() )
-        {
-            m_additionalLayoutInfo.groupSwitcher = "grp:alt_shift_toggle";
-        }
-
-        basicArguments.append(
-            xkbmap_layout_args_with_group_switch( { m_additionalLayoutInfo.additionalLayout, m_selectedLayout },
-                                                  { m_additionalLayoutInfo.additionalVariant, m_selectedVariant },
-                                                  m_additionalLayoutInfo.groupSwitcher ) );
-        QProcess::execute( "setxkbmap", basicArguments );
-
-        cDebug() << "xkbmap selection changed to: " << m_selectedLayout << '-' << m_selectedVariant << "(added "
-                 << m_additionalLayoutInfo.additionalLayout << "-" << m_additionalLayoutInfo.additionalVariant
-                 << " since current layout is not ASCII-capable)";
-    }
-    else
-    {
-        basicArguments.append( xkbmap_layout_args( m_selectedLayout, m_selectedVariant ) );
-        QProcess::execute( "setxkbmap", basicArguments );
-        cDebug() << "xkbmap selection changed to: " << m_selectedLayout << '-' << m_selectedVariant;
-    }
-    m_applyTimer.stop();
-}
-
 // In a config-file's list of lines, replace lines <key>=<something> by <key>=<value>
 static void
 replaceKey( QStringList& content, const QString& key, const QString& value )
@@ -368,21 +346,21 @@ rewriteKWin( const QString& path, const QString& model, const QString& layouts,
 }
 
 void
-Config::applyKWin()
+applyKWin( const BasicLayoutInfo& settings, AdditionalLayoutInfo& extra )
 {
     const auto paths = QStandardPaths::standardLocations( QStandardPaths::ConfigLocation );
 
-    auto join = [ &additional = m_additionalLayoutInfo.additionalLayout ]( const QString& s1, const QString& s2 )
+    auto join = [ &additional = extra.additionalLayout ]( const QString& s1, const QString& s2 )
     { return additional.isEmpty() ? s1 : QStringLiteral( "%1,%2" ).arg( s1, s2 ); };
 
-    const QString layouts = join( m_selectedLayout, m_additionalLayoutInfo.additionalLayout );
-    const QString variants = join( m_selectedVariant, m_additionalLayoutInfo.additionalVariant );
+    const QString layouts = join( settings.selectedLayout, extra.additionalLayout );
+    const QString variants = join( settings.selectedVariant, extra.additionalVariant );
 
     bool updated = false;
     for ( const auto& path : paths )
     {
         const QString candidate = path + QStringLiteral( "/kxkbrc" );
-        if ( rewriteKWin( candidate, m_selectedModel, layouts, variants ) )
+        if ( rewriteKWin( candidate, settings.selectedModel, layouts, variants ) )
         {
             updated = true;
             break;
@@ -397,6 +375,25 @@ Config::applyKWin()
     }
 }
 
+void
+Config::apply()
+{
+    m_additionalLayoutInfo = getAdditionalLayoutInfo( m_current.selectedLayout );
+    if ( m_configureXkb )
+    {
+        applyXkb( m_current, m_additionalLayoutInfo );
+    }
+    if ( m_configureLocale1 )
+    {
+        applyLocale1( m_current, m_additionalLayoutInfo );
+    }
+    if ( m_configureKWin )
+    {
+        applyKWin( m_current, m_additionalLayoutInfo );
+    }
+    m_applyTimer.stop();
+    // Writing /etc/ files is not needed "live"
+}
 
 KeyboardModelsModel*
 Config::keyboardModels() const
@@ -574,6 +571,26 @@ Config::detectCurrentKeyboardLayout()
             break;
         }
     }
+    // The models have updated the m_current settings, copy them
+    m_original = m_current;
+}
+
+void
+Config::cancel()
+{
+    const auto extra = getAdditionalLayoutInfo( m_original.selectedLayout );
+    if ( m_configureXkb )
+    {
+        applyXkb( m_original, m_additionalLayoutInfo );
+    }
+    if ( m_configureLocale1 )
+    {
+        applyLocale1( m_original, m_additionalLayoutInfo );
+    }
+    if ( m_configureKWin )
+    {
+        applyKWin( m_original, m_additionalLayoutInfo );
+    }
 }
 
 QString
@@ -599,9 +616,9 @@ Config::createJobs()
 {
     QList< Calamares::job_ptr > list;
 
-    Calamares::Job* j = new SetKeyboardLayoutJob( m_selectedModel,
-                                                  m_selectedLayout,
-                                                  m_selectedVariant,
+    Calamares::Job* j = new SetKeyboardLayoutJob( m_current.selectedModel,
+                                                  m_current.selectedLayout,
+                                                  m_current.selectedVariant,
                                                   m_additionalLayoutInfo,
                                                   m_xOrgConfFileName,
                                                   m_convertedKeymapPath,
@@ -748,10 +765,10 @@ void
 Config::finalize()
 {
     Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
-    if ( !m_selectedLayout.isEmpty() )
+    if ( !m_current.selectedLayout.isEmpty() )
     {
-        gs->insert( "keyboardLayout", m_selectedLayout );
-        gs->insert( "keyboardVariant", m_selectedVariant );  //empty means default variant
+        gs->insert( "keyboardLayout", m_current.selectedLayout );
+        gs->insert( "keyboardVariant", m_current.selectedVariant );  //empty means default variant
 
         if ( !m_additionalLayoutInfo.additionalLayout.isEmpty() )
         {
diff --git a/src/modules/keyboard/Config.h b/src/modules/keyboard/Config.h
index e2a8c4f0e3ac2c9e4ae02e8ffa2575e1261a843d..d48c734bea7b0f004a5ec8a36152a1a80784b07d 100644
--- a/src/modules/keyboard/Config.h
+++ b/src/modules/keyboard/Config.h
@@ -44,6 +44,9 @@ public:
     /// @brief When leaving the page, write to GS
     void finalize();
 
+    /// @brief Restore the system to whatever layout was in use when detectCurrentKeyboardLayout() was called
+    void cancel();
+
     static AdditionalLayoutInfo getAdditionalLayoutInfo( const QString& layout );
 
     /* A model is a physical configuration of a keyboard, e.g. 105-key PC
@@ -94,9 +97,6 @@ private:
      */
     void somethingChanged();
     void apply();
-    void applyLocale1();
-    void applyXkb();
-    void applyKWin();
 
     void getCurrentKeyboardLayoutXkb( QString& currentLayout, QString& currentVariant, QString& currentModel );
     void getCurrentKeyboardLayoutLocale1( QString& currentLayout, QString& currentVariant, QString& currentModel );
@@ -106,10 +106,8 @@ private:
     KeyboardVariantsModel* m_keyboardVariantsModel;
     KeyboardGroupsSwitchersModel* m_KeyboardGroupSwitcherModel;
 
-    QString m_selectedLayout;
-    QString m_selectedModel;
-    QString m_selectedVariant;
-    QString m_selectedGroup;
+    BasicLayoutInfo m_current;
+    BasicLayoutInfo m_original;
 
     // Layout (and corresponding info) added if current one doesn't support ASCII (e.g. Russian or Japanese)
     AdditionalLayoutInfo m_additionalLayoutInfo;
diff --git a/src/modules/keyboard/KeyboardViewStep.cpp b/src/modules/keyboard/KeyboardViewStep.cpp
index 3eaf4dcc5d956d6c62dcd933444d95180475ba8d..92df2d0bcca405849231d6d7a79068b87acda5d9 100644
--- a/src/modules/keyboard/KeyboardViewStep.cpp
+++ b/src/modules/keyboard/KeyboardViewStep.cpp
@@ -104,6 +104,11 @@ KeyboardViewStep::onLeave()
     m_config->finalize();
 }
 
+void
+KeyboardViewStep::onCancel()
+{
+    m_config->cancel();
+}
 
 void
 KeyboardViewStep::setConfigurationMap( const QVariantMap& configurationMap )
diff --git a/src/modules/keyboard/KeyboardViewStep.h b/src/modules/keyboard/KeyboardViewStep.h
index 902b888fd3785e12ea9cc35046c4b4b9cb24b2c5..96fd4c897ea4830b9f5e5335b7a2e45552b15db0 100644
--- a/src/modules/keyboard/KeyboardViewStep.h
+++ b/src/modules/keyboard/KeyboardViewStep.h
@@ -43,6 +43,7 @@ public:
 
     void onActivate() override;
     void onLeave() override;
+    void onCancel() override;
 
     void setConfigurationMap( const QVariantMap& configurationMap ) override;
 
diff --git a/src/modules/keyboardq/KeyboardQmlViewStep.cpp b/src/modules/keyboardq/KeyboardQmlViewStep.cpp
index f63d03b81db281962175749639cf88871ee78bcd..1acdd6ec60cee55af2aa111347ba86b62e72a2bd 100644
--- a/src/modules/keyboardq/KeyboardQmlViewStep.cpp
+++ b/src/modules/keyboardq/KeyboardQmlViewStep.cpp
@@ -80,6 +80,12 @@ KeyboardQmlViewStep::onLeave()
     m_config->finalize();
 }
 
+void
+KeyboardQmlViewStep::onCancel()
+{
+    m_config->cancel();
+}
+
 QObject*
 KeyboardQmlViewStep::getConfig()
 {
diff --git a/src/modules/keyboardq/KeyboardQmlViewStep.h b/src/modules/keyboardq/KeyboardQmlViewStep.h
index eb31c3d595c09204304f7d6da872a488ce33838c..540e753aab06ff5bc68074cdac4c76b6bbc30a71 100644
--- a/src/modules/keyboardq/KeyboardQmlViewStep.h
+++ b/src/modules/keyboardq/KeyboardQmlViewStep.h
@@ -39,6 +39,7 @@ public:
 
     void onActivate() override;
     void onLeave() override;
+    void onCancel() override;
 
     void setConfigurationMap( const QVariantMap& configurationMap ) override;
     QObject* getConfig() override;
diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp
index b2ef0e890b81262458c4511ce317cf7721a339a7..3e27a52279ede5c759329ad3376382cb19ea4dab 100644
--- a/src/modules/locale/Config.cpp
+++ b/src/modules/locale/Config.cpp
@@ -378,7 +378,7 @@ Config::currentLocationStatus() const
 {
     if ( m_currentLocation )
     {
-        return tr( "Set timezone to %1.", "@action" ).arg( currentTimezoneName());
+        return tr( "Set timezone to %1.", "@action" ).arg( currentTimezoneName() );
     }
     return QString();
 }
@@ -513,6 +513,8 @@ getGeoIP( const QVariantMap& configurationMap, std::unique_ptr< Calamares::GeoIP
 void
 Config::setConfigurationMap( const QVariantMap& configurationMap )
 {
+    m_originalTimezone = Calamares::GeoIP::splitTZString( QTimeZone::systemTimeZoneId() );
+
     getLocaleGenLines( configurationMap, m_localeGenLines );
     getAdjustLiveTimezone( configurationMap, m_adjustLiveTimezone );
     getStartingTimezone( configurationMap, m_startingTimezone );
@@ -588,3 +590,12 @@ Config::completeGeoIP()
     m_geoipWatcher.reset();
     m_geoip.reset();
 }
+
+void
+Config::cancel()
+{
+    if ( m_adjustLiveTimezone && m_originalTimezone.isValid() )
+    {
+        QProcess::execute( "timedatectl", { "set-timezone", m_originalTimezone.asString() } );
+    }
+}
diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h
index a26d25a9c9727e6926e072e4e6cfe0b4d667e075..fb1fae8621c278ae7a79695349dcb731b75cf588 100644
--- a/src/modules/locale/Config.h
+++ b/src/modules/locale/Config.h
@@ -83,10 +83,12 @@ public:
 
     const Calamares::Locale::TimeZoneData* currentLocation() const { return m_currentLocation; }
 
-
     /// Special case, set location from starting timezone if not already set
     void setCurrentLocation();
 
+    /// Restores original timezone, if any
+    void cancel();
+
 private:
     Calamares::Locale::TimeZoneData* currentLocation_c() const
     {
@@ -176,6 +178,9 @@ private:
      */
     Calamares::GeoIP::RegionZonePair m_startingTimezone;
 
+    /// @brief The timezone set in the system when Calamares started (not from config)
+    Calamares::GeoIP::RegionZonePair m_originalTimezone;
+
     /** @brief Handler for GeoIP lookup (if configured)
      *
      * The GeoIP lookup needs to be started at some suitable time,
diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp
index c40e3ae51872113634caacd6df42787e15889f73..87290a8514bfc8fea64d93569917d92376d26b9c 100644
--- a/src/modules/locale/LocaleViewStep.cpp
+++ b/src/modules/locale/LocaleViewStep.cpp
@@ -130,6 +130,12 @@ LocaleViewStep::onLeave()
     m_config->finalizeGlobalStorage();
 }
 
+void
+LocaleViewStep::onCancel()
+{
+    m_config->cancel();
+}
+
 void
 LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap )
 {
diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h
index 12b05f9f8b0462fc9b1a12c51dc6d08a3fb2e54d..6244219476de6b633eb30405218cf7a337be746c 100644
--- a/src/modules/locale/LocaleViewStep.h
+++ b/src/modules/locale/LocaleViewStep.h
@@ -44,6 +44,7 @@ public:
 
     void onActivate() override;
     void onLeave() override;
+    void onCancel() override;
 
     void setConfigurationMap( const QVariantMap& configurationMap ) override;
 
diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp
index 6863bb78adfad107314005eb718d2fe33a0a8b03..37f131c9543bed8d6569ad33d9ae379c387c4049 100644
--- a/src/modules/localeq/LocaleQmlViewStep.cpp
+++ b/src/modules/localeq/LocaleQmlViewStep.cpp
@@ -83,6 +83,12 @@ LocaleQmlViewStep::onLeave()
     m_config->finalizeGlobalStorage();
 }
 
+void
+LocaleQmlViewStep::onCancel()
+{
+    m_config->cancel();
+}
+
 void
 LocaleQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap )
 {
diff --git a/src/modules/localeq/LocaleQmlViewStep.h b/src/modules/localeq/LocaleQmlViewStep.h
index ca70ca5d95308b3d76983c5ba05c33d6033e7710..a7b20ba0470e3cc8026342dbe1fb521a3646c1b0 100644
--- a/src/modules/localeq/LocaleQmlViewStep.h
+++ b/src/modules/localeq/LocaleQmlViewStep.h
@@ -34,8 +34,9 @@ public:
     bool isAtBeginning() const override;
     bool isAtEnd() const override;
 
-    virtual void onActivate() override;
-    virtual void onLeave() override;
+    void onActivate() override;
+    void onLeave() override;
+    void onCancel() override;
 
     Calamares::JobList jobs() const override;
 
diff --git a/src/modules/partition/Config.cpp b/src/modules/partition/Config.cpp
index 085c451791e96d10c34e286b200007d33c2c039f..b2f88789a5ef92682cb8c1767bf7931b9333db84 100644
--- a/src/modules/partition/Config.cpp
+++ b/src/modules/partition/Config.cpp
@@ -451,9 +451,11 @@ Config::setConfigurationMap( const QVariantMap& configurationMap )
     {
         bool bogus = true;
         const auto lvmConfiguration = Calamares::getSubMap( configurationMap, "lvm", bogus );
-        m_isLVMEnabled = Calamares::getBool( lvmConfiguration, "enable", true);
+        m_isLVMEnabled = Calamares::getBool( lvmConfiguration, "enable", true );
     }
 
+    m_essentialMounts= Calamares::getStringList( configurationMap, "essentialMounts" );
+
     Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
     gs->insert( "armInstall", Calamares::getBool( configurationMap, "armInstall", false ) );
     fillGSConfigurationEFI( gs, configurationMap );
diff --git a/src/modules/partition/Config.h b/src/modules/partition/Config.h
index 13da58ac44c445d482af2db68ae1977fd515b92b..d2dfec64d16157b067a06c34dc2bb21a1e89f015 100644
--- a/src/modules/partition/Config.h
+++ b/src/modules/partition/Config.h
@@ -40,6 +40,8 @@ class Config : public QObject
 
     Q_PROPERTY( bool lvmEnabled READ isLVMEnabled CONSTANT FINAL )
 
+    Q_PROPERTY( QStringList essentialMounts READ essentialMounts CONSTANT FINAL )
+
 public:
     Config( QObject* parent );
     ~Config() override = default;
@@ -178,6 +180,14 @@ public:
 
     bool isLVMEnabled() const { return m_isLVMEnabled; }
 
+    /** @brief A list of names that can follow /dev/mapper/ that must not be closed
+     *
+     * These names (if any) are skipped by the ClearMountsJob.
+     * The names may contain a trailing '*' which acts as a wildcard.
+     * In any other position, '*' is interpreted literally.
+     */
+    QStringList essentialMounts() const { return m_essentialMounts; }
+
 public Q_SLOTS:
     void setInstallChoice( int );  ///< Translates a button ID or so to InstallChoice
     void setInstallChoice( InstallChoice );
@@ -213,6 +223,7 @@ private:
     bool m_preCheckEncryption = false;
     bool m_showNotEncryptedBootMessage = true;
     bool m_isLVMEnabled = true;
+    QStringList m_essentialMounts;
 };
 
 /** @brief Given a set of swap choices, return a sensible value from it.
diff --git a/src/modules/partition/core/PartitionCoreModule.cpp b/src/modules/partition/core/PartitionCoreModule.cpp
index c2fd61db9aebf956d2c593edcfd8c77ef03d58c8..d92c66f72dccdd2837f76573a03103679c7f2e54 100644
--- a/src/modules/partition/core/PartitionCoreModule.cpp
+++ b/src/modules/partition/core/PartitionCoreModule.cpp
@@ -610,7 +610,7 @@ PartitionCoreModule::setPartitionFlags( Device* device, Partition* partition, Pa
 STATICTEST QStringList
 findEssentialLVs( const QList< PartitionCoreModule::DeviceInfo* >& infos )
 {
-    QStringList doNotClose;
+    QStringList essentialLV;
     cDebug() << "Checking LVM use on" << infos.count() << "devices";
     for ( const auto* info : infos )
     {
@@ -635,12 +635,12 @@ findEssentialLVs( const QList< PartitionCoreModule::DeviceInfo* >& infos )
                     cDebug() << Logger::SubEntry << partPath
                              << "is an essential LV filesystem=" << partition->fileSystem().type();
                     QString lvName = partPath.right( partPath.length() - devicePath.length() );
-                    doNotClose.append( info->device->name() + '-' + lvName );
+                    essentialLV.append( info->device->name() + '-' + lvName );
                 }
             }
         }
     }
-    return doNotClose;
+    return essentialLV;
 }
 
 Calamares::JobList
@@ -670,14 +670,14 @@ PartitionCoreModule::jobs( const Config* config ) const
 #ifdef DEBUG_PARTITION_SKIP
     cWarning() << "Partitioning actions are skipped.";
 #else
-    const QStringList doNotClose = findEssentialLVs( m_deviceInfos );
+    const QStringList essentialMounts = findEssentialLVs( m_deviceInfos ) + config->essentialMounts();
 
     for ( const auto* info : m_deviceInfos )
     {
         if ( info->isDirty() )
         {
             auto* job = new ClearMountsJob( info->device.data() );
-            job->setMapperExceptions( doNotClose );
+            job->setMapperExceptions( essentialMounts );
             lst << Calamares::job_ptr( job );
         }
     }
diff --git a/src/modules/partition/jobs/ClearMountsJob.cpp b/src/modules/partition/jobs/ClearMountsJob.cpp
index 2fbafb5dc754d40dccbb18aa512c67d6a3712287..b4ebfebf490d3759ad51fcef42d3b0402c58bff5 100644
--- a/src/modules/partition/jobs/ClearMountsJob.cpp
+++ b/src/modules/partition/jobs/ClearMountsJob.cpp
@@ -123,6 +123,23 @@ isSpecial( const QString& baseName )
     return specialForFedora || specialMapperControl || specialVentoy;
 }
 
+static inline bool
+matchesExceptions( const QStringList& mapperExceptions, const QString& basename )
+{
+    for ( const auto& e : mapperExceptions )
+    {
+        if ( basename == e )
+        {
+            return true;
+        }
+        if ( e.endsWith( '*' ) && basename.startsWith( e.left( e.length() - 1 ) ) )
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
 /** @brief Returns a list of unneeded crypto devices
  *
  * These are the crypto devices to unmount and close; some are "needed"
@@ -139,7 +156,7 @@ getCryptoDevices( const QStringList& mapperExceptions )
     for ( const QFileInfo& fi : fiList )
     {
         QString baseName = fi.baseName();
-        if ( isSpecial( baseName ) || mapperExceptions.contains( baseName ) )
+        if ( isSpecial( baseName ) || matchesExceptions( mapperExceptions, baseName ) )
         {
             continue;
         }
diff --git a/src/modules/partition/partition.conf b/src/modules/partition/partition.conf
index 2f56df71582a368bcf64d3f69a6a718f1a3b0b45..4c78f58140fa65cb84893371545ff998f5038fc5 100644
--- a/src/modules/partition/partition.conf
+++ b/src/modules/partition/partition.conf
@@ -266,6 +266,13 @@ defaultFileSystemType:  "ext4"
 #                         even if the directory is part of a filesystem on a
 #                         different mountpoint. Defaults to false.
 
+# The ClearMounts job unmounts / unmaps things before partitioning.
+# Some special entries under /dev/mapper are excepted from this process.
+# The example lists the three hard-coded exceptions which always apply
+# (they don't need to be listed here). Add other names or wildcards (with
+# a trailing '*') to this list if the live-ISO has additional mounts.
+essentialMounts: [ "live-*", "control", "ventoy" ]
+
 # Show/hide LUKS related functionality in automated partitioning modes.
 # Disable this if you choose not to deploy early unlocking support in GRUB2
 # and/or your distribution's initramfs solution.
diff --git a/src/modules/partition/partition.schema.yaml b/src/modules/partition/partition.schema.yaml
index 4bd2fa4ae0f6e94d3e759f9c4b205377ad0e2bdf..07763aef4ca6879f6a203047cdf9f39743091020 100644
--- a/src/modules/partition/partition.schema.yaml
+++ b/src/modules/partition/partition.schema.yaml
@@ -42,6 +42,7 @@ properties:
     luksGeneration: { type: string, enum: [luks1, luks2] }  # Also allows "luks" as alias of "luks1"
     enableLuksAutomatedPartitioning: { type: boolean, default: false }
     preCheckEncryption: { type: boolean, default: false }
+    essentialMounts: { type: array, items: { type: string } } # List of names under /dev/mapper not to close
 
     allowManualPartitioning: { type: boolean, default: true }
     showNotEncryptedBootMessage: { type: boolean, default: true }