Compare commits

...

32 Commits

Author SHA1 Message Date
Petteri Aimonen
45fc9f3ef6 Publishing nanopb-0.2.9.1 2014-09-11 19:14:45 +03:00
Petteri Aimonen
115de6e555 Update changelog 2014-09-11 19:13:59 +03:00
Petteri Aimonen
9c92410e2a Add a better fuzz test.
Attempts to verify all the properties defined in the security model,
while also being portable and able to run on many platforms.
2014-09-11 18:25:23 +03:00
Petteri Aimonen
6df566859d Protect against size_t overflows in pb_dec_bytes/pb_dec_string.
Possible consequences of bug:
1) Denial of service by causing a crash
   Possible when all of the following apply:
      - Untrusted data is passed to pb_decode()
      - The top-level message contains a static string field as the first field.
   Causes a single write of '0' byte to 1 byte before the message struct.

2) Remote code execution
   Possible when all of the following apply:
      - 64-bit platform
      - The message or a submessage contains a static/pointer string field.
      - Decoding directly from a custom pb_istream_t
      - bytes_left on the stream is set to larger than 4 GB
   Causes a write of up to 4 GB of data past the string field.

3) Possible heap corruption or remote code execution
   Possible when all of the following apply:
      - less than 64-bit platform
      - The message or a submessage contains a pointer-type bytes field.
   Causes a write of sizeof(pb_size_t) bytes of data past a 0-byte long
   malloc()ed buffer. On many malloc() implementations, this causes at
   most a crash. However, remote code execution through a controlled jump
   cannot be ruled out.

--

Detailed analysis follows

In the following consideration, I define "platform bitness" as equal to
number of bits in size_t datatype. Therefore most 8-bit platforms are
regarded as 16-bit for the purposes of this discussion.

1. The overflow in pb_dec_string

The overflow happens in this computation:

uint32_t size;
size_t alloc_size;
alloc_size = size + 1;

There are two ways in which the overflow can occur: In the uint32_t
addition, or in the cast to size_t. This depends on the platform
bitness.

On 32- and 64-bit platforms, the size has to be UINT32_MAX for the
overflow to occur. In that case alloc_size will be 0.

On 16-bit platforms, overflow will happen whenever size is more than
UINT16_MAX, and resulting alloc_size is attacker controlled.

For static fields, the alloc_size value is just checked against the
field data size. For pointer fields, the alloc_size value is passed to
malloc(). End result in both cases is the same, the storage is 0 or
just a few bytes in length.

On 16-bit platforms, another overflow occurs in the call to pb_read(),
when passing the original size. An attacker will want the passed value
to be larger than the alloc_size, therefore the only reasonable choice
is to have size = UINT16_MAX and alloc_size = 0. Any larger multiple
will truncate to the same values.

At this point we have read atleast the tag and the string length of the
message, i.e. atleast 3 bytes. The maximum initial value for stream
bytes_left is SIZE_MAX, thus at this point at most SIZE_MAX-3 bytes are
remaining.

On 32-bit and 16-bit platforms this means that the size passed to
pb_read() is always larger than the number of remaining bytes. This
causes pb_read() to fail immediately, before reading any bytes.

On 64-bit platforms, it is possible for the bytes_left value to be set
to a value larger than UINT32_MAX, which is the wraparound point in
size calculation. In this case pb_read() will succeed and write up to 4
GB of attacker controlled data over the RAM that comes after the string
field.

On all platforms, there is an unconditional write of a terminating null
byte. Because the size of size_t typically reflects the size of the
processor address space, a write at UINT16_MAX or UINT32_MAX bytes
after the string field actually wraps back to before the string field.
Consequently, on 32-bit and 16-bit platforms, the bug causes a single
write of '0' byte at one byte before the string field.

If the string field is in the middle of a message, this will just
corrupt other data in the message struct. Because the message contents
is attacker controlled anyway, this is a non-issue. However, if the
string field is the first field in the top-level message, it can
corrupt other data on the stack/heap before it. Typically a single '0'
write at a location not controlled by attacker is enough only for a
denial-of-service attack.

When using pointer fields and malloc(), the attacker controlled
alloc_size will cause a 0-size allocation to happen. By the same logic
as before, on 32-bit and 16-bit platforms this causes a '0' byte write
only. On 64-bit platforms, however, it will again allow up to 4 GB of
malicious data to be written over memory, if the stream length allows
the read.

2. The overflow in pb_dec_bytes

This overflow happens in the PB_BYTES_ARRAY_T_ALLOCSIZE macro:

The computation is done in size_t data type this time. This means that
an overflow is possible only when n is larger than SIZE_MAX -
offsetof(..). The offsetof value in this case is equal to
sizeof(pb_size_t) bytes.

Because the incoming size value is limited to 32 bits, no overflow can
happen here on 64-bit platforms.

The size will be passed to pb_read(). Like before, on 32-bit and 16-bit
platforms the read will always fail before writing anything.

This leaves only the write of bdest->size as exploitable. On statically
allocated fields, the size field will always be allocated, regardless
of alloc_size. In this case, no buffer overflow is possible here, but
user code could possibly use the attacker controlled size value and
read past a buffer.

If the field is allocated through malloc(), this will allow a write of
sizeof(pb_size_t) attacker controlled bytes to past a 0-byte long
buffer. In typical malloc implementations, this will either fit in
unused alignment padding area, or cause a heap corruption and a crash.
Under very exceptional situation it could allow attacker to influence
the behaviour of malloc(), possibly jumping into an attacker-controlled
location and thus leading to remote code execution.
2014-09-11 18:25:23 +03:00
Petteri Aimonen
cc1c3a7963 Add just-to-be-sure check to allocate_field().
This check will help to detect bugs earlier, and is quite lightweight
compared to malloc() anyway.
2014-09-11 18:25:23 +03:00
Petteri Aimonen
33585924de Fix memory leak with duplicated fields and PB_ENABLE_MALLOC.
If a required or optional field appeared twice in the message data,
pb_decode will overwrite the old data with new one. That is fine, but
with submessage fields, it didn't release the allocated subfields before
overwriting.

This bug can manifest if all of the following conditions are true:

1. There is a message with a "optional" or "required" submessage field
   that has type:FT_POINTER.

2. The submessage contains atleast one field with type:FT_POINTER.

3. The message data to be decoded has the submessage field twice in it.
2014-09-11 18:25:18 +03:00
Petteri Aimonen
b7add1e577 Fix crash in pb_release() if called twice on same message.
There was a double-free bug in pb_release() because it didn't set size fields
to zero after deallocation. Most commonly this happens if pb_decode() fails,
internally calls pb_release() and then application code also calls pb_release().
2014-09-11 18:16:01 +03:00
Petteri Aimonen
2f05a35b5f Publishing nanopb-0.2.9 2014-08-09 22:01:04 +03:00
Petteri Aimonen
4f76e64929 Update changelog 2014-08-04 19:13:39 +03:00
Petteri Aimonen
ec3bff4ba1 Generate #defines for initializing message structures.
Usage like:
MyMessage foo = MyMessage_init_default;

MyMessage_init_default will initialize to default values defined in .proto.

MyMessage_init_zero will initialize to null/zero values. Same results as {}
or {0}, but will avoid compiler warnings by initializing everything explicitly.

Update issue 79
Status: FixedInGit
2014-08-04 18:40:40 +03:00
Petteri Aimonen
1d7f60fec3 Add skip_message option to generator.
Update issue 121
Status: FixedInGit
2014-07-20 14:56:12 +03:00
Petteri Aimonen
5749606f5d Add support for inverted patterns in test framework. 2014-07-20 14:55:47 +03:00
Petteri Aimonen
eaa3c7b157 Cleanup and comment the code of network_server example.
Update issue 123
Status: FixedInGit
2014-07-20 14:44:41 +03:00
Petteri Aimonen
3cf9668c75 Do not automatically add a dot with generator -e option.
Now -e option in generator is more versatile. Especially it avoids
double-dot problem with some build systems.

Given foobar.proto, we now get:
-e .pb  =>  foobar.pb.c (default)
-e _pb  =>  foobar_pb.c
-e ''   =>  foobar.c

Note that if you have used -e option previously, you will have to prepend
. to the argument to get the same filenames as before.

Update issue 122
Status: FixedInGit
2014-07-20 14:25:11 +03:00
Petteri Aimonen
7f97ad549e Give better messages about the .options file path.
Update issue 124
Status: FixedInGit
2014-07-20 14:18:21 +03:00
Petteri Aimonen
f2f9f8a9ed Fix problem with .options file and extension fields.
The options for an extension field were being looked up under wrong name
(MessageName instead of MessageName.fieldname).

Fixed the problem and added regression test. Created a new subfolder for
regression test cases.

Update issue 125
Status: FixedInGit
2014-07-20 14:02:56 +03:00
Petteri Aimonen
788d2825b0 Add unit tests for allocate_field(). 2014-06-02 21:20:57 +03:00
Petteri Aimonen
99bc1d4f97 Make clearer that size = 0 in allocate_field() is not allowed.
Back in design phase the code used realloc() for freeing the memory
also. However, this is not entirely portable, and therefore the finished
implementation used free() separately.

There were some remnants of the size = 0 code in the allocate_field()
code, which made it somewhat confusing. This change makes it clearer
that size = 0 is not allowed (and not used by nanopb).
2014-06-02 21:12:38 +03:00
Petteri Aimonen
8a857a7f75 Don't use SIZE_MAX macro, as it is not in C89.
Update issue 120
Status: FixedInGit
2014-06-02 21:09:06 +03:00
Petteri Aimonen
8611958a7f Add PB_PACKED_STRUCT support for Keil MDK-ARM toolchain
Patch from Jon Read.

Update issue 119
Status: FixedInGit
2014-05-30 13:45:48 +03:00
Petteri Aimonen
2e9797af58 Setting version to 0.2.9-dev 2014-05-20 19:52:09 +03:00
Petteri Aimonen
2c51fb7771 Update changelog for 0.2.8 2014-05-20 19:46:48 +03:00
Petteri Aimonen
916bcb3643 Publishing nanopb-0.2.8 2014-05-20 19:35:00 +03:00
Petteri Aimonen
9cf788de54 Fix bug in alltypes test case that made fuzzing difficult. 2014-05-17 20:28:33 +03:00
Petteri Aimonen
5ef128616b Fix security issue with PB_ENABLE_MALLOC.
The multiplication in allocate_field could potentially overflow,
leading to allocating too little memory. This could subsequently
allow an attacker to cause a write past the buffer, overwriting
other memory contents.

The attack is possible if untrusted message data is decoded using
nanopb, and the message type includes a pointer-type string or bytes
field, or a repeated numeric field. Submessage fields are not
affected.

This issue only affects systems that have been compiled with
PB_ENABLE_MALLOC enabled. Only version nanopb-0.2.7 is affected,
as prior versions do not include this functionality.

Update issue 117
Status: FixedInGit
2014-05-17 20:06:55 +03:00
Petteri Aimonen
ba2ab9ea65 Docs update, remove malloc from limitations list 2014-04-26 20:11:54 +03:00
Petteri Aimonen
e6a57e512f Add option to not add timestamps to .pb.h and .pb.c preambles.
Patch by rusnakp.

Update issue 115
Status: FixedInGit
2014-04-18 15:40:40 +03:00
Petteri Aimonen
d177af1639 Fix typos in scons command line options 2014-04-15 20:30:50 +03:00
Petteri Aimonen
3b36235cef Remove -O0 from tests CFLAGS so that optimized builds can be tested also 2014-04-15 20:27:38 +03:00
Petteri Aimonen
1d249a48ea Fix bug in missing_fields test case 2014-04-09 19:39:12 +03:00
Petteri Aimonen
3e83d81b09 Use -fsanitize=undefined when running tests with clang 2014-04-09 19:28:57 +03:00
Petteri Aimonen
938c7ac3f3 Setting version to 0.2.8-dev 2014-04-07 20:45:04 +03:00
37 changed files with 1248 additions and 170 deletions

View File

@@ -1,3 +1,27 @@
nanopb-0.2.9.1 (2014-09-11)
Fix security issue due to size_t overflows. (issue 132)
Fix memory leak with duplicated fields and PB_ENABLE_MALLOC
Fix crash if pb_release() is called twice.
nanopb-0.2.9 (2014-08-09)
NOTE: If you are using the -e option with the generator, you have
to prepend . to the argument to get the same behaviour as before.
Do not automatically add a dot with generator -e option. (issue 122)
Fix problem with .options file and extension fields. (issue 125)
Don't use SIZE_MAX macro, as it is not in C89. (issue 120)
Generate #defines for initializing message structures. (issue 79)
Add skip_message option to generator. (issue 121)
Add PB_PACKED_STRUCT support for Keil MDK-ARM toolchain (issue 119)
Give better messages about the .options file path. (issue 124)
Improved tests
nanopb-0.2.8 (2014-05-20)
Fix security issue with PB_ENABLE_MALLOC. (issue 117)
Add option to not add timestamps to .pb.h and .pb.c preambles. (issue 115)
Documentation updates
Improved tests
nanopb-0.2.7 (2014-04-07)
Fix bug with default values for extension fields (issue 111)
Fix some MISRA-C warnings (issue 91)

View File

@@ -47,7 +47,6 @@ Features and limitations
**Limitations**
#) User must provide callbacks when decoding arrays or strings without maximum size. Malloc support could be added as a separate module.
#) Some speed has been sacrificed for code size.
#) Encoding is focused on writing to streams. For memory buffers only it could be made more efficient.
#) The deprecated Protocol Buffers feature called "groups" is not supported.

View File

@@ -23,9 +23,13 @@
#include "fileproto.pb.h"
#include "common.h"
/* This callback function will be called once for each filename received
* from the server. The filenames will be printed out immediately, so that
* no memory has to be allocated for them.
*/
bool printfile_callback(pb_istream_t *stream, const pb_field_t *field, void **arg)
{
FileInfo fileinfo;
FileInfo fileinfo = {};
if (!pb_decode(stream, FileInfo_fields, &fileinfo))
return false;
@@ -35,14 +39,20 @@ bool printfile_callback(pb_istream_t *stream, const pb_field_t *field, void **ar
return true;
}
/* This function sends a request to socket 'fd' to list the files in
* directory given in 'path'. The results received from server will
* be printed to stdout.
*/
bool listdir(int fd, char *path)
{
ListFilesRequest request;
ListFilesResponse response;
pb_istream_t input = pb_istream_from_socket(fd);
/* Construct and send the request to server */
{
ListFilesRequest request = {};
pb_ostream_t output = pb_ostream_from_socket(fd);
uint8_t zero = 0;
/* In our protocol, path is optional. If it is not given,
* the server will list the root directory. */
if (path == NULL)
{
request.has_path = false;
@@ -59,15 +69,25 @@ bool listdir(int fd, char *path)
strcpy(request.path, path);
}
/* Encode the request. It is written to the socket immediately
* through our custom stream. */
if (!pb_encode(&output, ListFilesRequest_fields, &request))
{
fprintf(stderr, "Encoding failed.\n");
fprintf(stderr, "Encoding failed: %s\n", PB_GET_ERROR(&output));
return false;
}
/* We signal the end of request with a 0 tag. */
pb_write(&output, &zero, 1);
}
/* Read back the response from server */
{
ListFilesResponse response = {};
pb_istream_t input = pb_istream_from_socket(fd);
/* Give a pointer to our callback function, which will handle the
* filenames as they arrive. */
response.file.funcs.decode = &printfile_callback;
if (!pb_decode(&input, ListFilesResponse_fields, &response))
@@ -76,11 +96,14 @@ bool listdir(int fd, char *path)
return false;
}
/* If the message from server decodes properly, but directory was
* not found on server side, we get path_error == true. */
if (response.path_error)
{
fprintf(stderr, "Server reported error.\n");
return false;
}
}
return true;
}
@@ -96,6 +119,7 @@ int main(int argc, char **argv)
sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* Connect to server running on localhost:1234 */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
@@ -107,9 +131,11 @@ int main(int argc, char **argv)
return 1;
}
/* Send the directory listing request */
if (!listdir(sockfd, path))
return 2;
/* Close connection */
close(sockfd);
return 0;

View File

@@ -23,11 +23,16 @@
#include "fileproto.pb.h"
#include "common.h"
/* This callback function will be called once during the encoding.
* It will write out any number of FileInfo entries, without consuming unnecessary memory.
* This is accomplished by fetching the filenames one at a time and encoding them
* immediately.
*/
bool listdir_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg)
{
DIR *dir = (DIR*) *arg;
struct dirent *file;
FileInfo fileinfo;
FileInfo fileinfo = {};
while ((file = readdir(dir)) != NULL)
{
@@ -35,9 +40,12 @@ bool listdir_callback(pb_ostream_t *stream, const pb_field_t *field, void * cons
strncpy(fileinfo.name, file->d_name, sizeof(fileinfo.name));
fileinfo.name[sizeof(fileinfo.name) - 1] = '\0';
/* This encodes the header for the field, based on the constant info
* from pb_field_t. */
if (!pb_encode_tag_for_field(stream, field))
return false;
/* This encodes the data for the field, based on our FileInfo structure. */
if (!pb_encode_submessage(stream, FileInfo_fields, &fileinfo))
return false;
}
@@ -45,13 +53,18 @@ bool listdir_callback(pb_ostream_t *stream, const pb_field_t *field, void * cons
return true;
}
/* Handle one arriving client connection.
* Clients are expected to send a ListFilesRequest, terminated by a '0'.
* Server will respond with a ListFilesResponse message.
*/
void handle_connection(int connfd)
{
ListFilesRequest request;
ListFilesResponse response;
DIR *directory = NULL;
/* Decode the message from the client and open the requested directory. */
{
ListFilesRequest request = {};
pb_istream_t input = pb_istream_from_socket(connfd);
pb_ostream_t output = pb_ostream_from_socket(connfd);
DIR *directory;
if (!pb_decode(&input, ListFilesRequest_fields, &request))
{
@@ -60,19 +73,26 @@ void handle_connection(int connfd)
}
directory = opendir(request.path);
printf("Listing directory: %s\n", request.path);
}
/* List the files in the directory and transmit the response to client */
{
ListFilesResponse response = {};
pb_ostream_t output = pb_ostream_from_socket(connfd);
if (directory == NULL)
{
perror("opendir");
/* Directory was not found, transmit error status */
response.has_path_error = true;
response.path_error = true;
response.file.funcs.encode = NULL;
}
else
{
/* Directory was found, transmit filenames */
response.has_path_error = false;
response.file.funcs.encode = &listdir_callback;
response.file.arg = directory;
@@ -80,8 +100,12 @@ void handle_connection(int connfd)
if (!pb_encode(&output, ListFilesResponse_fields, &response))
{
printf("Encoding failed.\n");
printf("Encoding failed: %s\n", PB_GET_ERROR(&output));
}
}
if (directory != NULL)
closedir(directory);
}
int main(int argc, char **argv)
@@ -90,8 +114,8 @@ int main(int argc, char **argv)
struct sockaddr_in servaddr;
int reuse = 1;
/* Listen on localhost:1234 for TCP connections */
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
memset(&servaddr, 0, sizeof(servaddr));
@@ -112,6 +136,7 @@ int main(int argc, char **argv)
for(;;)
{
/* Wait for a client */
connfd = accept(listenfd, NULL, NULL);
if (connfd < 0)
@@ -128,4 +153,6 @@ int main(int argc, char **argv)
close(connfd);
}
return 0;
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
'''Generate header file for nanopb from a ProtoBuf FileDescriptorSet.'''
nanopb_version = "nanopb-0.2.7"
nanopb_version = "nanopb-0.2.9.1"
import sys
@@ -292,28 +292,37 @@ class Field:
result = None
return result
def default_decl(self, declaration_only = False):
'''Return definition for this field's default value.'''
if self.default is None:
return None
def get_initializer(self, null_init):
'''Return literal expression for this field's default value.'''
ctype, default = self.ctype, self.default
array_decl = ''
if self.pbtype == 'MESSAGE':
if null_init:
return '%s_init_zero' % self.ctype
else:
return '%s_init_default' % self.ctype
if self.default is None or null_init:
if self.pbtype == 'STRING':
return '""'
elif self.pbtype == 'BYTES':
return '{0, {0}}'
elif self.pbtype == 'ENUM':
return '(%s)0' % self.ctype
else:
return '0'
default = str(self.default)
if self.pbtype == 'STRING':
if self.allocation != 'STATIC':
return None # Not implemented
array_decl = '[%d]' % self.max_size
default = str(self.default).encode('string_escape')
default = default.encode('utf-8').encode('string_escape')
default = default.replace('"', '\\"')
default = '"' + default + '"'
elif self.pbtype == 'BYTES':
if self.allocation != 'STATIC':
return None # Not implemented
data = self.default.decode('string_escape')
data = default.decode('string_escape')
data = ['0x%02x' % ord(c) for c in data]
if len(data) == 0:
default = '{0, {0}}'
else:
default = '{%d, {%s}}' % (len(data), ','.join(data))
elif self.pbtype in ['FIXED32', 'UINT32']:
default += 'u'
@@ -322,6 +331,25 @@ class Field:
elif self.pbtype in ['SFIXED64', 'INT64']:
default += 'll'
return default
def default_decl(self, declaration_only = False):
'''Return definition for this field's default value.'''
if self.default is None:
return None
ctype = self.ctype
default = self.get_initializer(False)
array_decl = ''
if self.pbtype == 'STRING':
if self.allocation != 'STATIC':
return None # Not implemented
array_decl = '[%d]' % self.max_size
elif self.pbtype == 'BYTES':
if self.allocation != 'STATIC':
return None # Not implemented
if declaration_only:
return 'extern const %s %s_default%s;' % (ctype, self.struct_name + self.name, array_decl)
else:
@@ -553,6 +581,32 @@ class Message:
result += types + '\n'
return result
def get_initializer(self, null_init):
if not self.ordered_fields:
return '{0}'
parts = []
for field in self.ordered_fields:
if field.allocation == 'STATIC':
if field.rules == 'REPEATED':
parts.append('0')
parts.append('{'
+ ', '.join([field.get_initializer(null_init)] * field.max_count)
+ '}')
elif field.rules == 'OPTIONAL':
parts.append('false')
parts.append(field.get_initializer(null_init))
else:
parts.append(field.get_initializer(null_init))
elif field.allocation == 'POINTER':
parts.append('NULL')
elif field.allocation == 'CALLBACK':
if field.pbtype == 'EXTENSION':
parts.append('NULL')
else:
parts.append('{{NULL}, NULL}')
return '{' + ', '.join(parts) + '}'
def default_decl(self, declaration_only = False):
result = ""
for field in self.fields:
@@ -639,13 +693,17 @@ def parse_file(fdesc, file_options):
for names, message in iterate_messages(fdesc, base_name):
message_options = get_nanopb_suboptions(message, file_options, names)
if message_options.skip_message:
continue
messages.append(Message(names, message, message_options))
for enum in message.enum_type:
enum_options = get_nanopb_suboptions(enum, message_options, names + enum.name)
enums.append(Enum(names, enum, enum_options))
for names, extension in iterate_extensions(fdesc, base_name):
field_options = get_nanopb_suboptions(extension, file_options, names)
field_options = get_nanopb_suboptions(extension, file_options, names + extension.name)
if field_options.type != nanopb_pb2.FT_IGNORE:
extensions.append(ExtensionField(names, extension, field_options))
@@ -707,6 +765,9 @@ def generate_header(dependencies, headername, enums, messages, extensions, optio
'''
yield '/* Automatically generated nanopb header */\n'
if options.notimestamp:
yield '/* Generated by %s */\n\n' % (nanopb_version)
else:
yield '/* Generated by %s at %s. */\n\n' % (nanopb_version, time.asctime())
symbol = make_identifier(headername)
@@ -721,7 +782,7 @@ def generate_header(dependencies, headername, enums, messages, extensions, optio
for dependency in dependencies:
noext = os.path.splitext(dependency)[0]
yield options.genformat % (noext + '.' + options.extension + '.h')
yield options.genformat % (noext + options.extension + '.h')
yield '\n'
yield '#ifdef __cplusplus\n'
@@ -748,6 +809,15 @@ def generate_header(dependencies, headername, enums, messages, extensions, optio
yield msg.default_decl(True)
yield '\n'
yield '/* Initializer values for message structs */\n'
for msg in messages:
identifier = '%s_init_default' % msg.name
yield '#define %-40s %s\n' % (identifier, msg.get_initializer(False))
for msg in messages:
identifier = '%s_init_zero' % msg.name
yield '#define %-40s %s\n' % (identifier, msg.get_initializer(True))
yield '\n'
yield '/* Field tags (for use in manual encoding/decoding) */\n'
for msg in sort_dependencies(messages):
for field in msg.fields:
@@ -780,6 +850,9 @@ def generate_source(headername, enums, messages, extensions, options):
'''Generate content for a source file.'''
yield '/* Automatically generated nanopb constant definitions */\n'
if options.notimestamp:
yield '/* Generated by %s */\n\n' % (nanopb_version)
else:
yield '/* Generated by %s at %s. */\n\n' % (nanopb_version, time.asctime())
yield options.genformat % (headername)
yield '\n'
@@ -953,8 +1026,8 @@ optparser = OptionParser(
"Output will be written to file.pb.h and file.pb.c.")
optparser.add_option("-x", dest="exclude", metavar="FILE", action="append", default=[],
help="Exclude file from generated #include list.")
optparser.add_option("-e", "--extension", dest="extension", metavar="EXTENSION", default="pb",
help="Set extension to use instead of 'pb' for generated files. [default: %default]")
optparser.add_option("-e", "--extension", dest="extension", metavar="EXTENSION", default=".pb",
help="Set extension to use instead of '.pb' for generated files. [default: %default]")
optparser.add_option("-f", "--options-file", dest="options_file", metavar="FILE", default="%s.options",
help="Set name of a separate generator options file.")
optparser.add_option("-Q", "--generated-include-format", dest="genformat",
@@ -963,6 +1036,8 @@ optparser.add_option("-Q", "--generated-include-format", dest="genformat",
optparser.add_option("-L", "--library-include-format", dest="libformat",
metavar="FORMAT", default='#include <%s>\n',
help="Set format string to use for including the nanopb pb.h header. [default: %default]")
optparser.add_option("-T", "--no-timestamp", dest="notimestamp", action="store_true", default=False,
help="Don't add timestamp to .pb.h and .pb.c preambles")
optparser.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False,
help="Don't print anything except errors.")
optparser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
@@ -992,11 +1067,13 @@ def process_file(filename, fdesc, options):
fdesc = descriptor.FileDescriptorSet.FromString(data).file[0]
# Check if there is a separate .options file
had_abspath = False
try:
optfilename = options.options_file % os.path.splitext(filename)[0]
except TypeError:
# No %s specified, use the filename as-is
optfilename = options.options_file
had_abspath = True
if os.path.isfile(optfilename):
if options.verbose:
@@ -1004,7 +1081,14 @@ def process_file(filename, fdesc, options):
Globals.separate_options = read_options_file(open(optfilename, "rU"))
else:
# If we are given a full filename and it does not exist, give an error.
# However, don't give error when we automatically look for .options file
# with the same name as .proto.
if options.verbose or had_abspath:
sys.stderr.write('Options file not found: ' + optfilename)
Globals.separate_options = []
Globals.matched_namemasks = set()
# Parse the file
@@ -1013,8 +1097,8 @@ def process_file(filename, fdesc, options):
# Decide the file names
noext = os.path.splitext(filename)[0]
headername = noext + '.' + options.extension + '.h'
sourcename = noext + '.' + options.extension + '.c'
headername = noext + options.extension + '.h'
sourcename = noext + options.extension + '.c'
headerbasename = os.path.basename(headername)
# List of .proto files that should not be included in the C header file

View File

@@ -37,6 +37,9 @@ message NanoPBOptions {
// Note: this cannot be used on CPUs that break on unaligned
// accesses to variables.
optional bool packed_struct = 5 [default = false];
// Skip this message
optional bool skip_message = 6 [default = false];
}
// Extensions to protoc 'Descriptor' type in order to define options

6
pb.h
View File

@@ -46,7 +46,7 @@
/* Version of the nanopb library. Just in case you want to check it in
* your own program. */
#define NANOPB_VERSION nanopb-0.2.7
#define NANOPB_VERSION nanopb-0.2.9.1
/* Include all the system headers needed by nanopb. You will need the
* definitions of the following:
@@ -80,8 +80,8 @@
# define PB_PACKED_STRUCT_START
# define PB_PACKED_STRUCT_END
# define pb_packed __attribute__((packed))
#elif defined(__ICCARM__)
/* For IAR ARM compiler */
#elif defined(__ICCARM__) || defined(__CC_ARM)
/* For IAR ARM and Keil MDK-ARM compilers */
# define PB_PACKED_STRUCT_START _Pragma("pack(push, 1)")
# define PB_PACKED_STRUCT_END _Pragma("pack(pop)")
# define pb_packed

View File

@@ -57,6 +57,11 @@ static bool checkreturn pb_dec_submessage(pb_istream_t *stream, const pb_field_t
static bool checkreturn pb_skip_varint(pb_istream_t *stream);
static bool checkreturn pb_skip_string(pb_istream_t *stream);
#ifdef PB_ENABLE_MALLOC
static bool checkreturn allocate_field(pb_istream_t *stream, void *pData, size_t data_size, size_t array_size);
static void pb_release_single_field(const pb_field_iterator_t *iter);
#endif
/* --- Function pointers to field decoders ---
* Order in the array must match pb_action_t LTYPE numbering.
*/
@@ -470,16 +475,37 @@ static bool checkreturn decode_static_field(pb_istream_t *stream, pb_wire_type_t
#ifdef PB_ENABLE_MALLOC
/* Allocate storage for the field and store the pointer at iter->pData.
* array_size is the number of entries to reserve in an array. */
* array_size is the number of entries to reserve in an array.
* Zero size is not allowed, use pb_free() for releasing.
*/
static bool checkreturn allocate_field(pb_istream_t *stream, void *pData, size_t data_size, size_t array_size)
{
void *ptr = *(void**)pData;
size_t size = array_size * data_size;
if (data_size == 0 || array_size == 0)
PB_RETURN_ERROR(stream, "invalid size");
/* Check for multiplication overflows.
* This code avoids the costly division if the sizes are small enough.
* Multiplication is safe as long as only half of bits are set
* in either multiplicand.
*/
{
const size_t check_limit = (size_t)1 << (sizeof(size_t) * 4);
if (data_size >= check_limit || array_size >= check_limit)
{
const size_t size_max = (size_t)-1;
if (size_max / array_size < data_size)
{
PB_RETURN_ERROR(stream, "size too large");
}
}
}
/* Allocate new or expand previous allocation */
/* Note: on failure the old pointer will remain in the structure,
* the message must be freed by caller also on error return. */
ptr = pb_realloc(ptr, size);
ptr = pb_realloc(ptr, array_size * data_size);
if (ptr == NULL)
PB_RETURN_ERROR(stream, "realloc failed");
@@ -519,6 +545,13 @@ static bool checkreturn decode_pointer_field(pb_istream_t *stream, pb_wire_type_
{
case PB_HTYPE_REQUIRED:
case PB_HTYPE_OPTIONAL:
if (PB_LTYPE(type) == PB_LTYPE_SUBMESSAGE &&
*(void**)iter->pData != NULL)
{
/* Duplicate field, have to release the old allocation first. */
pb_release_single_field(iter);
}
if (PB_LTYPE(type) == PB_LTYPE_STRING ||
PB_LTYPE(type) == PB_LTYPE_BYTES)
{
@@ -918,19 +951,10 @@ bool pb_decode_delimited(pb_istream_t *stream, const pb_field_t fields[], void *
}
#ifdef PB_ENABLE_MALLOC
void pb_release(const pb_field_t fields[], void *dest_struct)
static void pb_release_single_field(const pb_field_iterator_t *iter)
{
pb_field_iterator_t iter;
pb_field_init(&iter, fields, dest_struct);
do
{
pb_type_t type;
type = iter.pos->type;
/* Avoid crash on empty message types (zero fields) */
if (iter.pos->tag == 0)
continue;
type = iter->pos->type;
if (PB_ATYPE(type) == PB_ATYPE_POINTER)
{
@@ -939,36 +963,54 @@ void pb_release(const pb_field_t fields[], void *dest_struct)
PB_LTYPE(type) == PB_LTYPE_BYTES))
{
/* Release entries in repeated string or bytes array */
void **pItem = *(void***)iter.pData;
size_t count = *(size_t*)iter.pSize;
void **pItem = *(void***)iter->pData;
pb_size_t count = *(pb_size_t*)iter->pSize;
while (count--)
{
pb_free(*pItem);
*pItem++ = NULL;
}
*(pb_size_t*)iter->pSize = 0;
}
else if (PB_LTYPE(type) == PB_LTYPE_SUBMESSAGE)
{
/* Release fields in submessages */
void *pItem = *(void**)iter.pData;
size_t count = (pItem ? 1 : 0);
void *pItem = *(void**)iter->pData;
if (pItem)
{
pb_size_t count = 1;
if (PB_HTYPE(type) == PB_HTYPE_REPEATED)
{
count = *(size_t*)iter.pSize;
count = *(pb_size_t*)iter->pSize;
*(pb_size_t*)iter->pSize = 0;
}
while (count--)
{
pb_release((const pb_field_t*)iter.pos->ptr, pItem);
pItem = (uint8_t*)pItem + iter.pos->data_size;
pb_release((const pb_field_t*)iter->pos->ptr, pItem);
pItem = (uint8_t*)pItem + iter->pos->data_size;
}
}
}
/* Release main item */
pb_free(*(void**)iter.pData);
*(void**)iter.pData = NULL;
pb_free(*(void**)iter->pData);
*(void**)iter->pData = NULL;
}
}
void pb_release(const pb_field_t fields[], void *dest_struct)
{
pb_field_iterator_t iter;
pb_field_init(&iter, fields, dest_struct);
if (iter.pos->tag == 0)
return; /* Empty message type */
do
{
pb_release_single_field(&iter);
} while (pb_field_next(&iter));
}
#endif
@@ -1096,29 +1138,35 @@ static bool checkreturn pb_dec_fixed64(pb_istream_t *stream, const pb_field_t *f
static bool checkreturn pb_dec_bytes(pb_istream_t *stream, const pb_field_t *field, void *dest)
{
uint32_t size;
size_t alloc_size;
pb_bytes_array_t *bdest;
if (!pb_decode_varint32(stream, &size))
return false;
alloc_size = PB_BYTES_ARRAY_T_ALLOCSIZE(size);
if (size > alloc_size)
PB_RETURN_ERROR(stream, "size too large");
if (PB_ATYPE(field->type) == PB_ATYPE_POINTER)
{
#ifndef PB_ENABLE_MALLOC
PB_RETURN_ERROR(stream, "no malloc support");
#else
if (!allocate_field(stream, dest, PB_BYTES_ARRAY_T_ALLOCSIZE(size), 1))
if (!allocate_field(stream, dest, alloc_size, 1))
return false;
bdest = *(pb_bytes_array_t**)dest;
#endif
}
else
{
if (PB_BYTES_ARRAY_T_ALLOCSIZE(size) > field->data_size)
if (alloc_size > field->data_size)
PB_RETURN_ERROR(stream, "bytes overflow");
bdest = (pb_bytes_array_t*)dest;
}
bdest->size = size;
return pb_read(stream, bdest->bytes, size);
}
@@ -1133,6 +1181,9 @@ static bool checkreturn pb_dec_string(pb_istream_t *stream, const pb_field_t *fi
/* Space for null terminator */
alloc_size = size + 1;
if (alloc_size < size)
PB_RETURN_ERROR(stream, "size too large");
if (PB_ATYPE(field->type) == PB_ATYPE_POINTER)
{
#ifndef PB_ENABLE_MALLOC

View File

@@ -19,8 +19,8 @@ env = Environment(ENV = os.environ, tools = ['default', 'nanopb'])
# Allow overriding the compiler with scons CC=???
if 'CC' in ARGUMENTS: env.Replace(CC = ARGUMENTS['CC'])
if 'CXX' in ARGUMENTS: env.Replace(CXX = ARGUMENTS['CXX'])
if 'CFLAGS' in ARGUMENTS: env.Append(CCFLAGS = ARGUMENTS['CFLAGS'])
if 'CXXFLAGS' in ARGUMENTS: env.Append(CCFLAGS = ARGUMENTS['CXXFLAGS'])
if 'CCFLAGS' in ARGUMENTS: env.Append(CCFLAGS = ARGUMENTS['CCFLAGS'])
if 'CXXFLAGS' in ARGUMENTS: env.Append(CXXFLAGS = ARGUMENTS['CXXFLAGS'])
# Add the builders defined in site_init.py
add_nanopb_builders(env)
@@ -33,13 +33,16 @@ env.Append(PROTOCPATH = '#../generator')
# Check the compilation environment, unless we are just cleaning up.
if not env.GetOption('clean'):
def check_ccflags(context, flags):
def check_ccflags(context, flags, linkflags = ''):
'''Check if given CCFLAGS are supported'''
context.Message('Checking support for CCFLAGS="%s"... ' % flags)
oldflags = context.env['CCFLAGS']
oldlinkflags = context.env['CCFLAGS']
context.env.Append(CCFLAGS = flags)
context.env.Append(LINKFLAGS = linkflags)
result = context.TryCompile("int main() {return 0;}", '.c')
context.env.Replace(CCFLAGS = oldflags)
context.env.Replace(LINKFLAGS = oldlinkflags)
context.Result(result)
return result
@@ -54,6 +57,7 @@ if not env.GetOption('clean'):
if not stdbool or not stdint or not stddef or not string:
conf.env.Append(CPPDEFINES = {'PB_SYSTEM_HEADER': '\\"pb_syshdr.h\\"'})
conf.env.Append(CPPPATH = "#../extra")
conf.env.Append(SYSHDR = '\\"pb_syshdr.h\\"')
if stdbool: conf.env.Append(CPPDEFINES = {'HAVE_STDBOOL_H': 1})
if stdint: conf.env.Append(CPPDEFINES = {'HAVE_STDINT_H': 1})
@@ -83,6 +87,13 @@ if not env.GetOption('clean'):
if conf.CheckCCFLAGS(extra):
conf.env.Append(CORECFLAGS = extra)
# Check if we can use undefined behaviour sanitizer (only with clang)
extra = '-fsanitize=undefined '
if 'clang' in env['CC']:
if conf.CheckCCFLAGS(extra, linkflags = extra):
conf.env.Append(CORECFLAGS = extra)
conf.env.Append(LINKFLAGS = extra)
# End the config stuff
env = conf.Finish()
@@ -91,7 +102,7 @@ if 'gcc' in env['CC']:
# GNU Compiler Collection
# Debug info, warnings as errors
env.Append(CFLAGS = '-ansi -pedantic -g -O0 -Wall -Werror -fprofile-arcs -ftest-coverage -fstack-protector-all')
env.Append(CFLAGS = '-ansi -pedantic -g -Wall -Werror -fprofile-arcs -ftest-coverage ')
env.Append(CORECFLAGS = '-Wextra')
env.Append(LINKFLAGS = '-g --coverage')
@@ -99,7 +110,7 @@ if 'gcc' in env['CC']:
env.Append(CFLAGS = '-Wno-long-long')
elif 'clang' in env['CC']:
# CLang
env.Append(CFLAGS = '-ansi -g -O0 -Wall -Werror')
env.Append(CFLAGS = '-ansi -g -Wall -Werror')
env.Append(CORECFLAGS = ' -Wextra -Wcast-qual -Wconversion')
elif 'cl' in env['CC']:
# Microsoft Visual C++
@@ -119,10 +130,10 @@ elif 'tcc' in env['CC']:
env.SetDefault(CORECFLAGS = '')
if 'clang++' in env['CXX']:
env.Append(CXXFLAGS = '-g -O0 -Wall -Werror -Wextra -Wno-missing-field-initializers')
elif 'g++' in env['CXX']:
env.Append(CXXFLAGS = '-g -O0 -Wall -Werror -Wextra -Wno-missing-field-initializers')
if 'clang' in env['CXX']:
env.Append(CXXFLAGS = '-g -Wall -Werror -Wextra -Wno-missing-field-initializers')
elif 'g++' in env['CXX'] or 'gcc' in env['CXX']:
env.Append(CXXFLAGS = '-g -Wall -Werror -Wextra -Wno-missing-field-initializers')
elif 'cl' in env['CXX']:
env.Append(CXXFLAGS = '/Zi /W2 /WX')
@@ -131,6 +142,6 @@ import os.path
env['VARIANT_DIR'] = 'build'
env['BUILD'] = '#' + env['VARIANT_DIR']
env['COMMON'] = '#' + env['VARIANT_DIR'] + '/common'
for subdir in Glob('*/SConscript'):
for subdir in Glob('*/SConscript') + Glob('regression/*/SConscript'):
SConscript(subdir, exports = 'env', variant_dir = env['VARIANT_DIR'] + '/' + os.path.dirname(str(subdir)))

View File

@@ -19,10 +19,12 @@
the decoding and checks the fields. */
bool check_alltypes(pb_istream_t *stream, int mode)
{
AllTypes alltypes;
/* Uses _init_default to just make sure that it works. */
AllTypes alltypes = AllTypes_init_default;
/* Fill with garbage to better detect initialization errors */
memset(&alltypes, 0xAA, sizeof(alltypes));
alltypes.extensions = 0;
if (!pb_decode(stream, AllTypes_fields, &alltypes))
return false;

View File

@@ -13,7 +13,7 @@ int main(int argc, char **argv)
int mode = (argc > 1) ? atoi(argv[1]) : 0;
/* Initialize the structure with constants */
AllTypes alltypes = {0};
AllTypes alltypes = AllTypes_init_zero;
alltypes.req_int32 = -1001;
alltypes.req_int64 = -1002;

View File

@@ -220,6 +220,7 @@ bool check_alltypes(pb_istream_t *stream, int mode)
/* Fill with garbage to better detect initialization errors */
memset(&alltypes, 0xAA, sizeof(alltypes));
alltypes.extensions = 0;
alltypes.req_int32.funcs.decode = &read_varint;
alltypes.req_int32.arg = (void*)-1001;

View File

@@ -19,6 +19,7 @@ bool check_alltypes(pb_istream_t *stream, int mode)
/* Fill with garbage to better detect initialization errors */
memset(&alltypes, 0xAA, sizeof(alltypes));
alltypes.extensions = 0;
if (!pb_decode(stream, AllTypes_fields, &alltypes))
return false;

View File

@@ -16,7 +16,7 @@
bool print_person(pb_istream_t *stream)
{
int i;
Person person;
Person person = Person_init_zero;
if (!pb_decode(stream, Person_fields, &person))
return false;

View File

@@ -12,7 +12,7 @@
bool print_person(pb_istream_t *stream)
{
int i;
Person person;
Person person = Person_init_zero;
if (!pb_decode(stream, Person_fields, &person))
return false;

View File

@@ -1,4 +1,5 @@
/* This includes the whole .c file to get access to static functions. */
#define PB_ENABLE_MALLOC
#include "pb_decode.c"
#include <stdio.h>
@@ -299,6 +300,28 @@ int main()
dest.submsg.data_count == 5)
}
{
pb_istream_t s = {0};
void *data = NULL;
COMMENT("Testing allocate_field")
TEST(allocate_field(&s, &data, 10, 10) && data != NULL);
TEST(allocate_field(&s, &data, 10, 20) && data != NULL);
{
void *oldvalue = data;
size_t very_big = (size_t)-1;
size_t somewhat_big = very_big / 2 + 1;
size_t not_so_big = (size_t)1 << (4 * sizeof(size_t));
TEST(!allocate_field(&s, &data, very_big, 2) && data == oldvalue);
TEST(!allocate_field(&s, &data, somewhat_big, 2) && data == oldvalue);
TEST(!allocate_field(&s, &data, not_so_big, not_so_big) && data == oldvalue);
}
pb_free(data);
}
if (status != 0)
fprintf(stdout, "\n\nSome tests FAILED!\n");

View File

@@ -107,5 +107,7 @@ message AllTypes {
// Just to make sure that the size of the fields has been calculated
// properly, i.e. otherwise a bug in last field might not be detected.
required int32 end = 10099;
extensions 200 to 255;
}

View File

@@ -107,5 +107,7 @@ message AllTypes {
// Just to make sure that the size of the fields has been calculated
// properly, i.e. otherwise a bug in last field might not be detected.
required int32 end = 13432099;
extensions 200 to 255;
}

55
tests/fuzztest/SConscript Normal file
View File

@@ -0,0 +1,55 @@
# Run a fuzz test to verify robustness against corrupted/malicious data.
Import("env")
# We need our own pb_decode.o for the malloc support
env = env.Clone()
env.Append(CPPDEFINES = {'PB_ENABLE_MALLOC': 1,
'PB_SYSTEM_HEADER': '\\"fuzz_syshdr.h\\"'})
env.Append(CPPPATH = ".")
if 'SYSHDR' in env:
env.Append(CPPDEFINES = {'PB_OLD_SYSHDR': env['SYSHDR']})
# Disable libmudflap, because it will confuse valgrind
# and other memory leak detection tools.
if '-fmudflap' in env["CCFLAGS"]:
env["CCFLAGS"].remove("-fmudflap")
env["LINKFLAGS"].remove("-fmudflap")
env["LIBS"].remove("mudflap")
strict = env.Clone()
strict.Append(CFLAGS = strict['CORECFLAGS'])
strict.Object("pb_decode_with_malloc.o", "$NANOPB/pb_decode.c")
strict.Object("pb_encode_with_malloc.o", "$NANOPB/pb_encode.c")
# We want both pointer and static versions of the AllTypes message
env.Command("alltypes_static.proto", "#alltypes/alltypes.proto",
lambda target, source, env:
open(str(target[0]), 'w').write("package alltypes_static;\n"
+ open(str(source[0])).read()))
env.Command("alltypes_pointer.proto", "#alltypes/alltypes.proto",
lambda target, source, env:
open(str(target[0]), 'w').write("package alltypes_pointer;\n"
+ open(str(source[0])).read()))
p1 = env.NanopbProto(["alltypes_pointer", "alltypes_pointer.options"])
p2 = env.NanopbProto(["alltypes_static", "alltypes_static.options"])
fuzz = env.Program(["fuzztest.c",
"alltypes_pointer.pb.c",
"alltypes_static.pb.c",
"pb_encode_with_malloc.o",
"pb_decode_with_malloc.o",
"malloc_wrappers.c"])
Depends([p1, p2, fuzz], ["fuzz_syshdr.h", "malloc_wrappers.h"])
env.RunTest(fuzz)
fuzzstub = env.Program(["fuzzstub.c",
"alltypes_pointer.pb.c",
"alltypes_static.pb.c",
"pb_encode_with_malloc.o",
"pb_decode_with_malloc.o",
"malloc_wrappers.c"])

View File

@@ -0,0 +1,3 @@
# Generate all fields as pointers.
* type:FT_POINTER

View File

@@ -0,0 +1,3 @@
* max_size:32
* max_count:8
*.extensions type:FT_IGNORE

View File

@@ -0,0 +1,15 @@
/* This is just a wrapper in order to get our own malloc wrappers into nanopb core. */
#define pb_realloc(ptr,size) counting_realloc(ptr,size)
#define pb_free(ptr) counting_free(ptr)
#ifdef PB_OLD_SYSHDR
#include PB_OLD_SYSHDR
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#include <malloc_wrappers.h>

189
tests/fuzztest/fuzzstub.c Normal file
View File

@@ -0,0 +1,189 @@
/* Fuzz testing for the nanopb core.
* This can be used with external fuzzers, e.g. radamsa.
* It performs most of the same checks as fuzztest, but does not feature data generation.
*/
#include <pb_decode.h>
#include <pb_encode.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include "malloc_wrappers.h"
#include "alltypes_static.pb.h"
#include "alltypes_pointer.pb.h"
#define BUFSIZE 4096
static bool do_static_decode(uint8_t *buffer, size_t msglen, bool assert_success)
{
pb_istream_t stream;
bool status;
alltypes_static_AllTypes *msg = malloc_with_check(sizeof(alltypes_static_AllTypes));
stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg);
if (!status && assert_success)
{
/* Anything that was successfully encoded, should be decodeable.
* One exception: strings without null terminator are encoded up
* to end of buffer, but refused on decode because the terminator
* would not fit. */
if (strcmp(stream.errmsg, "string overflow") != 0)
assert(status);
}
free_with_check(msg);
return status;
}
static bool do_pointer_decode(uint8_t *buffer, size_t msglen, bool assert_success)
{
pb_istream_t stream;
bool status;
alltypes_pointer_AllTypes *msg;
msg = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
memset(msg, 0, sizeof(alltypes_pointer_AllTypes));
stream = pb_istream_from_buffer(buffer, msglen);
assert(get_alloc_count() == 0);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg);
if (assert_success)
assert(status);
pb_release(alltypes_pointer_AllTypes_fields, msg);
assert(get_alloc_count() == 0);
free_with_check(msg);
return status;
}
/* Do a decode -> encode -> decode -> encode roundtrip */
static void do_static_roundtrip(uint8_t *buffer, size_t msglen)
{
bool status;
uint8_t *buf2 = malloc_with_check(BUFSIZE);
uint8_t *buf3 = malloc_with_check(BUFSIZE);
size_t msglen2, msglen3;
alltypes_static_AllTypes *msg1 = malloc_with_check(sizeof(alltypes_static_AllTypes));
alltypes_static_AllTypes *msg2 = malloc_with_check(sizeof(alltypes_static_AllTypes));
memset(msg1, 0, sizeof(alltypes_static_AllTypes));
memset(msg2, 0, sizeof(alltypes_static_AllTypes));
{
pb_istream_t stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg1);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf2, BUFSIZE);
status = pb_encode(&stream, alltypes_static_AllTypes_fields, msg1);
assert(status);
msglen2 = stream.bytes_written;
}
{
pb_istream_t stream = pb_istream_from_buffer(buf2, msglen2);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg2);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf3, BUFSIZE);
status = pb_encode(&stream, alltypes_static_AllTypes_fields, msg2);
assert(status);
msglen3 = stream.bytes_written;
}
assert(msglen2 == msglen3);
assert(memcmp(buf2, buf3, msglen2) == 0);
free_with_check(msg1);
free_with_check(msg2);
free_with_check(buf2);
free_with_check(buf3);
}
/* Do decode -> encode -> decode -> encode roundtrip */
static void do_pointer_roundtrip(uint8_t *buffer, size_t msglen)
{
bool status;
uint8_t *buf2 = malloc_with_check(BUFSIZE);
uint8_t *buf3 = malloc_with_check(BUFSIZE);
size_t msglen2, msglen3;
alltypes_pointer_AllTypes *msg1 = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
alltypes_pointer_AllTypes *msg2 = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
memset(msg1, 0, sizeof(alltypes_pointer_AllTypes));
memset(msg2, 0, sizeof(alltypes_pointer_AllTypes));
{
pb_istream_t stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg1);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf2, BUFSIZE);
status = pb_encode(&stream, alltypes_pointer_AllTypes_fields, msg1);
assert(status);
msglen2 = stream.bytes_written;
}
{
pb_istream_t stream = pb_istream_from_buffer(buf2, msglen2);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg2);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf3, BUFSIZE);
status = pb_encode(&stream, alltypes_pointer_AllTypes_fields, msg2);
assert(status);
msglen3 = stream.bytes_written;
}
assert(msglen2 == msglen3);
assert(memcmp(buf2, buf3, msglen2) == 0);
pb_release(alltypes_pointer_AllTypes_fields, msg1);
pb_release(alltypes_pointer_AllTypes_fields, msg2);
free_with_check(msg1);
free_with_check(msg2);
free_with_check(buf2);
free_with_check(buf3);
}
static void run_iteration()
{
uint8_t *buffer = malloc_with_check(BUFSIZE);
size_t msglen;
bool status;
msglen = fread(buffer, BUFSIZE, 1, stdin);
status = do_static_decode(buffer, msglen, false);
if (status)
do_static_roundtrip(buffer, msglen);
status = do_pointer_decode(buffer, msglen, false);
if (status)
do_pointer_roundtrip(buffer, msglen);
free_with_check(buffer);
}
int main(int argc, char **argv)
{
run_iteration();
return 0;
}

431
tests/fuzztest/fuzztest.c Normal file
View File

@@ -0,0 +1,431 @@
/* Fuzz testing for the nanopb core.
* Attempts to verify all the properties defined in the security model document.
*/
#include <pb_decode.h>
#include <pb_encode.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include "malloc_wrappers.h"
#include "alltypes_static.pb.h"
#include "alltypes_pointer.pb.h"
static uint64_t random_seed;
/* Uses xorshift64 here instead of rand() for both speed and
* reproducibility across platforms. */
static uint32_t rand_word()
{
random_seed ^= random_seed >> 12;
random_seed ^= random_seed << 25;
random_seed ^= random_seed >> 27;
return random_seed * 2685821657736338717ULL;
}
/* Get a random integer in range, with approximately flat distribution. */
static int rand_int(int min, int max)
{
return rand_word() % (max + 1 - min) + min;
}
static bool rand_bool()
{
return rand_word() & 1;
}
/* Get a random byte, with skewed distribution.
* Important corner cases like 0xFF, 0x00 and 0xFE occur more
* often than other values. */
static uint8_t rand_byte()
{
uint32_t w = rand_word();
uint8_t b = w & 0xFF;
if (w & 0x100000)
b >>= (w >> 8) & 7;
if (w & 0x200000)
b <<= (w >> 12) & 7;
if (w & 0x400000)
b ^= 0xFF;
return b;
}
/* Get a random length, with skewed distribution.
* Favors the shorter lengths, but always atleast 1. */
static size_t rand_len(size_t max)
{
uint32_t w = rand_word();
size_t s;
if (w & 0x800000)
w &= 3;
else if (w & 0x400000)
w &= 15;
else if (w & 0x200000)
w &= 255;
s = (w % max);
if (s == 0)
s = 1;
return s;
}
/* Fills a buffer with random data with skewed distribution. */
static void rand_fill(uint8_t *buf, size_t count)
{
while (count--)
*buf++ = rand_byte();
}
/* Fill with random protobuf-like data */
static size_t rand_fill_protobuf(uint8_t *buf, size_t min_bytes, size_t max_bytes, int min_tag)
{
pb_ostream_t stream = pb_ostream_from_buffer(buf, max_bytes);
while(stream.bytes_written < min_bytes)
{
pb_wire_type_t wt = rand_int(0, 3);
if (wt == 3) wt = 5; /* Gap in values */
if (!pb_encode_tag(&stream, wt, rand_int(min_tag, min_tag + 512)))
break;
if (wt == PB_WT_VARINT)
{
uint64_t value;
rand_fill((uint8_t*)&value, sizeof(value));
pb_encode_varint(&stream, value);
}
else if (wt == PB_WT_64BIT)
{
uint64_t value;
rand_fill((uint8_t*)&value, sizeof(value));
pb_encode_fixed64(&stream, &value);
}
else if (wt == PB_WT_32BIT)
{
uint32_t value;
rand_fill((uint8_t*)&value, sizeof(value));
pb_encode_fixed32(&stream, &value);
}
else if (wt == PB_WT_STRING)
{
size_t len;
uint8_t *buf;
if (min_bytes > stream.bytes_written)
len = rand_len(min_bytes - stream.bytes_written);
else
len = 0;
buf = malloc(len);
pb_encode_varint(&stream, len);
rand_fill(buf, len);
pb_write(&stream, buf, len);
free(buf);
}
}
return stream.bytes_written;
}
/* Given a buffer of data, mess it up a bit */
static void rand_mess(uint8_t *buf, size_t count)
{
int m = rand_int(0, 3);
if (m == 0)
{
/* Replace random substring */
int s = rand_int(0, count - 1);
int l = rand_len(count - s);
rand_fill(buf + s, l);
}
else if (m == 1)
{
/* Swap random bytes */
int a = rand_int(0, count - 1);
int b = rand_int(0, count - 1);
int x = buf[a];
buf[a] = buf[b];
buf[b] = x;
}
else if (m == 2)
{
/* Duplicate substring */
int s = rand_int(0, count - 2);
int l = rand_len((count - s) / 2);
memcpy(buf + s + l, buf + s, l);
}
else if (m == 3)
{
/* Add random protobuf noise */
int s = rand_int(0, count - 1);
int l = rand_len(count - s);
rand_fill_protobuf(buf + s, l, count - s, 1);
}
}
/* Some default data to put in the message */
static const alltypes_static_AllTypes initval = alltypes_static_AllTypes_init_default;
#define BUFSIZE 4096
static bool do_static_encode(uint8_t *buffer, size_t *msglen)
{
pb_ostream_t stream;
bool status;
/* Allocate a message and fill it with defaults */
alltypes_static_AllTypes *msg = malloc_with_check(sizeof(alltypes_static_AllTypes));
memcpy(msg, &initval, sizeof(initval));
/* Apply randomness to the data before encoding */
while (rand_int(0, 7))
rand_mess((uint8_t*)msg, sizeof(alltypes_static_AllTypes));
stream = pb_ostream_from_buffer(buffer, BUFSIZE);
status = pb_encode(&stream, alltypes_static_AllTypes_fields, msg);
assert(stream.bytes_written <= BUFSIZE);
assert(stream.bytes_written <= alltypes_static_AllTypes_size);
*msglen = stream.bytes_written;
free_with_check(msg);
return status;
}
/* Append or prepend protobuf noise */
static void do_protobuf_noise(uint8_t *buffer, size_t *msglen)
{
int m = rand_int(0, 2);
size_t max_size = BUFSIZE - 32 - *msglen;
if (m == 1)
{
/* Prepend */
uint8_t *tmp = malloc_with_check(BUFSIZE);
size_t s = rand_fill_protobuf(tmp, rand_len(max_size), BUFSIZE - *msglen, 512);
memmove(buffer + s, buffer, *msglen);
memcpy(buffer, tmp, s);
free_with_check(tmp);
*msglen += s;
}
else if (m == 2)
{
/* Append */
size_t s = rand_fill_protobuf(buffer + *msglen, rand_len(max_size), BUFSIZE - *msglen, 512);
*msglen += s;
}
}
static bool do_static_decode(uint8_t *buffer, size_t msglen, bool assert_success)
{
pb_istream_t stream;
bool status;
alltypes_static_AllTypes *msg = malloc_with_check(sizeof(alltypes_static_AllTypes));
rand_fill((uint8_t*)msg, sizeof(alltypes_static_AllTypes));
stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg);
if (!status && assert_success)
{
/* Anything that was successfully encoded, should be decodeable.
* One exception: strings without null terminator are encoded up
* to end of buffer, but refused on decode because the terminator
* would not fit. */
if (strcmp(stream.errmsg, "string overflow") != 0)
assert(status);
}
free_with_check(msg);
return status;
}
static bool do_pointer_decode(uint8_t *buffer, size_t msglen, bool assert_success)
{
pb_istream_t stream;
bool status;
alltypes_pointer_AllTypes *msg;
msg = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
memset(msg, 0, sizeof(alltypes_pointer_AllTypes));
stream = pb_istream_from_buffer(buffer, msglen);
assert(get_alloc_count() == 0);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg);
if (assert_success)
assert(status);
pb_release(alltypes_pointer_AllTypes_fields, msg);
assert(get_alloc_count() == 0);
free_with_check(msg);
return status;
}
/* Do a decode -> encode -> decode -> encode roundtrip */
static void do_static_roundtrip(uint8_t *buffer, size_t msglen)
{
bool status;
uint8_t *buf2 = malloc_with_check(BUFSIZE);
uint8_t *buf3 = malloc_with_check(BUFSIZE);
size_t msglen2, msglen3;
alltypes_static_AllTypes *msg1 = malloc_with_check(sizeof(alltypes_static_AllTypes));
alltypes_static_AllTypes *msg2 = malloc_with_check(sizeof(alltypes_static_AllTypes));
memset(msg1, 0, sizeof(alltypes_static_AllTypes));
memset(msg2, 0, sizeof(alltypes_static_AllTypes));
{
pb_istream_t stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg1);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf2, BUFSIZE);
status = pb_encode(&stream, alltypes_static_AllTypes_fields, msg1);
assert(status);
msglen2 = stream.bytes_written;
}
{
pb_istream_t stream = pb_istream_from_buffer(buf2, msglen2);
status = pb_decode(&stream, alltypes_static_AllTypes_fields, msg2);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf3, BUFSIZE);
status = pb_encode(&stream, alltypes_static_AllTypes_fields, msg2);
assert(status);
msglen3 = stream.bytes_written;
}
assert(msglen2 == msglen3);
assert(memcmp(buf2, buf3, msglen2) == 0);
free_with_check(msg1);
free_with_check(msg2);
free_with_check(buf2);
free_with_check(buf3);
}
/* Do decode -> encode -> decode -> encode roundtrip */
static void do_pointer_roundtrip(uint8_t *buffer, size_t msglen)
{
bool status;
uint8_t *buf2 = malloc_with_check(BUFSIZE);
uint8_t *buf3 = malloc_with_check(BUFSIZE);
size_t msglen2, msglen3;
alltypes_pointer_AllTypes *msg1 = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
alltypes_pointer_AllTypes *msg2 = malloc_with_check(sizeof(alltypes_pointer_AllTypes));
memset(msg1, 0, sizeof(alltypes_pointer_AllTypes));
memset(msg2, 0, sizeof(alltypes_pointer_AllTypes));
{
pb_istream_t stream = pb_istream_from_buffer(buffer, msglen);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg1);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf2, BUFSIZE);
status = pb_encode(&stream, alltypes_pointer_AllTypes_fields, msg1);
assert(status);
msglen2 = stream.bytes_written;
}
{
pb_istream_t stream = pb_istream_from_buffer(buf2, msglen2);
status = pb_decode(&stream, alltypes_pointer_AllTypes_fields, msg2);
assert(status);
}
{
pb_ostream_t stream = pb_ostream_from_buffer(buf3, BUFSIZE);
status = pb_encode(&stream, alltypes_pointer_AllTypes_fields, msg2);
assert(status);
msglen3 = stream.bytes_written;
}
assert(msglen2 == msglen3);
assert(memcmp(buf2, buf3, msglen2) == 0);
pb_release(alltypes_pointer_AllTypes_fields, msg1);
pb_release(alltypes_pointer_AllTypes_fields, msg2);
free_with_check(msg1);
free_with_check(msg2);
free_with_check(buf2);
free_with_check(buf3);
}
static void run_iteration()
{
uint8_t *buffer = malloc_with_check(BUFSIZE);
size_t msglen;
bool status;
rand_fill(buffer, BUFSIZE);
if (do_static_encode(buffer, &msglen))
{
do_protobuf_noise(buffer, &msglen);
status = do_static_decode(buffer, msglen, true);
if (status)
do_static_roundtrip(buffer, msglen);
status = do_pointer_decode(buffer, msglen, true);
if (status)
do_pointer_roundtrip(buffer, msglen);
/* Apply randomness to the encoded data */
while (rand_bool())
rand_mess(buffer, BUFSIZE);
/* Apply randomness to encoded data length */
if (rand_bool())
msglen = rand_int(0, BUFSIZE);
status = do_static_decode(buffer, msglen, false);
do_pointer_decode(buffer, msglen, status);
if (status)
{
do_static_roundtrip(buffer, msglen);
do_pointer_roundtrip(buffer, msglen);
}
}
free_with_check(buffer);
}
int main(int argc, char **argv)
{
int i;
if (argc > 1)
{
random_seed = atol(argv[1]);
}
else
{
random_seed = time(NULL);
}
fprintf(stderr, "Random seed: %llu\n", (long long unsigned)random_seed);
for (i = 0; i < 10000; i++)
{
run_iteration();
}
return 0;
}

View File

@@ -0,0 +1,54 @@
#include "malloc_wrappers.h"
#include <stdint.h>
#include <assert.h>
#include <string.h>
static size_t alloc_count = 0;
/* Allocate memory and place check values before and after. */
void* malloc_with_check(size_t size)
{
size_t size32 = (size + 3) / 4 + 3;
uint32_t *buf = malloc(size32 * sizeof(uint32_t));
buf[0] = size32;
buf[1] = 0xDEADBEEF;
buf[size32 - 1] = 0xBADBAD;
return buf + 2;
}
/* Free memory allocated with malloc_with_check() and do the checks. */
void free_with_check(void *mem)
{
uint32_t *buf = (uint32_t*)mem - 2;
assert(buf[1] == 0xDEADBEEF);
assert(buf[buf[0] - 1] == 0xBADBAD);
free(buf);
}
/* Track memory usage */
void* counting_realloc(void *ptr, size_t size)
{
/* Don't allocate crazy amounts of RAM when fuzzing */
if (size > 1000000)
return NULL;
if (!ptr && size)
alloc_count++;
return realloc(ptr, size);
}
void counting_free(void *ptr)
{
if (ptr)
{
assert(alloc_count > 0);
alloc_count--;
free(ptr);
}
}
size_t get_alloc_count()
{
return alloc_count;
}

View File

@@ -0,0 +1,7 @@
#include <stdlib.h>
void* malloc_with_check(size_t size);
void free_with_check(void *mem);
void* counting_realloc(void *ptr, size_t size);
void counting_free(void *ptr);
size_t get_alloc_count();

12
tests/fuzztest/run_radamsa.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
TMP=`tempfile`
echo $TMP
while true
do
radamsa sample_data/* > $TMP
$1 < $TMP
test $? -gt 127 && break
done

Binary file not shown.

Binary file not shown.

View File

@@ -8,6 +8,7 @@
int main()
{
uint8_t buffer[512];
size_t size;
/* Create a message with one missing field */
{
@@ -19,12 +20,14 @@ int main()
printf("Encode failed.\n");
return 1;
}
size = stream.bytes_written;
}
/* Test that it decodes properly if we don't require that field */
{
MissingField msg = {0};
pb_istream_t stream = pb_istream_from_buffer(buffer, sizeof(buffer));
pb_istream_t stream = pb_istream_from_buffer(buffer, size);
if (!pb_decode(&stream, MissingField_fields, &msg))
{
@@ -36,7 +39,7 @@ int main()
/* Test that it does *not* decode properly if we require the field */
{
AllFields msg = {0};
pb_istream_t stream = pb_istream_from_buffer(buffer, sizeof(buffer));
pb_istream_t stream = pb_istream_from_buffer(buffer, size);
if (pb_decode(&stream, AllFields_fields, &msg))
{

View File

@@ -5,3 +5,6 @@ pb_callback_t int32_callback;
\sEnumValue1 = 1
Message5_EnumValue1
} pb_packed my_packed_struct;
! skipped_field
! SkippedMessage

View File

@@ -63,11 +63,15 @@ message my_packed_struct
}
// Message with ignored field
// Note: doesn't really test if the field is missing in the output,
// but atleast tests that the output compiles.
message Message6
{
required int32 field1 = 1;
optional int32 field2 = 2 [(nanopb).type = FT_IGNORE];
optional int32 skipped_field = 2 [(nanopb).type = FT_IGNORE];
}
// Message that is skipped
message SkippedMessage
{
option (nanopb_msgopt).skip_message = true;
required int32 foo = 1;
}

View File

@@ -0,0 +1,9 @@
# Regression test for Issue 125: Wrong identifier name for extension fields
Import("env")
env.NanopbProto(["extensionbug", "extensionbug.options"])
env.Object('extensionbug.pb.c')
env.Match(['extensionbug.pb.h', 'extensionbug.expected'])

View File

@@ -0,0 +1,3 @@
pb_extension_type_t Message2_extras
uint32_t field2

View File

@@ -0,0 +1,4 @@
* type:FT_IGNORE
Message2.extras type:FT_STATIC
Message2.field2 type:FT_STATIC

View File

@@ -0,0 +1,16 @@
message Message1
{
optional uint32 fieldA = 1;
extensions 30 to max;
}
message Message2
{
extend Message1
{
optional Message2 extras = 30;
}
optional uint32 field1 = 1;
optional uint32 field2 = 2;
}

View File

@@ -85,9 +85,20 @@ def add_nanopb_builders(env):
data = open(str(source[0]), 'rU').read()
patterns = open(str(source[1]))
for pattern in patterns:
if pattern.strip() and not re.search(pattern.strip(), data, re.MULTILINE):
if pattern.strip():
invert = False
if pattern.startswith('! '):
invert = True
pattern = pattern[2:]
status = re.search(pattern.strip(), data, re.MULTILINE)
if not status and not invert:
print '\033[31m[FAIL]\033[0m Pattern not found in ' + str(source[0]) + ': ' + pattern
return 1
elif status and invert:
print '\033[31m[FAIL]\033[0m Pattern should not exist, but does in ' + str(source[0]) + ': ' + pattern
return 1
else:
print '\033[32m[ OK ]\033[0m All patterns found in ' + str(source[0])
return 0