diff --git a/install.ps1 b/install.ps1 index 98b0ed7d..7903d8ff 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "1a10ac215c60706699d267fcfa31c4947d6aa792b46455e078a5861fa8c5e2ea" +$defaultInstallSha256 = "16732df165f2f3cf971ae2049aea4b89c7b6a48925d395417c9a363e9bb57970" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 66908857..364d8105 100644 --- a/install.py +++ b/install.py @@ -2604,6 +2604,17 @@ def _claim_partial(path: Path) -> BinaryIO | None: except OSError: handle.close() return None + # Opened and locked are two steps: in between, the run that held the + # partial may have renamed it into place and released it. The lock then + # sits on the installed BIOS, which looks like a finished partial and + # would be truncated. Only the file still under the partial's name is ours. + try: + ours = os.path.samestat(os.fstat(handle.fileno()), os.lstat(path)) + except OSError: + ours = False + if not ours: + handle.close() + return None return handle diff --git a/install.sh b/install.sh index 6957c00a..640614fb 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="1a10ac215c60706699d267fcfa31c4947d6aa792b46455e078a5861fa8c5e2ea" +DEFAULT_INSTALL_SHA256="16732df165f2f3cf971ae2049aea4b89c7b6a48925d395417c9a363e9bb57970" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 9eb27c3d..cb257561 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -145,6 +145,31 @@ class TwoRunsOneFolder(unittest.TestCase): self.assertIsNotNone(install._claim_partial(part)) +class APartialRenamedBeforeTheLock(unittest.TestCase): + """Open and lock are two steps. The run holding the partial could finish + in between, rename it into place and release it: the second run then + locked the installed BIOS, took it for a finished partial and truncated it.""" + + @unittest.skipIf(os.name == "nt", "flock semantics") + def test_the_installed_file_is_not_claimed(self): + import fcntl + + with tempfile.TemporaryDirectory() as tmp: + part = Path(tmp) / ".big.bin.part" + final = Path(tmp) / "big.bin" + part.write_bytes(PAYLOAD) + real_flock = fcntl.flock + + def finish_first(fd, op): + os.replace(part, final) + return real_flock(fd, op) + + with mock.patch.object(fcntl, "flock", finish_first): + claimed = install._claim_partial(part) + self.assertIsNone(claimed) + self.assertEqual(final.read_bytes(), PAYLOAD) + + class InstalledFileModes(unittest.TestCase): """A file came out 0600, the mode of the temporary it was written to: run with sudo for a root-owned BIOS folder, the emulator's own user could not