Skip to content

Commit

Permalink
[monodroid] Prevent overlapped decompression of embedded assemblies (d…
Browse files Browse the repository at this point in the history
…otnet#7732)

Fixes: dotnet#7335

Context: d236af5

Commit d236af5 introduced embedded assembly compression, using LZ4,
which speeds up startup and reduces final package size.

Assemblies are compressed at build time and, at the same time, pre-
allocated buffers for the **decompressed** data are allocated in
`libxamarin-app.so`.  The buffers are then passed to the LZ4 APIs,
all threads using the same output buffer.  The assumption was that we
can do fine without locking as even if overlapped decompression
happens, the output data will be the same and so even if two threads
do the same thing at the same time, the data will be valid at all
times, so long as at least one thread completes the decompression.

This assumption proved to be **largely** true, but it appears that in
high concurrency cases it is possible that the data in the
decompression buffer differs.  This can result in app crashes:

	A/libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 3092 (.NET ThreadPool), pid 2727 (myapp.name)
	A/DEBUG: pid: 2727, tid: 3092, name: .NET ThreadPool  >>> myapp.name <<<
	A/DEBUG:       #1 pc 0000000000029b1c  /data/app/myapp.name-B9t_3dF9i8mDxJEKodZw5w==/split_config.arm64_v8a.apk!libmono-android.release.so (offset 0x103d000) (xamarin::android::internal::MonodroidRuntime::mono_log_handler(char const*, char const*, char const*, int, void*)+144) (BuildId: 29c5a3805a0bedee1eede9b6668d7c676aa63371)
	A/DEBUG:       #2 pc 00000000002680bc  /data/app/myapp.name-B9t_3dF9i8mDxJEKodZw5w==/split_config.arm64_v8a.apk!libmonosgen-2.0.so (offset 0x109b000) (BuildId: 4a5dd4396e8816b7f69881838bd549285213d53b)
	A/DEBUG:       #3 pc 00000000002681e8  /data/app/myapp.name-B9t_3dF9i8mDxJEKodZw5w==/split_config.arm64_v8a.apk!libmonosgen-2.0.so (offset 0x109b000) (BuildId: 4a5dd4396e8816b7f69881838bd549285213d53b)
	A/DEBUG:       dotnet#4 pc 000000000008555c  /data/app/myapp.name-B9t_3dF9i8mDxJEKodZw5w==/split_config.arm64_v8a.apk!libmonosgen-2.0.so (offset 0x109b000) (mono_metadata_string_heap+188) (BuildId: 4a5dd4396e8816b7f69881838bd549285213d53b)
	…

My guess is that LZ4 either uses the output buffer as a scratchpad
area when decompressing or that it initializes/modifies the buffer
before writing actual data in it.  With overlapped decompression, it
may lead to one thread overwriting valid data previously written by
another thread, so that when the latter returns the buffer it thought
to have had valid data may contain certain bytes temporarily
overwritten by the decompression session in the other, still running,
thread.  It may happen that MonoVM reads the corrupted data just when
it is still invalid (before the still running decompression session
actually writes the valid data), a classic race condition.

To fix this, the decompression block is now protected with a startup-
aware mutex.  Mutex will be held only after the initial startup phase
is completed, so there should not be much loss of startup performance.
  • Loading branch information
grendello committed Feb 21, 2023
1 parent 77c6886 commit ab29a7c
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 24 deletions.
42 changes: 21 additions & 21 deletions src/monodroid/jni/embedded-assemblies.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "mono-image-loader.hh"
#include "xamarin-app.hh"
#include "cpp-util.hh"
#include "monodroid-glue-internal.hh"
#include "startup-aware-lock.hh"
#include "timing-internal.hh"
#include "search.hh"
Expand Down Expand Up @@ -73,6 +74,13 @@ void EmbeddedAssemblies::set_assemblies_prefix (const char *prefix)
assemblies_prefix_override = prefix != nullptr ? utils.strdup_new (prefix) : nullptr;
}

force_inline void
EmbeddedAssemblies::set_assembly_data_and_size (uint8_t* source_assembly_data, uint32_t source_assembly_data_size, uint8_t*& dest_assembly_data, uint32_t& dest_assembly_data_size) noexcept
{
dest_assembly_data = source_assembly_data;
dest_assembly_data_size = source_assembly_data_size;
}

force_inline void
EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[maybe_unused]] const char *name, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept
{
Expand All @@ -91,17 +99,18 @@ EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[mayb
CompressedAssemblyDescriptor &cad = compressed_assemblies.descriptors[header->descriptor_index];
assembly_data_size = data_size - sizeof(CompressedAssemblyHeader);
if (!cad.loaded) {
StartupAwareLock decompress_lock (assembly_decompress_mutex);

if (cad.loaded) {
set_assembly_data_and_size (reinterpret_cast<uint8_t*>(cad.data), cad.uncompressed_file_size, assembly_data, assembly_data_size);
return;
}

if (XA_UNLIKELY (cad.data == nullptr)) {
log_fatal (LOG_ASSEMBLY, "Invalid compressed assembly descriptor at %u: no data", header->descriptor_index);
exit (FATAL_EXIT_MISSING_ASSEMBLY);
}

bool log_timing = FastTiming::enabled () && !FastTiming::is_bare_mode ();
size_t decompress_time_index;
if (XA_UNLIKELY (log_timing)) {
decompress_time_index = internal_timing->start_event (TimingEventKind::AssemblyDecompression);
}

if (header->uncompressed_length != cad.uncompressed_file_size) {
if (header->uncompressed_length > cad.uncompressed_file_size) {
log_fatal (LOG_ASSEMBLY, "Compressed assembly '%s' is larger than when the application was built (expected at most %u, got %u). Assemblies don't grow just like that!", name, cad.uncompressed_file_size, header->uncompressed_length);
Expand All @@ -115,11 +124,6 @@ EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[mayb
const char *data_start = reinterpret_cast<const char*>(data + sizeof(CompressedAssemblyHeader));
int ret = LZ4_decompress_safe (data_start, reinterpret_cast<char*>(cad.data), static_cast<int>(assembly_data_size), static_cast<int>(cad.uncompressed_file_size));

if (XA_UNLIKELY (log_timing)) {
internal_timing->end_event (decompress_time_index, true /* uses_more_info */);
internal_timing->add_more_info (decompress_time_index, name);
}

if (ret < 0) {
log_fatal (LOG_ASSEMBLY, "Decompression of assembly %s failed with code %d", name, ret);
exit (FATAL_EXIT_MISSING_ASSEMBLY);
Expand All @@ -131,13 +135,12 @@ EmbeddedAssemblies::get_assembly_data (uint8_t *data, uint32_t data_size, [[mayb
}
cad.loaded = true;
}
assembly_data = reinterpret_cast<uint8_t*>(cad.data);
assembly_data_size = cad.uncompressed_file_size;

set_assembly_data_and_size (reinterpret_cast<uint8_t*>(cad.data), cad.uncompressed_file_size, assembly_data, assembly_data_size);
} else
#endif
{
assembly_data = data;
assembly_data_size = data_size;
set_assembly_data_and_size (data, data_size, assembly_data, assembly_data_size);
}
}

Expand Down Expand Up @@ -357,10 +360,6 @@ EmbeddedAssemblies::assembly_store_open_from_bundles (dynamic_local_string<SENSI
len -= sizeof(SharedConstants::DLL_EXTENSION) - 1;
}

std::string clipped_name;
clipped_name.assign (name.get (), len);
log_info (LOG_ASSEMBLY, "Clipped name: %s", clipped_name.c_str ());

hash_t name_hash = xxhash::hash (name.get (), len);
log_debug (LOG_ASSEMBLY, "assembly_store_open_from_bundles: looking for bundled name: '%s' (hash 0x%zx)", name.get (), name_hash);

Expand Down Expand Up @@ -412,14 +411,15 @@ EmbeddedAssemblies::assembly_store_open_from_bundles (dynamic_local_string<SENSI

log_debug (
LOG_ASSEMBLY,
"Mapped: image_data == %p; debug_info_data == %p; config_data == %p; descriptor == %p; data size == %u; debug data size == %u; config data size == %u",
"Mapped: image_data == %p; debug_info_data == %p; config_data == %p; descriptor == %p; data size == %u; debug data size == %u; config data size == %u; name == '%s'",
assembly_runtime_info.image_data,
assembly_runtime_info.debug_info_data,
assembly_runtime_info.config_data,
assembly_runtime_info.descriptor,
assembly_runtime_info.descriptor->data_size,
assembly_runtime_info.descriptor->debug_data_size,
assembly_runtime_info.descriptor->config_data_size
assembly_runtime_info.descriptor->config_data_size,
name.get ()
);
}

Expand Down
8 changes: 5 additions & 3 deletions src/monodroid/jni/embedded-assemblies.hh
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,10 @@ namespace xamarin::android::internal {
#else // def NET
static MonoAssembly* open_from_bundles_refonly (MonoAssemblyName *aname, char **assemblies_path, void *user_data);
#endif // ndef NET
static void get_assembly_data (uint8_t *data, uint32_t data_size, const char *name, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;
static void get_assembly_data (XamarinAndroidBundledAssembly const& e, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;
static void get_assembly_data (AssemblyStoreSingleAssemblyRuntimeData const& e, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;
void set_assembly_data_and_size (uint8_t* source_assembly_data, uint32_t source_assembly_data_size, uint8_t*& dest_assembly_data, uint32_t& dest_assembly_data_size) noexcept;
void get_assembly_data (uint8_t *data, uint32_t data_size, const char *name, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;
void get_assembly_data (XamarinAndroidBundledAssembly const& e, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;
void get_assembly_data (AssemblyStoreSingleAssemblyRuntimeData const& e, uint8_t*& assembly_data, uint32_t& assembly_data_size) noexcept;

void zip_load_entries (int fd, const char *apk_name, monodroid_should_register should_register);
void zip_load_individual_assembly_entries (std::vector<uint8_t> const& buf, uint32_t num_entries, monodroid_should_register should_register, ZipEntryLoadState &state) noexcept;
Expand Down Expand Up @@ -333,6 +334,7 @@ namespace xamarin::android::internal {

AssemblyStoreHeader *index_assembly_store_header = nullptr;
AssemblyStoreHashEntry *assembly_store_hashes;
std::mutex assembly_decompress_mutex;
};
}

Expand Down

0 comments on commit ab29a7c

Please sign in to comment.