Compare commits

...

8 Commits

Author SHA1 Message Date
PJB3005
801e0f289c Version: 239.0.5 2025-12-01 16:07:22 +01:00
PJB3005
058c4ffd30 Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
(cherry picked from commit e36628a6d436ea08d6d31441c101a88a5504c515)
2025-12-01 16:07:22 +01:00
PJB3005
6719bb3ae2 Version: 239.0.4 2025-09-26 13:40:52 +02:00
PJB3005
14dcba31b9 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:52 +02:00
PJB3005
d993fd9804 Version: 239.0.3 2025-09-19 09:17:38 +02:00
Skye
474e42daaf Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:38 +02:00
PJB3005
e70f68aa73 Version: 239.0.2 2025-09-14 14:58:27 +02:00
PJB3005
f2aa5e8bb5 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:26 +02:00
22 changed files with 182 additions and 47 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,18 @@ END TEMPLATE-->
*None yet*
## 239.0.5
## 239.0.4
## 239.0.3
## 239.0.2
## 239.0.1
### Bugfixes

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -382,7 +382,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -60,7 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,7 +379,13 @@ namespace Robust.Shared.ContentPack
{
if (root is DirLoader loader)
{
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}
}

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -9,7 +9,6 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using NetSerializer;
namespace Robust.Shared.Serialization;
/// <summary>
/// Custom serializer implementation for <see cref="BitArray"/>.
/// </summary>
/// <remarks>
/// <para>
/// This type is necessary as, since .NET 10, the internal layout of <see cref="BitArray"/> was changed.
/// The type now (internally) implements <see cref="ISerializable"/> for backwards compatibility with existing
/// <c>BinaryFormatter</c> code, but NetSerializer does not support <see cref="ISerializable"/>.
/// </para>
/// <para>
/// This code is designed to be backportable &amp; network compatible with the previous behavior on .NET 9.
/// </para>
/// </remarks>
internal sealed class NetBitArraySerializer : IStaticTypeSerializer
{
// For reference, the layout of BitArray before .NET 10 was:
// private int[] m_array;
// private int m_length;
// private int _version;
// NetSerializer serialized these in the following order (sorted by name):
// _version, m_array, m_length
public bool Handles(Type type)
{
return type == typeof(BitArray);
}
public IEnumerable<Type> GetSubtypes(Type type)
{
return [typeof(int[]), typeof(int)];
}
public MethodInfo GetStaticWriter(Type type)
{
return typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!;
}
public MethodInfo GetStaticReader(Type type)
{
return typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!;
}
[UsedImplicitly]
private static void Write(Serializer serializer, Stream stream, BitArray value)
{
var intCount = (31 + value.Length) >> 5;
var ints = new int[intCount];
value.CopyTo(ints, 0);
serializer.SerializeDirect(stream, 0); // _version
serializer.SerializeDirect(stream, ints); // m_array
serializer.SerializeDirect(stream, value.Length); // m_length
}
[UsedImplicitly]
private static void Read(Serializer serializer, Stream stream, out BitArray value)
{
serializer.DeserializeDirect<int>(stream, out _); // _version
serializer.DeserializeDirect<int[]>(stream, out var array); // m_array
serializer.DeserializeDirect<int>(stream, out var length); // m_length
value = new BitArray(array)
{
Length = length
};
}
}

View File

@@ -91,6 +91,7 @@ namespace Robust.Shared.Serialization
MappedStringSerializer.TypeSerializer,
new Vector2Serializer(),
new Matrix3x2Serializer(),
new NetBitArraySerializer()
}
};
_serializer = new Serializer(types, settings);

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]