diff --git a/README.md b/README.md index 6dfd73f..174d400 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ Linux implementation where the measures are actually energy counters for the who charge of computing the process attribution. Only Linux/amd64 and macOS (amd64/apple silicon) are supported at the moment. Of note, this tool needs to be run -via `sudo` because power consumption information is considered as security sensitive (as it can +privileged code either via running it with `sudo` (preferred) or giving it access to the read the resources it needs +because power consumption information is considered as security sensitive (as it can enable [side-channel attacks](https://en.wikipedia.org/wiki/Side-channel_attack)) See below for platform-specific information. @@ -45,8 +46,16 @@ information. Power monitoring is performed using the bundled `[powermetrics](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW10)` -tool, which is run with specific parameters and which -output is then parsed into a usable representation. +tool, which is run with specific parameters and which output is then parsed into a usable representation. +There are several options to give access to the tool: + +- Add the user running the server to the list of `sudoers` with no-password access to `/usr/bin/powermetrics` +- Run with `sudo` (though this is impractical during development) +- Provide a secret to be able to run the `powermetrics` process (and only this process) using `sudo`. The server will + look for the secret under the `power-server.sudo.secret` property key. Please look + at https://quarkus.io/guides/config-secrets for more details on how to do this securely with Quarkus. Note that this + option is only provided to facilitate development, notably dev mode, and will only work when re-building the project + with the appropriate dependencies. ### Linux diff --git a/server/src/main/java/net/laprun/sustainability/power/Security.java b/server/src/main/java/net/laprun/sustainability/power/Security.java new file mode 100644 index 0000000..46af7e3 --- /dev/null +++ b/server/src/main/java/net/laprun/sustainability/power/Security.java @@ -0,0 +1,84 @@ +package net.laprun.sustainability.power; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.List; +import java.util.prefs.Preferences; + +import jakarta.inject.Singleton; + +import io.smallrye.config.SmallRyeConfig; + +@Singleton +public class Security { + private final static boolean isRoot; + public static final String SECRET_PROPERTY_KEY = "power-server.sudo.secret"; + + static { + isRoot = isRunningAsAdministrator(); + } + + private final String pwd; + + public Security(SmallRyeConfig config) { + pwd = config.getConfigValue(SECRET_PROPERTY_KEY).getValue(); + if (pwd == null && !isRoot) { + throw new IllegalStateException( + "This application requires sudo access. Either provide a sudo secret using the 'power-server.sudo.secret' property or run using sudo."); + } + } + + // figure out if we're running as admin by trying to write a system-level preference + // see: https://stackoverflow.com/a/23538961/5752008 + private synchronized static boolean isRunningAsAdministrator() { + final var preferences = Preferences.systemRoot(); + + // avoid outputting errors + System.setErr(new PrintStream(new OutputStream() { + @Override + public void write(int b) { + } + })); + + try { + preferences.put("foo", "bar"); // SecurityException on Windows + preferences.remove("foo"); + preferences.flush(); // BackingStoreException on Linux and macOS + return true; + } catch (Exception exception) { + return false; + } finally { + System.setErr(System.err); + } + + } + + public Process execPowermetrics(String... options) throws IOException { + if (options == null || options.length == 0) { + throw new IllegalArgumentException("No powermetrics options specified"); + } + final var args = new String[options.length + 2]; + args[0] = "powermetrics"; + args[1] = "--samplers"; + System.arraycopy(options, 0, args, 2, options.length); + return sudo(args); + } + + public Process sudo(String... cmd) throws IOException { + if (cmd == null || cmd.length == 0) { + throw new IllegalArgumentException("No command specified to run with sudo"); + } + + if (!isRoot) { + final var args = new String[cmd.length + 2]; + args[0] = "sudo"; + args[1] = "-S"; + System.arraycopy(cmd, 0, args, 2, cmd.length); + final var runWithSudo = new ProcessBuilder(args); + return ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast(); + } else { + return new ProcessBuilder().command(cmd).start(); + } + } +} diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java b/server/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java index 820a204..fabd0d7 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java @@ -1,8 +1,10 @@ package net.laprun.sustainability.power.sensors; import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; import jakarta.inject.Singleton; +import net.laprun.sustainability.power.Security; import net.laprun.sustainability.power.sensors.linux.rapl.IntelRAPLSensor; import net.laprun.sustainability.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor; @@ -10,14 +12,17 @@ public class PowerSensorProducer { private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); + @Inject + Security security; + @Produces public PowerSensor sensor() { - return determinePowerSensor(); + return determinePowerSensor(security); } - public static PowerSensor determinePowerSensor() { + public static PowerSensor determinePowerSensor(Security security) { if (OS_NAME.contains("mac os x")) { - return new ProcessMacOSPowermetricsSensor(); + return new ProcessMacOSPowermetricsSensor(security); } if (!OS_NAME.contains("linux")) { diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java index 06e0807..8ebb651 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java @@ -2,15 +2,18 @@ import java.io.InputStream; +import net.laprun.sustainability.power.Security; + public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor { + private final Security security; private Process powermetrics; - public ProcessMacOSPowermetricsSensor() { + public ProcessMacOSPowermetricsSensor(Security security) { + this.security = security; + // extract metadata try { - final var exec = new ProcessBuilder() - .command("powermetrics", "--samplers", "cpu_power", "-i", "10", "-n", "1") - .start(); + final var exec = security.execPowermetrics("cpu_power", "-i", "10", "-n", "1"); // if the process is already dead, get the error if (!exec.isAlive()) { @@ -35,8 +38,8 @@ public void start(long frequency) throws Exception { if (!isStarted()) { // it takes some time for the external process in addition to the sampling time so adjust the sampling frequency to account for this so that at most one measure occurs during the sampling time window final var freq = Long.toString(frequency - 50); - powermetrics = new ProcessBuilder().command("powermetrics", "--samplers", "cpu_power,tasks", - "--show-process-samp-norm", "--show-process-gpu", "-i", freq).start(); + powermetrics = security.execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i", + freq); } }