Introduce ossl_ssize_t and use it in ASN1_STRING_set.
We have a number of APIs that cannot migrate to size_t because OpenSSL
used negative numbers as some special indicator. This makes it hard to
become size_t-clean.
However, in reality, the largest buffer size is SSIZE_MAX, or, more
accurately PTRDIFF_MAX. But every platform I've ever seen make ptrdiff_t
and size_t the same size. malloc is just obligated to fail allocations
that don't fit in ssize_t. ssize_t itself is not portable (Windows
doesn't have it), but we can define ossl_ssize_t to be ptrdiff_t.
OpenSSL also has an ossl_ssize_t (though they don't use it much), so
we're also improving compatibility.
Start this out with ASN1_STRING_set. It still internally refuses to
construct a string bigger than INT_MAX; the struct can't hold this and
even if we fix the struct, no other code, inside or outside the library,
can tolerate it. But now code which passes in a size_t (including our
own) can do so without overflow.
Bug: 428, 516
Change-Id: I17aa6971733f34dfda7d971882d0f062e92340e9
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/54953
Commit-Queue: Bob Beck <bbe@google.com>
Auto-Submit: David Benjamin <davidben@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
diff --git a/crypto/asn1/a_bitstr.c b/crypto/asn1/a_bitstr.c
index c67efeb..3d60d3b 100644
--- a/crypto/asn1/a_bitstr.c
+++ b/crypto/asn1/a_bitstr.c
@@ -66,7 +66,8 @@
#include "internal.h"
-int ASN1_BIT_STRING_set(ASN1_BIT_STRING *x, const unsigned char *d, int len) {
+int ASN1_BIT_STRING_set(ASN1_BIT_STRING *x, const unsigned char *d,
+ ossl_ssize_t len) {
return ASN1_STRING_set(x, d, len);
}
diff --git a/crypto/asn1/asn1_lib.c b/crypto/asn1/asn1_lib.c
index 98061db..a0e8f48 100644
--- a/crypto/asn1/asn1_lib.c
+++ b/crypto/asn1/asn1_lib.c
@@ -271,19 +271,27 @@
return ret;
}
-int ASN1_STRING_set(ASN1_STRING *str, const void *_data, int len) {
- unsigned char *c;
+int ASN1_STRING_set(ASN1_STRING *str, const void *_data, ossl_ssize_t len_s) {
const char *data = _data;
-
- if (len < 0) {
+ size_t len;
+ if (len_s < 0) {
if (data == NULL) {
return 0;
- } else {
- len = strlen(data);
}
+ len = strlen(data);
+ } else {
+ len = (size_t)len_s;
}
- if ((str->length <= len) || (str->data == NULL)) {
- c = str->data;
+
+ // |ASN1_STRING| cannot represent strings that exceed |int|, and we must
+ // reserve space for a trailing NUL below.
+ if (len > INT_MAX || len + 1 < len) {
+ OPENSSL_PUT_ERROR(ASN1, ERR_R_OVERFLOW);
+ return 0;
+ }
+
+ if (str->length <= (int)len || str->data == NULL) {
+ unsigned char *c = str->data;
if (c == NULL) {
str->data = OPENSSL_malloc(len + 1);
} else {
@@ -296,10 +304,13 @@
return 0;
}
}
- str->length = len;
+ str->length = (int)len;
if (data != NULL) {
OPENSSL_memcpy(str->data, data, len);
- // an allowance for strings :-)
+ // Historically, OpenSSL would NUL-terminate most (but not all)
+ // |ASN1_STRING|s, in case anyone accidentally passed |str->data| into a
+ // function expecting a C string. We retain this behavior for compatibility,
+ // but code must not rely on this. See CVE-2021-3712.
str->data[len] = '\0';
}
return 1;
diff --git a/crypto/crypto.c b/crypto/crypto.c
index af7e560..12cbb888 100644
--- a/crypto/crypto.c
+++ b/crypto/crypto.c
@@ -14,11 +14,16 @@
#include <openssl/crypto.h>
+#include <assert.h>
+
#include "fipsmodule/rand/fork_detect.h"
#include "fipsmodule/rand/internal.h"
#include "internal.h"
+static_assert(sizeof(ossl_ssize_t) == sizeof(size_t),
+ "ossl_ssize_t should be the same size as size_t");
+
#if !defined(OPENSSL_NO_ASM) && !defined(OPENSSL_STATIC_ARMCAP) && \
(defined(OPENSSL_X86) || defined(OPENSSL_X86_64) || \
defined(OPENSSL_ARM) || defined(OPENSSL_AARCH64) || \
diff --git a/include/openssl/asn1.h b/include/openssl/asn1.h
index 693b38e..c7e294b 100644
--- a/include/openssl/asn1.h
+++ b/include/openssl/asn1.h
@@ -608,7 +608,8 @@
// |data|. It returns one on success and zero on error. If |data| is NULL, it
// updates the length and allocates the buffer as needed, but does not
// initialize the contents.
-OPENSSL_EXPORT int ASN1_STRING_set(ASN1_STRING *str, const void *data, int len);
+OPENSSL_EXPORT int ASN1_STRING_set(ASN1_STRING *str, const void *data,
+ ossl_ssize_t len);
// ASN1_STRING_set0 sets the contents of |str| to |len| bytes from |data|. It
// takes ownership of |data|, which must have been allocated with
@@ -985,7 +986,8 @@
// TODO(davidben): Maybe it should? Wrapping a byte string in a bit string is a
// common use case.
OPENSSL_EXPORT int ASN1_BIT_STRING_set(ASN1_BIT_STRING *str,
- const unsigned char *d, int length);
+ const unsigned char *d,
+ ossl_ssize_t length);
// ASN1_BIT_STRING_set_bit sets bit |n| of |str| to one if |value| is non-zero
// and zero if |value| is zero, resizing |str| as needed. It then truncates
diff --git a/include/openssl/base.h b/include/openssl/base.h
index 7afcc4f..1e61e98 100644
--- a/include/openssl/base.h
+++ b/include/openssl/base.h
@@ -326,6 +326,15 @@
#define BORINGSSL_ENUM_INT
#endif
+// ossl_ssize_t is a signed type which is large enough to fit the size of any
+// valid memory allocation. We prefer using |size_t|, but sometimes we need a
+// signed type for OpenSSL API compatibility. This type can be used in such
+// cases to avoid overflow.
+//
+// Not all |size_t| values fit in |ossl_ssize_t|, but all |size_t| values that
+// are sizes of or indices into C objects, can be converted without overflow.
+typedef ptrdiff_t ossl_ssize_t;
+
// CRYPTO_THREADID is a dummy value.
typedef int CRYPTO_THREADID;