Skip to content

Commit 9acb783

Browse files
Set LIBFFI_TMPDIR at startup (#80651) (#80699)
* Set LIBFFI_TMPDIR at startup (#80651) Today if `libffi` cannot allocate pages of memory which are both writeable and executable then it will attempt to write code to a temporary file. Elasticsearch configures itself a suitable temporary directory for use by JNA but by default `libffi` won't find this directory and will try various other places. In certain configurations, none of the other places that `libffi` tries are suitable. With older versions of JNA this would result in a `SIGSEGV`; since #80617 the JVM will exit with an exception. With this commit we use the `LIBFFI_TMPDIR` environment variable to configure `libffi` to use the same directory as JNA for its temporary files if they are needed. Closes #18272 Closes #73309 Closes #74545 Closes #77014 Closes #77053 Relates #77285 Co-authored-by: Rory Hunter <roryhunter2@gmail.com> * Fix incorrect SSL usage Co-authored-by: Rory Hunter <roryhunter2@gmail.com>
1 parent 27d5315 commit 9acb783

File tree

7 files changed

+215
-26
lines changed

7 files changed

+215
-26
lines changed

distribution/src/bin/elasticsearch

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ if [ -z "$ES_TMPDIR" ]; then
3232
ES_TMPDIR=`"$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory`
3333
fi
3434

35+
if [ -z "$LIBFFI_TMPDIR" ]; then
36+
LIBFFI_TMPDIR="$ES_TMPDIR"
37+
export LIBFFI_TMPDIR
38+
fi
39+
3540
# get keystore password before setting java options to avoid
3641
# conflicting GC configurations for the keystore tools
3742
unset KEYSTORE_PASSWORD

docs/reference/setup/sysconfig/executable-jna-tmpdir.asciidoc

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,34 @@
44
[NOTE]
55
This is only relevant for Linux.
66

7-
Elasticsearch uses the Java Native Access (JNA) library for executing some
8-
platform-dependent native code. On Linux, the native code backing this library
9-
is extracted at runtime from the JNA archive. This code is extracted
10-
to the Elasticsearch temporary directory which defaults to a sub-directory of
11-
`/tmp` and can be configured with the <<es-tmpdir,ES_TMPDIR>> variable.
12-
Alternatively, this location can be controlled with the JVM flag
13-
`-Djna.tmpdir=<path>`. As the native library is mapped into the JVM virtual
14-
address space as executable, the underlying mount point of the location that
15-
this code is extracted to must *not* be mounted with `noexec` as this prevents
16-
the JVM process from being able to map this code as executable. On some hardened
17-
Linux installations this is a default mount option for `/tmp`. One indication
18-
that the underlying mount is mounted with `noexec` is that at startup JNA will
19-
fail to load with a `java.lang.UnsatisfiedLinkerError` exception with a message
20-
along the lines of `failed to map segment from shared object`. Note that the
21-
exception message can differ amongst JVM versions. Additionally, the components
22-
of Elasticsearch that rely on execution of native code via JNA will fail with
23-
messages indicating that it is `because JNA is not available`. If you are seeing
24-
such error messages, you must remount the temporary directory used for JNA to
25-
not be mounted with `noexec`.
7+
{es} uses the Java Native Access (JNA) library, and another library called
8+
`libffi`, for executing some platform-dependent native code. On Linux, the
9+
native code backing these libraries is extracted at runtime into a temporary
10+
directory and then mapped into executable pages in {es}'s address space. This
11+
requires the underlying files not to be on a filesystem mounted with the
12+
`noexec` option.
13+
14+
By default, {es} will create its temporary directory within `/tmp`. However,
15+
some hardened Linux installations mount `/tmp` with the `noexec` option by
16+
default. This prevents JNA and `libffi` from working correctly. For instance,
17+
at startup JNA may fail to load with an `java.lang.UnsatisfiedLinkerError`
18+
exception or with a message that says something similar to
19+
`failed to map segment from shared object`. Note that the exception message can
20+
differ amongst JVM versions. Additionally, the components of {es} that rely on
21+
execution of native code via JNA may fail with messages indicating that it is
22+
`because JNA is not available`.
23+
24+
To resolve these problems, either remove the `noexec` option from your `/tmp`
25+
filesystem, or configure {es} to use a different location for its temporary
26+
directory by setting the <<es-tmpdir,`$ES_TMPDIR`>> environment variable. For
27+
instance:
28+
29+
["source","sh",subs="attributes"]
30+
--------------------------------------------
31+
export ES_TMPDIR=/usr/share/elasticsearch/tmp
32+
--------------------------------------------
33+
34+
Alternatively, you can configure the path that JNA uses for its temporary files
35+
with the <<set-jvm-options,JVM flag>> `-Djna.tmpdir=<path>` and you can
36+
configure the path that `libffi` uses for its temporary files with the
37+
`LIBFFI_TMPDIR` environment variable.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.packaging.test;
10+
11+
import org.apache.http.client.fluent.Request;
12+
import org.elasticsearch.core.CheckedConsumer;
13+
import org.elasticsearch.packaging.util.Distribution;
14+
import org.elasticsearch.packaging.util.ServerUtils;
15+
import org.elasticsearch.packaging.util.Shell;
16+
import org.elasticsearch.packaging.util.docker.DockerRun;
17+
import org.junit.After;
18+
import org.junit.Before;
19+
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
23+
import static org.elasticsearch.packaging.util.FileUtils.append;
24+
import static org.elasticsearch.packaging.util.docker.Docker.removeContainer;
25+
import static org.elasticsearch.packaging.util.docker.Docker.runContainer;
26+
import static org.elasticsearch.packaging.util.docker.Docker.runContainerExpectingFailure;
27+
import static org.elasticsearch.packaging.util.docker.Docker.waitForElasticsearch;
28+
import static org.hamcrest.Matchers.containsString;
29+
import static org.junit.Assume.assumeFalse;
30+
import static org.junit.Assume.assumeTrue;
31+
32+
public class TemporaryDirectoryConfigTests extends PackagingTestCase {
33+
34+
@Before
35+
public void onlyLinux() {
36+
assumeTrue("only Linux", distribution.platform == Distribution.Platform.LINUX);
37+
}
38+
39+
@After
40+
public void cleanupContainer() {
41+
if (distribution().isDocker()) {
42+
removeContainer();
43+
}
44+
}
45+
46+
public void test10Install() throws Exception {
47+
install();
48+
}
49+
50+
public void test20AcceptsCustomPath() throws Exception {
51+
assumeFalse(distribution().isDocker());
52+
53+
final Path tmpDir = createTempDir("libffi");
54+
sh.getEnv().put("LIBFFI_TMPDIR", tmpDir.toString());
55+
withLibffiTmpdir(
56+
tmpDir.toString(),
57+
confPath -> assertWhileRunning(() -> ServerUtils.makeRequest(Request.Get("http://localhost:9200/")))
58+
); // just checking it doesn't throw
59+
}
60+
61+
public void test21AcceptsCustomPathInDocker() throws Exception {
62+
assumeTrue(distribution().isDocker());
63+
64+
final Path tmpDir = createTempDir("libffi");
65+
66+
installation = runContainer(
67+
distribution(),
68+
DockerRun.builder()
69+
// There's no actual need for this to be a bind-mounted dir, but it's the quickest
70+
// way to create a directory in the container before the entrypoint runs.
71+
.volume(tmpDir, tmpDir)
72+
.envVar("ELASTIC_PASSWORD", "nothunter2")
73+
.envVar("LIBFFI_TMPDIR", tmpDir.toString())
74+
);
75+
76+
waitForElasticsearch("green", null, installation, "elastic", "nothunter2");
77+
}
78+
79+
public void test30VerifiesCustomPath() throws Exception {
80+
assumeFalse(distribution().isDocker());
81+
82+
final Path tmpFile = createTempDir("libffi").resolve("file");
83+
Files.createFile(tmpFile);
84+
withLibffiTmpdir(
85+
tmpFile.toString(),
86+
confPath -> assertElasticsearchFailure(runElasticsearchStartCommand(null, false, false), "LIBFFI_TMPDIR", null)
87+
);
88+
}
89+
90+
public void test31VerifiesCustomPathInDocker() throws Exception {
91+
assumeTrue(distribution().isDocker());
92+
93+
final Path tmpDir = createTempDir("libffi");
94+
final Path tmpFile = tmpDir.resolve("file");
95+
Files.createFile(tmpFile);
96+
97+
final Shell.Result result = runContainerExpectingFailure(
98+
distribution(),
99+
DockerRun.builder().volume(tmpDir, tmpDir).envVar("LIBFFI_TMPDIR", tmpFile.toString())
100+
);
101+
assertThat(result.stderr, containsString("LIBFFI_TMPDIR"));
102+
}
103+
104+
private void withLibffiTmpdir(String tmpDir, CheckedConsumer<Path, Exception> action) throws Exception {
105+
sh.getEnv().put("LIBFFI_TMPDIR", tmpDir);
106+
withCustomConfig(confPath -> {
107+
if (distribution.isPackage()) {
108+
append(installation.envFile, "LIBFFI_TMPDIR=" + tmpDir);
109+
}
110+
action.accept(confPath);
111+
});
112+
113+
}
114+
}

qa/os/src/test/java/org/elasticsearch/packaging/util/docker/Docker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private static void waitForElasticsearchToExit() {
202202
} catch (Exception e) {
203203
logger.warn("Caught exception while waiting for ES to exit", e);
204204
}
205-
} while (attempt++ < 5);
205+
} while (attempt++ < 60);
206206

207207
if (isElasticsearchRunning) {
208208
final Shell.Result dockerLogs = getContainerLogs();

server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ private void setup(boolean addShutdownHook, Environment environment) throws Boot
173173
throw new BootstrapException(e);
174174
}
175175

176+
try {
177+
environment.validateNativesConfig(); // temporary directories are important for JNA
178+
} catch (IOException e) {
179+
throw new BootstrapException(e);
180+
}
176181
initializeNatives(
177182
environment.tmpFile(),
178183
BootstrapSettings.MEMORY_LOCK_SETTING.get(settings),

server/src/main/java/org/elasticsearch/env/Environment.java

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package org.elasticsearch.env;
1010

11+
import org.apache.lucene.util.Constants;
1112
import org.elasticsearch.common.settings.Setting;
1213
import org.elasticsearch.common.settings.Setting.Property;
1314
import org.elasticsearch.common.settings.Settings;
@@ -321,12 +322,48 @@ public Path tmpFile() {
321322

322323
/** Ensure the configured temp directory is a valid directory */
323324
public void validateTmpFile() throws IOException {
324-
if (Files.exists(tmpFile) == false) {
325-
throw new FileNotFoundException("Temporary file directory [" + tmpFile + "] does not exist or is not accessible");
325+
validateTemporaryDirectory("Temporary directory", tmpFile);
326+
}
327+
328+
/**
329+
* Ensure the temp directories needed for JNA are set up correctly.
330+
*/
331+
public void validateNativesConfig() throws IOException {
332+
validateTmpFile();
333+
if (Constants.LINUX) {
334+
validateTemporaryDirectory(LIBFFI_TMPDIR_ENVIRONMENT_VARIABLE + " environment variable", getLibffiTemporaryDirectory());
335+
}
336+
}
337+
338+
private static void validateTemporaryDirectory(String description, Path path) throws IOException {
339+
if (path == null) {
340+
throw new NullPointerException(description + " was not specified");
341+
}
342+
if (Files.exists(path) == false) {
343+
throw new FileNotFoundException(description + " [" + path + "] does not exist or is not accessible");
326344
}
327-
if (Files.isDirectory(tmpFile) == false) {
328-
throw new IOException("Configured temporary file directory [" + tmpFile + "] is not a directory");
345+
if (Files.isDirectory(path) == false) {
346+
throw new IOException(description + " [" + path + "] is not a directory");
347+
}
348+
}
349+
350+
private static final String LIBFFI_TMPDIR_ENVIRONMENT_VARIABLE = "LIBFFI_TMPDIR";
351+
352+
@SuppressForbidden(reason = "using PathUtils#get since libffi resolves paths without interference from the JVM")
353+
private static Path getLibffiTemporaryDirectory() {
354+
final String environmentVariable = System.getenv(LIBFFI_TMPDIR_ENVIRONMENT_VARIABLE);
355+
if (environmentVariable == null) {
356+
return null;
357+
}
358+
// Explicitly resolve into an absolute path since the working directory might be different from the one in which we were launched
359+
// and it would be confusing to report that the given relative path doesn't exist simply because it's being resolved relative to a
360+
// different location than the one the user expects.
361+
final String workingDirectory = System.getProperty("user.dir");
362+
if (workingDirectory == null) {
363+
assert false;
364+
return null;
329365
}
366+
return PathUtils.get(workingDirectory).resolve(environmentVariable);
330367
}
331368

332369
/** Returns true if the data path is a list, false otherwise */

server/src/test/java/org/elasticsearch/env/EnvironmentTests.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,31 @@ public void testNonExistentTempPathValidation() {
139139
Settings build = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build();
140140
Environment environment = new Environment(build, null, true, createTempDir().resolve("this_does_not_exist"));
141141
FileNotFoundException e = expectThrows(FileNotFoundException.class, environment::validateTmpFile);
142-
assertThat(e.getMessage(), startsWith("Temporary file directory ["));
142+
assertThat(e.getMessage(), startsWith("Temporary directory ["));
143143
assertThat(e.getMessage(), endsWith("this_does_not_exist] does not exist or is not accessible"));
144144
}
145145

146146
public void testTempPathValidationWhenRegularFile() throws IOException {
147147
Settings build = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build();
148148
Environment environment = new Environment(build, null, true, createTempFile("something", ".test"));
149149
IOException e = expectThrows(IOException.class, environment::validateTmpFile);
150-
assertThat(e.getMessage(), startsWith("Configured temporary file directory ["));
150+
assertThat(e.getMessage(), startsWith("Temporary directory ["));
151+
assertThat(e.getMessage(), endsWith(".test] is not a directory"));
152+
}
153+
154+
public void testNonExistentTempPathValidationForNatives() {
155+
Settings build = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build();
156+
Environment environment = new Environment(build, null, true, createTempDir().resolve("this_does_not_exist"));
157+
FileNotFoundException e = expectThrows(FileNotFoundException.class, environment::validateNativesConfig);
158+
assertThat(e.getMessage(), startsWith("Temporary directory ["));
159+
assertThat(e.getMessage(), endsWith("this_does_not_exist] does not exist or is not accessible"));
160+
}
161+
162+
public void testTempPathValidationWhenRegularFileForNatives() throws IOException {
163+
Settings build = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build();
164+
Environment environment = new Environment(build, null, true, createTempFile("something", ".test"));
165+
IOException e = expectThrows(IOException.class, environment::validateNativesConfig);
166+
assertThat(e.getMessage(), startsWith("Temporary directory ["));
151167
assertThat(e.getMessage(), endsWith(".test] is not a directory"));
152168
}
153169

0 commit comments

Comments
 (0)