mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
[PlayGo] Reject authoritative unknown chunk loci (#242)
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.PlayGo;
|
namespace SharpEmu.Libs.PlayGo;
|
||||||
|
|
||||||
@@ -423,7 +424,10 @@ public static class PlayGoExports
|
|||||||
// Titles rely on this as an enumeration terminator: Monster Truck
|
// Titles rely on this as an enumeration terminator: Monster Truck
|
||||||
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every
|
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every
|
||||||
// id makes that scan wrap the ushort range and spin forever.
|
// id makes that scan wrap the ushort range and spin forever.
|
||||||
return OrbisPlayGoErrorBadChunkId;
|
loci[i] = PlayGoLocusNotDownloaded;
|
||||||
|
return ctx.Memory.TryWrite(outLoci, loci)
|
||||||
|
? OrbisPlayGoErrorBadChunkId
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
loci[i] = PlayGoLocusLocalFast;
|
loci[i] = PlayGoLocusLocalFast;
|
||||||
@@ -669,7 +673,8 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
return Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
return _metadata.ChunkIdKnowledge == PlayGoChunkIdKnowledge.Unknown ||
|
||||||
|
Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +685,10 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
// No app0 override to probe for sidecar files: same fully-installed
|
// No app0 override to probe for sidecar files: same fully-installed
|
||||||
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
||||||
return new PlayGoMetadata(true, [(ushort)0]);
|
return new PlayGoMetadata(
|
||||||
|
true,
|
||||||
|
[(ushort)0],
|
||||||
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||||
@@ -695,19 +703,19 @@ public static class PlayGoExports
|
|||||||
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||||
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||||
TracePlayGo("metadata_missing; fully-installed single chunk");
|
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||||
return new PlayGoMetadata(true, [(ushort)0]);
|
return new PlayGoMetadata(
|
||||||
|
true,
|
||||||
|
[(ushort)0],
|
||||||
|
PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||||
if (chunkIds.Length == 0)
|
return new PlayGoMetadata(
|
||||||
{
|
true,
|
||||||
// Unreadable/empty sidecar: fall back to chunk 0, not an empty set
|
chunkIds,
|
||||||
// (which IsKnownChunkId would treat as "every id valid").
|
chunkIds.Length == 0
|
||||||
TracePlayGo("metadata_unreadable; fully-installed single chunk");
|
? PlayGoChunkIdKnowledge.Unknown
|
||||||
return new PlayGoMetadata(true, [(ushort)0]);
|
: PlayGoChunkIdKnowledge.Authoritative);
|
||||||
}
|
|
||||||
|
|
||||||
return new PlayGoMetadata(true, chunkIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||||
@@ -720,6 +728,8 @@ public static class PlayGoExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var xml = File.ReadAllText(chunkDefsXml);
|
var xml = File.ReadAllText(chunkDefsXml);
|
||||||
|
_ = XDocument.Parse(xml, LoadOptions.None);
|
||||||
|
|
||||||
var chunkIds = new HashSet<ushort>();
|
var chunkIds = new HashSet<ushort>();
|
||||||
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
||||||
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
||||||
@@ -736,6 +746,10 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
return Array.Empty<ushort>();
|
return Array.Empty<ushort>();
|
||||||
}
|
}
|
||||||
|
catch (System.Xml.XmlException)
|
||||||
|
{
|
||||||
|
return Array.Empty<ushort>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
||||||
@@ -774,8 +788,35 @@ public static class PlayGoExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
|
internal static void ResetForTests()
|
||||||
{
|
{
|
||||||
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_initialized = false;
|
||||||
|
_opened = false;
|
||||||
|
_metadata = PlayGoMetadata.Empty;
|
||||||
|
_installSpeed = PlayGoInstallSpeedTrickle;
|
||||||
|
_languageMask = ulong.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _unknownChunkDiagnostics, 0);
|
||||||
|
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PlayGoChunkIdKnowledge
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Authoritative,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PlayGoMetadata(
|
||||||
|
bool Available,
|
||||||
|
ushort[] ChunkIds,
|
||||||
|
PlayGoChunkIdKnowledge ChunkIdKnowledge)
|
||||||
|
{
|
||||||
|
public static readonly PlayGoMetadata Empty = new(
|
||||||
|
false,
|
||||||
|
Array.Empty<ushort>(),
|
||||||
|
PlayGoChunkIdKnowledge.Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.PlayGo;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.PlayGo;
|
||||||
|
|
||||||
|
[CollectionDefinition("PlayGoState", DisableParallelization = true)]
|
||||||
|
public sealed class PlayGoStateCollection
|
||||||
|
{
|
||||||
|
public const string Name = "PlayGoState";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection(PlayGoStateCollection.Name)]
|
||||||
|
public sealed class PlayGoExportsTests : IDisposable
|
||||||
|
{
|
||||||
|
private const int BadChunkId = unchecked((int)0x80B2000C);
|
||||||
|
private const byte LocusNotDownloaded = 0;
|
||||||
|
private const byte LocusLocalFast = 3;
|
||||||
|
private const ulong MemoryBase = 0x1_0000_0000;
|
||||||
|
private const ulong InitParamsAddress = MemoryBase + 0x100;
|
||||||
|
private const ulong InitBufferAddress = MemoryBase + 0x1000;
|
||||||
|
private const ulong HandleAddress = MemoryBase + 0x200;
|
||||||
|
private const ulong ChunkIdsAddress = MemoryBase + 0x300;
|
||||||
|
private const ulong LociAddress = MemoryBase + 0x400;
|
||||||
|
|
||||||
|
private readonly string? _originalApp0Root;
|
||||||
|
private readonly string _app0Root;
|
||||||
|
private readonly FakeCpuMemory _memory = new(MemoryBase, 0x10000);
|
||||||
|
private readonly CpuContext _ctx;
|
||||||
|
|
||||||
|
public PlayGoExportsTests()
|
||||||
|
{
|
||||||
|
_originalApp0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
|
_app0Root = Path.Combine(Path.GetTempPath(), $"sharpemu-playgo-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(_app0Root);
|
||||||
|
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", _app0Root);
|
||||||
|
PlayGoExports.ResetForTests();
|
||||||
|
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLocus_MetadataFreeApp0_RejectsPastAuthoritativeDefaultChunk()
|
||||||
|
{
|
||||||
|
var handle = InitializeAndOpen();
|
||||||
|
|
||||||
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, GetLocus(handle, [0], 0x7F));
|
||||||
|
Assert.Equal(new byte[] { LocusLocalFast }, ReadLoci(1));
|
||||||
|
|
||||||
|
Assert.Equal(BadChunkId, GetLocus(handle, [1]));
|
||||||
|
Assert.Equal(new byte[] { LocusNotDownloaded }, ReadLoci(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLocus_ParsedChunkDefinitions_WritesPrefixAndRejectsFirstUnknownChunk()
|
||||||
|
{
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(_app0Root, "playgo-chunkdefs.xml"),
|
||||||
|
"<playgo default_chunk=\"2\"><chunk id=\"2\"/><chunk id=\"7\"/></playgo>");
|
||||||
|
var handle = InitializeAndOpen();
|
||||||
|
|
||||||
|
Assert.Equal(BadChunkId, GetLocus(handle, [2, 3], 0xCC));
|
||||||
|
Assert.Equal(new byte[] { LocusLocalFast, LocusNotDownloaded }, ReadLoci(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(UnusableMetadataKind.DatOnly)]
|
||||||
|
[InlineData(UnusableMetadataKind.MalformedChunkDefinitions)]
|
||||||
|
[InlineData(UnusableMetadataKind.UnrecognizedChunkDefinitions)]
|
||||||
|
public void GetLocus_UnparseableMetadata_RemainsPermissive(UnusableMetadataKind metadataKind)
|
||||||
|
{
|
||||||
|
switch (metadataKind)
|
||||||
|
{
|
||||||
|
case UnusableMetadataKind.DatOnly:
|
||||||
|
var sceSys = Directory.CreateDirectory(Path.Combine(_app0Root, "sce_sys"));
|
||||||
|
File.WriteAllBytes(Path.Combine(sceSys.FullName, "playgo-chunk.dat"), [0x70, 0x6C, 0x67, 0x6F]);
|
||||||
|
break;
|
||||||
|
case UnusableMetadataKind.MalformedChunkDefinitions:
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(_app0Root, "playgo-chunkdefs.xml"),
|
||||||
|
"<playgo><chunk id=\"2\"></playgo>");
|
||||||
|
break;
|
||||||
|
case UnusableMetadataKind.UnrecognizedChunkDefinitions:
|
||||||
|
File.WriteAllText(Path.Combine(_app0Root, "playgo-chunkdefs.xml"), "<not-playgo/>");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(metadataKind), metadataKind, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = InitializeAndOpen();
|
||||||
|
|
||||||
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, GetLocus(handle, [42]));
|
||||||
|
Assert.Equal(new byte[] { LocusLocalFast }, ReadLoci(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLocus_FaultingChunkAndOutputPointers_ReturnMemoryFault()
|
||||||
|
{
|
||||||
|
var handle = InitializeAndOpen();
|
||||||
|
const ulong faultingAddress = 0xDEAD_0000_0000;
|
||||||
|
Assert.True(_memory.TryWrite(LociAddress, new byte[] { 0xA5 }));
|
||||||
|
|
||||||
|
SetGetLocusArguments(handle, faultingAddress, 1, LociAddress);
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||||
|
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||||
|
Assert.Equal(new byte[] { 0xA5 }, ReadLoci(1));
|
||||||
|
|
||||||
|
WriteChunkIds([1]);
|
||||||
|
SetGetLocusArguments(handle, ChunkIdsAddress, 1, faultingAddress);
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||||
|
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLocus_AllEntriesSentinel_DoesNotReadChunksOrWriteLoci()
|
||||||
|
{
|
||||||
|
var handle = InitializeAndOpen();
|
||||||
|
Assert.True(_memory.TryWrite(LociAddress, [0xA5]));
|
||||||
|
|
||||||
|
SetGetLocusArguments(handle, 0xDEAD_0000_0000, uint.MaxValue, LociAddress);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
PlayGoExports.PlayGoGetLocus(_ctx));
|
||||||
|
Assert.Equal(new byte[] { 0xA5 }, ReadLoci(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
PlayGoExports.ResetForTests();
|
||||||
|
Environment.SetEnvironmentVariable("SHARPEMU_APP0_DIR", _originalApp0Root);
|
||||||
|
Directory.Delete(_app0Root, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint InitializeAndOpen()
|
||||||
|
{
|
||||||
|
Span<byte> initParams = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(initParams, InitBufferAddress);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(initParams[8..], 0x200000);
|
||||||
|
Assert.True(_memory.TryWrite(InitParamsAddress, initParams));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = InitParamsAddress;
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
PlayGoExports.PlayGoInitialize(_ctx));
|
||||||
|
|
||||||
|
_ctx[CpuRegister.Rdi] = HandleAddress;
|
||||||
|
_ctx[CpuRegister.Rsi] = 0;
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
PlayGoExports.PlayGoOpen(_ctx));
|
||||||
|
|
||||||
|
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||||
|
Assert.True(_memory.TryRead(HandleAddress, handleBytes));
|
||||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(handleBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetLocus(uint handle, ushort[] chunkIds, byte? fill = null)
|
||||||
|
{
|
||||||
|
WriteChunkIds(chunkIds);
|
||||||
|
if (fill is { } fillValue)
|
||||||
|
{
|
||||||
|
var initialLoci = new byte[chunkIds.Length];
|
||||||
|
Array.Fill(initialLoci, fillValue);
|
||||||
|
Assert.True(_memory.TryWrite(LociAddress, initialLoci));
|
||||||
|
}
|
||||||
|
|
||||||
|
SetGetLocusArguments(handle, ChunkIdsAddress, (uint)chunkIds.Length, LociAddress);
|
||||||
|
return PlayGoExports.PlayGoGetLocus(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteChunkIds(ushort[] chunkIds)
|
||||||
|
{
|
||||||
|
var bytes = new byte[chunkIds.Length * sizeof(ushort)];
|
||||||
|
for (var i = 0; i < chunkIds.Length; i++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(bytes.AsSpan(i * sizeof(ushort)), chunkIds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(_memory.TryWrite(ChunkIdsAddress, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadLoci(int count)
|
||||||
|
{
|
||||||
|
var loci = new byte[count];
|
||||||
|
Assert.True(_memory.TryRead(LociAddress, loci));
|
||||||
|
return loci;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetGetLocusArguments(uint handle, ulong chunkIds, uint count, ulong outLoci)
|
||||||
|
{
|
||||||
|
_ctx[CpuRegister.Rdi] = handle;
|
||||||
|
_ctx[CpuRegister.Rsi] = chunkIds;
|
||||||
|
_ctx[CpuRegister.Rdx] = count;
|
||||||
|
_ctx[CpuRegister.Rcx] = outLoci;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UnusableMetadataKind
|
||||||
|
{
|
||||||
|
DatOnly,
|
||||||
|
MalformedChunkDefinitions,
|
||||||
|
UnrecognizedChunkDefinitions,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user