Skip to content

Commit af5a3ee

Browse files
committed
unix: hide symbols beloning to dependencies on Linux
This partially addresses #114. Previously, we were exporting symbols from dependencies like OpenSSL and X11 from our dynamic binaries. This can cause problems when other libraries are loaded into the same address space and attempt to bind to the symbols being exported by Python. This commit changes things such that symbols for Python dependencies are now hidden and can no longer be bound to. Python's own symbols are still exported of course, as Python extensions (and the `python` executable) need to call into them. This change is only implemented on Linux for the moment. Apple targets require a bit more effort to make work. As part of this, we update validation to confirm visibility of certain symbols in ELF binaries. The new validation logic correctly rejects distributions built before this change.
1 parent a73cc7c commit af5a3ee

File tree

3 files changed

+144
-2
lines changed

3 files changed

+144
-2
lines changed

cpython-unix/build-cpython.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,17 @@ fi
682682
# So we need to set both.
683683
CFLAGS="${EXTRA_TARGET_CFLAGS} -fPIC -I${TOOLS_PATH}/deps/include -I${TOOLS_PATH}/deps/include/ncursesw"
684684
LDFLAGS="${EXTRA_TARGET_LDFLAGS} -L${TOOLS_PATH}/deps/lib"
685+
686+
# Some target configurations use `-fvisibility=hidden`. Python's configure handles
687+
# symbol visibility properly itself. So let it do its thing.
688+
CFLAGS=${CFLAGS//-fvisibility=hidden/}
689+
690+
# But some symbols from some dependency libraries are still non-hidden for some
691+
# reason. We force the linker to do our bidding.
692+
if [ "${PYBUILD_PLATFORM}" != "macos" ]; then
693+
LDFLAGS="${LDFLAGS} -Wl,--exclude-libs,ALL"
694+
fi
695+
685696
EXTRA_CONFIGURE_FLAGS=
686697

687698
if [ "${PYBUILD_PLATFORM}" = "macos" ]; then

cpython-unix/targets.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ i686-unknown-linux-gnu:
279279
target_cc: clang
280280
target_cflags:
281281
- '-m32'
282+
- '-fvisibility=hidden'
282283
target_ldflags:
283284
- '-m32'
284285
needs:
@@ -606,6 +607,8 @@ x86_64-unknown-linux-gnu:
606607
host_cc: clang
607608
host_cxx: clang++
608609
target_cc: clang
610+
target_cflags:
611+
- '-fvisibility=hidden'
609612
needs:
610613
- bdb
611614
- binutils
@@ -644,6 +647,7 @@ x86_64_v2-unknown-linux-gnu:
644647
target_cc: clang
645648
target_cflags:
646649
- '-march=x86-64-v2'
650+
- '-fvisibility=hidden'
647651
needs:
648652
- bdb
649653
- binutils
@@ -682,6 +686,7 @@ x86_64_v3-unknown-linux-gnu:
682686
target_cc: clang
683687
target_cflags:
684688
- '-march=x86-64-v3'
689+
- '-fvisibility=hidden'
685690
needs:
686691
- bdb
687692
- binutils
@@ -720,6 +725,7 @@ x86_64_v4-unknown-linux-gnu:
720725
target_cc: clang
721726
target_cflags:
722727
- '-march=x86-64-v4'
728+
- '-fvisibility=hidden'
723729
needs:
724730
- bdb
725731
- binutils
@@ -756,6 +762,8 @@ x86_64-unknown-linux-musl:
756762
host_cc: clang
757763
host_cxx: clang++
758764
target_cc: musl-clang
765+
target_cflags:
766+
- '-fvisibility=hidden'
759767
needs:
760768
- bdb
761769
- binutils
@@ -795,6 +803,7 @@ x86_64_v2-unknown-linux-musl:
795803
target_cc: musl-clang
796804
target_cflags:
797805
- '-march=x86-64-v2'
806+
- '-fvisibility=hidden'
798807
needs:
799808
- bdb
800809
- binutils
@@ -834,6 +843,7 @@ x86_64_v3-unknown-linux-musl:
834843
target_cc: musl-clang
835844
target_cflags:
836845
- '-march=x86-64-v3'
846+
- '-fvisibility=hidden'
837847
needs:
838848
- bdb
839849
- binutils
@@ -873,6 +883,7 @@ x86_64_v4-unknown-linux-musl:
873883
target_cc: musl-clang
874884
target_cflags:
875885
- '-march=x86-64-v4'
886+
- '-fvisibility=hidden'
876887
needs:
877888
- bdb
878889
- binutils

src/validation.rs

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use {
77
anyhow::{anyhow, Context, Result},
88
clap::ArgMatches,
99
object::{
10-
elf::{FileHeader32, FileHeader64},
10+
elf::{
11+
FileHeader32, FileHeader64, ET_DYN, ET_EXEC, STB_GLOBAL, STB_WEAK, STV_DEFAULT,
12+
STV_HIDDEN,
13+
},
1114
macho::{MachHeader32, MachHeader64},
1215
read::{
1316
elf::{Dyn, FileHeader, SectionHeader, Sym},
@@ -440,6 +443,75 @@ const MACHO_ALLOWED_WEAK_SYMBOLS_38_NON_AARCH64: &[&str] = &[
440443
"_statvfs",
441444
];
442445

446+
/// Symbols defined in dependency packages.
447+
///
448+
/// We use this list to spot test behavior of symbols belonging to dependency packages.
449+
/// The list is obviously not complete.
450+
const DEPENDENCY_PACKAGE_SYMBOLS: &[&str] = &[
451+
// libX11
452+
"XClearWindow",
453+
"XFlush",
454+
// OpenSSL
455+
"BIO_ADDR_new",
456+
"BN_new",
457+
"DH_new",
458+
"SSL_extension_supported",
459+
"SSL_read",
460+
"CRYPTO_memcmp",
461+
"ecp_nistz256_neg",
462+
"OPENSSL_instrument_bus",
463+
"x25519_fe64_add",
464+
// libdb
465+
"__txn_begin",
466+
// libedit / readline
467+
"rl_prompt",
468+
"readline",
469+
"current_history",
470+
"history_expand",
471+
// libffi
472+
"ffi_call",
473+
"ffi_type_void",
474+
// ncurses
475+
"new_field",
476+
"set_field_term",
477+
"set_menu_init",
478+
"winstr",
479+
// gdbm
480+
"gdbm_close",
481+
"gdbm_import",
482+
// sqlite3
483+
"sqlite3_initialize",
484+
"sqlite3_close",
485+
// libxcb
486+
"xcb_create_window",
487+
"xcb_get_property",
488+
// libz
489+
"deflateEnd",
490+
"gzclose",
491+
"inflate",
492+
// tix
493+
"Tix_DItemCreate",
494+
"Tix_GrFormat",
495+
// liblzma
496+
"lzma_index_init",
497+
"lzma_stream_encoder",
498+
// tcl
499+
"Tcl_Alloc",
500+
"Tcl_ChannelName",
501+
"Tcl_CreateInterp",
502+
// tk
503+
"TkBindInit",
504+
"TkCreateFrame",
505+
"Tk_FreeGC",
506+
];
507+
508+
const PYTHON_EXPORTED_SYMBOLS: &[&str] = &[
509+
"Py_Initialize",
510+
"PyList_New",
511+
// From limited API.
512+
"Py_CompileString",
513+
];
514+
443515
static WANTED_WINDOWS_STATIC_PATHS: Lazy<BTreeSet<PathBuf>> = Lazy::new(|| {
444516
[
445517
PathBuf::from("python/build/lib/libffi.lib"),
@@ -478,6 +550,9 @@ pub struct ValidationContext {
478550
/// Dynamic libraries required to be loaded.
479551
pub seen_dylibs: BTreeSet<String>,
480552

553+
/// Symbols exported from dynamic libpython library.
554+
pub libpython_exported_symbols: BTreeSet<String>,
555+
481556
/// Undefined Mach-O symbols that are required / non-weak.
482557
pub macho_undefined_symbols_strong: RequiredSymbols,
483558

@@ -490,6 +565,8 @@ impl ValidationContext {
490565
pub fn merge(&mut self, other: Self) {
491566
self.errors.extend(other.errors);
492567
self.seen_dylibs.extend(other.seen_dylibs);
568+
self.libpython_exported_symbols
569+
.extend(other.libpython_exported_symbols);
493570
self.macho_undefined_symbols_strong
494571
.merge(other.macho_undefined_symbols_strong);
495572
self.macho_undefined_symbols_weak
@@ -699,6 +776,34 @@ fn validate_elf<'data, Elf: FileHeader<Endian = Endianness>>(
699776
}
700777
}
701778
}
779+
780+
// Ensure specific symbols in dynamic binaries have proper visibility.
781+
if matches!(elf.e_type(endian), ET_EXEC | ET_DYN) {
782+
// Non-local symbols belonging to dependencies should have hidden visibility
783+
// to prevent them from being exported.
784+
if DEPENDENCY_PACKAGE_SYMBOLS.contains(&name.as_ref())
785+
&& matches!(symbol.st_bind(), STB_GLOBAL | STB_WEAK)
786+
&& symbol.st_visibility() != STV_HIDDEN
787+
{
788+
context.errors.push(format!(
789+
"{} contains non-hidden dependency symbol {}",
790+
path.display(),
791+
name
792+
));
793+
}
794+
795+
if let Some(filename) = path.file_name() {
796+
let filename = filename.to_string_lossy();
797+
798+
if filename.starts_with("libpython") && filename.ends_with(".so.1.0") {
799+
if matches!(symbol.st_bind(), STB_GLOBAL | STB_WEAK)
800+
&& symbol.st_visibility() == STV_DEFAULT
801+
{
802+
context.libpython_exported_symbols.insert(name.to_string());
803+
}
804+
}
805+
}
806+
}
702807
}
703808
}
704809
}
@@ -1121,7 +1226,7 @@ fn validate_distribution(
11211226
}
11221227
}
11231228

1124-
// We've now read the contents of the archive. Move on to analyizing the results.
1229+
// We've now read the contents of the archive. Move on to analyzing the results.
11251230

11261231
for path in seen_symlink_targets {
11271232
if !seen_paths.contains(&path) {
@@ -1160,6 +1265,21 @@ fn validate_distribution(
11601265
}
11611266
}
11621267

1268+
// If we've collected symbols exported from libpython, ensure the Python symbols are
1269+
// in the set.
1270+
if !context.libpython_exported_symbols.is_empty() {
1271+
for symbol in PYTHON_EXPORTED_SYMBOLS {
1272+
if !context
1273+
.libpython_exported_symbols
1274+
.contains(&symbol.to_string())
1275+
{
1276+
context
1277+
.errors
1278+
.push(format!("libpython does not export {}", symbol));
1279+
}
1280+
}
1281+
}
1282+
11631283
// On Apple Python 3.8 we need to ban most weak symbol references because 3.8 doesn't have
11641284
// the proper runtime guards in place to prevent them from being resolved at runtime,
11651285
// which would lead to a crash. See

0 commit comments

Comments
 (0)