fix(launcher): Campaign LA LA3 narrow review fixes

This commit is contained in:
Erik 2026-08-14 17:06:47 +02:00
parent 26feba8186
commit 347a1a5d16
9 changed files with 495 additions and 145 deletions

View file

@ -19,6 +19,8 @@ namespace AcDream.Launcher.Core.Profiles;
public sealed class LauncherProfileStore
{
internal const int CurrentVersion = 1;
internal const UnixFileMode OwnerOnlyFileMode =
UnixFileMode.UserRead | UnixFileMode.UserWrite;
private static readonly JsonSerializerOptions SerializerOptions = new()
{
@ -115,14 +117,12 @@ public sealed class LauncherProfileStore
/// <summary>
/// Persists <see cref="Document"/> to <see cref="FilePath"/> via a
/// write-then-atomic-rename so a crash mid-write never leaves a
/// truncated credentials file. On Linux, the temp file is chmod'd to
/// owner read/write (0600) immediately after creation — BEFORE any
/// plaintext credential is serialized into it — so there is no window
/// where the temp file carries the process umask's (potentially
/// world/group-readable) default permissions while holding a
/// password; the final path gets the same restriction after the
/// rename (Campaign LA's plaintext-credential decision, spec §5,
/// decisions log; the temp-file window itself is review finding F4).
/// truncated credentials file. On Linux, the temp file is created
/// atomically with owner read/write (0600) as its requested creation
/// mode — before its path is observable and before any plaintext
/// credential is serialized into it. The final path retains that mode
/// through the rename (Campaign LA's plaintext-credential decision,
/// spec §5, decisions log).
/// A failure between temp-file creation and the rename deletes the
/// stale temp file rather than leaving it behind.
/// </summary>
@ -135,15 +135,18 @@ public sealed class LauncherProfileStore
}
string tempPath = FilePath + ".tmp";
DeleteStaleTempFile(tempPath);
try
{
using (FileStream stream = File.Create(tempPath))
using (FileStream stream = CreateCredentialTempFile(tempPath))
{
if (OperatingSystem.IsLinux())
{
File.SetUnixFileMode(
tempPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite);
// UnixCreateMode is subject to the process umask. It
// guarantees the file is never created with group/other
// access; normalize the owner bits while the still-empty
// file is open so the persisted contract is exactly 0600.
File.SetUnixFileMode(tempPath, OwnerOnlyFileMode);
}
JsonSerializer.Serialize(stream, Document, SerializerOptions);
@ -159,12 +162,39 @@ public sealed class LauncherProfileStore
if (OperatingSystem.IsLinux())
{
File.SetUnixFileMode(
FilePath,
UnixFileMode.UserRead | UnixFileMode.UserWrite);
File.SetUnixFileMode(FilePath, OwnerOnlyFileMode);
}
}
/// <summary>
/// Builds the exact options used for the plaintext-credential temp
/// file. <see cref="FileMode.CreateNew"/> makes creation atomic and
/// refuses to follow an existing stale or raced path. On Linux,
/// <see cref="FileStreamOptions.UnixCreateMode"/> supplies 0600 to
/// the OS create operation itself, eliminating the observable
/// create-then-chmod window. Windows leaves UnixCreateMode unset and
/// therefore retains its normal user-profile ACL behavior.
/// </summary>
internal static FileStreamOptions CreateCredentialTempFileOptions()
{
var options = new FileStreamOptions
{
Mode = FileMode.CreateNew,
Access = FileAccess.Write,
Share = FileShare.None,
};
if (OperatingSystem.IsLinux())
{
options.UnixCreateMode = OwnerOnlyFileMode;
}
return options;
}
internal static FileStream CreateCredentialTempFile(string tempPath) =>
new(tempPath, CreateCredentialTempFileOptions());
private static void DeleteStaleTempFile(string tempPath)
{
try