Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Dockerfile.standalone
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ COPY schema/plex_schema.sql /usr/local/lib/plex-postgresql/
COPY schema/sqlite_schema.sql /usr/local/lib/plex-postgresql/
COPY schema/sqlite_column_types.sql /usr/local/lib/plex-postgresql/
COPY schema/pg_compat_functions.sql /usr/local/lib/plex-postgresql/
COPY schema/seed_data.sql /usr/local/lib/plex-postgresql/
COPY scripts/migrate_lib.sh /usr/local/lib/plex-postgresql/
COPY scripts/migrate_table.py /usr/local/lib/plex-postgresql/
COPY scripts/seed_shadow_table_from_pg.py /usr/local/lib/plex-postgresql/
Expand All @@ -121,6 +122,9 @@ RUN printf '%s\n' \
'#!/usr/bin/env bash' \
'mkdir -p /run/plex-temp' \
'chmod 1777 /run/plex-temp 2>/dev/null || true' \
'export LANG=C.utf8' \
'export LC_ALL=C.utf8' \
'export LC_CTYPE=C.utf8' \
'export LD_LIBRARY_PATH="/usr/local/lib/plex-postgresql:/usr/lib/plexmediaserver/lib:$LD_LIBRARY_PATH"' \
'export LD_PRELOAD="/usr/local/lib/plex-postgresql/db_interpose_pg.so"' \
'if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]]; then' \
Expand All @@ -129,6 +133,8 @@ RUN printf '%s\n' \
'exec /usr/local/bin/subreaper "/usr/lib/plexmediaserver/Plex Media Server" "$@"' \
> /usr/local/lib/plex-postgresql/plex-with-shim.sh && \
chmod +x /usr/local/lib/plex-postgresql/plex-with-shim.sh && \
sed -i 's|s6-setuidgid plex /usr/lib/plexmediaserver/Plex\\ Media\\ Server|s6-setuidgid plex /usr/local/lib/plex-postgresql/plex-with-shim.sh|' \
/etc/services.d/plex/run && \
sed -i 's|s6-setuidgid abc "/usr/lib/plexmediaserver/Plex Media Server"|s6-setuidgid abc /usr/local/lib/plex-postgresql/plex-with-shim.sh|' \
/etc/services.d/plex/run && \
sed -i 's|"/usr/lib/plexmediaserver/Plex Media Server"|/usr/local/lib/plex-postgresql/plex-with-shim.sh|' \
Expand Down
6 changes: 4 additions & 2 deletions docker-compose.standalone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ services:
- PLEX_PG_VALIDATE_OUTPUT_SAMPLE_PCT=${PLEX_PG_VALIDATE_OUTPUT_SAMPLE_PCT:-5}
# Optional ARM override for OpenSSL CPU feature probing in some VMs/containers
# - PLEX_PG_OPENSSL_ARMCAP=0
- PLEX_PG_LOG_LEVEL=ERROR
- PLEX_PG_LOG_FILE=/config/plex_redirect_pg.log
- PLEX_PG_LOG_LEVEL=DEBUG
- PLEX_PG_ENABLE_SIGNAL_LOG=1
- 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
volumes:
Expand Down
159 changes: 155 additions & 4 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 @@ -5,6 +5,36 @@ use crate::db_interpose_bind::support::{
};
use crate::log_debug_lazy;

fn parse_machine_identifier(content: &str) -> Option<String> {
if let Some(pos) = content.find("MachineIdentifier=\"") {
let start = pos + "MachineIdentifier=\"".len();
if let Some(end) = content[start..].find("\"") {
let hex = &content[start..start+end];
if hex.len() == 32 {
return Some(format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
));
}
}
}
None
}

fn get_machine_identifier() -> String {
let default_uuid = "53cfd87b-f8b2-4db2-af2d-6aaa373b2b34".to_string();
let path = "/config/Library/Application Support/Plex Media Server/Preferences.xml";
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return default_uuid,
};
parse_machine_identifier(&content).unwrap_or(default_uuid)
}

unsafe fn store_text_param(
pg_stmt: *mut PgStmt,
pg_idx: usize,
Expand Down Expand Up @@ -82,11 +112,67 @@ unsafe fn store_blob_hex_param(
pub(super) fn bind_text_impl(
p_stmt: *mut sqlite3_stmt,
idx: c_int,
val: *const c_char,
n_bytes: c_int,
mut val: *const c_char,
mut n_bytes: c_int,
destructor: *mut c_void,
) -> c_int {
let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT, p_stmt) };
let mut _intercepted_uuid = String::new();

if !pg_stmt.is_null() && !val.is_null() {
let stmt = unsafe { &*pg_stmt };
let sql_bytes = unsafe { if !stmt.sql.is_null() { crate::byte_utils::cstr_bytes(stmt.sql) } else { b"" } };

let bypass = std::env::var("BYPASS_UUID_INTERCEPT").is_ok();
if !bypass && crate::byte_utils::contains_bytes(sql_bytes, b"devices") {
let actual_len = if n_bytes < 0 {
unsafe { libc::strlen(val) as usize }
} else {
n_bytes as usize
};
if actual_len == 0 {
let param_name = crate::db_interpose_metadata::rust_my_sqlite3_bind_parameter_name(p_stmt, idx);
let is_identifier = if !param_name.is_null() {
let name_str = unsafe { std::ffi::CStr::from_ptr(param_name).to_string_lossy().to_lowercase() };
name_str.contains("identifier")
} else {
let sql_str = crate::db_interpose_conn_utils::cstr_to_string_or(stmt.sql, "").to_lowercase();
sql_str.contains("where identifier") || (sql_str.contains("select") && idx == 1)
};

if is_identifier {
_intercepted_uuid = get_machine_identifier();
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;
}
}
}
}

if !pg_stmt.is_null() {
let stmt = unsafe { &*pg_stmt };
let sql_bytes = unsafe { if !stmt.sql.is_null() { crate::byte_utils::cstr_bytes(stmt.sql) } else { b"" } };
if crate::byte_utils::contains_bytes(sql_bytes, b"devices") || crate::byte_utils::contains_bytes(sql_bytes, b"library_sections") || crate::byte_utils::contains_bytes(sql_bytes, b"plugins") {
let val_str = if val.is_null() { "NULL".to_string() } else {
let actual_len = if n_bytes < 0 {
unsafe { libc::strlen(val) as usize }
} else {
n_bytes as usize
};
let bytes = unsafe { std::slice::from_raw_parts(val as *const u8, actual_len.min(100)) };
String::from_utf8_lossy(bytes).into_owned()
};
log_debug_lazy!(
"BIND TEXT: stmt={:p} idx={} val='{}' n_bytes={} sql={}",
pg_stmt,
idx,
val_str,
n_bytes,
crate::db_interpose_conn_utils::cstr_to_string_or(stmt.sql, "NULL")
);
}
}

let rc = if is_pg_routed_noncached(pg_stmt) {
// Skip orig_sqlite3_bind_text — PG param storage below is sufficient.
Expand Down Expand Up @@ -218,12 +304,44 @@ pub(super) fn bind_blob64_impl(
pub(super) fn bind_text64_impl(
p_stmt: *mut sqlite3_stmt,
idx: c_int,
val: *const c_char,
n_bytes: u64,
mut val: *const c_char,
mut n_bytes: u64,
destructor: *mut c_void,
encoding: c_uchar,
) -> c_int {
let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_TEXT64, p_stmt) };
let mut _intercepted_uuid = String::new();

if !pg_stmt.is_null() && !val.is_null() {
let stmt = unsafe { &*pg_stmt };
let sql_bytes = unsafe { if !stmt.sql.is_null() { crate::byte_utils::cstr_bytes(stmt.sql) } else { b"" } };

let bypass = std::env::var("BYPASS_UUID_INTERCEPT").is_ok();
if !bypass && crate::byte_utils::contains_bytes(sql_bytes, b"devices") {
let actual_len = if n_bytes == u64::MAX {
unsafe { libc::strlen(val) as usize }
} else {
n_bytes as usize
};
if actual_len == 0 {
let param_name = crate::db_interpose_metadata::rust_my_sqlite3_bind_parameter_name(p_stmt, idx);
let is_identifier = if !param_name.is_null() {
let name_str = unsafe { std::ffi::CStr::from_ptr(param_name).to_string_lossy().to_lowercase() };
name_str.contains("identifier")
} else {
let sql_str = crate::db_interpose_conn_utils::cstr_to_string_or(stmt.sql, "").to_lowercase();
sql_str.contains("where identifier") || (sql_str.contains("select") && idx == 1)
};

if is_identifier {
_intercepted_uuid = get_machine_identifier();
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;
}
}
}
}

let rc = if is_pg_routed_noncached(pg_stmt) {
if !val.is_null() {
Expand Down Expand Up @@ -269,3 +387,36 @@ pub(super) fn bind_text64_impl(
crate::pg_mem_telemetry::rust_mem_telemetry_maybe_log();
rc
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_machine_identifier_valid() {
let xml = r#"<?xml version="1.0" encoding="utf-8"?><Preferences MachineIdentifier="53cfd87bf8b24db2af2d6aaa373b2b34" 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"/>"#;
let uuid = parse_machine_identifier(xml);
assert_eq!(uuid, None);
}

#[test]
fn test_parse_machine_identifier_missing() {
let xml = r#"<Preferences AcceptedEULA="1"/>"#;
let uuid = parse_machine_identifier(xml);
assert_eq!(uuid, None);
}

#[test]
fn test_get_machine_identifier_fallback() {
let uuid = get_machine_identifier();
// Since /config/... doesn't exist in unit test runner environment, it must fallback to default UUID
assert_eq!(uuid, "53cfd87b-f8b2-4db2-af2d-6aaa373b2b34");
}
}
1 change: 1 addition & 0 deletions rust/plex-pg-core/src/db_interpose_bind/value_binds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub(super) fn bind_value_impl(
) -> c_int {
let (pg_stmt, guard) = unsafe { begin_bind(PHASE_BIND_VALUE, p_stmt) };


let rc = if is_pg_routed_noncached(pg_stmt) {
SQLITE_OK
} else {
Expand Down
20 changes: 19 additions & 1 deletion rust/plex-pg-core/src/db_interpose_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,5 +439,23 @@ unsafe fn my_sqlite3_step_impl(p_stmt: *mut sqlite3_stmt) -> c_int {
}
}

orig_step(p_stmt)
let rc = orig_step(p_stmt);
if rc != SQLITE_ROW && rc != SQLITE_DONE {
let db = call_sqlite3_db_handle(p_stmt);
let errmsg = if db.is_null() {
b"no db handle\0".as_ptr() as *const c_char
} else {
crate::db_interpose_metadata::rust_my_sqlite3_errmsg(db)
};
let sql = call_sqlite3_sql(p_stmt);
libc::fprintf(
stderr_ptr(),
b"[SQLITE_STEP_ERROR] rc=%d errmsg='%s' sql='%.900s'\n\0".as_ptr() as *const c_char,
rc,
errmsg,
if sql.is_null() { b"<null>\0".as_ptr() as *const c_char } else { sql },
);
libc::fflush(stderr_ptr());
}
rc
}
29 changes: 29 additions & 0 deletions rust/plex-pg-core/src/db_interpose_step_read_utils/next_result.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;
use crate::log_debug_lazy;
use crate::byte_utils::{contains_icase_bytes, cstr_bytes};
use crate::db_interpose_conn_utils::{cstr_prefix, cstr_to_string_or};

pub(super) fn advance_cached_result_impl(stmt: *mut PgStmt) -> c_int {
if stmt.is_null() {
Expand Down Expand Up @@ -172,5 +174,32 @@ pub(super) fn log_debug_context_impl(stmt: *mut PgStmt, exec_conn: *mut PgConnec
stmt,
exec_conn
);
let has_match = unsafe {
!stmt_ref.sql.is_null()
&& (contains_icase_bytes(cstr_bytes(stmt_ref.sql), b"devices")
|| contains_icase_bytes(cstr_bytes(stmt_ref.sql), b"library_sections"))
|| !stmt_ref.pg_sql.is_null()
&& (contains_icase_bytes(cstr_bytes(stmt_ref.pg_sql), b"devices")
|| contains_icase_bytes(cstr_bytes(stmt_ref.pg_sql), b"library_sections"))
};
if has_match {
log_debug_lazy!(
"STEP READ devices/library_sections: param_count={} is_pg={}",
stmt_ref.param_count,
stmt_ref.is_pg
);
for i in 0..(stmt_ref.param_count as usize) {
if i < stmt_ref.param_values.len() {
let p = stmt_ref.param_values[i];
log_debug_lazy!(" PARAM[{}]: {}", i, cstr_to_string_or(p, "NULL"));
} else {
log_debug_lazy!(" PARAM[{}]: OUT_OF_BOUNDS", i);
}
}
log_debug_lazy!(" SQL: {}", cstr_prefix(stmt_ref.sql, 1000, "NULL"));
if !stmt_ref.pg_sql.is_null() {
log_debug_lazy!(" PG_SQL: {}", cstr_prefix(stmt_ref.pg_sql, 1000, "NULL"));
}
}
}
}
26 changes: 26 additions & 0 deletions rust/plex-pg-core/src/db_interpose_step_write_utils/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ pub extern "C" fn rust_step_write_log_debug_context(
let stmt = unsafe { &*pg_stmt };

unsafe {
let has_match = !stmt.sql.is_null()
&& (contains_icase_bytes(cstr_bytes(stmt.sql), b"devices")
|| contains_icase_bytes(cstr_bytes(stmt.sql), b"library_sections"))
|| !stmt.pg_sql.is_null()
&& (contains_icase_bytes(cstr_bytes(stmt.pg_sql), b"devices")
|| contains_icase_bytes(cstr_bytes(stmt.pg_sql), b"library_sections"));
if has_match {
log_debug_lazy!(
"STEP WRITE devices/library_sections: param_count={} is_pg={}",
stmt.param_count,
stmt.is_pg
);
for i in 0..(stmt.param_count as usize) {
if i < stmt.param_values.len() {
let p = stmt.param_values[i];
log_debug_lazy!(" PARAM[{}]: {}", i, cstr_to_string_or(p, "NULL"));
} else {
log_debug_lazy!(" PARAM[{}]: OUT_OF_BOUNDS", i);
}
}
log_debug_lazy!(" SQL: {}", cstr_prefix(stmt.sql, 1000, "NULL"));
if !stmt.pg_sql.is_null() {
log_debug_lazy!(" PG_SQL: {}", cstr_prefix(stmt.pg_sql, 1000, "NULL"));
}
}

if !stmt.pg_sql.is_null()
&& contains_bytes(cstr_bytes(stmt.pg_sql), b"play_queue_generators")
{
Expand Down
19 changes: 12 additions & 7 deletions rust/plex-pg-core/src/db_interpose_stmt_lifecycle/statement_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub(super) fn finalize_impl(p_stmt: *mut sqlite3_stmt) -> c_int {
pg_stmt_ref.sql
};

remember_finalized_stmt(p_stmt, final_sql, is_pg_value);

let cached = pg_find_cached_stmt(p_stmt);
if cached == pg_stmt {
log_debug("finalize: stmt in both global and TLS, clearing TLS ref");
Expand All @@ -120,18 +122,22 @@ pub(super) fn finalize_impl(p_stmt: *mut sqlite3_stmt) -> c_int {
cached_ref.sql
};
}

remember_finalized_stmt(p_stmt, final_sql, is_pg_value);

log_debug_lazy!(
"finalize: stmt only in TLS (ref_count={}), clearing",
cached_ref.ref_count.load(Ordering::Relaxed)
);
pg_clear_cached_stmt(p_stmt);
pg_stmt_unref(cached);
}
}

if final_sql.is_null() {
if let Some(f) = orig_sqlite3_sql {
final_sql = f(p_stmt);
} else {
if final_sql.is_null() {
if let Some(f) = orig_sqlite3_sql {
final_sql = f(p_stmt);
}
}
remember_finalized_stmt(p_stmt, final_sql, is_pg_value);
}
}

Expand All @@ -142,7 +148,6 @@ pub(super) fn finalize_impl(p_stmt: *mut sqlite3_stmt) -> c_int {
.unwrap_or(SQLITE_ERROR);
}
clear_prepared_stmt(p_stmt);
remember_finalized_stmt(p_stmt, final_sql, is_pg_value);
rc
}
}
Expand Down
Loading
Loading