Bug 1345368 - land NSS 37ccb22f8e51, r=me

--HG--
extra : rebase_source : 9e311a3410733d0db12818c57542c8321b8fddad
This commit is contained in:
Franziskus Kiefer 2017-03-17 06:01:56 +01:00
parent a38edf6b85
commit deb6b3311d
19 changed files with 241 additions and 209 deletions

View File

@ -1 +1 @@
cf81ccc154dd
37ccb22f8e51

View File

@ -13,7 +13,7 @@ ADD setup.sh /tmp/setup.sh
RUN bash /tmp/setup.sh
# Change user.
USER worker
# USER worker # See bug 1347473.
# Env variables.
ENV HOME /home/worker

View File

@ -108,6 +108,7 @@
'ct_verif%': 0,
'nss_public_dist_dir%': '<(nss_dist_dir)/public',
'nss_private_dist_dir%': '<(nss_dist_dir)/private',
'only_dev_random%': 1,
},
'target_defaults': {
# Settings specific to targets should go here.
@ -218,16 +219,18 @@
'-Wl,--gc-sections',
],
'conditions': [
['OS=="dragonfly" or OS=="freebsd" or OS=="netbsd" or OS=="openbsd"', {
# Bug 1321317 - unix_rand.c:880: undefined reference to `environ'
'ldflags': [
'-Wl,--warn-unresolved-symbols',
],
}],
['no_zdefs==0', {
'ldflags': [
'-Wl,-z,defs',
],
'conditions': [
['OS=="dragonfly" or OS=="freebsd" or OS=="netbsd" or OS=="openbsd"', {
# Bug 1321317 - unix_rand.c:880: undefined reference to `environ'
'ldflags': [
'-Wl,--warn-unresolved-symbols',
],
}],
],
}],
],
}],
@ -360,10 +363,12 @@
[ 'fuzz_tls==1', {
'cflags': [
'-Wno-unused-function',
'-Wno-unused-variable',
],
'xcode_settings': {
'OTHER_CFLAGS': [
'-Wno-unused-function',
'-Wno-unused-variable',
],
},
}],

View File

@ -10,4 +10,3 @@
*/
#error "Do not include this header file."

View File

@ -19,7 +19,7 @@ if [ "$1" = "install" ]; then
if [ "$hg" -eq 1 ]; then
hgrc="$(hg root)"/.hg/hgrc
if ! grep -q '^pretxncommit.clang-format' "$hgrc"; then
echo '[hook]' >> "$hgrc"
echo '[hooks]' >> "$hgrc"
echo 'pretxncommit.clang-format = [ ! -x ./coreconf/precommit.clang-format.sh ] || ./coreconf/precommit.clang-format.sh' >> "$hgrc"
echo "Installed mercurial pretxncommit hook"
exit

View File

@ -55,7 +55,7 @@
"UnsolicitedServerNameAck-TLS1*":"Boring wants us to fail with an unexpected_extension alert, we simply ignore ssl_server_name_xtn.",
"RequireAnyClientCertificate-TLS1*":"Bug 1339387",
"SendExtensionOnClientCertificate-TLS13":"Bug 1339392",
"ALPNClient-Mismatch-TLS1*":"Bug 1339418"
"ALPNClient-Mismatch-TLS13": "NSS sends alerts in response to errors in protected handshake messages in the clear"
},
"ErrorMap" : {
":HANDSHAKE_FAILURE_ON_CLIENT_HELLO:":"SSL_ERROR_NO_CYPHER_OVERLAP",

View File

@ -476,6 +476,15 @@ TEST_P(TlsExtensionTestPre13, AlpnReturnedBadNameLength) {
ssl_app_layer_protocol_xtn, extension));
}
TEST_P(TlsExtensionTestPre13, AlpnReturnedUnknownName) {
EnableAlpn();
const uint8_t val[] = {0x00, 0x02, 0x01, 0x67};
DataBuffer extension(val, sizeof(val));
ServerHelloErrorTest(std::make_shared<TlsExtensionReplacer>(
ssl_app_layer_protocol_xtn, extension),
kTlsAlertIllegalParameter);
}
TEST_P(TlsExtensionTestDtls, SrtpShort) {
EnableSrtp();
ClientHelloErrorTest(

View File

@ -226,4 +226,101 @@ FUZZ_P(TlsConnectGeneric, BogusClientAuthSignature) {
std::make_shared<TlsSignatureDamager>(kTlsHandshakeCertificateVerify));
Connect();
}
// Check that session ticket resumption works.
FUZZ_P(TlsConnectGeneric, SessionTicketResumption) {
ConfigureSessionCache(RESUME_BOTH, RESUME_TICKET);
Connect();
SendReceive();
Reset();
ConfigureSessionCache(RESUME_BOTH, RESUME_TICKET);
ExpectResumption(RESUME_TICKET);
Connect();
SendReceive();
}
class TlsSessionTicketMacDamager : public TlsExtensionFilter {
public:
TlsSessionTicketMacDamager() {}
virtual PacketFilter::Action FilterExtension(uint16_t extension_type,
const DataBuffer& input,
DataBuffer* output) {
if (extension_type != ssl_session_ticket_xtn &&
extension_type != ssl_tls13_pre_shared_key_xtn) {
return KEEP;
}
*output = input;
// Handle everything before TLS 1.3.
if (extension_type == ssl_session_ticket_xtn) {
// Modify the last byte of the MAC.
output->data()[output->len() - 1] ^= 0xff;
}
// Handle TLS 1.3.
if (extension_type == ssl_tls13_pre_shared_key_xtn) {
TlsParser parser(input);
uint32_t ids_len;
EXPECT_TRUE(parser.Read(&ids_len, 2) && ids_len > 0);
uint32_t ticket_len;
EXPECT_TRUE(parser.Read(&ticket_len, 2) && ticket_len > 0);
// Modify the last byte of the MAC.
output->data()[2 + 2 + ticket_len - 1] ^= 0xff;
}
return CHANGE;
}
};
// Check that session ticket resumption works with a bad MAC.
FUZZ_P(TlsConnectGeneric, SessionTicketResumptionBadMac) {
ConfigureSessionCache(RESUME_BOTH, RESUME_TICKET);
Connect();
SendReceive();
Reset();
ConfigureSessionCache(RESUME_BOTH, RESUME_TICKET);
ExpectResumption(RESUME_TICKET);
client_->SetPacketFilter(std::make_shared<TlsSessionTicketMacDamager>());
Connect();
SendReceive();
}
// Check that session tickets are not encrypted.
FUZZ_P(TlsConnectGeneric, UnencryptedSessionTickets) {
ConfigureSessionCache(RESUME_TICKET, RESUME_TICKET);
auto i1 = std::make_shared<TlsInspectorRecordHandshakeMessage>(
kTlsHandshakeNewSessionTicket);
server_->SetPacketFilter(i1);
Connect();
size_t offset = 4; /* lifetime */
if (version_ == SSL_LIBRARY_VERSION_TLS_1_3) {
offset += 1 + 1 + /* ke_modes */
1 + 1; /* auth_modes */
}
offset += 2 + /* ticket length */
16 + /* SESS_TICKET_KEY_NAME_LEN */
16 + /* AES-128 IV */
2 + /* ciphertext length */
2; /* TLS_EX_SESS_TICKET_VERSION */
// Check the protocol version number.
uint32_t tls_version = 0;
EXPECT_TRUE(i1->buffer().Read(offset, sizeof(version_), &tls_version));
EXPECT_EQ(version_, static_cast<decltype(version_)>(tls_version));
// Check the cipher suite.
uint32_t suite = 0;
EXPECT_TRUE(i1->buffer().Read(offset + sizeof(version_), 2, &suite));
client_->CheckCipherSuite(static_cast<uint16_t>(suite));
}
}

View File

@ -241,6 +241,10 @@ bool TlsAgent::GetPeerChainLength(size_t* count) {
return true;
}
void TlsAgent::CheckCipherSuite(uint16_t cipher_suite) {
EXPECT_EQ(csinfo_.cipherSuite, cipher_suite);
}
void TlsAgent::RequestClientAuth(bool requireAuth) {
EXPECT_TRUE(EnsureTlsSetup());
ASSERT_EQ(SERVER, role_);

View File

@ -163,6 +163,7 @@ class TlsAgent : public PollTarget {
void ConfigNamedGroups(const std::vector<SSLNamedGroup>& groups);
void DisableECDHEServerKeyReuse();
bool GetPeerChainLength(size_t* count);
void CheckCipherSuite(uint16_t cipher_suite);
const std::string& name() const { return name_; }

View File

@ -26,6 +26,7 @@ const uint8_t kTlsApplicationDataType = 23;
const uint8_t kTlsHandshakeClientHello = 1;
const uint8_t kTlsHandshakeServerHello = 2;
const uint8_t kTlsHandshakeNewSessionTicket = 4;
const uint8_t kTlsHandshakeHelloRetryRequest = 6;
const uint8_t kTlsHandshakeEncryptedExtensions = 8;
const uint8_t kTlsHandshakeCertificate = 11;

View File

@ -177,6 +177,11 @@
'CT_VERIF',
],
}],
[ 'only_dev_random==1', {
'defines': [
'SEED_ONLY_DEV_URANDOM',
]
}],
[ 'OS=="mac"', {
'conditions': [
[ 'target_arch=="ia32"', {

View File

@ -8,7 +8,9 @@
#include "seccomon.h"
#if defined(XP_UNIX) || defined(XP_BEOS)
#if (defined(XP_UNIX) || defined(XP_BEOS)) && defined(SEED_ONLY_DEV_URANDOM)
#include "unix_urandom.c"
#elif defined(XP_UNIX) || defined(XP_BEOS)
#include "unix_rand.c"
#endif
#ifdef XP_WIN

View File

@ -682,134 +682,6 @@ RNG_GetNoise(void *buf, size_t maxbytes)
return n;
}
#define SAFE_POPEN_MAXARGS 10 /* must be at least 2 */
/*
* safe_popen is static to this module and we know what arguments it is
* called with. Note that this version only supports a single open child
* process at any time.
*/
static pid_t safe_popen_pid;
static struct sigaction oldact;
static FILE *
safe_popen(char *cmd)
{
int p[2], fd, argc;
pid_t pid;
char *argv[SAFE_POPEN_MAXARGS + 1];
FILE *fp;
static char blank[] = " \t";
static struct sigaction newact;
if (pipe(p) < 0)
return 0;
fp = fdopen(p[0], "r");
if (fp == 0) {
close(p[0]);
close(p[1]);
return 0;
}
/* Setup signals so that SIGCHLD is ignored as we want to do waitpid */
newact.sa_handler = SIG_DFL;
newact.sa_flags = 0;
sigfillset(&newact.sa_mask);
sigaction(SIGCHLD, &newact, &oldact);
pid = fork();
switch (pid) {
int ndesc;
case -1:
fclose(fp); /* this closes p[0], the fd associated with fp */
close(p[1]);
sigaction(SIGCHLD, &oldact, NULL);
return 0;
case 0:
/* dup write-side of pipe to stderr and stdout */
if (p[1] != 1)
dup2(p[1], 1);
if (p[1] != 2)
dup2(p[1], 2);
/*
* close the other file descriptors, except stdin which we
* try reassociating with /dev/null, first (bug 174993)
*/
if (!freopen("/dev/null", "r", stdin))
close(0);
ndesc = getdtablesize();
for (fd = PR_MIN(65536, ndesc); --fd > 2; close(fd))
;
/* clean up environment in the child process */
putenv("PATH=/bin:/usr/bin:/sbin:/usr/sbin:/etc:/usr/etc");
putenv("SHELL=/bin/sh");
putenv("IFS= \t");
/*
* The caller may have passed us a string that is in text
* space. It may be illegal to modify the string
*/
cmd = strdup(cmd);
/* format argv */
argv[0] = strtok(cmd, blank);
argc = 1;
while ((argv[argc] = strtok(0, blank)) != 0) {
if (++argc == SAFE_POPEN_MAXARGS) {
argv[argc] = 0;
break;
}
}
/* and away we go */
execvp(argv[0], argv);
exit(127);
break;
default:
close(p[1]);
break;
}
/* non-zero means there's a cmd running */
safe_popen_pid = pid;
return fp;
}
static int
safe_pclose(FILE *fp)
{
pid_t pid;
int status = -1, rv;
if ((pid = safe_popen_pid) == 0)
return -1;
safe_popen_pid = 0;
fclose(fp);
/* yield the processor so the child gets some time to exit normally */
PR_Sleep(PR_INTERVAL_NO_WAIT);
/* if the child hasn't exited, kill it -- we're done with its output */
while ((rv = waitpid(pid, &status, WNOHANG)) == -1 && errno == EINTR)
;
if (rv == 0) {
kill(pid, SIGKILL);
while ((rv = waitpid(pid, &status, 0)) == -1 && errno == EINTR)
;
}
/* Reset SIGCHLD signal hander before returning */
sigaction(SIGCHLD, &oldact, NULL);
return status;
}
#ifdef DARWIN
#include <TargetConditionals.h>
#if !TARGET_OS_IPHONE
@ -817,15 +689,9 @@ safe_pclose(FILE *fp)
#endif
#endif
/* Fork netstat to collect its output by default. Do not unset this unless
* another source of entropy is available
*/
#define DO_NETSTAT 1
void
RNG_SystemInfoForRNG(void)
{
FILE *fp;
char buf[BUFSIZ];
size_t bytes;
const char *const *cp;
@ -860,12 +726,6 @@ RNG_SystemInfoForRNG(void)
};
#endif
#if defined(BSDI)
static char netstat_ni_cmd[] = "netstat -nis";
#else
static char netstat_ni_cmd[] = "netstat -ni";
#endif
GiveSystemInfo();
bytes = RNG_GetNoise(buf, sizeof(buf));
@ -890,7 +750,6 @@ RNG_SystemInfoForRNG(void)
if (gethostname(buf, sizeof(buf)) == 0) {
RNG_RandomUpdate(buf, strlen(buf));
}
GiveSystemInfo();
/* grab some data from system's PRNG before any other files. */
bytes = RNG_FileUpdate("/dev/urandom", SYSTEM_RNG_SEED_COUNT);
@ -914,33 +773,12 @@ RNG_SystemInfoForRNG(void)
for (cp = files; *cp; cp++)
RNG_FileForRNG(*cp);
/*
* Bug 100447: On BSD/OS 4.2 and 4.3, we have problem calling safe_popen
* in a pthreads environment. Therefore, we call safe_popen last and on
* BSD/OS we do not call safe_popen when we succeeded in getting data
* from /dev/urandom.
*
* Bug 174993: On platforms providing /dev/urandom, don't fork netstat
* either, if data has been gathered successfully.
*/
#if defined(BSDI) || defined(FREEBSD) || defined(NETBSD) || defined(OPENBSD) || defined(DARWIN) || defined(LINUX) || defined(HPUX)
if (bytes)
return;
#endif
#ifdef SOLARIS
/*
* On Solaris, NSS may be initialized automatically from libldap in
* applications that are unaware of the use of NSS. safe_popen forks, and
* sometimes creates issues with some applications' pthread_atfork handlers.
* We always have /dev/urandom on Solaris 9 and above as an entropy source,
* and for Solaris 8 we have the libkstat interface, so we don't need to
* fork netstat.
*/
#undef DO_NETSTAT
if (!bytes) {
/* On Solaris 8, /dev/urandom isn't available, so we use libkstat. */
PRUint32 kstat_bytes = 0;
@ -951,15 +789,6 @@ RNG_SystemInfoForRNG(void)
PORT_Assert(bytes);
}
#endif
#ifdef DO_NETSTAT
fp = safe_popen(netstat_ni_cmd);
if (fp != NULL) {
while ((bytes = fread(buf, 1, sizeof(buf), fp)) > 0)
RNG_RandomUpdate(buf, bytes);
safe_pclose(fp);
}
#endif
}
#define TOTAL_FILE_LIMIT 1000000 /* one million */

View File

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <fcntl.h>
#include <unistd.h>
#include "secerr.h"
#include "secrng.h"
#include "prprf.h"
void
RNG_SystemInfoForRNG(void)
{
PRUint8 bytes[SYSTEM_RNG_SEED_COUNT];
size_t numBytes = RNG_SystemRNG(bytes, SYSTEM_RNG_SEED_COUNT);
if (!numBytes) {
/* error is set */
return;
}
RNG_RandomUpdate(bytes, numBytes);
}
size_t
RNG_SystemRNG(void *dest, size_t maxLen)
{
int fd;
int bytes;
size_t fileBytes = 0;
unsigned char *buffer = dest;
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
PORT_SetError(SEC_ERROR_NEED_RANDOM);
return 0;
}
while (fileBytes < maxLen) {
bytes = read(fd, buffer, maxLen - fileBytes);
if (bytes <= 0) {
break;
}
fileBytes += bytes;
buffer += bytes;
}
(void)close(fd);
if (fileBytes != maxLen) {
PORT_SetError(SEC_ERROR_NEED_RANDOM);
return 0;
}
return fileBytes;
}

View File

@ -354,6 +354,27 @@ ssl3_ParseEncryptedSessionTicket(sslSocket *ss, SECItem *data,
return SECSuccess;
}
PRBool
ssl_AlpnTagAllowed(const sslSocket *ss, const SECItem *tag)
{
const unsigned char *data = ss->opt.nextProtoNego.data;
unsigned int length = ss->opt.nextProtoNego.len;
unsigned int offset = 0;
if (!tag->len)
return PR_TRUE;
while (offset < length) {
unsigned int taglen = (unsigned int)data[offset];
if ((taglen == tag->len) &&
!PORT_Memcmp(data + offset + 1, tag->data, tag->len))
return PR_TRUE;
offset += 1 + taglen;
}
return PR_FALSE;
}
/* handle an incoming Next Protocol Negotiation extension. */
SECStatus
ssl3_ServerHandleNextProtoNegoXtn(const sslSocket *ss, TLSExtensionData *xtnData, PRUint16 ex_type,
@ -568,6 +589,12 @@ ssl3_ClientHandleAppProtoXtn(const sslSocket *ss, TLSExtensionData *xtnData, PRU
return SECFailure;
}
if (!ssl_AlpnTagAllowed(ss, &protocol_name)) {
ssl3_ExtSendAlert(ss, alert_fatal, illegal_parameter);
PORT_SetError(SSL_ERROR_NEXT_PROTOCOL_DATA_INVALID);
return SECFailure;
}
SECITEM_FreeItem(&xtnData->nextProto, PR_FALSE);
xtnData->nextProtoState = SSL_NEXT_PROTO_SELECTED;
xtnData->negotiated[xtnData->numNegotiated++] = ex_type;
@ -977,9 +1004,13 @@ ssl3_EncodeSessionTicket(sslSocket *ss,
+ sizeof(ticket->ticket_lifetime_hint) /* ticket lifetime hint */
+ sizeof(ticket->flags) /* ticket flags */
+ 1 + alpnSelection.len; /* npn value + length field. */
#ifdef UNSAFE_FUZZER_MODE
padding_length = 0;
#else
padding_length = AES_BLOCK_SIZE -
(ciphertext_length %
AES_BLOCK_SIZE);
#endif
ciphertext_length += padding_length;
if (SECITEM_AllocItem(NULL, &plaintext_item, ciphertext_length) == NULL)
@ -1132,6 +1163,10 @@ ssl3_EncodeSessionTicket(sslSocket *ss,
/* Generate encrypted portion of ticket. */
PORT_Assert(aes_key);
#ifdef UNSAFE_FUZZER_MODE
ciphertext.len = plaintext_item.len;
PORT_Memcpy(ciphertext.data, plaintext_item.data, plaintext_item.len);
#else
aes_ctx = PK11_CreateContextBySymKey(cipherMech, CKA_ENCRYPT, aes_key, &ivItem);
if (!aes_ctx)
goto loser;
@ -1143,10 +1178,10 @@ ssl3_EncodeSessionTicket(sslSocket *ss,
PK11_DestroyContext(aes_ctx, PR_TRUE);
if (rv != SECSuccess)
goto loser;
#endif
/* Convert ciphertext length to network order. */
length_buf[0] = (ciphertext.len >> 8) & 0xff;
length_buf[1] = (ciphertext.len) & 0xff;
(void)ssl_EncodeUintX(ciphertext.len, 2, length_buf);
/* Compute MAC. */
PORT_Assert(mac_key);
@ -1310,9 +1345,11 @@ ssl3_ProcessSessionTicketCommon(sslSocket *ss, SECItem *data)
*/
if (PORT_Memcmp(enc_session_ticket.key_name, key_name,
SESS_TICKET_KEY_NAME_LEN) != 0) {
#ifndef UNSAFE_FUZZER_MODE
SSL_DBG(("%d: SSL[%d]: Session ticket key_name sent mismatch.",
SSL_GETPID(), ss->fd));
goto no_ticket;
#endif
}
/* Verify the MAC on the ticket. MAC verification may also
@ -1349,9 +1386,11 @@ ssl3_ProcessSessionTicketCommon(sslSocket *ss, SECItem *data)
if (NSS_SecureMemcmp(computed_mac, enc_session_ticket.mac,
computed_mac_length) !=
0) {
#ifndef UNSAFE_FUZZER_MODE
SSL_DBG(("%d: SSL[%d]: Session ticket MAC mismatch.",
SSL_GETPID(), ss->fd));
goto no_ticket;
#endif
}
/* We ignore key_name for now.
@ -1365,6 +1404,12 @@ ssl3_ProcessSessionTicketCommon(sslSocket *ss, SECItem *data)
enc_session_ticket.encrypted_state.len);
PORT_Assert(aes_key);
#ifdef UNSAFE_FUZZER_MODE
decrypted_state->len = enc_session_ticket.encrypted_state.len;
PORT_Memcpy(decrypted_state->data,
enc_session_ticket.encrypted_state.data,
enc_session_ticket.encrypted_state.len);
#else
ivItem.data = enc_session_ticket.iv;
ivItem.len = AES_BLOCK_SIZE;
aes_ctx = PK11_CreateContextBySymKey(cipherMech, CKA_DECRYPT,
@ -1395,6 +1440,7 @@ ssl3_ProcessSessionTicketCommon(sslSocket *ss, SECItem *data)
if (padding_length != (PRUint32)*padding)
goto no_ticket;
}
#endif
/* Deserialize session state. */
buffer = decrypted_state->data;
@ -1562,9 +1608,12 @@ ssl3_ProcessSessionTicketCommon(sslSocket *ss, SECItem *data)
goto no_ticket;
}
#ifndef UNSAFE_FUZZER_MODE
/* Done parsing. Check that all bytes have been consumed. */
if (buffer_len != padding_length)
if (buffer_len != padding_length) {
goto no_ticket;
}
#endif
/* Use the ticket if it has not expired, otherwise free the allocated
* memory since the ticket is of no use.

View File

@ -1856,6 +1856,8 @@ ssl3_TLSPRFWithMasterSecret(sslSocket *ss, ssl3CipherSpec *spec,
const unsigned char *val, unsigned int valLen,
unsigned char *out, unsigned int outLen);
PRBool ssl_AlpnTagAllowed(const sslSocket *ss, const SECItem *tag);
#ifdef TRACE
#define SSL_TRACE(msg) ssl_Trace msg
#else

View File

@ -941,27 +941,6 @@ tls13_CanResume(sslSocket *ss, const sslSessionID *sid)
return PR_TRUE;
}
static PRBool
tls13_AlpnTagAllowed(const sslSocket *ss, const SECItem *tag)
{
const unsigned char *data = ss->opt.nextProtoNego.data;
unsigned int length = ss->opt.nextProtoNego.len;
unsigned int offset = 0;
if (!tag->len)
return PR_TRUE;
while (offset < length) {
unsigned int taglen = (unsigned int)data[offset];
if ((taglen == tag->len) &&
!PORT_Memcmp(data + offset + 1, tag->data, tag->len))
return PR_TRUE;
offset += 1 + taglen;
}
return PR_FALSE;
}
static PRBool
tls13_CanNegotiateZeroRtt(sslSocket *ss, const sslSessionID *sid)
{
@ -4296,7 +4275,7 @@ tls13_ClientAllow0Rtt(const sslSocket *ss, const sslSessionID *sid)
return PR_FALSE;
if ((sid->u.ssl3.locked.sessionTicket.flags & ticket_allow_early_data) == 0)
return PR_FALSE;
return tls13_AlpnTagAllowed(ss, &sid->u.ssl3.alpnSelection);
return ssl_AlpnTagAllowed(ss, &sid->u.ssl3.alpnSelection);
}
SECStatus

View File

@ -174,7 +174,7 @@ ssl_gtest_start()
# Helper function used when 'parallel' isn't available.
parallel_fallback()
{
eval ${*//\{\}/0}
eval "${@//\{\}/0}"
}
parse_report()