Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.

Commit 63d7d1b

Browse files
Yifu Wangfacebook-github-bot
authored andcommitted
make allocators and sanitizers work for processes created with multiprocessing's spawn method in dev mode
Summary: #### Problem Currently, the entrypoint for in-place Python binaries (i.e. built with dev mode) executes the following steps to load system native dependencies (e.g. sanitizers and allocators): - Backup `LD_PRELOAD` set by the caller - Append system native dependencies to `LD_PRELOAD` - Inject a prologue in user code which restores `LD_PRELOAD` set by the caller - `execv` Python interpreter The steps work as intended for single process Python programs. However, when a Python program spawns child processes, the child processes will not load native dependencies, since they simply `execv`'s the vanilla Python interpreter. A few examples why this is problematic: - The ASAN runtime library is a system native dependency. Without loading it, a child process that loads user native dependencies compiled with ASAN will crash during static initialization because it can't find `_asan_init`. - `jemalloc` is also a system native dependency. Many if not most ML use cases "bans" dev mode because of these problems. It is very unfortunate considering the developer efficiency dev mode provides. In addition, a huge amount of unit tests have to run in a more expensive build mode because of these problems. For an earlier discussion, see [this post](https://fb.workplace.com/groups/fbpython/permalink/2897630276944987/). #### Solution Move the system native dependencies loading logic out of the Python binary entrypoint into an interpreter wrapper, and set the interpreter as `sys.executable` in the injected prologue: - The Python binary entrypoint now uses the interpreter wrapper, which has the same command line interface as the Python interpreter, to run the main module. - `multiprocessing`'s `spawn` method now uses the interpreter wrapper to create child processes, ensuring system native dependencies get loaded correctly. #### Alternative Considered One alternative considered is to simply not removing system native dependencies from `LD_PRELOAD`, so they are present in the spawned processes. However, this causes some linking issues, which were perhaps the reason `LD_PRELOAD` was restored in the first place: in-place Python binaries have access to binaries install on devservers that are not built with the target platform (e.g. `/bin/sh` which is used by some Python standard libraries). These binaries does not link properly with the system native dependencies. #### References An old RFC for this change: D16210828 The counterpart for opt mode: D16350169 fbshipit-source-id: ab83ba439a66f369794c2febea6993fdaa934e6f
1 parent e28cde0 commit 63d7d1b

5 files changed

Lines changed: 318 additions & 150 deletions

File tree

build.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@
10111011
<include name="com/facebook/buck/maven/build-file.st"/>
10121012
<include name="com/facebook/buck/python/*.py"/>
10131013
<include name="com/facebook/buck/python/run_inplace.py.in"/>
1014+
<include name="com/facebook/buck/python/run_inplace_interpreter.py.in"/>
10141015
<include name="com/facebook/buck/python/run_inplace_lite.py.in"/>
10151016
<include name="com/facebook/buck/parser/function/BuckPyFunction.stg"/>
10161017
<include name="com/facebook/buck/shell/sh_binary_template"/>

src/com/facebook/buck/features/python/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ java_library_with_plugins(
6969
"__test_main__.py",
7070
"compile.py",
7171
"run_inplace.py.in",
72+
"run_inplace_interpreter.py.in",
7273
"run_inplace_lite.py.in",
7374
],
7475
tests = [

src/com/facebook/buck/features/python/PythonInPlaceBinary.java

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@
1818

1919
import com.facebook.buck.core.build.buildable.context.BuildableContext;
2020
import com.facebook.buck.core.build.context.BuildContext;
21+
import com.facebook.buck.core.filesystems.AbsPath;
2122
import com.facebook.buck.core.filesystems.RelPath;
2223
import com.facebook.buck.core.model.BuildTarget;
2324
import com.facebook.buck.core.model.OutputLabel;
2425
import com.facebook.buck.core.model.TargetConfiguration;
26+
import com.facebook.buck.core.model.impl.BuildTargetPaths;
2527
import com.facebook.buck.core.rulekey.AddToRuleKey;
2628
import com.facebook.buck.core.rules.BuildRule;
2729
import com.facebook.buck.core.rules.BuildRuleResolver;
2830
import com.facebook.buck.core.rules.attr.HasRuntimeDeps;
2931
import com.facebook.buck.core.rules.impl.SymlinkTree;
32+
import com.facebook.buck.core.sourcepath.ExplicitBuildTargetSourcePath;
3033
import com.facebook.buck.core.toolchain.tool.Tool;
3134
import com.facebook.buck.core.toolchain.tool.impl.CommandTool;
3235
import com.facebook.buck.cxx.toolchain.CxxPlatform;
@@ -39,6 +42,7 @@
3942
import com.facebook.buck.step.Step;
4043
import com.facebook.buck.step.fs.MkdirStep;
4144
import com.facebook.buck.step.isolatedsteps.common.WriteFileIsolatedStep;
45+
import com.facebook.buck.test.selectors.Nullable;
4246
import com.facebook.buck.util.Escaper;
4347
import com.facebook.buck.util.stream.RichStream;
4448
import com.google.common.base.Joiner;
@@ -57,6 +61,7 @@
5761
public class PythonInPlaceBinary extends PythonBinary implements HasRuntimeDeps {
5862

5963
private static final String RUN_INPLACE_RESOURCE = "run_inplace.py.in";
64+
private static final String RUN_INPLACE_INTERPRETER_RESOURCE = "run_inplace_interpreter.py.in";
6065
private static final String RUN_INPLACE_LITE_RESOURCE = "run_inplace_lite.py.in";
6166

6267
// TODO(agallagher): Task #8098647: This rule has no steps, so it
@@ -68,8 +73,10 @@ public class PythonInPlaceBinary extends PythonBinary implements HasRuntimeDeps
6873
//
6974
// We should upate the Python test rule to account for this.
7075
private final SymlinkTree linkTree;
76+
private final RelPath interpreterGenPath;
7177
@AddToRuleKey private final Tool python;
72-
@AddToRuleKey private final Supplier<String> script;
78+
@AddToRuleKey private final Supplier<String> binScript;
79+
@AddToRuleKey private final Supplier<String> interpreterScript;
7380

7481
PythonInPlaceBinary(
7582
BuildTarget buildTarget,
@@ -98,18 +105,27 @@ public class PythonInPlaceBinary extends PythonBinary implements HasRuntimeDeps
98105
legacyOutputPath);
99106
this.linkTree = linkTree;
100107
this.python = python;
101-
this.script =
102-
getScript(
108+
this.interpreterGenPath =
109+
getInterpreterGenPath(buildTarget, projectFilesystem, pexExtension, legacyOutputPath);
110+
AbsPath targetRoot =
111+
projectFilesystem
112+
.resolve(getBinPath(buildTarget, projectFilesystem, pexExtension, legacyOutputPath))
113+
.getParent();
114+
this.binScript =
115+
getBinScript(
116+
pythonPlatform,
117+
mainModule,
118+
targetRoot.relativize(linkTree.getRoot()),
119+
targetRoot.relativize(projectFilesystem.resolve(interpreterGenPath)),
120+
packageStyle);
121+
this.interpreterScript =
122+
getInterpreterScript(
103123
ruleResolver,
104124
buildTarget.getTargetConfiguration(),
105125
pythonPlatform,
106126
cxxPlatform,
107-
mainModule,
108127
components,
109-
projectFilesystem
110-
.resolve(getBinPath(buildTarget, projectFilesystem, pexExtension, legacyOutputPath))
111-
.getParent()
112-
.relativize(linkTree.getRoot()),
128+
targetRoot.relativize(linkTree.getRoot()),
113129
preloadLibraries,
114130
packageStyle);
115131
}
@@ -123,6 +139,10 @@ private static String getRunInplaceResource() {
123139
return getNamedResource(RUN_INPLACE_RESOURCE);
124140
}
125141

142+
private static String getRunInplaceInterpreterResource() {
143+
return getNamedResource(RUN_INPLACE_INTERPRETER_RESOURCE);
144+
}
145+
126146
private static String getRunInplaceLiteResource() {
127147
return getNamedResource(RUN_INPLACE_LITE_RESOURCE);
128148
}
@@ -136,29 +156,63 @@ private static String getNamedResource(String resourceName) {
136156
}
137157
}
138158

139-
private static Supplier<String> getScript(
159+
private static RelPath getInterpreterGenPath(
160+
BuildTarget target,
161+
ProjectFilesystem filesystem,
162+
String extension,
163+
boolean legacyOutputPath) {
164+
if (!legacyOutputPath) {
165+
target = target.withFlavors();
166+
}
167+
return BuildTargetPaths.getGenPath(
168+
filesystem.getBuckPaths(), target, "%s#interpreter" + extension);
169+
}
170+
171+
private static Supplier<String> getBinScript(
172+
PythonPlatform pythonPlatform,
173+
String mainModule,
174+
RelPath linkTreeRoot,
175+
RelPath interpreterPath,
176+
PackageStyle packageStyle) {
177+
return () -> {
178+
String linkTreeRootStr = Escaper.escapeAsPythonString(linkTreeRoot.toString());
179+
String interpreterPathStr = Escaper.escapeAsPythonString(interpreterPath.toString());
180+
return new ST(
181+
new STGroup(),
182+
packageStyle == PackageStyle.INPLACE
183+
? getRunInplaceResource()
184+
: getRunInplaceLiteResource())
185+
.add("PYTHON", pythonPlatform.getEnvironment().getPythonPath())
186+
.add("PYTHON_INTERPRETER_FLAGS", pythonPlatform.getInplaceBinaryInterpreterFlags())
187+
.add("MODULES_DIR", linkTreeRootStr)
188+
.add("MAIN_MODULE", Escaper.escapeAsPythonString(mainModule))
189+
.add("INTERPRETER_REL_PATH", interpreterPathStr)
190+
.render();
191+
};
192+
}
193+
194+
@Nullable
195+
private static Supplier<String> getInterpreterScript(
140196
BuildRuleResolver resolver,
141197
TargetConfiguration targetConfiguration,
142198
PythonPlatform pythonPlatform,
143199
CxxPlatform cxxPlatform,
144-
String mainModule,
145200
PythonPackageComponents components,
146201
RelPath relativeLinkTreeRoot,
147202
ImmutableSet<String> preloadLibraries,
148203
PackageStyle packageStyle) {
149204
String relativeLinkTreeRootStr = Escaper.escapeAsPythonString(relativeLinkTreeRoot.toString());
150205
Linker ld = cxxPlatform.getLd().resolve(resolver, targetConfiguration);
206+
// Lite mode doesn't need a par-style interpreter as there's no LD_PRELOADs involved.
207+
if (packageStyle != PackageStyle.INPLACE) {
208+
return null;
209+
}
151210
return () -> {
152211
ST st =
153-
new ST(
154-
new STGroup(),
155-
packageStyle == PackageStyle.INPLACE
156-
? getRunInplaceResource()
157-
: getRunInplaceLiteResource())
212+
new ST(new STGroup(), getRunInplaceInterpreterResource())
158213
.add("PYTHON", pythonPlatform.getEnvironment().getPythonPath())
159-
.add("MAIN_MODULE", Escaper.escapeAsPythonString(mainModule))
160-
.add("MODULES_DIR", relativeLinkTreeRootStr)
161-
.add("PYTHON_INTERPRETER_FLAGS", pythonPlatform.getInplaceBinaryInterpreterFlags());
214+
.add("PYTHON_INTERPRETER_FLAGS", pythonPlatform.getInplaceBinaryInterpreterFlags())
215+
.add("MODULES_DIR", relativeLinkTreeRootStr);
162216

163217
// Only add platform-specific values when the binary includes native libraries.
164218
if (components.getNativeLibraries().getComponents().isEmpty()) {
@@ -187,11 +241,24 @@ public ImmutableList<Step> getBuildSteps(
187241
BuildContext context, BuildableContext buildableContext) {
188242
RelPath binPath = context.getSourcePathResolver().getCellUnsafeRelPath(getSourcePathToOutput());
189243
buildableContext.recordArtifact(binPath.getPath());
190-
return ImmutableList.of(
191-
MkdirStep.of(
192-
BuildCellRelativePath.fromCellRelativePath(
193-
context.getBuildCellRootPath(), getProjectFilesystem(), binPath.getParent())),
194-
WriteFileIsolatedStep.of(script, binPath, /* executable */ true));
244+
ImmutableList.Builder<Step> stepsBuilder = new ImmutableList.Builder<Step>();
245+
stepsBuilder
246+
.add(
247+
MkdirStep.of(
248+
BuildCellRelativePath.fromCellRelativePath(
249+
context.getBuildCellRootPath(), getProjectFilesystem(), binPath.getParent())))
250+
.add(WriteFileIsolatedStep.of(binScript, binPath, /* executable */ true));
251+
252+
if (interpreterScript != null) {
253+
RelPath interpreterPath =
254+
context
255+
.getSourcePathResolver()
256+
.getCellUnsafeRelPath(
257+
ExplicitBuildTargetSourcePath.of(getBuildTarget(), interpreterGenPath));
258+
stepsBuilder.add(
259+
WriteFileIsolatedStep.of(interpreterScript, interpreterPath, /* executable */ true));
260+
}
261+
return stepsBuilder.build();
195262
}
196263

197264
@Override

src/com/facebook/buck/features/python/run_inplace.py.in

Lines changed: 6 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ import subprocess
77
import sys
88

99
main_module = <MAIN_MODULE>
10-
modules_dir = <MODULES_DIR>
11-
native_libs_env_var = <NATIVE_LIBS_ENV_VAR>
12-
native_libs_dir = <NATIVE_LIBS_DIR>
13-
native_libs_preload_env_var = <NATIVE_LIBS_PRELOAD_ENV_VAR>
14-
native_libs_preload = <NATIVE_LIBS_PRELOAD>
1510

1611
def try_resolve_possible_symlink(path):
1712
import ctypes
@@ -63,126 +58,10 @@ if platform.system() == "Windows":
6358
# does *not* dereference symlinks on windows until, like, 3.8 maybe.
6459
dirpath = os.path.dirname(try_resolve_possible_symlink(sys.argv[0]))
6560

66-
env_vals_to_restore = {}
67-
# Update the environment variable for the dynamic loader to the native
68-
# libraries location.
69-
if native_libs_dir is not None:
70-
old_native_libs_dir = os.environ.get(native_libs_env_var)
71-
os.environ[native_libs_env_var] = os.path.join(dirpath, native_libs_dir)
72-
env_vals_to_restore[native_libs_env_var] = old_native_libs_dir
73-
74-
# Update the environment variable for the dynamic loader to find libraries
75-
# to preload.
76-
if native_libs_preload is not None:
77-
old_native_libs_preload = os.environ.get(native_libs_preload_env_var)
78-
env_vals_to_restore[native_libs_preload_env_var] = old_native_libs_preload
79-
80-
# On macos, preloaded libs are found via paths.
81-
os.environ[native_libs_preload_env_var] = ":".join(
82-
os.path.join(dirpath, native_libs_dir, l)
83-
for l in native_libs_preload.split(":")
84-
)
85-
86-
# Allow users to decorate the main module. In normal Python invocations this
87-
# can be done by prefixing the arguments with `-m decoratingmodule`. It's not
88-
# that easy for par files. The startup script below sets up `sys.path` from
89-
# within the Python interpreter. Enable decorating the main module after
90-
# `sys.path` has been setup by setting the PAR_MAIN_OVERRIDE environment
91-
# variable.
92-
decorate_main_module = os.environ.pop("PAR_MAIN_OVERRIDE", None)
93-
if decorate_main_module:
94-
# Pass the original main module as environment variable for the process.
95-
# Allowing the decorating module to pick it up.
96-
os.environ["PAR_MAIN_ORIGINAL"] = main_module
97-
main_module = decorate_main_module
98-
99-
module_call = "runpy._run_module_as_main({main_module!r}, False)".format(
100-
main_module=main_module
101-
)
102-
103-
# Allow users to run the main module under pdb. Encode the call into the
104-
# startup script, because pdb does not support the -c argument we use to invoke
105-
# our startup wrapper.
106-
#
107-
# Note: use pop to avoid leaking the environment variable to the child process.
108-
if os.environ.pop("PYTHONDEBUGWITHPDB", None):
109-
# Support passing initial commands to pdb. We cannot pass the -c argument
110-
# to pdb. Instead, allow users to pass initial commands through the
111-
# PYTHONPDBINITIALCOMMANDS env var, separated by the | character.
112-
initial_commands = []
113-
if "PYTHONPDBINITIALCOMMANDS" in os.environ:
114-
# Note: use pop to avoid leaking the environment variable to the child
115-
# process.
116-
initial_commands_string = os.environ.pop("PYTHONPDBINITIALCOMMANDS", None)
117-
initial_commands = initial_commands_string.split("|")
118-
119-
# Note: indentation of this block of code is important as it gets included
120-
# in the bigger block below.
121-
module_call = """
122-
from pdb import Pdb
123-
pdb = Pdb()
124-
pdb.rcLines.extend({initial_commands!r})
125-
pdb.runcall(runpy._run_module_as_main, {main_module!r}, False)
126-
""".format(
127-
main_module=main_module,
128-
initial_commands=initial_commands,
129-
)
130-
131-
# Note: this full block of code will be included as the argument to Python,
132-
# and will be the first thing that shows up in the process arguments as displayed
133-
# by programs like ps and top.
134-
#
135-
# We include arg0 at the start of this comment just to make it more visible what program
136-
# is being run in the ps and top output.
137-
STARTUP = """\
138-
# {arg0!r}
139-
# Wrap everything in a private function to prevent globals being captured by
140-
# the `runpy._run_module_as_main` below.
141-
def __run():
142-
import sys
143-
144-
# We set the paths beforehand to have a minimal amount of imports before
145-
# nuking PWD from sys.path. Otherwise, there can be problems if someone runs
146-
# from a directory with a similarly named file, even if their code is properly
147-
# namespaced. e.g. if one has foo/bar/contextlib.py and while in foo/bar runs
148-
# `buck run foo/bar:bin`, runpy will fail as it tries to import
149-
# foo/bar/contextlib.py. You're just out of luck if you have sys.py or os.py
150-
151-
# Set `argv[0]` to the executing script.
152-
assert sys.argv[0] == '-c'
153-
sys.argv[0] = {arg0!r}
154-
155-
# Replace the working directory with location of the modules directory.
156-
assert sys.path[0] == ''
157-
sys.path[0] = {pythonpath!r}
158-
159-
import os
160-
import runpy
161-
162-
def setenv(var, val):
163-
if val is None:
164-
os.environ.pop(var, None)
165-
else:
166-
os.environ[var] = val
167-
168-
def restoreenv(d):
169-
for k, v in d.items():
170-
setenv(k, v)
171-
172-
restoreenv({env_vals!r})
173-
{module_call}
174-
175-
__run()
176-
""".format(
177-
arg0=sys.argv[0],
178-
pythonpath=os.path.join(dirpath, modules_dir),
179-
env_vals=env_vals_to_restore,
180-
main_module=main_module,
181-
this_file=__file__,
182-
module_call=module_call,
183-
)
184-
185-
args = [sys.executable, "<PYTHON_INTERPRETER_FLAGS>", "-c", STARTUP]
61+
# Run the main module with the interpreter wrapper, which loads native libaries
62+
# and adds neccessary prologue.
63+
interpreter_path = os.path.join(dirpath, <INTERPRETER_REL_PATH>)
64+
args = [interpreter_path, "<PYTHON_INTERPRETER_FLAGS>", "-m", main_module] + sys.argv[1:]
18665

18766
# Default to 'd' warnings, but allow users to control this via PYTHONWARNINGS
18867
# The -E causes python to ignore all PYTHON* environment vars so we have to
@@ -213,7 +92,7 @@ if platform.system() == "Windows":
21392
# path if we have to, which is on Windows. That said, this complicates signal
21493
# handling, so we need to set up some signal forwarding logic.
21594

216-
p = subprocess.Popen(args + sys.argv[1:])
95+
p = subprocess.Popen(args)
21796

21897
def handler(signum, frame):
21998
# If we're getting this, we need to forward signum to subprocesses
@@ -231,4 +110,4 @@ if platform.system() == "Windows":
231110
p.wait()
232111
sys.exit(p.returncode)
233112
else:
234-
os.execv(sys.executable, args + sys.argv[1:])
113+
os.execv(interpreter_path, args)

0 commit comments

Comments
 (0)