summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java5
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java3
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java8
-rw-r--r--jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java34
-rw-r--r--jrt/src/com/yahoo/jrt/TlsCryptoSocket.java19
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java8
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java60
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java13
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java23
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java13
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java51
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java21
-rw-r--r--security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java62
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java2
-rw-r--r--vespamalloc/CMakeLists.txt1
-rwxr-xr-xvespamalloc/bin/parsememorydump.pl314
16 files changed, 233 insertions, 404 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
index f5dbcb6a699..531a815922b 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
@@ -37,7 +37,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.logging.Level;
-import java.util.stream.Stream;
/**
* REST API for issuing and refreshing node certificates in a hosted Vespa system.
@@ -177,9 +176,7 @@ public class CertificateAuthorityApiHandler extends ThreadedHttpRequestHandler {
private AthenzService getRequestAthenzService(HttpRequest request) {
return getRequestCertificateChain(request).stream()
.findFirst()
- .map(X509CertificateUtils::getSubjectCommonNames)
- .map(List::stream)
- .flatMap(Stream::findFirst)
+ .flatMap(X509CertificateUtils::getSubjectCommonName)
.map(AthenzService::new)
.orElseThrow(() -> new RuntimeException("No certificate found"));
}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java
index bffed6eb0b1..2ab959fcaa0 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java
@@ -18,6 +18,7 @@ import com.yahoo.security.KeyAlgorithm;
import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
+import com.yahoo.security.tls.CapabilityMode;
import com.yahoo.security.tls.CapabilitySet;
import com.yahoo.security.tls.ConnectionAuthContext;
import com.yahoo.slime.Cursor;
@@ -250,7 +251,7 @@ public class MultiTenantRpcAuthorizerTest {
private static Request mockJrtRpcRequest(String payload) {
ConnectionAuthContext authContext =
- new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.all(), Set.of());
+ new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.all(), Set.of(), CapabilityMode.ENFORCE);
Target target = mock(Target.class);
when(target.connectionAuthContext()).thenReturn(authContext);
Request request = mock(Request.class);
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
index 0a8733fb124..c6238149be1 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -471,6 +471,14 @@ public class Flags {
"Takes effect on next tick",
HOSTNAME);
+ public static final UnboundStringFlag TLS_CAPABILITIES_ENFORCEMENT_MODE = defineStringFlag(
+ "tls-capabilities-enforcement-mode", "disable",
+ List.of("bjorncs", "vekterli"), "2022-07-21", "2024-01-01",
+ "Configure Vespa TLS capability enforcement mode",
+ "Takes effect on restart of Docker container",
+ APPLICATION_ID,HOSTNAME,NODE_TYPE,TENANT_ID,VESPA_VERSION
+ );
+
/** WARNING: public for testing: All flags should be defined in {@link Flags}. */
public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners,
String createdAt, String expiresAt, String description,
diff --git a/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java
index bb2eafcf711..9bb497e96ed 100644
--- a/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java
+++ b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java
@@ -2,24 +2,14 @@
package com.yahoo.jrt;
import com.yahoo.security.tls.Capability;
-import com.yahoo.security.tls.CapabilityMode;
import com.yahoo.security.tls.CapabilitySet;
-import com.yahoo.security.tls.ConnectionAuthContext;
-import com.yahoo.security.tls.TransportSecurityUtils;
-
-import java.util.logging.Logger;
-
-import static com.yahoo.security.tls.CapabilityMode.DISABLE;
-import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY;
+import com.yahoo.security.tls.MissingCapabilitiesException;
/**
* @author bjorncs
*/
public class RequireCapabilitiesFilter implements RequestAccessFilter {
- private static final Logger log = Logger.getLogger(RequireCapabilitiesFilter.class.getName());
- private static final CapabilityMode MODE = TransportSecurityUtils.getCapabilityMode();
-
private final CapabilitySet requiredCapabilities;
public RequireCapabilitiesFilter(CapabilitySet requiredCapabilities) {
@@ -32,23 +22,13 @@ public class RequireCapabilitiesFilter implements RequestAccessFilter {
@Override
public boolean allow(Request r) {
- if (MODE == DISABLE) return true;
- ConnectionAuthContext ctx = r.target().connectionAuthContext();
- CapabilitySet peerCapabilities = ctx.capabilities();
- boolean authorized = peerCapabilities.has(requiredCapabilities);
- if (!authorized) {
- String msg = "%sPermission denied for RPC method '%s'. Peer at %s with %s. Call requires %s, but peer has %s"
- .formatted(MODE == LOG_ONLY ? "Dry-run: " : "", r.methodName(), r.target().peerSpec(), ctx.peerCertificateString().orElseThrow(),
- requiredCapabilities.toNames(), peerCapabilities.toNames());
- if (MODE == LOG_ONLY) {
- log.info(msg);
- return true;
- } else {
- log.warning(msg);
- return false;
- }
+ try {
+ r.target().connectionAuthContext()
+ .verifyCapabilities(requiredCapabilities, "RPC", r.methodName(), r.target().peerSpec().toString());
+ return true;
+ } catch (MissingCapabilitiesException e) {
+ return false;
}
- return true;
}
}
diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java
index 13274dc3ba5..d83c1ee8baa 100644
--- a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java
+++ b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java
@@ -2,7 +2,8 @@
package com.yahoo.jrt;
import com.yahoo.security.tls.ConnectionAuthContext;
-import com.yahoo.security.tls.PeerAuthorizerTrustManager;
+import com.yahoo.security.tls.PeerAuthorizationFailedException;
+import com.yahoo.security.tls.TransportSecurityUtils;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
@@ -97,15 +98,6 @@ public class TlsCryptoSocket implements CryptoSocket {
channelRead();
break;
case NEED_WORK:
- if (authContext == null) {
- PeerAuthorizerTrustManager.getConnectionAuthContext(sslEngine) // only available during handshake
- .ifPresent(ctx -> {
- if (!ctx.authorized()) {
- metrics.incrementPeerAuthorizationFailures();
- }
- authContext = ctx;
- });
- }
break;
case COMPLETED:
return HandshakeState.COMPLETED;
@@ -122,6 +114,10 @@ public class TlsCryptoSocket implements CryptoSocket {
SSLSession session = sslEngine.getSession();
sessionApplicationBufferSize = session.getApplicationBufferSize();
sessionPacketBufferSize = session.getPacketBufferSize();
+ authContext = TransportSecurityUtils.getConnectionAuthContext(session).orElseThrow();
+ if (!authContext.authorized()) {
+ metrics.incrementPeerAuthorizationFailures();
+ }
log.fine(() -> String.format("Handshake complete: protocol=%s, cipherSuite=%s", session.getProtocol(), session.getCipherSuite()));
if (sslEngine.getUseClientMode()) {
metrics.incrementClientTlsConnectionsEstablished();
@@ -143,8 +139,7 @@ public class TlsCryptoSocket implements CryptoSocket {
}
}
} catch (SSLHandshakeException e) {
- // sslEngine.getDelegatedTask().run() and handshakeWrap() may throw SSLHandshakeException, potentially handshakeUnwrap() and sslEngine.beginHandshake() as well.
- if (authContext == null || authContext.authorized()) { // don't include handshake failures due from PeerAuthorizerTrustManager
+ if (!(e.getCause() instanceof PeerAuthorizationFailedException)) {
metrics.incrementTlsCertificateVerificationFailures();
}
throw e;
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java
index ec402719efa..7e6c7f394cd 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java
@@ -30,15 +30,17 @@ public class CapabilitySet {
;
private final String name;
- private final EnumSet<Capability> caps;
+ private final CapabilitySet set;
Predefined(String name, Capability... caps) {
this.name = name;
- this.caps = caps.length == 0 ? EnumSet.noneOf(Capability.class) : EnumSet.copyOf(List.of(caps)); }
+ this.set = caps.length == 0 ? CapabilitySet.none() : CapabilitySet.from(caps); }
public static Optional<Predefined> fromName(String name) {
return Arrays.stream(values()).filter(p -> p.name.equals(name)).findAny();
}
+
+ public CapabilitySet capabilities() { return set; }
}
private static final CapabilitySet ALL_CAPABILITIES = new CapabilitySet(EnumSet.allOf(Capability.class));
@@ -52,7 +54,7 @@ public class CapabilitySet {
EnumSet<Capability> caps = EnumSet.noneOf(Capability.class);
for (String name : names) {
Predefined predefined = Predefined.fromName(name).orElse(null);
- if (predefined != null) caps.addAll(predefined.caps);
+ if (predefined != null) caps.addAll(predefined.set.caps);
else caps.add(Capability.fromName(name));
}
return new CapabilitySet(caps);
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java
index b4e8878fb01..f231e8429ce 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java
@@ -7,28 +7,78 @@ import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.logging.Logger;
import static com.yahoo.security.SubjectAlternativeName.Type.DNS;
import static com.yahoo.security.SubjectAlternativeName.Type.URI;
+import static com.yahoo.security.tls.CapabilityMode.DISABLE;
+import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY;
/**
* @author bjorncs
*/
public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain,
CapabilitySet capabilities,
- Set<String> matchedPolicies) {
+ Set<String> matchedPolicies,
+ CapabilityMode capabilityMode) {
- private static final ConnectionAuthContext DEFAULT_ALL_CAPABILITIES = new ConnectionAuthContext(List.of());
+ private static final Logger log = Logger.getLogger(ConnectionAuthContext.class.getName());
public ConnectionAuthContext {
peerCertificateChain = List.copyOf(peerCertificateChain);
matchedPolicies = Set.copyOf(matchedPolicies);
}
- private ConnectionAuthContext(List<X509Certificate> certs) { this(certs, CapabilitySet.all(), Set.of()); }
+ private ConnectionAuthContext(List<X509Certificate> certs, CapabilityMode capabilityMode) {
+ this(certs, CapabilitySet.all(), Set.of(), capabilityMode);
+ }
public boolean authorized() { return !capabilities.hasNone(); }
+ /** Throws checked exception to force caller to handle verification failed. */
+ public void verifyCapabilities(CapabilitySet requiredCapabilities) throws MissingCapabilitiesException {
+ verifyCapabilities(requiredCapabilities, null, null, null);
+ }
+
+ /**
+ * Throws checked exception to force caller to handle verification failed.
+ * Provided strings are used for improved logging only
+ * */
+ public void verifyCapabilities(CapabilitySet requiredCapabilities, String action, String resource, String peer)
+ throws MissingCapabilitiesException {
+ if (capabilityMode == DISABLE) return;
+ boolean hasCapabilities = capabilities.has(requiredCapabilities);
+ if (!hasCapabilities) {
+ String msg = createPermissionDeniedErrorMessage(requiredCapabilities, action, resource, peer);
+ if (capabilityMode == LOG_ONLY) {
+ log.info(msg);
+ } else {
+ // Ideally log as warning, but we have no mechanism for de-duplicating repeated log spamming.
+ log.fine(msg);
+ throw new MissingCapabilitiesException(msg);
+ }
+ }
+ }
+
+ String createPermissionDeniedErrorMessage(
+ CapabilitySet required, String action, String resource, String peer) {
+ StringBuilder b = new StringBuilder();
+ if (capabilityMode == LOG_ONLY) b.append("Dry-run: ");
+ b.append("Permission denied");
+ if (resource != null) {
+ b.append(" for '");
+ if (action != null) {
+ b.append(action).append("' on '");
+ }
+ b.append(resource).append("'");
+ }
+ b.append(". Peer ");
+ if (peer != null) b.append("'").append(peer).append("' ");
+ return b.append("with ").append(peerCertificateString().orElse("<missing-certificate>")).append(". Requires capabilities ")
+ .append(required.toNames()).append(" but peer has ").append(capabilities.toNames())
+ .append(".").toString();
+ }
+
public Optional<X509Certificate> peerCertificate() {
return peerCertificateChain.isEmpty() ? Optional.empty() : Optional.of(peerCertificateChain.get(0));
}
@@ -62,11 +112,11 @@ public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain,
}
/** Construct instance with all capabilities */
- public static ConnectionAuthContext defaultAllCapabilities() { return DEFAULT_ALL_CAPABILITIES; }
+ public static ConnectionAuthContext defaultAllCapabilities() { return new ConnectionAuthContext(List.of(), DISABLE); }
/** Construct instance with all capabilities */
public static ConnectionAuthContext defaultAllCapabilities(List<X509Certificate> certs) {
- return new ConnectionAuthContext(certs);
+ return new ConnectionAuthContext(certs, DISABLE);
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java
new file mode 100644
index 00000000000..1c3ad9444e4
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java
@@ -0,0 +1,13 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+/**
+ * Intentionally checked to force caller to handle missing permissions at call site.
+ *
+ * @author bjorncs
+ */
+public class MissingCapabilitiesException extends Exception {
+
+ public MissingCapabilitiesException(String message) { super(message); }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java
new file mode 100644
index 00000000000..02dbf3bb8e7
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java
@@ -0,0 +1,23 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * @author bjorncs
+ */
+public class PeerAuthorizationFailedException extends CertificateException {
+ private final List<X509Certificate> certChain;
+
+ public PeerAuthorizationFailedException(String msg, List<X509Certificate> certChain) {
+ super(msg);
+ this.certChain = certChain;
+ }
+
+ public PeerAuthorizationFailedException(String msg) { this(msg, List.of()); }
+
+ public List<X509Certificate> peerCertificateChain() { return certChain; }
+}
+
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java
index 5db86fd93bc..951b5c57c9e 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java
@@ -7,7 +7,6 @@ import com.yahoo.security.X509CertificateUtils;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
@@ -39,7 +38,7 @@ public class PeerAuthorizer {
X509Certificate cert = certChain.get(0);
Set<String> matchedPolicies = new HashSet<>();
Set<CapabilitySet> grantedCapabilities = new HashSet<>();
- String cn = getCommonName(cert).orElse(null);
+ String cn = X509CertificateUtils.getSubjectCommonName(cert).orElse(null);
List<String> sans = getSubjectAlternativeNames(cert);
log.fine(() -> String.format("Subject info from x509 certificate: CN=[%s], 'SAN=%s", cn, sans));
for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) {
@@ -48,7 +47,10 @@ public class PeerAuthorizer {
grantedCapabilities.add(peerPolicy.capabilities());
}
}
- return new ConnectionAuthContext(certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies);
+ // TODO Pass this through constructor
+ CapabilityMode capabilityMode = TransportSecurityUtils.getCapabilityMode();
+ return new ConnectionAuthContext(
+ certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies, capabilityMode);
}
private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List<String> sans) {
@@ -69,11 +71,6 @@ public class PeerAuthorizer {
}
}
- private static Optional<String> getCommonName(X509Certificate peerCertificate) {
- return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream()
- .findFirst();
- }
-
private static List<String> getSubjectAlternativeNames(X509Certificate peerCertificate) {
return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream()
.filter(san -> san.getType() == DNS || san.getType() == IP || san.getType() == URI)
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java
index b92cd6c9538..c3dcdf1dc9e 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java
@@ -6,6 +6,7 @@ import com.yahoo.security.X509CertificateUtils;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.X509ExtendedTrustManager;
import java.net.Socket;
@@ -13,18 +14,20 @@ import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
-import java.util.Optional;
+import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A {@link X509ExtendedTrustManager} that performs additional certificate verification through {@link PeerAuthorizer}.
*
+ * Implementation assumes that provided {@link X509ExtendedTrustManager} will throw {@link IllegalArgumentException}
+ * when chain is empty or null.
+ *
* @author bjorncs
*/
-// Note: Implementation assumes that provided X509ExtendedTrustManager will throw IllegalArgumentException when chain is empty or null
-public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
+class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
- public static final String HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx";
+ static final String AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx";
private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName());
@@ -33,20 +36,16 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
private final AuthorizationMode mode;
private final HostnameVerification hostnameVerification;
- public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers,
- AuthorizationMode mode,
- HostnameVerification hostnameVerification,
- X509ExtendedTrustManager defaultTrustManager) {
+ PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode,
+ HostnameVerification hostnameVerification, X509ExtendedTrustManager defaultTrustManager) {
this.authorizer = new PeerAuthorizer(authorizedPeers);
this.mode = mode;
this.hostnameVerification = hostnameVerification;
this.defaultTrustManager = defaultTrustManager;
}
- public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers,
- AuthorizationMode mode,
- HostnameVerification hostnameVerification,
- KeyStore truststore) {
+ PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode,
+ HostnameVerification hostnameVerification, KeyStore truststore) {
this(authorizedPeers, mode, hostnameVerification, TrustManagerUtils.createDefaultX509TrustManager(truststore));
}
@@ -65,27 +64,27 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
defaultTrustManager.checkClientTrusted(chain, authType, socket);
- authorizePeer(chain, authType, true, null);
+ authorizePeer(chain, authType, true, ((SSLSocket)socket).getHandshakeSession());
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
overrideHostnameVerificationForClient(socket);
defaultTrustManager.checkServerTrusted(chain, authType, socket);
- authorizePeer(chain, authType, false, null);
+ authorizePeer(chain, authType, false, ((SSLSocket)socket).getHandshakeSession());
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
defaultTrustManager.checkClientTrusted(chain, authType, sslEngine);
- authorizePeer(chain, authType, true, sslEngine);
+ authorizePeer(chain, authType, true, sslEngine.getHandshakeSession());
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
overrideHostnameVerificationForClient(sslEngine);
defaultTrustManager.checkServerTrusted(chain, authType, sslEngine);
- authorizePeer(chain, authType, false, sslEngine);
+ authorizePeer(chain, authType, false, sslEngine.getHandshakeSession());
}
@Override
@@ -93,21 +92,17 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
return defaultTrustManager.getAcceptedIssuers();
}
- /**
- * Note: The authorization result is only available during handshake. The underlying handshake session is removed once handshake is complete.
- */
- public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine sslEngine) {
- return Optional.ofNullable(sslEngine.getHandshakeSession())
- .flatMap(session -> Optional.ofNullable((ConnectionAuthContext) session.getValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY)));
- }
-
- private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient, SSLEngine sslEngine) throws CertificateException {
+ private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient,
+ SSLSession handshakeSessionOrNull) throws PeerAuthorizationFailedException {
log.fine(() -> "Verifying certificate: " + createInfoString(certChain[0], authType, isVerifyingClient));
ConnectionAuthContext result = mode != AuthorizationMode.DISABLE
? authorizer.authorizePeer(List.of(certChain))
: ConnectionAuthContext.defaultAllCapabilities(List.of(certChain));
- if (sslEngine != null) { // getHandshakeSession() will never return null in this context
- sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result);
+ if (handshakeSessionOrNull != null) {
+ handshakeSessionOrNull.putValue(AUTH_CONTEXT_PROPERTY, result);
+ } else {
+ log.log(Level.FINE,
+ () -> "Warning: unable to provide ConnectionAuthContext as no SSLSession is available");
}
if (result.authorized()) {
log.fine(() -> String.format("Verification result: %s", result));
@@ -115,7 +110,7 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager {
String errorMessage = "Authorization failed: " + createInfoString(certChain[0], authType, isVerifyingClient);
log.warning(errorMessage);
if (mode == AuthorizationMode.ENFORCE) {
- throw new CertificateException(errorMessage);
+ throw new PeerAuthorizationFailedException(errorMessage, List.of(certChain));
}
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java
index 21d97613f95..ae6cef65156 100644
--- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java
+++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java
@@ -1,6 +1,9 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.security.tls;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
@@ -87,6 +90,24 @@ public class TransportSecurityUtils {
}
}
+ /**
+ * @return {@link ConnectionAuthContext} instance if {@link SSLEngine} was constructed by a {@link TlsContext}.
+ * Only available after TLS handshake is completed.
+ */
+ public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSession s) {
+ return Optional.ofNullable((ConnectionAuthContext) s.getValue(PeerAuthorizerTrustManager.AUTH_CONTEXT_PROPERTY));
+ }
+
+ /** @see #getConnectionAuthContext(SSLSession) */
+ public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine e) {
+ return getConnectionAuthContext(e.getSession());
+ }
+
+ /** @see #getConnectionAuthContext(SSLSession) */
+ public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSocket s) {
+ return getConnectionAuthContext(s.getSession());
+ }
+
private static Optional<String> getEnvironmentVariable(Map<String, String> environmentVariables, String variableName) {
return Optional.ofNullable(environmentVariables.get(variableName))
.filter(var -> !var.isEmpty());
diff --git a/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java b/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java
new file mode 100644
index 00000000000..c30a812a30d
--- /dev/null
+++ b/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java
@@ -0,0 +1,62 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security.tls;
+
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.X509CertificateBuilder;
+import org.junit.jupiter.api.Test;
+
+import javax.security.auth.x500.X500Principal;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Set;
+
+import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author bjorncs
+ */
+class ConnectionAuthContextTest {
+
+ @Test
+ void fails_on_missing_capabilities() {
+ ConnectionAuthContext ctx = createConnectionAuthContext();
+ assertThrows(MissingCapabilitiesException.class,
+ () -> ctx.verifyCapabilities(CapabilitySet.from(Capability.CONTENT__STATUS_PAGES)));
+ }
+
+ @Test
+ void creates_correct_error_message() {
+ ConnectionAuthContext ctx = createConnectionAuthContext();
+ CapabilitySet requiredCaps = CapabilitySet.from(Capability.CONTENT__STATUS_PAGES);
+ String expectedMessage = """
+ Permission denied for 'myaction' on 'myresource'. Peer 'mypeer' with [CN='myidentity'].
+ Requires capabilities [vespa.content.status_pages] but peer has
+ [vespa.content.document_api, vespa.content.search_api, vespa.slobrok.api].
+ """;
+ String actualMessage = ctx.createPermissionDeniedErrorMessage(requiredCaps, "myaction", "myresource", "mypeer");
+ assertThat(actualMessage).isEqualToIgnoringWhitespace(expectedMessage);
+ }
+
+ private static ConnectionAuthContext createConnectionAuthContext() {
+ return new ConnectionAuthContext(
+ List.of(createCertificate()), CapabilitySet.Predefined.CONTAINER_NODE.capabilities(), Set.of(),
+ CapabilityMode.ENFORCE);
+ }
+
+ private static X509Certificate createCertificate() {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ return X509CertificateBuilder.fromKeypair(
+ keyPair, new X500Principal("CN=myidentity"), Instant.EPOCH,
+ Instant.EPOCH.plus(100000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE)
+ .build();
+ }
+
+
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java
index 7542e976260..9d47ce79f87 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java
@@ -50,7 +50,7 @@ public class AthenzX509CertificateUtils {
}
public static AthenzRole getRolesFromRoleCertificate(X509Certificate certificate) {
- String commonName = com.yahoo.security.X509CertificateUtils.getSubjectCommonNames(certificate).get(0);
+ String commonName = X509CertificateUtils.getSubjectCommonName(certificate).orElseThrow();
return AthenzRole.fromResourceNameString(commonName);
}
diff --git a/vespamalloc/CMakeLists.txt b/vespamalloc/CMakeLists.txt
index df8e38653bb..af71d8b7d82 100644
--- a/vespamalloc/CMakeLists.txt
+++ b/vespamalloc/CMakeLists.txt
@@ -26,7 +26,6 @@ vespa_define_module(
src/vespamalloc/util
)
-vespa_install_script(bin/parsememorydump.pl vespa-malloc-parse-memorydump.pl bin)
else()
install(DIRECTORY DESTINATION lib64/vespa)
endif()
diff --git a/vespamalloc/bin/parsememorydump.pl b/vespamalloc/bin/parsememorydump.pl
deleted file mode 100755
index 95c70859b9a..00000000000
--- a/vespamalloc/bin/parsememorydump.pl
+++ /dev/null
@@ -1,314 +0,0 @@
-#!/usr/bin/perl -w
-# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-# This is a tool to parse the output given by vespamalloc when dumping all
-# memory usage with stacktraces.
-#
-# This tool will try to group similar stack traces together, identify them,
-# and show a report of how much memory you use for various tasks in the given
-# dump.
-#
-# Suggested usage:
-# - Manually rotate your vespa.log file.
-# - Send signal to vespamalloc to make it dump stack traces.
-# - When dump is done, cat all the log files generated in correct order into
-# one file.
-# - Send this file to STDIN of this script.
-#
-# By doing it like this, it's easy to rerun this script and to update it to
-# work with anything encountered in your report that this script doesn't
-# currently handle.
-#
-# It is for instance likely, that you need to add some more regexes to match
-# unknown stack traces into appropriate groups.
-
-use strict;
-
-# If this variable is set, unrecognized stack traces will be concatenated to
-# one unrecognized chunk.
-my $combineUnrecognizedStacks = 1;
-# If this variable is set, all stack traces are shown, no matter if they are
-# recognized or not.
-my $showRecognizedStacks = 0;
-# If this variable is set, memory usage is grouped on the first word in the
-# group name. Thus, by using a proper name convention, one can distinguish
-# how much memory are used on various parts of the system
-my $sortOnCategories = 1;
-
-# These patterns are used to recognized what memory is used for based on the
-# stacktrace of the allocation. The keys are regular expressions that can match
-# stacktraces. The value is the group name this allocation will be marked as if
-# the trace matches the regex. If matching multiple entries, the first entry
-# found will be the match.
-
-# Note that as I made this tool to analyze VDS memory usage, the stuff added is
-# just stuff I found using memory in that current run. We should probably add
-# more patterns as needed.
-my %patterns = (
-
- 'FRT_MemoryTub::BigAlloc.*mbus::MessageBus::send.*' => 'RPC send messagebus message - big alloc',
- 'FRT_MemoryTub::BigAlloc.*storage::rpc::Destination::sendRequest', => 'RPC send storage API command - big alloc',
- 'FRT_RPCRequestPool::AllocRPCRequest.*storage::rpc::Destination::sendRequest' => 'RPC send storage API command',
-
- 'FNET_DataBuffer::Shrink.*FNET_Connection::HandleWriteEvent' => 'RPC shrinked write buffer',
- 'FNET_DataBuffer::Shrink.*FNET_Connection::HandleReadEvent' => 'RPC shrinked read buffer',
-
- 'FRT_MemoryTub::BigAlloc.*FRT_RPCRequestPacket::Decode.*FNET_Connection::HandleReadEvent' => 'RPC read event - decode - big alloc',
- 'FRT_RPCRequestPool::AllocRPCRequest.*FNET_Connection::HandlePacket.*FNET_Connection::HandleReadEvent' => 'RPC read event - handle packet',
- 'FNET_DataBuffer::Pack.*FNET_Connection::HandleReadEvent' => 'RPC packed read buffer',
- 'FNET_DataBuffer::Pack.*FNET_Connection::HandleWriteEvent' => 'RPC packed write buffer',
- 'FNET_PacketQueue_NoLock::ExpandBuf.*FNET_Connection::HandleWriteEvent()' => 'RPC expand write buffer',
- 'FNET_ChannelPool::AllocChannelCluster.*storage::rpc::Destination::sendRequest' => 'RPC storage API alloc channel cluster',
-
- 'storage::SlotFileBuffer::getAlignedBuffer.*SlotFileImpl::close' => 'Persistence layer - move buffer to cache',
- 'storage::SlotFileBuffer::getAlignedBuffer.*SlotFileBuffer::getBuffer' => 'Persistence layer - Get aligned buffer',
- 'storage::SlotFileBuffer::getInputBuffer' => 'Persistence layer - Input buffer',
- 'storage::SlotFileBuffer::getIndexBuffer' => 'Persistence layer - Index buffer',
- 'storage::SlotFileBuffer::getOutputBuffer' => 'Persistence layer - Output buffer',
-
- 'storage::api::\S*::makeReply.*storage::MessageDispatcher::handleCommand' => 'Messages - Replies stored in message dispatcher',
- 'document::SerializableArray::onDeserialize.*storage::CommunicationManager::onEvent\(std::auto_ptr<storage::rpc::Event>\)'
- => 'Messages - Documents from storage API messages - serializable arrays',
- 'document::SerializableDocumentSharedPointer.*storage::rpc::Handle::onEvent\(std::auto_ptr<storage::rpc::Event>\)'
- => 'Messages - Documents from storage API messages - original byte buffers',
- 'storage::api::StorageMessageAddress::create.*storage::rpc::Handle::onEvent\(std::auto_ptr<storage::rpc::Event>\)'
- => 'Messages - Storage API message addresses',
- 'document::IdString::createIdString.*storage::api::StorageMessage::onDeserialize'
- => 'Messages - Document identifiers',
- 'storage::api::ApplyBucketDiffCommand::Entry::Entry.*storage::FileStorThread::run'
- => 'Messages - Merge apply entries',
- 'storage::api::ApplyBucketDiffCommand::Entry::Entry.*storage::api::StorageMessage::onDeserialize'
- => 'Messages - Merge apply entries',
- => 'Messages - Queued multi operation commands - cloned for local use in message dispatcher',
-
- 'storage::BufHolder::reserve.*storage::FileStorThread::onGetIterCommand' => 'Visiting - Downsized docblocks',
- 'storage::BufHolder::resize.*storage::FileStorThread::onGetIterCommand' => 'Visiting - Upsized docblocks',
-
- 'JudyLIns.*storage::StorBucketDatabase::(get|insert)' => 'Bucket database - Judy',
- 'JudyLDel.*WrappedEntry::remove' => 'Bucket database - Judy',
- 'std::vector<.*storage::LockableMap.*::WrappedEntry::remove()' => 'Bucket database - Lockable map lock list',
-
- 'document::Printable::toString'
- => 'Other - Temporary data created during toString operations',
-
- 'metrics::MetricsManager::getSnapshotForConsumer' => 'Metrics - Snapshot created',
- 'storage::BucketManagerMetrics::BucketManagerMetrics\(\)' => 'Metrics - Stored metrics',
- 'storage::FileStorThreadMetrics::FileStorThreadMetrics\(std::basic_string' => 'Metrics - Stored metrics',
- 'metrics::MetricsSet::addAll' => 'Metrics - Add all',
- 'storage::StatusMetricConsumer::run\(\)' => 'Metrics',
-
- 'slobrok::api::MirrorAPI::PerformTask\(\)' => 'Slobrok - Mirror API perform task'
-
-);
-
-my $signal;
-my $starttime;
-my $size;
-my %vals;
-my %counts;
-
-my $mallocfault;
-my $local = 0;
-my $global = 0;
-my $globwaste = 0;
-
-# Go through all the input gotten on STDIN.
-foreach (<>) {
- if (/^(\d+)\.\d+\s+\S+\s+\d+\s+\S+\s+\S+\s+warning\s+(.*)$/) {
- # We only care for warnings to logs printed by vespamalloc
- my ($time, $line) = ($1, $2);
- if ($line =~ /SignalHandler (\d+) caught/) {
- # If a dump is just starting, reset all the variables that keeps
- # state
- $signal = $1;
- $size = 0;
- %vals = ();
- %counts = ();
- $local = 0;
- $global = 0;
- $globwaste = 0;
- $mallocfault = undef;
- $starttime = $time;
- print "\nProcessing dump from " . localtime($starttime) . "\n";
- } elsif ($line =~ /^SignalHandler $signal done/) {
- # If we are at the end of the vespamalloc report, print a report of
- # what we have found.
- &printReport();
- $starttime = undef;
- } elsif (!$starttime) {
- # Ignore output that is not within dump
- } elsif ($line =~ /^SC\s*\d+\(\s*(\d+)\)\s*GetAlloc\(\s*(\d+)\)\s*GetFree\(\s*\d+\)\s*ExChangeAlloc\(\s*(\d+)\)\s*ExChangeFree\(\s*(\d+)\)\s*ExactAlloc\(\s*(\d+)\)\s*Returned\(\s*(\d+)\)\s*Malloc\(\s*(\d+)\)\s*/) {
- # Track size groups allocated in hopes of figuring out how much data
- # waste there is in vespa malloc
- my ($size, $get, $exalloc, $exfree, $exact, $returned, $malloc) = ($1, $2, $3, $4, $5, $6, $7);
- #print "$line\n";
- #print "Size $size, GetAlloc $get, ExChangeAlloc $exalloc, ExChangeFree $exfree, ExactAlloc $exact, Returned $returned, Malloc $malloc\n";
- my $mult = 65536;
- if ($size > $mult) { $mult = $size; }
- my $alloced = $get + $exalloc + $exact;
- my $ret = $exfree + $returned;
- if ($size != 2097152) {
- #print "Adding ($alloced - $ret) * $mult = " . (($alloced - $ret) * $mult) . "\n";
- my $global = ($alloced - $ret) * $mult;
- my $waste = $malloc * 1024 * 1024 - ($alloced - $ret) * $mult;
- #print "Size $size - Global $global - Waste $waste\n";
- #$global += ($alloced - $ret) * $mult;
- if ($waste >= 0) {
- $global += $global;
- $globwaste += $waste;
- } else {
- $mallocfault = 1;
- }
- } else {
- $mallocfault = 1;
- }
- } elsif ($line =~ /SC\s*\d+\(\s*(\d+)\)\s*Local\(\s*(\d+)\)/) {
- # Track size groups allocated in hopes of figuring out how much data
- # waste there is in vespa malloc
-
- #print "Local $1 * $2 = ".($1 * $2)."\n";
- $local += $1 * $2;
- } elsif ($line =~ /^SizeClass\s*(\d+)/) {
- #print "$line\n";
- } elsif ($line =~ /^(Usage)/) {
- print "Vespa Malloc $line\n";
- } elsif ($line =~ /(DataSegment|Free|Start)/) {
- } elsif ($line =~ /^(\d+)\s+:\s+\{\s+\S+\s+\S+\s+(\S+[^\}]*)/) {
- # This should match any stacktrace reported. Add this stacktrace
- # to the report.
- my ($count, $value) = ($1, $2);
- # Unify stack trace
- $value =~ s/0x[0-9a-f]+//g;
- $value =~ s/\(\d+\)//g;
- $value =~ s/\(\)//g;
- # Remove multiple UNKNOWN lines in backtrace
- my @stack = split /\s+/, $value;
- $value = '';
- my $last = "";
- foreach (@stack) {
- my $current = &cppfilt($_);
- if ($last !~ /UNKNOWN/ || $current !~ /UNKNOWN/) { # Don't print multiple unknown entries after one another
- $value .= "\n " . &cppfilt($_);
- }
- $last = $current;
- }
- # Detect known stack traces
-
- #print "$count - $value\n";
- my $replaced = 0;
- foreach my $pat (keys %patterns) {
- #print "Does $value match $pat?\n";
- if ($value =~ /$pat/s) {
- if ($showRecognizedStacks) {
- $value = $patterns{$pat} . $value;
- } else {
- $value = $patterns{$pat};
- }
- $replaced = 1;
- last;
- }
- }
- if (!$replaced) {
- if ($combineUnrecognizedStacks) {
- $value = "Unrecognized memory allocations";
- } elsif ($sortOnCategories) {
- $value = "Unrecognized memory allocations" . $value;
- }
- }
- if (exists $vals{$value}) {
- $vals{$value} += $count * $size;
- $counts{$value} += $count;
- } else {
- $vals{$value} = $count * $size;
- $counts{$value} = $count;
- }
- } elsif ($line =~ /^Allocated Blocks SC\s*\d+\(\s*(\d+)/) {
- $size = $1;
- #print "Size: $1\n";
- } else {
- #print "$line\n";
- }
- } else {
- #print "$_";
- }
-}
-if ($starttime) {
- print "Input stopped in incomplete trace.\n";
-}
-
-exit(0);
-
-my %filtered;
-
-sub cppfilt {
- my $val = $_[0];
- if ($val =~ /UNKNOWN/) {
- return "UNKNOWN";
- } elsif (exists $filtered{$val}) {
- return $filtered{$val};
- } else {
- my $result = `c++filt $val`;
- chomp $result;
- $filtered{$val} = $result;
- return $result;
- }
-}
-
-sub getByteString {
- my $val = $_[0];
- if ($val < 5000) {
- return sprintf("%d B", $val);
- } elsif ($val < 5000000) {
- return sprintf("%d kB", $val / 1024);
- } else {
- return sprintf("%d MB", $val / (1024 * 1024));
- }
-}
-
-sub printReport {
- print "Total vespa malloc unused thread local data: " . &getByteString($local) . "\n";
- #print "Total global data: $global\n";
- print "Total vespa malloc global waste data: " . &getByteString($globwaste). "\n";
-
- my $total = 0;
- foreach (keys %vals) {
- $total += $vals{$_};
- }
- if (defined $mallocfault) {
- print "Warning: Some sketchy numbers from vespa malloc, so global and thread local "
- . "data is probably inaccurate.\n";
- }
-
- print "\nSummary of allocated data:\n";
- print &getByteString($total) . " bytes tracked total\n";
-
- if ($sortOnCategories) {
- my %categories;
- foreach (keys %vals) {
- if (/^\s*(\S+)/) {
- my $category = $1;
- if (exists $categories{$category}) {
- ${$categories{$category}}[0] += $vals{$_};
- push @{$categories{$category}}, $_;
- } else {
- $categories{$category} = [ $vals{$_}, $_ ];
- }
- }
- }
- foreach (sort { ${$categories{$b}}[0] <=> ${$categories{$a}}[0] } keys %categories) {
- my @values = @{ $categories{$_} };
- print &getByteString($values[0]) . " - $_\n";
- shift @values;
-
- foreach my $val (sort { $vals{$b} <=> $vals{$a} } @values) {
- my $stack = $val;
- $stack =~ s/\n/\n /sg;
- print " " . &getByteString($vals{$val}) . " - " . $counts{$val} . " allocations - $stack\n";
- }
- }
- } else {
- foreach (sort { $vals{$b} <=> $vals{$a} } keys %vals) {
- print &getByteString($vals{$_}) . " - " . $counts{$_} . " allocations - $_\n";
- }
- }
-}