Merge remote-tracking branch 'origin/main' into r7551

This commit is contained in:
Daniel Han 2026-07-29 07:52:59 +00:00
commit c6219fa4d3
5 changed files with 361 additions and 28 deletions

View file

@ -1888,8 +1888,11 @@ jobs:
# (step/substep -> Write-StudioStdoutMirror / Get-StudioAnsi). # (step/substep -> Write-StudioStdoutMirror / Get-StudioAnsi).
$script:StudioVtOk = $false $script:StudioVtOk = $false
$script:UnslothVerbose = $false $script:UnslothVerbose = $false
# Get-HostMachineArch is reached only on the absent path, where
# Test-VCRedistInstalled consults it before trusting the System32 DLL, so
# part A passes without it and only the clean-box part fails.
foreach ($fn in @('Get-StudioAnsi', 'Write-StudioStdoutMirror', 'step', 'substep', foreach ($fn in @('Get-StudioAnsi', 'Write-StudioStdoutMirror', 'step', 'substep',
'Invoke-SetupCommand', 'Refresh-Environment', 'Invoke-SetupCommand', 'Refresh-Environment', 'Get-HostMachineArch',
'Test-VCRedistInstalled', 'Ensure-VCRedist')) { 'Test-VCRedistInstalled', 'Ensure-VCRedist')) {
$src = Get-FunctionSource -Path $setup -Name $fn $src = Get-FunctionSource -Path $setup -Name $fn
if (-not $src) { throw "Function '$fn' not found in setup.ps1" } if (-not $src) { throw "Function '$fn' not found in setup.ps1" }

View file

@ -9,6 +9,7 @@ import ipaddress
import os import os
import secrets import secrets
import sqlite3 import sqlite3
import tempfile
import threading import threading
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, Tuple from typing import Optional, Tuple
@ -30,6 +31,97 @@ _BOOTSTRAP_PW_PATH = DB_PATH.parent / ".bootstrap_password"
_bootstrap_password: Optional[str] = None _bootstrap_password: Optional[str] = None
def _bootstrap_file_bytes(password: str) -> bytes:
"""Exact on-disk form: the secret plus one LF.
Bytes, not text: text mode writes CRLF on Windows, and `$(cat ...)` strips
the LF but leaves the CR attached to the credential.
"""
return (password + "\n").encode("utf-8")
def _persist_bootstrap_password(password: str) -> None:
"""Atomically write the bootstrap password 0600, LF terminated on every OS.
A partial write would destroy the only plaintext recovery credential.
"""
fd, tmp_name = tempfile.mkstemp(
prefix = f".{_BOOTSTRAP_PW_PATH.name}.", dir = _BOOTSTRAP_PW_PATH.parent
)
try:
with os.fdopen(fd, "wb") as f:
f.write(_bootstrap_file_bytes(password))
try:
os.chmod(tmp_name, 0o600)
except OSError:
pass
os.replace(tmp_name, _BOOTSTRAP_PW_PATH)
except BaseException:
try:
os.unlink(tmp_name)
except OSError:
pass
raise
def _normalise_bootstrap_file(raw: bytes, password: str) -> None:
"""Append the LF a pre-newline release left off.
Append-only, and only when the file is exactly the credential:
clear_bootstrap_password() may unlink or (when unlink fails, notably on
Windows while this descriptor is open) truncate through another descriptor
after we read, so a rewrite could restore revoked plaintext. An append
cannot: worst case is a lone "\\n" over a cleared file, which strips back to
no bootstrap password. Pre-newline releases wrote no terminator at all, so
that is the only shape in the wild; anything else reads fine, since every
reader strips, and is left alone.
"""
if raw != password.encode("utf-8"):
return
# O_BINARY: without it Windows opens in text mode and turns the LF straight
# back into CRLF, the bug being fixed.
fd = os.open(
_BOOTSTRAP_PW_PATH,
os.O_WRONLY | os.O_APPEND | getattr(os, "O_BINARY", 0),
)
try:
os.write(fd, b"\n")
try:
os.fchmod(fd, 0o600)
except (AttributeError, OSError):
# fchmod only reached Windows in 3.13.
pass
finally:
os.close(fd)
def _read_persisted_bootstrap_password() -> Optional[str]:
"""Read the persisted password, normalising the file if it is malformed."""
if not _BOOTSTRAP_PW_PATH.is_file():
return None
# No caller handles a raise, so an unreadable file has to mean "no bootstrap
# password", not a dead backend. We write UTF-8, so undecodable bytes are
# damage whose plaintext is worthless anyway.
try:
raw = _BOOTSTRAP_PW_PATH.read_bytes()
password = raw.decode("utf-8").strip()
except (OSError, UnicodeDecodeError):
return None
if not password:
return None
# Older releases wrote no terminator; best-effort, a read-only auth dir must
# not fail startup.
if raw != _bootstrap_file_bytes(password):
try:
_normalise_bootstrap_file(raw, password)
except OSError:
pass
return password
def generate_bootstrap_password() -> str: def generate_bootstrap_password() -> str:
"""Generate a 4-word diceware passphrase and persist it to disk. """Generate a 4-word diceware passphrase and persist it to disk.
@ -43,10 +135,10 @@ def generate_bootstrap_password() -> str:
return _bootstrap_password return _bootstrap_password
# Persisted from a previous run? # Persisted from a previous run?
if _BOOTSTRAP_PW_PATH.is_file(): persisted = _read_persisted_bootstrap_password()
_bootstrap_password = _BOOTSTRAP_PW_PATH.read_text(encoding = "utf-8").strip() if persisted:
if _bootstrap_password: _bootstrap_password = persisted
return _bootstrap_password return _bootstrap_password
# First startup: generate a fresh passphrase. # First startup: generate a fresh passphrase.
import diceware import diceware
@ -57,11 +149,7 @@ def generate_bootstrap_password() -> str:
# Persist so the same passphrase survives restarts until password change. # Persist so the same passphrase survives restarts until password change.
ensure_dir(_BOOTSTRAP_PW_PATH.parent) ensure_dir(_BOOTSTRAP_PW_PATH.parent)
_BOOTSTRAP_PW_PATH.write_text(_bootstrap_password, encoding = "utf-8") _persist_bootstrap_password(_bootstrap_password)
try:
os.chmod(_BOOTSTRAP_PW_PATH, 0o600)
except OSError:
pass
return _bootstrap_password return _bootstrap_password
@ -72,19 +160,14 @@ def get_bootstrap_password() -> Optional[str]:
def _load_bootstrap_password() -> Optional[str]: def _load_bootstrap_password() -> Optional[str]:
"""Load an existing bootstrap password without creating one.""" """Load an existing bootstrap password without creating one.
Upgrades take this path, not generate_bootstrap_password()
(ensure_default_admin short-circuits once the admin row exists), so it has
to normalise too.
"""
global _bootstrap_password global _bootstrap_password
_bootstrap_password = None _bootstrap_password = _read_persisted_bootstrap_password()
if _BOOTSTRAP_PW_PATH.is_file():
# No caller handles a raise, so an unreadable file has to mean "no bootstrap
# password", not a dead backend. We write UTF-8, so bytes that will not
# decode are damage whose plaintext is worthless anyway.
try:
bootstrap_password = _BOOTSTRAP_PW_PATH.read_text(encoding = "utf-8").strip()
except (OSError, UnicodeDecodeError):
return _bootstrap_password
if bootstrap_password:
_bootstrap_password = bootstrap_password
return _bootstrap_password return _bootstrap_password

View file

@ -134,6 +134,218 @@ def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch
assert storage.get_bootstrap_password() == bootstrap_pw assert storage.get_bootstrap_password() == bootstrap_pw
def test_bootstrap_password_file_ends_with_a_newline():
# Otherwise `cat` welds the passphrase onto the shell prompt.
storage.ensure_default_admin()
# Bytes: read_text would decode CRLF back to "\n" and hide a CR.
raw = storage._BOOTSTRAP_PW_PATH.read_bytes()
assert raw == storage.get_bootstrap_password().encode("utf-8") + b"\n"
def test_bootstrap_password_round_trips_across_a_restart_with_the_newline():
storage.ensure_default_admin()
original = storage.get_bootstrap_password()
storage._bootstrap_password = None
assert storage.generate_bootstrap_password() == original
def test_upgrade_normalises_the_bootstrap_file():
# Upgrade path: the admin row exists, so generate_bootstrap_password() never runs.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
storage.ensure_default_admin()
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n"
assert storage.get_bootstrap_password() == "legacy-bootstrap-secret"
@pytest.mark.parametrize(
"other",
[
b"legacy-bootstrap-secret\r\n", # only an unreleased build wrote this
b"legacy-bootstrap-secret\r",
b"legacy-bootstrap-secret ",
],
)
def test_only_an_exactly_unterminated_bootstrap_file_is_touched(other):
# Appending is safe only because it is restricted to the one released shape.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(other)
storage.ensure_default_admin()
assert storage.get_bootstrap_password() == "legacy-bootstrap-secret"
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == other
def test_upgrade_normalises_when_the_admin_row_is_missing():
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
assert storage.generate_bootstrap_password() == "legacy-bootstrap-secret"
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n"
def test_a_well_formed_bootstrap_file_is_not_rewritten():
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret\n")
mtime = storage._BOOTSTRAP_PW_PATH.stat().st_mtime_ns
storage.ensure_default_admin()
assert storage._BOOTSTRAP_PW_PATH.stat().st_mtime_ns == mtime
def test_migration_failure_does_not_break_startup(monkeypatch):
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
real_open = storage.os.open
def refuse(path, flags, *args, **kwargs):
if str(path) == str(storage._BOOTSTRAP_PW_PATH):
raise PermissionError("read-only auth dir")
return real_open(path, flags, *args, **kwargs)
monkeypatch.setattr(storage.os, "open", refuse)
storage.ensure_default_admin()
assert storage.get_bootstrap_password() == "legacy-bootstrap-secret"
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret"
def test_normalising_never_recreates_a_cleared_bootstrap_file(monkeypatch):
# A rename would resurrect revoked plaintext if the password changed after the read.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
real_open = storage.os.open
def clear_then_open(path, flags, *args, **kwargs):
if str(path) == str(storage._BOOTSTRAP_PW_PATH):
storage._BOOTSTRAP_PW_PATH.unlink(missing_ok = True)
return real_open(path, flags, *args, **kwargs)
monkeypatch.setattr(storage.os, "open", clear_then_open)
assert storage._read_persisted_bootstrap_password() == "legacy-bootstrap-secret"
assert not storage._BOOTSTRAP_PW_PATH.exists()
def test_normalising_does_not_overwrite_a_rotated_bootstrap_file(monkeypatch):
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
real_open = storage.os.open
def rotate_then_open(path, flags, *args, **kwargs):
if str(path) == str(storage._BOOTSTRAP_PW_PATH):
storage._BOOTSTRAP_PW_PATH.write_bytes(b"brand-new-secret\n")
return real_open(path, flags, *args, **kwargs)
monkeypatch.setattr(storage.os, "open", rotate_then_open)
storage._read_persisted_bootstrap_password()
# The append may add a second newline; the rotated credential must survive.
raw = storage._BOOTSTRAP_PW_PATH.read_bytes()
assert raw.strip() == b"brand-new-secret"
storage._bootstrap_password = None
assert storage._load_bootstrap_password() == "brand-new-secret"
def test_leading_whitespace_bootstrap_file_is_left_alone(monkeypatch):
# An in-place rewrite is not atomic, so only the exact unterminated shape is touched.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b" legacy-bootstrap-secret ")
storage.ensure_default_admin()
assert storage.get_bootstrap_password() == "legacy-bootstrap-secret"
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b" legacy-bootstrap-secret "
def test_normalising_opens_the_file_in_binary_mode(monkeypatch):
# Without O_BINARY, Windows text mode turns the written LF back into CRLF.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
monkeypatch.setattr(storage.os, "O_BINARY", 0x8000, raising = False)
seen = []
real_open = storage.os.open
def spy(path, flags, *args, **kwargs):
if str(path) == str(storage._BOOTSTRAP_PW_PATH):
seen.append(flags)
return real_open(path, flags & ~0x8000, *args, **kwargs)
monkeypatch.setattr(storage.os, "open", spy)
storage.ensure_default_admin()
assert seen and all(f & 0x8000 for f in seen), seen
def test_clearing_by_truncation_mid_normalisation_is_not_undone(monkeypatch):
# clear_bootstrap_password() truncates through its own descriptor when the unlink
# fails (Windows, while ours is open); the append must not restore the plaintext.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
real_open = storage.os.open
def truncate_then_open(path, flags, *args, **kwargs):
fd = real_open(path, flags, *args, **kwargs)
if str(path) == str(storage._BOOTSTRAP_PW_PATH):
storage._BOOTSTRAP_PW_PATH.write_text("", encoding = "utf-8")
return fd
monkeypatch.setattr(storage.os, "open", truncate_then_open)
storage._read_persisted_bootstrap_password()
# A lone newline over a cleared file still reads back as no password.
assert storage._BOOTSTRAP_PW_PATH.read_bytes().strip() == b""
storage._bootstrap_password = None
assert storage._load_bootstrap_password() is None
def test_normalising_works_without_fchmod(monkeypatch):
# os.fchmod only reached Windows in 3.13; its absence must not raise.
seed_user()
storage._BOOTSTRAP_PW_PATH.write_bytes(b"legacy-bootstrap-secret")
monkeypatch.delattr(storage.os, "fchmod", raising = False)
storage.ensure_default_admin()
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"legacy-bootstrap-secret\n"
assert storage.get_bootstrap_password() == "legacy-bootstrap-secret"
def test_persisting_the_bootstrap_password_is_atomic(monkeypatch, tmp_path):
# A partial write would destroy the only plaintext recovery credential.
storage._persist_bootstrap_password("original-secret")
def boom(src, dst):
raise OSError("crash before replace")
monkeypatch.setattr(storage.os, "replace", boom)
with pytest.raises(OSError):
storage._persist_bootstrap_password("new-secret")
assert storage._BOOTSTRAP_PW_PATH.read_bytes() == b"original-secret\n"
leftovers = [
p.name
for p in storage._BOOTSTRAP_PW_PATH.parent.iterdir()
if "bootstrap_password." in p.name
]
assert leftovers == []
def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap(): def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap():
seed_user() seed_user()
storage._BOOTSTRAP_PW_PATH.write_text(" \n", encoding = "utf-8") storage._BOOTSTRAP_PW_PATH.write_text(" \n", encoding = "utf-8")
@ -358,7 +570,7 @@ def test_write_desktop_secret_file_is_0600_on_unix(tmp_path):
studio_cli._write_auth_secret(path, "desktop-secret") studio_cli._write_auth_secret(path, "desktop-secret")
assert path.read_text() == "desktop-secret" assert path.read_bytes() == b"desktop-secret\n"
if platform.system() != "Windows": if platform.system() != "Windows":
assert oct(path.stat().st_mode & 0o777) == "0o600" assert oct(path.stat().st_mode & 0o777) == "0o600"
@ -525,7 +737,8 @@ if result.exit_code != 0:
capture_output = True, capture_output = True,
) )
assert result.returncode == 0, result.stderr + result.stdout assert result.returncode == 0, result.stderr + result.stdout
secret = (auth_dir / ".desktop_secret").read_text() # Strip like the src-tauri readers do.
secret = (auth_dir / ".desktop_secret").read_text().strip()
assert secret.startswith("desktop-") assert secret.startswith("desktop-")
conn = sqlite3.connect(auth_dir / "auth.db") conn = sqlite3.connect(auth_dir / "auth.db")

View file

@ -483,9 +483,12 @@ def _write_auth_secret(path: Path, secret: str) -> None:
os.chmod(tmp_path, 0o600) os.chmod(tmp_path, 0o600)
except OSError: except OSError:
pass pass
with os.fdopen(fd, "w", encoding = "utf-8") as f: # newline pins LF: text mode writes CRLF on Windows, and `$(cat ...)`
# strips the LF but leaves the CR glued to the credential.
with os.fdopen(fd, "w", encoding = "utf-8", newline = "\n") as f:
fd = -1 fd = -1
f.write(secret) # Newline so `cat` doesn't run it into the shell prompt; readers strip.
f.write(secret + "\n")
os.replace(tmp_path, path) os.replace(tmp_path, path)
except Exception: except Exception:
if fd >= 0: if fd >= 0:

View file

@ -251,7 +251,7 @@ def test_studio_default_prompt_rejects_current_password(monkeypatch, tmp_path):
studio_mod = _studio() studio_mod = _studio()
events = _install_prompt_env(monkeypatch, tmp_path, interactive = True) events = _install_prompt_env(monkeypatch, tmp_path, interactive = True)
_seed_auth(studio_mod) _seed_auth(studio_mod)
bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text() bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text().strip()
_invoke_studio_default(monkeypatch, events, ["--secure"]) _invoke_studio_default(monkeypatch, events, ["--secure"])
@ -1204,6 +1204,37 @@ def test_connect_auth_db_creates_private_files(monkeypatch, tmp_path):
assert stat.S_IMODE((auth_dir / "auth.db").stat().st_mode) == 0o600 assert stat.S_IMODE((auth_dir / "auth.db").stat().st_mode) == 0o600
def test_write_auth_secret_terminates_the_file_with_a_newline(monkeypatch, tmp_path):
# Shared by .bootstrap_password and .desktop_secret; every reader strips.
studio_mod = _studio()
path = tmp_path / ".desktop_secret"
studio_mod._write_auth_secret(path, "desktop-abc123")
# Bytes: read_text would decode CRLF back to "\n" and hide a CR.
assert path.read_bytes() == b"desktop-abc123\n"
def test_seeded_bootstrap_file_ends_with_a_newline(monkeypatch, tmp_path):
studio_mod = _studio()
monkeypatch.setattr(studio_mod, "STUDIO_HOME", tmp_path)
_seed_auth(studio_mod)
raw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_bytes()
assert raw.endswith(b"\n") and not raw.endswith(b"\r\n")
conn = sqlite3.connect(_auth_db(tmp_path))
try:
salt, pwd_hash = conn.execute(
"SELECT password_salt, password_hash FROM auth_user WHERE username = ?",
(studio_mod.DEFAULT_ADMIN_USERNAME,),
).fetchone()
finally:
conn.close()
assert studio_mod._pbkdf2_hex(raw.decode("utf-8").strip(), salt.encode("utf-8")) == pwd_hash
# ── non-interactive --password / UNSLOTH_STUDIO_PASSWORD / stdin ────── # ── non-interactive --password / UNSLOTH_STUDIO_PASSWORD / stdin ──────
@ -1284,7 +1315,7 @@ def test_studio_default_password_must_differ_fails_closed(monkeypatch, tmp_path)
studio_mod = _studio() studio_mod = _studio()
events = _install_prompt_env(monkeypatch, tmp_path, interactive = True) events = _install_prompt_env(monkeypatch, tmp_path, interactive = True)
_seed_auth(studio_mod) _seed_auth(studio_mod)
bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text() bootstrap_pw = (tmp_path / "auth" / studio_mod.BOOTSTRAP_PASSWORD_FILE).read_text().strip()
result = _invoke_studio_default(monkeypatch, events, ["--secure", "--password", bootstrap_pw]) result = _invoke_studio_default(monkeypatch, events, ["--secure", "--password", bootstrap_pw])