diff --git a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/MountDisksTests.cs b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/MountDisksTests.cs index bd36d49138..bfa6d77536 100644 --- a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/MountDisksTests.cs +++ b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/MountDisksTests.cs @@ -266,6 +266,110 @@ public async Task MountDisksMountsTheExpectedPathOnWindowsWhenAMountPrefixIsProv } } + [Test] + public async Task MountDisksSkipsReservedPartitionsThatCannotBeMountedOnWindows() + { + this.mockFixture.Setup(PlatformID.Win32NT); + + // A real, mountable data volume (has a volume index + drive letter) with no access path yet. + DiskVolume dataVolume = this.mockFixture.CreateDiskVolume(1, @"D:\", PlatformID.Win32NT, os: false, lun: 0); + dataVolume.AccessPaths = new List(); + + // A reserved/hidden partition with no mountable identity. + DiskVolume reservedVolume = new DiskVolume( + index: null, + devicePath: string.Empty, + accessPaths: new List(), + properties: new Dictionary + { + { "Type", "e3c9e316-0b5c-4db8-817d-f92df00215ae" }, + { "PartitionIndex", "1" }, + { "Hidden", "Yes" }, + { "Required", "No" } + }); + + Disk dataDisk = new Disk( + 1, + @"\\.\PHYSICALDISK1", + new List { reservedVolume, dataVolume }, + properties: new Dictionary { { "Index", 1 } }); + + Disk osDisk = this.mockFixture.CreateDisk(0, PlatformID.Win32NT, os: true, @"\\.\PHYSICALDISK0", @"C:\"); + + this.mockFixture.DiskManager.Setup(mgr => mgr.GetDisksAsync(It.IsAny())) + .ReturnsAsync(new List { osDisk, dataDisk }); + this.mockFixture.File.Setup(f => f.Exists(It.IsAny())).Returns(true); + this.mockFixture.Directory.Setup(d => d.Exists(It.IsAny())).Returns(true); + + using (MountDisks diskMounter = new MountDisks(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await diskMounter.ExecuteAsync(CancellationToken.None); + + // The reserved partition must never be handed to the disk manager for mounting. + this.mockFixture.DiskManager.Verify( + mgr => mgr.CreateMountPointAsync(reservedVolume, It.IsAny(), It.IsAny()), + Times.Never); + + // The real data volume must still be mounted exactly once. + string expectedMountPoint = this.mockFixture.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + dataVolume.GetDefaultMountPointName()); + + this.mockFixture.DiskManager.Verify( + mgr => mgr.CreateMountPointAsync(dataVolume, expectedMountPoint, It.IsAny()), + Times.Once); + } + } + + [Test] + public async Task MountDisksSkipsPartitionsWithoutADevicePathOnUnix() + { + // On Unix, a mount requires a device path (e.g. /dev/sdc1). A partition without one + // has no mountable identity and must be skipped rather than passed to the disk manager. + this.mockFixture.Setup(PlatformID.Unix); + + // A real, mountable data volume (has a device path) with no access path yet. + DiskVolume dataVolume = this.mockFixture.CreateDiskVolume(1, "/dev/sdc1", PlatformID.Unix, os: false, lun: 0); + dataVolume.AccessPaths = new List(); + + // A partition with no device path (nothing to mount). + DiskVolume reservedVolume = new DiskVolume( + index: null, + devicePath: string.Empty, + accessPaths: new List(), + properties: new Dictionary { { "name", "reserved" } }); + + Disk dataDisk = new Disk( + 1, + "/dev/sdc", + new List { reservedVolume, dataVolume }, + properties: new Dictionary { { "logicalname", "/dev/sdc" } }); + + Disk osDisk = this.mockFixture.CreateDisk(0, PlatformID.Unix, os: true, "/dev/sda", "/dev/sda1"); + + this.mockFixture.DiskManager.Setup(mgr => mgr.GetDisksAsync(It.IsAny())) + .ReturnsAsync(new List { osDisk, dataDisk }); + this.mockFixture.File.Setup(f => f.Exists(It.IsAny())).Returns(true); + this.mockFixture.Directory.Setup(d => d.Exists(It.IsAny())).Returns(true); + + using (MountDisks diskMounter = new MountDisks(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await diskMounter.ExecuteAsync(CancellationToken.None); + + // The partition without a device path must never be handed to the disk manager. + this.mockFixture.DiskManager.Verify( + mgr => mgr.CreateMountPointAsync(reservedVolume, It.IsAny(), It.IsAny()), + Times.Never); + + // The real data volume must still be mounted exactly once. + string expectedMountPoint = $"/home/{Environment.UserName}/{dataVolume.GetDefaultMountPointName()}"; + + this.mockFixture.DiskManager.Verify( + mgr => mgr.CreateMountPointAsync(dataVolume, expectedMountPoint, It.IsAny()), + Times.Once); + } + } + private void SetupTest(PlatformID platformID, bool withMultipleVolumes = false) { this.diskVolumes = new List(); diff --git a/src/VirtualClient/VirtualClient.Dependencies/MountDisks.cs b/src/VirtualClient/VirtualClient.Dependencies/MountDisks.cs index cb2daadf0e..55ba381ce5 100644 --- a/src/VirtualClient/VirtualClient.Dependencies/MountDisks.cs +++ b/src/VirtualClient/VirtualClient.Dependencies/MountDisks.cs @@ -134,12 +134,12 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel // Don't mount any partition in OS drive. foreach (Disk disk in disks.Where(d => !d.IsOperatingSystem())) { - IEnumerable diskVolumes = disk.Volumes.Where(v => v.AccessPaths?.Any() != true); + IEnumerable diskVolumes = disk.Volumes.Where(v => v.AccessPaths?.Any() != true && this.IsMountable(v)); if (diskVolumes?.Any() == true) { // mount every volume that doesn't have an accessPath. - foreach (DiskVolume volume in disk.Volumes.Where(v => v.AccessPaths?.Any() != true)) + foreach (DiskVolume volume in diskVolumes) { string newMountPoint = null; string mountPointName = volume.GetDefaultMountPointName(prefix: mountPrefix); @@ -216,6 +216,27 @@ await this.systemManager.SetFullPermissionsAsync( return mountPointsCreated; } + private bool IsMountable(DiskVolume volume) + { + // A mount point can only be assigned to a volume that has the identity its platform's + // mount operation requires. Reserved/metadata partitions (e.g. the Windows Microsoft + // Reserved Partition) have none of these and cannot be mounted, so they must be skipped. + switch (this.Platform) + { + case PlatformID.Win32NT: + // Windows mounts a volume by its volume index or drive letter. + return volume.Index != null + || volume.Properties?.ContainsKey(Disk.WindowsDiskProperties.Letter) == true; + + case PlatformID.Unix: + // Unix mounts a volume by its device path (e.g. /dev/sdc1). + return !string.IsNullOrWhiteSpace(volume.DevicePath); + + default: + return true; + } + } + private IEnumerable GetTargetDisks(IEnumerable disks, string diskFilter) { List filteredDisks = new List();