diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs
index 0bc3dc8e..ed852733 100644
--- a/Source/LibationFileManager/AudibleFileStorage.cs
+++ b/Source/LibationFileManager/AudibleFileStorage.cs
@@ -31,8 +31,19 @@ public abstract class AudibleFileStorage
{
try
{
- return (DateTime.UtcNow - lastInProgressFail) < RetryInProgressInterval ? null
- : new DirectoryInfo(Configuration.Instance.InProgress).CreateSubdirectoryEx(subDirectory);
+ if ((DateTime.UtcNow - lastInProgressFail) < RetryInProgressInterval)
+ return null;
+
+ var parent = new DirectoryInfo(Configuration.Instance.InProgress);
+ var dir = parent.CreateSubdirectoryEx(subDirectory);
+
+ // On Unix, lock down the parent and the subdir to the current user (0700).
+ // Prevents other local users from reading partial downloads / decrypted artifacts
+ // when InProgress lives on a world-traversable filesystem like /tmp.
+ TrySetOwnerOnlyMode(parent.FullName);
+ TrySetOwnerOnlyMode(dir.FullName);
+
+ return dir;
}
catch (Exception ex)
{
@@ -42,6 +53,20 @@ public abstract class AudibleFileStorage
}
}
+ private static void TrySetOwnerOnlyMode(string path)
+ {
+ if (OperatingSystem.IsWindows())
+ return;
+ try
+ {
+ File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
+ }
+ catch
+ {
+ // best-effort. user may have intentionally set their own perms, or the FS may not support it.
+ }
+ }
+
public static LongPath? DownloadsInProgressDirectory => CreateInProgressDirectory("DownloadsInProgress")?.FullName;
public static LongPath? DecryptInProgressDirectory => CreateInProgressDirectory("DecryptInProgress")?.FullName;
diff --git a/Source/LibationFileManager/Configuration.KnownDirectories.cs b/Source/LibationFileManager/Configuration.KnownDirectories.cs
index 30368246..f2b037d5 100644
--- a/Source/LibationFileManager/Configuration.KnownDirectories.cs
+++ b/Source/LibationFileManager/Configuration.KnownDirectories.cs
@@ -13,7 +13,10 @@ public partial class Configuration
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LibationFiles.LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));
- public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
+ // Use a per-user subdir so we don't collide with -- or get blocked by -- another local
+ // user's leftover dir on shared-/tmp systems (Linux). On Windows and macOS, GetTempPath
+ // already returns a per-user location, so this is just a harmless extra path segment.
+ public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"Libation-{Environment.UserName}"));
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"));
diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs
index dfd28477..cf1f82ee 100644
--- a/Source/LibationUiBase/Upgrader.cs
+++ b/Source/LibationUiBase/Upgrader.cs
@@ -62,8 +62,9 @@ public class Upgrader : UpgraderBase
}
//Silently download the upgrade in the background, save it to a temp file.
-
- var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
+ var zipFile = GetUpgradeDownloadPath(upgradeProperties.ZipUrl);
+ if (zipFile is null)
+ return null;
Serilog.Log.Logger.Information($"Downloading {zipFile}");
@@ -103,6 +104,29 @@ public class Upgrader : UpgraderBase
return null;
}
}
+
+ ///
+ /// Allocate a fresh per-run temp directory for the upgrade zip and return the full path
+ /// the zip should be downloaded to. Uses a random subdirectory name (and 0700 perms on
+ /// Unix) so we never extract or execute from a predictable, shared-temp location.
+ ///
+ /// Destination path for the upgrade zip, or null if the temp directory
+ /// could not be created (in which case the upgrade-failed event has already been raised).
+ private string? GetUpgradeDownloadPath(string zipUrl)
+ {
+ try
+ {
+ var stagingDir = Directory.CreateTempSubdirectory("Libation-upgrade-").FullName;
+ return Path.Combine(stagingDir, Path.GetFileName(zipUrl));
+ }
+ catch (Exception ex)
+ {
+ var message = "Failed to create a temp directory for the upgrade download.";
+ Serilog.Log.Logger.Error(ex, message);
+ OnUpgradeFailed(message, ex);
+ return null;
+ }
+ }
}
public class MockUpgrader : UpgraderBase