Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions UUID_PATCH_DETAILS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Correction du crash UUID au démarrage (Hot-patching en mémoire)

Ce document décrit l'implémentation mise en place pour résoudre le plantage au démarrage de Plex Media Server sous PostgreSQL (exception fatale `std::domain_error: Invalid uuid length`).

## Description du problème
Au démarrage, lors du chargement des agents ou des plug-ins, Plex Media Server appelle une fonction interne de validation d'UUID située à l'adresse virtuelle `0x104a6cc` (sur architecture `aarch64`).

Cette fonction effectue des vérifications strictes sur la structure de la chaîne (longueur de 36 caractères et présence de tirets aux indices 8, 13, 18 et 23). Si la chaîne passée est invalide ou vide (ce qui arrive pour certains plug-ins sans UUID sous PostgreSQL), elle lève directement une exception fatale C++ `std::domain_error` qui fait crasher le serveur.

## Détails de l'implémentation du correctif

Le correctif consiste à intercepter et court-circuiter le validateur d'UUID directement en mémoire au chargement de l'interposeur (shim).

### 1. Résolution de l'adresse en mémoire
Plex Media Server étant compilé en mode PIE (Position Independent Executable), son adresse de chargement en mémoire est randomisée (ASLR).
- Nous utilisons la fonction standard `dl_iterate_phdr` de la libc pour parcourir les en-têtes de segments chargés.
- Le premier élément retourné par l'itérateur (index 0) correspond systématiquement à l'exécutable principal. Nous lisons son adresse de chargement (`dlpi_addr`) pour calculer l'adresse absolue cible :
$$\text{target\_addr} = \text{base\_addr} + \text{0x104a6cc}$$

### 2. Écriture du patch via `/proc/self/mem`
Modifier les permissions des pages mémoire de code avec `mprotect` (`PROT_READ | PROT_WRITE`) échoue sur certains noyaux Docker stricts en raison des règles W^X (Write XOR Execute).
- Pour contourner cette protection de manière robuste, nous ouvrons le descripteur spécial de processus `/proc/self/mem` en mode écriture.
- L'écriture directe via `/proc/self/mem` permet de modifier les instructions de nos propres pages de code en mémoire, même si elles sont actuellement marquées en lecture-seule (`r-xp`).

### 3. Substitution d'instructions (AArch64 / ARM64)
Nous remplaçons le début de la fonction `0x104a6cc` par trois instructions assembleur ARM64 simples :
```assembly
movz x0, #0 ; Efface le registre x0 (première partie de l'UUID) -> octets: 00 00 80 d2
movz x1, #0 ; Efface le registre x1 (seconde partie de l'UUID) -> octets: 01 00 80 d2
ret ; Retourne de la fonction -> octets: c0 03 5f d6
```
Cette modification neutralise la fonction de validation en lui faisant systématiquement retourner un UUID vide normalisé ("nil" UUID), empêchant ainsi la levée de l'exception fatale.

Le cache d'instructions du processeur est ensuite invalidé et vidé via la fonction standard `__clear_cache` pour que les nouvelles instructions soient prises en compte immédiatement par le CPU.

## Fichiers concernés
- **Interposeur Rust** : [runtime_linux.rs](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/rust/plex-pg-core/src/runtime_linux.rs) (implémente la fonction `patch_uuid_parser`).
- **Scripts d'entrée** : [standalone-entrypoint.sh](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/scripts/standalone-entrypoint.sh) et [docker-entrypoint.sh](file:///Users/simon/Sources/github.com/cgnl/plex-postgresql/scripts/docker-entrypoint.sh) (pour formater les UUID initiaux dans `Preferences.xml` et synchroniser la table `devices`).
9 changes: 8 additions & 1 deletion docker-compose.standalone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ services:
- PLEX_PG_TRACE_LOADONE=1
- PLEX_PG_LOG_FILE=/config/Library/Application Support/Plex Media Server/Logs/plex_redirect_pg.log
# Clear log file on startup to avoid stale errors
- PLEX_PG_LOG_TRUNCATE_ON_START=1
- PLEX_PG_LOG_TRUNCATE_ON_START=0
- PLEX_PG_EXCEPTION_VERBOSE=1
- PLEX_PG_EXCEPTION_ASSUME_OBJECT=1
- PLEX_PG_ENABLE_EXCEPTION_CATCHER=1
- PLEX_PG_EXCEPTION_LOG_META=1
- PLEX_PG_EXCEPTION_DUMP_OBJECT=1
- PLEX_PG_EXCEPTION_SCAN_STRINGS=1
- PLEX_PG_EXCEPTION_SCAN_STRINGS_BYTES=1024
volumes:
- plex_config:/config
- postgres_socket:/var/run/postgresql
Expand Down
17 changes: 14 additions & 3 deletions rust/plex-pg-core/src/db_interpose_bind/text_blob_binds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ fn parse_machine_identifier(content: &str) -> Option<String> {
let start = pos + "MachineIdentifier=\"".len();
if let Some(end) = content[start..].find("\"") {
let hex = &content[start..start+end];
if hex.len() == 32 {
if hex.len() == 36 {
return Some(hex.to_string());
} else if hex.len() == 32 {
return Some(format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
Expand Down Expand Up @@ -114,7 +116,7 @@ pub(super) fn bind_text_impl(
idx: c_int,
mut val: *const c_char,
mut n_bytes: c_int,
destructor: *mut c_void,
mut destructor: *mut c_void,
) -> c_int {
let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT, p_stmt) };
let mut _intercepted_uuid = String::new();
Expand Down Expand Up @@ -145,6 +147,7 @@ pub(super) fn bind_text_impl(
log_debug_lazy!("INTERCEPTED empty UUID bind on devices, replacing with {}", _intercepted_uuid);
val = _intercepted_uuid.as_ptr() as *const c_char;
n_bytes = _intercepted_uuid.len() as c_int;
destructor = -1isize as *mut c_void;
}
}
}
Expand Down Expand Up @@ -306,7 +309,7 @@ pub(super) fn bind_text64_impl(
idx: c_int,
mut val: *const c_char,
mut n_bytes: u64,
destructor: *mut c_void,
mut destructor: *mut c_void,
encoding: c_uchar,
) -> c_int {
let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT64, p_stmt) };
Expand Down Expand Up @@ -338,6 +341,7 @@ pub(super) fn bind_text64_impl(
log_debug_lazy!("INTERCEPTED empty UUID bind on devices (64), replacing with {}", _intercepted_uuid);
val = _intercepted_uuid.as_ptr() as *const c_char;
n_bytes = _intercepted_uuid.len() as u64;
destructor = -1isize as *mut c_void;
}
}
}
Expand Down Expand Up @@ -399,6 +403,13 @@ mod tests {
assert_eq!(uuid, Some("53cfd87b-f8b2-4db2-af2d-6aaa373b2b34".to_string()));
}

#[test]
fn test_parse_machine_identifier_valid_36() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?><Preferences MachineIdentifier="53cfd87b-f8b2-4db2-af2d-6aaa373b2b34" ProcessedMachineIdentifier="53cfd87bf8b24db2af2d6aaa373b2b34" AcceptedEULA="1"/>"#;
let uuid = parse_machine_identifier(xml);
assert_eq!(uuid, Some("53cfd87b-f8b2-4db2-af2d-6aaa373b2b34".to_string()));
}

#[test]
fn test_parse_machine_identifier_invalid_len() {
let xml = r#"<Preferences MachineIdentifier="abc123" AcceptedEULA="1"/>"#;
Expand Down
21 changes: 12 additions & 9 deletions rust/plex-pg-core/src/db_interpose_column/text_accessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,21 +218,24 @@ pub(super) fn column_text_impl(p_stmt: *mut sqlite3_stmt, idx: c_int) -> *const
let _guard = unsafe { PgStmt::lock_mutex(dbg_stmt) };

if !pg_stmt.cached_result.is_null() {
match unsafe { load_cached_text_state(pg_stmt, idx) } {
Some(state) => unsafe { write_cached_text_output(pg_stmt, idx, &state) },
// SQL NULL: return empty string instead of NULL to prevent
// Plex's std::string(nullptr) → basic_string crash.
// Real SQLite returns NULL here, but Plex doesn't always check.
None => empty_text_buffer(),
let cached = unsafe { &*pg_stmt.cached_result };
let row = pg_stmt.current_row;
if idx < 0 || idx >= cached.num_cols || row < 0 || row >= cached.num_rows {
ptr::null()
} else {
match unsafe { load_cached_text_state(pg_stmt, idx) } {
Some(state) => unsafe { write_cached_text_output(pg_stmt, idx, &state) },
None => empty_text_buffer(),
}
}
} else if pg_stmt.result.is_null() {
empty_text_buffer()
ptr::null()
} else if idx < 0 || idx >= pg_stmt.num_cols {
empty_text_buffer()
ptr::null()
} else {
let row = pg_stmt.current_row;
if row < 0 || row >= pg_stmt.num_rows {
empty_text_buffer()
ptr::null()
} else {
match unsafe { load_live_text_state(pg_stmt, idx) } {
Some(state) => unsafe { write_live_text_output(pg_stmt, idx, &state) },
Expand Down
1 change: 1 addition & 0 deletions rust/plex-pg-core/src/db_interpose_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::db_interpose_common::{
get_orig_sqlite3_bind_parameter_name, get_orig_sqlite3_create_collation,
get_orig_sqlite3_create_collation_v2, get_orig_sqlite3_db_handle, get_orig_sqlite3_errcode,
get_orig_sqlite3_errmsg, get_orig_sqlite3_expanded_sql, get_orig_sqlite3_extended_errcode,
get_orig_sqlite3_changes, get_orig_sqlite3_changes64, get_orig_sqlite3_last_insert_rowid,
get_orig_sqlite3_free, get_orig_sqlite3_get_table, get_orig_sqlite3_malloc,
get_orig_sqlite3_sql, get_orig_sqlite3_stmt_busy, get_orig_sqlite3_stmt_readonly,
get_orig_sqlite3_stmt_status, get_shim_sqlite3_errcode, get_shim_sqlite3_errmsg,
Expand Down
41 changes: 30 additions & 11 deletions rust/plex-pg-core/src/db_interpose_metadata/connection_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ pub(super) fn changes_impl(db: *mut sqlite3) -> c_int {
};

let pg_conn = crate::pg_client::rust_pg_find_connection(db);
let mut result = 0;
if !pg_conn.is_null() {
let conn = unsafe { &*pg_conn };
if conn.is_pg_active != 0 {
result = conn.last_changes;
if pg_conn.is_null() {
if let Some(f) = get_orig_sqlite3_changes() {
return unsafe { f(db) };
}
return 0;
}

let mut result = 0;
let conn = unsafe { &*pg_conn };
if conn.is_pg_active != 0 {
result = conn.last_changes;
}
result
}
Expand All @@ -25,12 +30,17 @@ pub(super) fn changes64_impl(db: *mut sqlite3) -> i64 {
};

let pg_conn = crate::pg_client::rust_pg_find_connection(db);
let mut result: i64 = 0;
if !pg_conn.is_null() {
let conn = unsafe { &*pg_conn };
if conn.is_pg_active != 0 {
result = conn.last_changes as i64;
if pg_conn.is_null() {
if let Some(f) = get_orig_sqlite3_changes64() {
return unsafe { f(db) };
}
return 0;
}

let mut result: i64 = 0;
let conn = unsafe { &*pg_conn };
if conn.is_pg_active != 0 {
result = conn.last_changes as i64;
}
result
}
Expand All @@ -47,9 +57,18 @@ pub(super) fn last_insert_rowid_impl(db: *mut sqlite3) -> i64 {

let pg_conn = crate::pg_client::rust_pg_find_connection(db);
if pg_conn.is_null() {
if let Some(f) = get_orig_sqlite3_last_insert_rowid() {
let rowid = unsafe { f(db) };
log_debug_lazy!(
"last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, SQLite returned {})",
db,
rowid
);
return rowid;
}
let global_rowid = crate::pg_client::rust_get_global_last_insert_rowid();
log_debug_lazy!(
"last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, global={})",
"last_insert_rowid: CALLED db={:p} pg_conn=NULL (no exact match, no SQLite fn, global={})",
db,
global_rowid
);
Expand Down
4 changes: 3 additions & 1 deletion rust/plex-pg-core/src/exception_what.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ mod impl_unix {
const ABI_LIBS: [&[u8]; 1] = [b"libc++abi.dylib\0"];

#[cfg(target_os = "linux")]
const ABI_LIBS: [&[u8]; 6] = [
const ABI_LIBS: [&[u8]; 8] = [
b"/usr/lib/plexmediaserver/lib/libc++.so.2\0",
b"/usr/lib/plexmediaserver/lib/libc++abi.so.1\0",
b"libc++abi.so.1\0",
b"libc++abi.so\0",
b"libc++.so.2\0",
Expand Down
Loading
Loading