diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java index 6064967ad15d6b..ff30a1397d1237 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java @@ -2726,6 +2726,16 @@ public static boolean isAclEnabled(Configuration conf) { public static final String NM_DOCKER_DEFAULT_TMPFS_MOUNTS = DOCKER_CONTAINER_RUNTIME_PREFIX + "default-tmpfs-mounts"; + /** + * Maximum allowed file size in bytes for the Docker client configuration + * file (config.json). If the file size exceeds this value, the container + * launch will be rejected. + */ + public static final String NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES = + DOCKER_CONTAINER_RUNTIME_PREFIX + "client-config-file-max-size-bytes"; + public static final int DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES = + 16 * 1024; + /** The mode in which the Java Container Sandbox should run detailed by * the JavaSandboxLinuxContainerRuntime. */ public static final String YARN_CONTAINER_SANDBOX = diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/util/DockerClientConfigHandler.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/util/DockerClientConfigHandler.java index 6351cb69c82e78..0e49ade6f0acea 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/util/DockerClientConfigHandler.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/util/DockerClientConfigHandler.java @@ -19,6 +19,7 @@ import org.apache.commons.io.FileUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.commons.io.IOUtils; @@ -27,6 +28,7 @@ import org.apache.hadoop.security.Credentials; import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.security.DockerCredentialTokenIdentifier; import com.fasterxml.jackson.core.JsonFactory; @@ -80,19 +82,37 @@ private DockerClientConfigHandler() { } */ public static Credentials readCredentialsFromConfigFile(Path configFile, Configuration conf, String applicationId) throws IOException { - // Read the config file - String contents = null; + + int configFileMaxSizeByte = conf.getInt( + YarnConfiguration.NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES, + YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES); + if (configFileMaxSizeByte <= 0) { + LOG.warn("Invalid value {} for {}, falling back to default {} bytes.", + configFileMaxSizeByte, + YarnConfiguration.NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES, + YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES); + configFileMaxSizeByte = + YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES; + } + + // Read the config file. Check the file size up front so + // a large config.json cannot OOM the NodeManager via IOUtils.toString. configFile = new Path(configFile.toUri()); FileSystem fs = configFile.getFileSystem(conf); - if (fs != null) { - FSDataInputStream fileHandle = fs.open(configFile); - if (fileHandle != null) { - contents = IOUtils.toString(fileHandle, StandardCharsets.UTF_8); - } + FileStatus status = fs.getFileStatus(configFile); + if (status.getLen() > configFileMaxSizeByte) { + throw new IOException("Docker client config " + configFile + " (" + + status.getLen() + " bytes) is larger than maximum allowed size (" + + configFileMaxSizeByte + " bytes)"); + } + + String contents; + try (FSDataInputStream fileHandle = fs.open(configFile)) { + contents = IOUtils.toString(fileHandle, StandardCharsets.UTF_8); } - if (contents == null) { - throw new IOException("Failed to read Docker client configuration: " - + configFile); + if (contents == null || contents.trim().isEmpty()) { + throw new IOException( + "Failed to read Docker client configuration: " + configFile); } // Parse the JSON and create the Tokens/Credentials. diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml index b128d0f0ef14f9..bccef0aded330b 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml @@ -2336,6 +2336,16 @@ + + + Maximum allowed file size in bytes for the Docker client configuration file + (config.json). If the file size exceeds this value, the container launch + will be rejected. Default is 16 KiB. + + yarn.nodemanager.runtime.linux.docker.client-config-file-max-size-bytes + 16384 + + The runC image tag to manifest plugin class to be used. diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/DockerLinuxContainerRuntime.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/DockerLinuxContainerRuntime.java index 14f5ffeefe0d3f..e38dd1632c11ea 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/DockerLinuxContainerRuntime.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/DockerLinuxContainerRuntime.java @@ -910,7 +910,7 @@ private Credentials getAdditionalDockerClientCredentials(String clientConfig, containerIdStr); } catch (IOException e) { throw new RuntimeException( - "Fail to read additional docker client config file from " + clientConfig); + "Failed to read additional docker client config file: " + clientConfig, e); } } return additionalDockerCredentials; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/TestDockerContainerRuntime.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/TestDockerContainerRuntime.java index 84f262438341f4..67e2d8423c6925 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/TestDockerContainerRuntime.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/TestDockerContainerRuntime.java @@ -58,6 +58,7 @@ import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeConstants; import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeContext; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -118,7 +119,9 @@ import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.OCIContainerRuntime.CONTAINER_PID_NAMESPACE_SUFFIX; import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.OCIContainerRuntime.RUN_PRIVILEGED_CONTAINER_SUFFIX; import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.OCIContainerRuntime.formatOciEnvKey; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -2512,6 +2515,72 @@ public void testLaunchContainerWithAdditionalDockerClientConfig(boolean pHttps) testLaunchContainer(null, getDockerClientConfigFile()); } + @Test + public void testLaunchContainerWithBigDockerClientConfig() throws Exception { + initHttps(false); + int maxSize = YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES; + int invalidSize = maxSize + 1; + + File file = new File(tmpPath, "docker-client-config-big"); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { + bw.write("x".repeat(invalidSize)); + } + RuntimeException e = assertThrows(RuntimeException.class, + () -> testLaunchContainer(null, file)); + assertNotNull(e.getCause(), "Wrapped IOException cause expected"); + assertThat(e.getCause().getMessage()) + .contains("(" + invalidSize + " bytes) is larger than maximum allowed size (" + + maxSize + " bytes)"); + } + + @Test + public void testLaunchContainerWithDockerClientConfigAtMaxSize() throws Exception { + initHttps(false); + int maxSize = YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES; + + // Build a valid Docker client config JSON of exactly maxSize bytes by + // padding whitespace after the opening brace (JSON ignores whitespace + // between tokens). This verifies the boundary is accepted by the size + // gate and that the resulting content still parses as JSON downstream. + String base = TestDockerClientConfigHandler.JSON; + int padLen = maxSize - base.length(); + assertTrue(padLen >= 0, + "Base JSON (" + base.length() + " bytes) must fit within maxSize (" + + maxSize + " bytes) for this test to be meaningful"); + String content = "{" + " ".repeat(padLen) + base.substring(1); + assertEquals(maxSize, content.getBytes(StandardCharsets.UTF_8).length, + "Padded JSON must be exactly maxSize bytes"); + + File file = new File(tmpPath, "docker-client-config-boundary"); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { + bw.write(content); + } + // Must NOT throw the size-gate error. Container launch should proceed + // through the normal path with this boundary-sized valid config. + testLaunchContainer(null, file); + } + + @Test + public void testLaunchContainerWithInvalidMaxSizeFallsBackToDefault() + throws Exception { + initHttps(false); + + conf.setInt(YarnConfiguration.NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES, -1); + int defaultMax = YarnConfiguration.DEFAULT_NM_DOCKER_CLIENT_CONFIG_MAX_SIZE_BYTES; + int invalidSize = defaultMax + 1; + + File file = new File(tmpPath, "docker-client-neg-max"); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { + bw.write("x".repeat(invalidSize)); + } + RuntimeException e = assertThrows(RuntimeException.class, + () -> testLaunchContainer(null, file)); + assertNotNull(e.getCause(), "Wrapped IOException cause expected"); + assertThat(e.getCause().getMessage()) + .contains("(" + invalidSize + " bytes) is larger than maximum allowed size (" + + defaultMax + " bytes)"); + } + public void testLaunchContainer(ByteBuffer tokens, File dockerConfigFile) throws ContainerExecutionException, PrivilegedOperationException, IOException {