| // Copyright 2022 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.sandbox; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Suppliers; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.flogger.GoogleLogger; |
| import com.google.common.io.Files; |
| import com.google.devtools.build.lib.util.OS; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.List; |
| import java.util.function.Supplier; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import javax.annotation.Nullable; |
| |
| /** This class manages cgroups directories for memory-limiting sandboxed processes. */ |
| public abstract class CgroupsInfo implements Cgroup { |
| |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); |
| |
| /** |
| * A regexp that matches cgroups entries in {@code /proc/mounts}. |
| * |
| * <p>Group 1 is empty (cgroups v1) or '2' (cgroups v2) Group 2 is the mount point. Group 3 is the |
| * options, which for v1 includes which hierarchies are mounted here. |
| */ |
| private static final Pattern CGROUPS_MOUNT_PATTERN = |
| Pattern.compile("^cgroup(|2)\\s+(\\S*)\\s+cgroup2?\\s+(\\S*).*"); |
| |
| private static final String PROC_SELF_MOUNTS_PATH = "/proc/self/mounts"; |
| private static final String PROC_SELF_CGROUP_PATH = "/proc/self/cgroup"; |
| |
| private static final CgroupsInfo rootCgroup = getRootCgroup(new File(PROC_SELF_MOUNTS_PATH)); |
| |
| private static final Supplier<CgroupsInfo> blazeSpawnsCgroupSupplier = |
| Suppliers.memoize(CgroupsInfo::createBlazeSpawnsCgroup); |
| |
| /** Returns whether the local machine supports cgroups. */ |
| public static boolean isSupported() { |
| return OS.getCurrent() == OS.LINUX && getBlazeSpawnsCgroup().canWrite(); |
| } |
| |
| /** |
| * Returns an instance of the root cgroup of the hierarchy, {@link InvalidCgroupsInfo} if invalid. |
| * |
| * <p>For v1, we only care about the memory hierarchy. |
| * |
| * @param procMountsFile the /proc/self/mounts file. |
| */ |
| @VisibleForTesting |
| static CgroupsInfo getRootCgroup(File procMountsFile) { |
| if (OS.getCurrent() != OS.LINUX) { |
| return new InvalidCgroupsInfo( |
| Type.ROOT, /* version= */ null, "Croups is not supported on non-linux environments."); |
| } |
| |
| List<String> procMountsContents; |
| try { |
| procMountsContents = Files.readLines(procMountsFile, UTF_8); |
| } catch (IOException e) { |
| return new InvalidCgroupsInfo(Type.ROOT, /* version= */ null, e); |
| } |
| File v1RootDir = null; |
| File v2RootDir = null; |
| for (String s : procMountsContents) { |
| Matcher m = CGROUPS_MOUNT_PATTERN.matcher(s); |
| if (m.matches()) { |
| if (m.group(1).isEmpty()) { |
| // v1 |
| if (m.group(3).contains("memory")) { |
| // For now, we only care about the memory cgroup |
| v1RootDir = new File(m.group(2)); |
| } |
| } else { |
| v2RootDir = new File(m.group(2)); |
| } |
| } |
| } |
| // If we found the memory controller in v1, we use that, just in case we have a hybrid system |
| // where some controllers are v1 and some are v2. It would be harder to detect if v2 has the |
| // memory controller |
| if (v1RootDir != null) { |
| return new CgroupsInfoV1(Type.ROOT, v1RootDir); |
| } |
| if (v2RootDir != null) { |
| return new CgroupsInfoV2(Type.ROOT, v2RootDir); |
| } |
| return new InvalidCgroupsInfo( |
| Type.ROOT, |
| /* version= */ null, |
| String.format( |
| "No cgroups mounted in %s: %s", procMountsFile.getPath(), procMountsContents)); |
| } |
| |
| /** |
| * Returns the singleton {@link Type.BLAZE_SPAWNS} cgroup created under the root cgroup, {@link |
| * InvalidCgroupsInfo} if invalid. |
| */ |
| public static CgroupsInfo getBlazeSpawnsCgroup() { |
| return blazeSpawnsCgroupSupplier.get(); |
| } |
| |
| private static CgroupsInfo createBlazeSpawnsCgroup() { |
| if (!rootCgroup.exists()) { |
| return new InvalidCgroupsInfo( |
| Type.BLAZE_SPAWNS, rootCgroup.getVersion(), "Root cgroup does not exist."); |
| } |
| return rootCgroup.createBlazeSpawnsCgroup(PROC_SELF_CGROUP_PATH); |
| } |
| |
| /** |
| * Creates a cgroups directory for Blaze to place spawns in. |
| * |
| * <p>This cgroups directory is created at most once per Blaze instance. |
| * |
| * @param procSelfCgroupPath path to the <code>/proc/self/cgroup</code> file |
| * @return A CgroupsInfo object representing the created cgroup that Blaze can use for |
| * sub-processes (the Blaze process itself is not moved into this directory). If unable to |
| * create, returns an {@link InvalidCgroupsInfo} containing the exception. |
| */ |
| public abstract CgroupsInfo createBlazeSpawnsCgroup(String procSelfCgroupPath); |
| |
| /** The version of Cgroups that is currently being used. */ |
| public enum Version { |
| V1, |
| V2, |
| } |
| |
| @Nullable protected Version version; |
| |
| /** |
| * The types of cgroups relevant to Blaze: |
| * |
| * <ul> |
| * <li>ROOT: corresponds to the root cgroup where * the hierarchy is mounted at; one of |
| * "/dev/cgroup/{controller}" or "/sys/fs/cgroup". |
| * <li>BLAZE_SPAWNS: corresponds the overarching cgroup that contains children {@link |
| * Type.SPAWN} cgroups. |
| * <li>SPAWN: corresponds to the cgroup for a single spawn - this could be a locally executed |
| * action or a worker process. |
| * </ul> |
| */ |
| public enum Type { |
| ROOT, |
| BLAZE_SPAWNS, |
| SPAWN, |
| } |
| |
| protected Type type; |
| |
| /** |
| * This is the directory where the cgroup is in, any related files pertaining to limits / resource |
| * usage or child cgroups (nested directories) are found here. |
| */ |
| @Nullable protected final File cgroupDir; |
| |
| public CgroupsInfo(Type type, @Nullable Version version, @Nullable File cgroupDir) { |
| this.version = version; |
| this.type = type; |
| this.cgroupDir = cgroupDir; |
| // Valid. |
| if (exists()) { |
| logger.atInfo().log( |
| "Successfully found / created %s (%s) cgroup at %s", version, type, cgroupDir.getPath()); |
| } |
| } |
| |
| /** Returns whether the cgroup at {@code cgroupDir} exists. */ |
| @Override |
| public boolean exists() { |
| return cgroupDir != null && cgroupDir.exists() && cgroupDir.isDirectory(); |
| } |
| |
| /** Returns whether Blaze can write to the current cgroup at {@code cgroupDir}. */ |
| public boolean canWrite() { |
| return exists() && cgroupDir.canWrite(); |
| } |
| |
| /** A cgroups directory for this Blaze instance to put sandboxes in. */ |
| public File getCgroupDir() { |
| return cgroupDir; |
| } |
| |
| @Override |
| public ImmutableSet<Path> paths() { |
| return ImmutableSet.of(getCgroupDir().toPath()); |
| } |
| |
| @Nullable |
| public Version getVersion() { |
| return version; |
| } |
| |
| public Type getType() { |
| return type; |
| } |
| |
| @Override |
| public int getMemoryUsageInKb() { |
| return 0; |
| } |
| |
| public int getMemoryUsageInKbFromFile(String filename) { |
| try { |
| String val = Files.readLines(new File(cgroupDir, filename), UTF_8).get(0); |
| return (int) (Long.parseLong(val) / 1024); |
| } catch (IOException e) { |
| return 0; |
| } |
| } |
| |
| @Override |
| public void addProcess(long pid) throws IOException { |
| Files.asCharSink(new File(cgroupDir, "cgroup.procs"), UTF_8).write(Long.toString(pid)); |
| } |
| |
| @Override |
| public void destroy() { |
| getCgroupDir().delete(); |
| } |
| |
| /** |
| * Creates a cgroups directory for individual spawns (local / workers). |
| * |
| * <p>Has to be called from a {@link Type.BLAZE_SPAWNS} cgroup. |
| * |
| * @param dirName the directory name of the spawn's cgroup. |
| * @param memoryLimitMb memory limit in Mb to set on the cgroup. If 0, no limit is set. |
| * @return an instance of the spawn's cgroup; if unable to create, returns an {@link |
| * InvalidCgroupsInfo} containing the exception. |
| */ |
| public abstract CgroupsInfo createIndividualSpawnCgroup(String dirName, int memoryLimitMb); |
| |
| /** |
| * Represents an invalid cgroup so that we can distinguish between whether a cgroup was not meant |
| * to be created (null) or if it was attempted but failed. |
| */ |
| public static class InvalidCgroupsInfo extends CgroupsInfo { |
| |
| private final Exception exception; |
| |
| public InvalidCgroupsInfo(Type type, @Nullable Version version, String errorMessage) { |
| super(type, version, null); |
| this.exception = new IllegalStateException(errorMessage); |
| logger.atInfo().withCause(exception).log("Unable to create cgroup."); |
| } |
| |
| public InvalidCgroupsInfo(Type type, @Nullable Version version, Exception exception) { |
| super(type, version, null); |
| logger.atInfo().withCause(exception).log("Unable to create cgroup."); |
| this.exception = exception; |
| } |
| |
| @Override |
| public boolean exists() { |
| return false; |
| } |
| |
| @Override |
| public boolean canWrite() { |
| return false; |
| } |
| |
| public Exception getException() { |
| return exception; |
| } |
| |
| @Override |
| public CgroupsInfo createBlazeSpawnsCgroup(String procSelfCgroupPath) { |
| return new InvalidCgroupsInfo( |
| Type.BLAZE_SPAWNS, |
| getVersion(), |
| "Unable to create BLAZE_SPAWNS cgroup from an invalid cgroup."); |
| } |
| |
| @Override |
| public CgroupsInfo createIndividualSpawnCgroup(String dirName, int memoryLimitMb) { |
| return new InvalidCgroupsInfo( |
| Type.SPAWN, getVersion(), "Unable to create SPAWN cgroup from an invalid cgroup."); |
| } |
| } |
| } |