[Mono][ARM64] JIT thunk target literals can be insufficiently aligned for concurrent updates
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 42/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Ambito
- backend, performance
Direzione di ricerca
Start with thunk allocation in src/mono/mono/mini/mini.c and ARM64 thunk emission and retargeting in src/mono/mono/mini/mini-arm64.c, at the runtime revision linked in the issue. Check how both normal and dynamic method allocations determine the thunk offset, then confirm the ARM64 literal alignment requirement and identify an upstream regression test. Done means a test covers the alignment defect and any proposed change preserves the reported method code sizes.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
[Mono][ARM64] JIT thunk target literals can be insufficiently aligned for concurrent updates
Summary
The ARM64 Mono JIT can place a 64-bit thunk destination at an address aligned to
4 bytes but not 8. A candidate fix aligns the appended thunk area and adjusts
both normal and dynamic-method allocation sizes. We observed misaligned literal
slots in the original runtime and only aligned observations with the candidate.
We suspect this contributes to intermittent invalid indirect branches, but have
not established the complete write/read history of the application crashes.
Environment
- Pixel 6a, Android ARM64; Godot C# application.
- Microsoft.NETCore.App.Runtime.Mono.android-arm64 10.0.14.
- Package VMR commit: 0427b9abbead3fa64c6655eb4290c537397131fa.
- Runtime revision identified by that VMR source manifest:
6aa72daa24f66c01c3d7b6a32519e9d21396deec. - Candidate: Release ARM64 build using Android NDK r29, effective native -O2,
JIT enabled. Packaged baseline was built with clang18; compiler/build differences
remain a confounder in the application comparison.
Source mechanism and proposed change
In mini.c, the thunk area follows code_size + unwindlen. On ARM64, emitted
instruction sizes need only preserve 4-byte alignment. In mini-arm64.c,
THUNK_SIZE is 16 and emit_thunk emits LDR x16, #8; BR x16 followed by a 64-bit
literal. Consequently, every literal in a 4-mod-8 thunk area is also 4-mod-8.
create_thunk can reuse and retarget an existing thunk while generated code runs.
Its writer lock does not make executing readers acquire that lock.
The attached patch rounds the offset to 8 before both allocation paths, records
the adjusted offset in existing thunk metadata, and asserts emitter alignment.
It leaves reported method code sizes unchanged. Alignment addresses tearing of
the described pointer access; it is not a claim that all code-publication and
lifetime concerns disappear.
Reproduction and observed results
The standalone host registers a public JIT-completed callback, uses public method
code bounds, and takes bounded /proc/self/mem snapshots of up to 2 KiB after each
method. It recognizes the exact literal-thunk instruction pair. The managed
payload exercises ordinary calls, DynamicMethod calls and concurrent first calls.
This is an alignment reproducer, not a deterministic reproduction of a Mono crash.
| Runtime | JIT method callbacks | Thunk observations | Misaligned observations |
|---|---|---|---|
| Packaged original | 1,037 | 2,939 | 1,555 |
| Candidate | 1,028 | 2,868 | 0 |
The candidate completed with managed exit 0. Baseline had one unreadable scan
window; candidate had one snapshot failure. Observations may duplicate slots or
include neighboring stubs because public APIs do not expose thunk-area lengths.
The probe does not establish exhaustive coverage or that runtime retargeting ran.
Sampled target values are not execution/write history.
Application evidence and validation limits
One captured failure went through this thunk form with its target literal at
address mod 8 = 4 and mod 64 = 60. The fault PC and x16 had the same low 32 bits
as the valid target saved in the post-fault snapshot, but a different high half.
This is consistent with a torn target observation, but the snapshot does not
prove the value at the instant of the load, concurrent writer identity, or the
complete instruction history. Caller resolution used LR minus 4 and exact
same-process JIT method bounds; appended thunk bytes were not attributed to a
method by extending its reported bounds.
The standalone alignment probe above demonstrates misaligned target-literal
observations in the packaged runtime and aligned observations with the patch.
The patch fixes the alignment defect.
On October 28, 2026, the patch completed a fresh application startup endurance
test: exactly 1,000 successful startups out of 1,000, a healthy batch completion
marker, no failure records, and zero captured signal records. Each startup
reached the main menu with no menu dwell. .NET metrics, typed HTTP-header
enumeration, and 16 concurrent download slots were enabled.
This result does not isolate the effect of the alignment change, prove that
misalignment caused the captured application signal, or establish that all native
crashes are fixed. The candidate's NDK/compiler differs from the packaged
baseline, as described under Environment. The probe coverage limitations above
still apply.
Source references
All references are pinned to the tested runtime revision:
We would appreciate confirmation of the alignment requirement and advice on an
appropriate upstream regression test. Full game memory captures are not included;
the reproduction sources below contain no application code or credentials.
README.md
# Mono ARM64 thunk alignment reproducer
This independent C host and C# console application use public Mono APIs and
standard .NET libraries. No engine, game, service, account, or proprietary
assembly is required. No application capture or runtime binary is distributed.
## Build and run
Set ANDROID_NDK_ROOT to an Android NDK installation. Set NDK_HOST_TAG to
linux-x86_64 on Linux (the default is darwin-x86_64). Install a .NET10 SDK, then
run `sh build.sh`.
Prepare a separate `runtime` directory from official Mono Android ARM64 runtime
and matching standard managed runtime packages: libmonosgen-2.0.so, its native
runtime dependencies, System.Private.CoreLib.dll and the standard framework
assemblies required by the console application. Do not copy an application's
publish directory. Copy out/managed/ThunkProbe.dll into this directory.
Push out/thunk-probe and runtime to a new directory on an ARM64 Android device,
for example /data/local/tmp/mono-thunk-alignment. Run from that directory:
```
LD_LIBRARY_PATH=runtime ./thunk-probe runtime/libmonosgen-2.0.so runtime
```
For comparison, put the patched library alongside thunk-probe and run:
```
LD_LIBRARY_PATH=runtime ./thunk-probe ./libmonosgen-patched.so runtime
```
Exit0 means managed execution succeeded and all observed literals were aligned;
exit72 flags misalignment or an invalid sampled target. This reproduces
misalignment, not a deterministic runtime crash. The probe reads bounded
/proc/self/mem snapshots after JIT completion. It may observe duplicate or nearby
stubs; snapshot failures and unreadable windows are reported. It does not prove
exhaustive coverage, target write history, or runtime repatching coverage.
build.sh
#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
: "${ANDROID_NDK_ROOT:?Set ANDROID_NDK_ROOT to an installed Android NDK}"
HOST_TAG=${NDK_HOST_TAG:-darwin-x86_64}
CLANG="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG/bin/aarch64-linux-android21-clang"
OUT="$ROOT/out"
mkdir -p "$OUT/managed"
dotnet build "$ROOT/ThunkProbe.csproj" -c Release -o "$OUT/managed"
"$CLANG" -std=c11 -O2 -Wall -Wextra -Werror -fPIE -pie "$ROOT/host.c" -ldl -o "$OUT/thunk-probe"
printf '%s\n' "Built $OUT/thunk-probe and $OUT/managed/ThunkProbe.dll"
align-arm64-thunks.patch
--- a/src/mono/mono/mini/mini.c
+++ b/src/mono/mono/mini/mini.c
@@ -2123,6 +2123,13 @@
unwindlen = mono_arch_unwindinfo_init_method_unwind_info (cfg);
#endif
+ /* Keep ARM64 thunk target literals naturally aligned for concurrent updates. */
+ cfg->thunks_offset = cfg->code_size + unwindlen;
+#if defined(TARGET_ARM64)
+ if (cfg->thunk_area)
+ cfg->thunks_offset = ALIGN_TO (cfg->thunks_offset, 8);
+#endif
+
if (cfg->method->dynamic) {
/* Allocate the code into a separate memory pool so it can be freed */
cfg->dynamic_info = g_new0 (MonoJitDynamicMethodInfo, 1);
@@ -2135,15 +2142,14 @@
g_hash_table_insert (jit_mm->dynamic_code_hash, cfg->method, cfg->dynamic_info);
jit_mm_unlock (jit_mm);
- code = (guint8 *)mono_code_manager_reserve (cfg->dynamic_info->code_mp, cfg->code_size + cfg->thunk_area + unwindlen);
+ code = (guint8 *)mono_code_manager_reserve (cfg->dynamic_info->code_mp, cfg->thunks_offset + cfg->thunk_area);
} else {
- code = (guint8 *)mono_mem_manager_code_reserve (code_mem_manager, cfg->code_size + cfg->thunk_area + unwindlen);
+ code = (guint8 *)mono_mem_manager_code_reserve (code_mem_manager, cfg->thunks_offset + cfg->thunk_area);
}
mono_codeman_enable_write ();
if (cfg->thunk_area) {
- cfg->thunks_offset = cfg->code_size + unwindlen;
cfg->thunks = code + cfg->thunks_offset;
memset (cfg->thunks, 0, cfg->thunk_area);
}
--- a/src/mono/mono/mini/mini-arm64.c
+++ b/src/mono/mono/mini/mini-arm64.c
@@ -1272,6 +1272,8 @@
emit_thunk (guint8 *code, gconstpointer target)
{
guint8 *p = code;
+
+ g_assert (((gsize)code & 7) == 0);
arm_ldrx_lit (code, ARMREG_IP0, code + 8);
arm_brx (code, ARMREG_IP0);
host.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/*
* This uses only the public profiler callbacks and opaque MonoJitInfo accessors.
* It deliberately does not assume the private MonoThunkJitInfo layout.
*/
typedef struct _MonoProfiler MonoProfiler;
typedef struct _MonoProfilerDesc *MonoProfilerHandle;
typedef struct _MonoMethod MonoMethod;
typedef struct _MonoJitInfo MonoJitInfo;
typedef void (*jit_done_callback)(MonoProfiler *, MonoMethod *, MonoJitInfo *);
typedef void *(*mono_profiler_create_fn)(MonoProfiler *);
typedef void (*mono_profiler_set_jit_done_callback_fn)(MonoProfilerHandle, jit_done_callback);
typedef void *(*mono_jit_info_get_code_start_fn)(MonoJitInfo *);
typedef int (*mono_jit_info_get_code_size_fn)(MonoJitInfo *);
typedef int (*coreclr_initialize_fn)(const char *, const char *, int, const char **, const char **, void **, unsigned int *);
typedef int (*coreclr_execute_assembly_fn)(void *, unsigned int, int, const char **, const char *, unsigned int *);
typedef void (*mono_set_assemblies_path_fn)(const char *);
static mono_jit_info_get_code_start_fn jit_info_start;
static mono_jit_info_get_code_size_fn jit_info_size;
static volatile unsigned long long methods_seen;
static volatile unsigned long long stubs_seen;
static volatile unsigned long long aligned_literals;
static volatile unsigned long long misaligned_literals;
static volatile unsigned long long invalid_targets;
static volatile unsigned long long unreadable_windows;
static volatile unsigned long long snapshot_failures;
static volatile unsigned long long partial_snapshots;
#define MAX_ROWS 256
struct observation {
uintptr_t method;
uintptr_t code_start;
uintptr_t literal_address;
uintptr_t target;
uint32_t code_size;
uint32_t flags;
};
static struct observation rows[MAX_ROWS];
static volatile unsigned int row_count;
static void *needed(void *library, const char *name) {
void *result = dlsym(library, name);
if (!result) fprintf(stderr, "missing %s: %s\n", name, dlerror());
return result;
}
/* Return the readable/executable map end containing address, or zero. */
static uintptr_t executable_map_end(uintptr_t address) {
FILE *maps = fopen("/proc/self/maps", "re");
if (!maps) return 0;
char line[512], perms[5];
unsigned long long start, end;
uintptr_t result = 0;
while (fgets(line, sizeof(line), maps)) {
if (sscanf(line, "%llx-%llx %4s", &start, &end, perms) == 3 &&
address >= (uintptr_t)start && address < (uintptr_t)end &&
perms[0] == 'r' && perms[2] == 'x') {
result = (uintptr_t)end;
break;
}
}
fclose(maps);
return result;
}
static void save_observation(MonoMethod *method, uintptr_t start, int size,
uintptr_t literal, uintptr_t target, uint32_t flags) {
unsigned int slot = __atomic_fetch_add(&row_count, 1, __ATOMIC_RELAXED);
if (slot >= MAX_ROWS) return;
rows[slot].method = (uintptr_t)method;
rows[slot].code_start = start;
rows[slot].code_size = (uint32_t)size;
rows[slot].literal_address = literal;
rows[slot].target = target;
rows[slot].flags = flags;
}
static void inspect_jitted_method(MonoProfiler *prof, MonoMethod *method, MonoJitInfo *info) {
(void)prof;
(void)method;
__atomic_fetch_add(&methods_seen, 1, __ATOMIC_RELAXED);
if (!jit_info_start || !jit_info_size) return;
const uint8_t *code = (const uint8_t *)jit_info_start(info);
int size = jit_info_size(info);
if (!code || size < 0) return;
/* The ARM64 mini JIT appends each method's thunk area after its reported code. */
uintptr_t scan = (uintptr_t)code + (uintptr_t)size;
uintptr_t map_end = executable_map_end(scan);
if (!map_end || map_end <= scan) {
__atomic_fetch_add(&unreadable_windows, 1, __ATOMIC_RELAXED);
return;
}
size_t available = (size_t)(map_end - scan);
if (available > 2048) available = 2048;
uint8_t snapshot[2048];
int mem = open("/proc/self/mem", O_RDONLY | O_CLOEXEC);
if (mem < 0) {
__atomic_fetch_add(&snapshot_failures, 1, __ATOMIC_RELAXED);
return;
}
ssize_t bytes = pread(mem, snapshot, available, (off_t)scan);
close(mem);
if (bytes < 16) {
__atomic_fetch_add(&snapshot_failures, 1, __ATOMIC_RELAXED);
return;
}
if ((size_t)bytes != available)
__atomic_fetch_add(&partial_snapshots, 1, __ATOMIC_RELAXED);
size_t captured = (size_t)bytes;
for (size_t offset = 0; offset + 16 <= captured; offset += 4) {
uint32_t first, second;
memcpy(&first, snapshot + offset, sizeof(first));
memcpy(&second, snapshot + offset + 4, sizeof(second));
/* ldr x16, #8 ; br x16 -- the exact literal thunk form under test. */
if (first != UINT32_C(0x58000050) || second != UINT32_C(0xd61f0200)) continue;
uintptr_t literal_address = scan + offset + 8;
uintptr_t target;
memcpy(&target, snapshot + offset + 8, sizeof(target));
__atomic_fetch_add(&stubs_seen, 1, __ATOMIC_RELAXED);
uint32_t flags = 0;
if ((literal_address & 7u) == 0)
{
__atomic_fetch_add(&aligned_literals, 1, __ATOMIC_RELAXED);
flags |= 1;
}
else
__atomic_fetch_add(&misaligned_literals, 1, __ATOMIC_RELAXED);
if ((target & 3u) != 0 || !executable_map_end(target))
__atomic_fetch_add(&invalid_targets, 1, __ATOMIC_RELAXED);
else
flags |= 2;
save_observation(method, (uintptr_t)code, size, literal_address, target, flags);
}
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: thunk-probe libmonosgen-2.0.so runtime-dir\n");
return 64;
}
void *mono = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);
if (!mono) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 65; }
coreclr_initialize_fn initialize = (coreclr_initialize_fn)needed(mono, "coreclr_initialize");
coreclr_execute_assembly_fn execute = (coreclr_execute_assembly_fn)needed(mono, "coreclr_execute_assembly");
mono_set_assemblies_path_fn set_path = (mono_set_assemblies_path_fn)needed(mono, "mono_set_assemblies_path");
mono_profiler_create_fn profiler_create = (mono_profiler_create_fn)needed(mono, "mono_profiler_create");
mono_profiler_set_jit_done_callback_fn set_jit_done = (mono_profiler_set_jit_done_callback_fn)needed(mono, "mono_profiler_set_jit_done_callback");
jit_info_start = (mono_jit_info_get_code_start_fn)needed(mono, "mono_jit_info_get_code_start");
jit_info_size = (mono_jit_info_get_code_size_fn)needed(mono, "mono_jit_info_get_code_size");
if (!initialize || !execute || !set_path || !profiler_create || !set_jit_done || !jit_info_start || !jit_info_size) return 66;
MonoProfilerHandle profiler = (MonoProfilerHandle)profiler_create(NULL);
if (!profiler) { fprintf(stderr, "mono_profiler_create failed\n"); return 67; }
set_jit_done(profiler, inspect_jitted_method);
set_path(argv[2]);
void *host = NULL;
unsigned int domain = 0;
int result = initialize(NULL, NULL, 0, NULL, NULL, &host, &domain);
if (result != 0) { fprintf(stderr, "coreclr_initialize=%d\n", result); return 68; }
char assembly_path[1024];
if (snprintf(assembly_path, sizeof(assembly_path), "%s/ThunkProbe.dll", argv[2]) >= (int)sizeof(assembly_path)) return 69;
unsigned int exit_code = 0;
const char *managed_args[] = { "ThunkProbe.dll" };
result = execute(host, domain, 1, managed_args, assembly_path, &exit_code);
unsigned long long observed_methods = __atomic_load_n(&methods_seen, __ATOMIC_ACQUIRE);
unsigned long long observed_stubs = __atomic_load_n(&stubs_seen, __ATOMIC_ACQUIRE);
unsigned long long observed_aligned = __atomic_load_n(&aligned_literals, __ATOMIC_ACQUIRE);
unsigned long long observed_misaligned = __atomic_load_n(&misaligned_literals, __ATOMIC_ACQUIRE);
unsigned long long observed_invalid = __atomic_load_n(&invalid_targets, __ATOMIC_ACQUIRE);
unsigned long long observed_unreadable = __atomic_load_n(&unreadable_windows, __ATOMIC_ACQUIRE);
unsigned long long observed_failures = __atomic_load_n(&snapshot_failures, __ATOMIC_ACQUIRE);
unsigned long long observed_partial = __atomic_load_n(&partial_snapshots, __ATOMIC_ACQUIRE);
unsigned int observed_rows = __atomic_load_n(&row_count, __ATOMIC_ACQUIRE);
unsigned int printed_rows = observed_rows < MAX_ROWS ? observed_rows : MAX_ROWS;
printf("coreclr_execute_assembly=%d exit=%u methods=%llu stubs=%llu aligned=%llu misaligned=%llu invalid_targets=%llu unreadable_windows=%llu snapshot_failures=%llu partial_snapshots=%llu rows=%u\n",
result, exit_code, observed_methods, observed_stubs, observed_aligned, observed_misaligned, observed_invalid, observed_unreadable, observed_failures, observed_partial, printed_rows);
for (unsigned int index = 0; index < printed_rows; index++) {
const struct observation *row = &rows[index];
printf("thunk_row method=0x%" PRIxPTR " code=0x%" PRIxPTR " size=%" PRIu32 " literal=0x%" PRIxPTR " target=0x%" PRIxPTR " flags=%" PRIu32 "\n",
row->method, row->code_start, row->code_size, row->literal_address, row->target, row->flags);
}
if (result != 0 || exit_code != 0) return 70;
if (!observed_stubs) return 71;
return observed_misaligned == 0 && observed_invalid == 0 ? 0 : 72;
}
Program.cs
using System;
using System.Collections.Concurrent;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace ThunkProbe;
public static class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Leaf(int value) => value * 17 + 3;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Ordinary(int value)
{
string input = "ExampleX/" + value;
return Leaf(input.Substring(0, 8).Length + value);
}
private static Func<int, int> NewDynamicMethod(int addend)
{
var method = new DynamicMethod("probe_dynamic_" + addend, typeof(int), new[] { typeof(int) }, typeof(Program).Module, true);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, addend);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Call, typeof(Program).GetMethod(nameof(Leaf), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!);
il.Emit(OpCodes.Ret);
return (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
}
public static int Main()
{
const int workers = 16;
using var gate = new ManualResetEventSlim(false);
var answers = new ConcurrentBag<int>();
Task[] tasks = new Task[workers];
for (int i = 0; i < workers; i++)
{
int captured = i;
tasks[i] = Task.Run(() =>
{
gate.Wait();
Func<int, int> dynamicCall = NewDynamicMethod(captured + 100);
int answer = Ordinary(captured) + dynamicCall(captured);
answers.Add(answer);
});
}
gate.Set();
Task.WaitAll(tasks);
int checksum = 0;
foreach (int answer in answers) checksum += answer;
Console.WriteLine($"workers={workers} answers={answers.Count} checksum={checksum}");
return answers.Count == workers && checksum != 0 ? 0 : 1;
}
}
ThunkProbe.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
- Lingua principale
- C#
- Stelle
- 18.4k
- Fork
- 5.8k
- Merge medio
- 2g 12h
- PR unite (30g)
- 600
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di dotnet/runtime
-
area-System.IO untriaged
Difficoltà 2/5 Mezza giornata Idoneità per principianti 72/100
dotnet/runtime#136802 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[wasi] .NET 10: WasmEnableSIMD with AOT fails because WasiApp.targets has -msimd128 commented outApertaarea-Build-mono untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
area-System.ComponentModel untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
dotnet/runtime#136608 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
MemoryMappedFile (Windows): failed MEM_COMMIT is ignored, accessor over reserved memory crashes the processForse già presa @RutulPatel8 l’ha presa oggi. Apertaarea-System.IO bug untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
dotnet/runtime#136599 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
area-System.IO untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
dotnet/runtime#136577 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di dotnet/runtime
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
libnativeapi/nativeapi#21 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 3 giorni
-
enhancement P3 performance
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
thomhurst/TUnit#7380 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno