Index: src/wp-includes/class-wp-roles.php
===================================================================
--- src/wp-includes/class-wp-roles.php	(revision 41624)
+++ src/wp-includes/class-wp-roles.php	(working copy)
@@ -65,12 +65,29 @@
 	public $use_db = true;
 
 	/**
+	 * The site ID the roles are initialized for.
+	 *
+	 * @since 4.9.0
+	 * @var int
+	 */
+	protected $site_id = 0;
+
+	/**
 	 * Constructor
 	 *
 	 * @since 2.0.0
+	 * @since 4.9.0 The $site_id argument was added.
+	 *
+	 * @global array $wp_user_roles Used to set the 'roles' property value.
+	 *
+	 * @param int $site_id Site ID to initialize roles for. Default is the current site.
 	 */
-	public function __construct() {
-		$this->_init();
+	public function __construct( $site_id = null ) {
+		global $wp_user_roles;
+
+		$this->use_db = empty( $wp_user_roles );
+
+		$this->for_site( $site_id );
 	}
 
 	/**
@@ -97,38 +114,12 @@
 	 * be used and the role option will not be updated or used.
 	 *
 	 * @since 2.1.0
-	 *
-	 * @global array $wp_user_roles Used to set the 'roles' property value.
+	 * @deprecated 4.9.0 Use WP_Roles::for_site()
 	 */
 	protected function _init() {
-		global $wp_user_roles, $wpdb;
+		_deprecated_function( __METHOD__, '4.9.0', 'WP_Roles::for_site()' );
 
-		$this->role_key = $wpdb->get_blog_prefix() . 'user_roles';
-		if ( ! empty( $wp_user_roles ) ) {
-			$this->roles = $wp_user_roles;
-			$this->use_db = false;
-		} else {
-			$this->roles = get_option( $this->role_key );
-		}
-
-		if ( empty( $this->roles ) )
-			return;
-
-		$this->role_objects = array();
-		$this->role_names =  array();
-		foreach ( array_keys( $this->roles ) as $role ) {
-			$this->role_objects[$role] = new WP_Role( $role, $this->roles[$role]['capabilities'] );
-			$this->role_names[$role] = $this->roles[$role]['name'];
-		}
-
-		/**
-		 * After the roles have been initialized, allow plugins to add their own roles.
-		 *
-		 * @since 4.7.0
-		 *
-		 * @param WP_Roles $this A reference to the WP_Roles object.
-		 */
-		do_action( 'wp_roles_init', $this );
+		$this->for_site();
 	}
 
 	/**
@@ -138,11 +129,12 @@
 	 * after switching wpdb to a new site ID.
 	 *
 	 * @since 3.5.0
-	 * @deprecated 4.7.0 Use new WP_Roles()
+	 * @deprecated 4.7.0 Use WP_Roles::for_site()
 	 */
 	public function reinit() {
-		_deprecated_function( __METHOD__, '4.7.0', 'new WP_Roles()' );
-		$this->_init();
+		_deprecated_function( __METHOD__, '4.7.0', 'WP_Roles::for_site()' );
+
+		$this->for_site();
 	}
 
 	/**
@@ -270,4 +262,100 @@
 	public function is_role( $role ) {
 		return isset( $this->role_names[$role] );
 	}
+
+	/**
+	 * Initializes all of the available roles.
+	 *
+	 * @since 4.9.0
+	 */
+	public function init_roles() {
+		if ( empty( $this->roles ) ) {
+			return;
+		}
+
+		$this->role_objects = array();
+		$this->role_names =  array();
+		foreach ( array_keys( $this->roles ) as $role ) {
+			$this->role_objects[ $role ] = new WP_Role( $role, $this->roles[ $role ]['capabilities'] );
+			$this->role_names[ $role ] = $this->roles[ $role ]['name'];
+		}
+
+		/**
+		 * After the roles have been initialized, allow plugins to add their own roles.
+		 *
+		 * @since 4.7.0
+		 *
+		 * @param WP_Roles $this A reference to the WP_Roles object.
+		 */
+		do_action( 'wp_roles_init', $this );
+	}
+
+	/**
+	 * Sets the site to operate on. Defaults to the current site.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
+	 * @param int $site_id Site ID to initialize roles for. Default is the current site.
+	 */
+	public function for_site( $site_id = null ) {
+		global $wpdb;
+
+		if ( ! empty( $site_id ) ) {
+			$this->site_id = absint( $site_id );
+		} else {
+			$this->site_id = get_current_blog_id();
+		}
+
+		$this->role_key = $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles';
+
+		if ( ! empty( $this->roles ) && ! $this->use_db ) {
+			return;
+		}
+
+		$this->roles = $this->get_roles_data();
+
+		$this->init_roles();
+	}
+
+	/**
+	 * Gets the ID of the site for which roles are currently initialized.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @return int Site ID.
+	 */
+	public function get_site_id() {
+		return $this->site_id;
+	}
+
+	/**
+	 * Gets the available roles data.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @global array $wp_user_roles Used to set the 'roles' property value.
+	 *
+	 * @return array Roles array.
+	 */
+	protected function get_roles_data() {
+		global $wp_user_roles;
+
+		if ( ! empty( $wp_user_roles ) ) {
+			return $wp_user_roles;
+		}
+
+		if ( is_multisite() && $this->site_id != get_current_blog_id() ) {
+			remove_action( 'switch_blog', 'wp_switch_roles_and_user', 1 );
+
+			$roles = get_blog_option( $this->site_id, $this->role_key, array() );
+
+			add_action( 'switch_blog', 'wp_switch_roles_and_user', 1, 2 );
+
+			return $roles;
+		}
+
+		return get_option( $this->role_key, array() );
+	}
 }
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 41624)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -792,7 +792,7 @@
  * @return true Always returns True.
  */
 function switch_to_blog( $new_blog, $deprecated = null ) {
-	global $wpdb, $wp_roles;
+	global $wpdb;
 
 	$blog_id = get_current_blog_id();
 	if ( empty( $new_blog ) ) {
@@ -847,12 +847,6 @@
 		}
 	}
 
-	if ( did_action( 'init' ) ) {
-		$wp_roles = new WP_Roles();
-		$current_user = wp_get_current_user();
-		$current_user->for_site( $new_blog );
-	}
-
 	/** This filter is documented in wp-includes/ms-blogs.php */
 	do_action( 'switch_blog', $new_blog, $prev_blog_id );
 	$GLOBALS['switched'] = true;
@@ -876,7 +870,7 @@
  * @return bool True on success, false if we're already on the current blog
  */
 function restore_current_blog() {
-	global $wpdb, $wp_roles;
+	global $wpdb;
 
 	if ( empty( $GLOBALS['_wp_switched_stack'] ) ) {
 		return false;
@@ -921,12 +915,6 @@
 		}
 	}
 
-	if ( did_action( 'init' ) ) {
-		$wp_roles = new WP_Roles();
-		$current_user = wp_get_current_user();
-		$current_user->for_site( $blog );
-	}
-
 	/** This filter is documented in wp-includes/ms-blogs.php */
 	do_action( 'switch_blog', $blog, $prev_blog_id );
 
@@ -937,6 +925,27 @@
 }
 
 /**
+ * Switches the initialized roles and current user capabilities to another site.
+ *
+ * @since 4.9.0
+ *
+ * @param int $new_site_id New site ID.
+ * @param int $old_site_id Old site ID.
+ */
+function wp_switch_roles_and_user( $new_site_id, $old_site_id ) {
+	if ( $new_site_id == $old_site_id ) {
+		return;
+	}
+
+	if ( ! did_action( 'init' ) ) {
+		return;
+	}
+
+	wp_roles()->for_site( $new_site_id );
+	wp_get_current_user()->for_site( $new_site_id );
+}
+
+/**
  * Determines if switch_to_blog() is in effect
  *
  * @since 3.5.0
Index: src/wp-includes/ms-default-filters.php
===================================================================
--- src/wp-includes/ms-default-filters.php	(revision 41624)
+++ src/wp-includes/ms-default-filters.php	(working copy)
@@ -32,6 +32,9 @@
 add_action( 'network_user_new_created_user',   'wp_send_new_user_notifications' );
 add_filter( 'sanitize_user', 'strtolower' );
 
+// Roles
+add_action( 'switch_blog', 'wp_switch_roles_and_user', 1, 2 );
+
 // Blogs
 add_filter( 'wpmu_validate_blog_signup', 'signup_nonce_check' );
 add_action( 'wpmu_new_blog', 'wpmu_log_new_registrations', 10, 2 );
Index: tests/phpunit/tests/user/capabilities.php
===================================================================
--- tests/phpunit/tests/user/capabilities.php	(revision 41624)
+++ tests/phpunit/tests/user/capabilities.php	(working copy)
@@ -1939,4 +1939,75 @@
 
 		$this->assertSame( 333, $user->get_site_id() );
 	}
+
+	/**
+	 * @ticket 38645
+	 * @group ms-required
+	 */
+	function test_init_roles_for_different_site() {
+		global $wpdb;
+
+		$site_id = self::factory()->blog->create();
+
+		switch_to_blog( $site_id );
+
+		$role_name = 'uploader';
+		add_role( $role_name, 'Uploader', array(
+			'read'         => true,
+			'upload_files' => true,
+		) );
+
+		restore_current_blog();
+
+		$wp_roles = wp_roles();
+		$wp_roles->for_site( $site_id );
+
+		$this->assertTrue( isset( $wp_roles->role_objects[ $role_name ] ) );
+	}
+
+	/**
+	 * @ticket 38645
+	 */
+	function test_get_roles_data() {
+		global $wpdb;
+
+		$custom_roles = array(
+			'test_role' => array(
+				'name'         => 'Test Role',
+				'capabilities' => array(
+					'do_foo' => true,
+					'do_bar' => false,
+				),
+			),
+		);
+
+		// Test `WP_Roles::get_roles_data()` by manually setting the roles option.
+		update_option( $wpdb->get_blog_prefix( get_current_blog_id() ) . 'user_roles', $custom_roles );
+
+		$roles = new WP_Roles();
+		$this->assertSame( $custom_roles, $roles->roles );
+	}
+
+	/**
+	 * @ticket 38645
+	 */
+	function test_roles_get_site_id_default() {
+		$roles = new WP_Roles();
+		$this->assertSame( get_current_blog_id(), $roles->get_site_id() );
+	}
+
+	/**
+	 * @ticket 38645
+	 */
+	function test_roles_get_site_id() {
+		global $wpdb;
+
+		// Suppressing errors here allows to get around creating an actual site,
+		// which is unnecessary for this test.
+		$suppress = $wpdb->suppress_errors();
+		$roles = new WP_Roles( 333 );
+		$wpdb->suppress_errors( $suppress );
+
+		$this->assertSame( 333, $roles->get_site_id() );
+	}
 }
