Index: src/wp-includes/class-wp-customize-widgets.php
===================================================================
--- src/wp-includes/class-wp-customize-widgets.php	(revision 27691)
+++ src/wp-includes/class-wp-customize-widgets.php	(working copy)
@@ -906,9 +906,9 @@
 	static function call_widget_update( $widget_id ) {
 		global $wp_registered_widget_updates, $wp_registered_widget_controls;
 
-		$options_transaction = new Options_Transaction();
+		$option_capture = new Option_Update_Capture();
 
-		$options_transaction->start();
+		$option_capture->start();
 		$parsed_id   = self::parse_widget_id( $widget_id );
 		$option_name = 'widget_' . $parsed_id['id_base'];
 
@@ -920,13 +920,13 @@
 		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
 			$sanitized_widget_setting = json_decode( self::get_post_value( 'sanitized_widget_setting' ), true );
 			if ( empty( $sanitized_widget_setting ) ) {
-				$options_transaction->rollback();
+				$option_capture->stop();
 				return new WP_Error( 'malformed_data', 'Malformed sanitized_widget_setting' );
 			}
 
 			$instance = self::sanitize_widget_instance( $sanitized_widget_setting );
 			if ( is_null( $instance ) ) {
-				$options_transaction->rollback();
+				$option_capture->stop();
 				return new WP_Error( 'unsanitary_data', 'Unsanitary sanitized_widget_setting' );
 			}
 
@@ -965,15 +965,15 @@
 		/**
 		 * Make sure the expected option was updated
 		 */
-		if ( 0 !== $options_transaction->count() ) {
-			if ( count( $options_transaction->options ) > 1 ) {
-				$options_transaction->rollback();
+		if ( 0 !== $option_capture->count() ) {
+			if ( count( $option_capture->options ) > 1 ) {
+				$option_capture->stop();
 				return new WP_Error( 'unexpected_update', 'Widget unexpectedly updated more than one option.' );
 			}
 
-			$updated_option_name = key( $options_transaction->options );
+			$updated_option_name = key( $option_capture->options );
 			if ( $updated_option_name !== $option_name ) {
-				$options_transaction->rollback();
+				$option_capture->stop();
 				return new WP_Error( 'wrong_option', sprintf( 'Widget updated option "%1$s", but expected "%2$s".', $updated_option_name, $option_name ) );
 			}
 		}
@@ -998,7 +998,7 @@
 			$instance = $option;
 		}
 
-		$options_transaction->rollback();
+		$option_capture->stop();
 		return compact( 'instance', 'form' );
 	}
 
@@ -1053,16 +1053,15 @@
 	}
 }
 
-class Options_Transaction {
+class Option_Update_Capture {
 
 	/**
-	 * @var array $options values updated while transaction is open
+	 * @var array $options values updated while capturing is happening
 	 */
 	public $options = array();
 
 	protected $_ignore_transients = true;
 	protected $_is_current = false;
-	protected $_operations = array();
 
 	function __construct( $ignore_transients = true ) {
 		$this->_ignore_transients = $ignore_transients;
@@ -1085,106 +1084,78 @@
 	}
 
 	/**
-	 * Get the number of operations performed in the transaction
+	 * Get the number of options updated
 	 * @return bool
 	 */
 	function count() {
-		return count( $this->_operations );
+		return count( $this->options );
 	}
 
 	/**
 	 * Start keeping track of changes to options, and cache their new values
 	 */
 	function start() {
-		$this->_is_current = true;
-		add_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
-		add_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
-		add_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
-		add_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+		if ( $this->_is_current ) {
+			return;
 	}
 
-	/**
-	 * @action added_option
-	 * @param $option_name
-	 * @param $new_value
-	 */
-	function _capture_added_option( $option_name, $new_value ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
-			return;
+		$this->_is_current = true;
+		add_filter( 'pre_update_option', array( $this, 'pre_update_option' ), 10, 3 );
 		}
-		$this->options[$option_name] = $new_value;
-		$operation = 'add';
-		$this->_operations[] = compact( 'operation', 'option_name', 'new_value' );
-	}
 
 	/**
-	 * @action updated_option
+	 *
+	 * @param  mixed  $new_value
 	 * @param string $option_name
-	 * @param mixed $old_value
 	 * @param mixed $new_value
+	 * @return mixed
 	 */
-	function _capture_updated_option( $option_name, $old_value, $new_value ) {
+	function pre_update_option( $new_value, $option_name, $old_value ) {
 		if ( $this->is_option_ignored( $option_name ) ) {
 			return;
 		}
-		$this->options[$option_name] = $new_value;
-		$operation = 'update';
-		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'new_value' );
+
+		if ( ! isset( $this->options[$option_name] ) ) {
+			add_filter( "pre_option_{$option_name}", array( $this, 'pre_get_option' ) );
 	}
 
-	protected $_pending_delete_option_autoload;
-	protected $_pending_delete_option_value;
+		$this->options[$option_name] = $new_value;
 
-	/**
-	 * It's too bad the old_value and autoload aren't passed into the deleted_option action
-	 * @action delete_option
-	 * @param string $option_name
-	 */
-	function _capture_pre_deleted_option( $option_name ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
-			return;
+		return $old_value;
 		}
-		global $wpdb;
-		$autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // db call ok; no-cache ok
-		$this->_pending_delete_option_autoload = $autoload;
-		$this->_pending_delete_option_value    = get_option( $option_name );
-	}
 
 	/**
-	 * @action deleted_option
-	 * @param string $option_name
+	 *
+	 * @param mixed $value
 	 */
-	function _capture_deleted_option( $option_name ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
-			return;
+	function pre_get_option( $value ) {
+		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
+		if ( isset( $this->options[$option_name] ) ) {
+			$value = $this->options[$option_name];
+			$value = apply_filters( 'option_' . $option_name, $value );
 		}
-		unset( $this->options[$option_name] );
-		$operation = 'delete';
-		$old_value = $this->_pending_delete_option_value;
-		$autoload  = $this->_pending_delete_option_autoload;
-		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'autoload' );
+
+		return $value;
 	}
 
 	/**
 	 * Undo any changes to the options since start() was called
 	 */
-	function rollback() {
-		remove_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
-		remove_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
-		remove_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
-		remove_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
-		while ( 0 !== count( $this->_operations ) ) {
-			$option_operation = array_pop( $this->_operations );
-			if ( 'add' === $option_operation['operation'] ) {
-				delete_option( $option_operation['option_name'] );
+	function stop() {
+		if ( ! $this->_is_current ) {
+			return;
 			}
-			else if ( 'delete' === $option_operation['operation'] ) {
-				add_option( $option_operation['option_name'], $option_operation['old_value'], null, $option_operation['autoload'] );
+
+		remove_filter( 'pre_update_option', array( $this, 'pre_update_option' ), 10, 3 );
+		foreach ( array_keys( $this->options ) as $option_name ) {
+			remove_filter( "pre_option_{$option_name}", array( $this, 'pre_get_option' ) );
 			}
-			else if ( 'update' === $option_operation['operation'] ) {
-				update_option( $option_operation['option_name'], $option_operation['old_value'] );
+
+		$this->options     = array();
+		$this->_is_current = false;
 			}
+
+	function __destruct() {
+		$this->stop();
 		}
-		$this->_is_current = false;
 	}
-}
Index: src/wp-includes/option.php
===================================================================
--- src/wp-includes/option.php	(revision 27691)
+++ src/wp-includes/option.php	(working copy)
@@ -255,6 +255,17 @@
 	 */
 	$value = apply_filters( 'pre_update_option_' . $option, $value, $old_value );
 
+	/**
+	 * Filter an option before its value is (maybe) serialized and updated.
+	 *
+	 * @since 3.9.0
+	 *
+	 * @param mixed  $value     The new, unserialized option value.
+	 * @param string $option    Name of the option.
+	 * @param mixed  $old_value The old option value.
+	 */
+	$value = apply_filters( 'pre_update_option', $value, $option, $old_value );
+
 	// If the new and old values are the same, no need to update.
 	if ( $value === $old_value )
 		return false;
