stephane / libmodbus

A Modbus library for Linux, Mac OS, FreeBSD and Windows
http://libmodbus.org
GNU Lesser General Public License v2.1
3.3k stars 1.71k forks source link

modbus_new_tcp_pi allocates way more memory than modbus_new_tcp and has arbitrary length limits on its string arguments #621

Closed psychon closed 1 year ago

psychon commented 2 years ago

Hi!

libmodbus version

Sure, but I am not using any version:

$ pkg-config --modversion libmodbus
Package libmodbus was not found in the pkg-config search path.
Perhaps you should add the directory containing `libmodbus.pc'
to the PKG_CONFIG_PATH environment variable
Package 'libmodbus', required by 'virtual:world', not found

OS and/or distribution

$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux bookworm/sid"
NAME="Debian GNU/Linux"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

Environment

Since you apparently want to know my architecture, here's my CPU:

Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz

Description

The "protocol independent" TCP code uses this struct:

https://github.com/stephane/libmodbus/blob/3da2d01916ef118aba30a1951d89e0ca0f90e2f7/src/modbus-tcp-private.h#L30-L42

This has arbitrary length limits of 1024 bytes of the node length and 31 bytes for the service.

Also, this allocates more than 1 KiB of memory even when I just provide 10 characters. That is 99% wasted memory for storing two short strings.

The implementation returns an error if these limits are reached:

https://github.com/stephane/libmodbus/blob/3da2d01916ef118aba30a1951d89e0ca0f90e2f7/src/modbus-tcp.c#L896-L897

https://github.com/stephane/libmodbus/blob/3da2d01916ef118aba30a1951d89e0ca0f90e2f7/src/modbus-tcp.c#L919-L920

Expected behaviour

Less memory usage and no arbitrary limits.

Actual behaviour

This section doesn't really fit this report, so I will instead use this for suggestions.

I see two ways for this:

diff --git a/src/modbus-tcp-private.h b/src/modbus-tcp-private.h
index 164c8c7..adef8a9 100644
--- a/src/modbus-tcp-private.h
+++ b/src/modbus-tcp-private.h
@@ -27,18 +27,15 @@ typedef struct _modbus_tcp {
     char ip[16];
 } modbus_tcp_t;

-#define _MODBUS_TCP_PI_NODE_LENGTH    1025
-#define _MODBUS_TCP_PI_SERVICE_LENGTH   32
-
 typedef struct _modbus_tcp_pi {
     /* Transaction ID */
     uint16_t t_id;
     /* TCP port */
     int port;
     /* Node */
-    char node[_MODBUS_TCP_PI_NODE_LENGTH];
+    char* node;
     /* Service */
-    char service[_MODBUS_TCP_PI_SERVICE_LENGTH];
+    char* service;
 } modbus_tcp_pi_t;

 #endif /* MODBUS_TCP_PRIVATE_H */
diff --git a/src/modbus-tcp.c b/src/modbus-tcp.c
index 5164273..47aaeca 100644
--- a/src/modbus-tcp.c
+++ b/src/modbus-tcp.c
@@ -744,6 +744,14 @@ static void _modbus_tcp_free(modbus_t *ctx) {
     free(ctx);
 }

+static void _modbus_tcp_pi_free(modbus_t *ctx) {
+    modbus_tcp_pi_t *ctx_tcp_pi = ctx->backend_data;
+    free(ctx_tcp_pi->node);
+    free(ctx_tcp_pi->service);
+    free(ctx->backend_data);
+    free(ctx);
+}
+
 const modbus_backend_t _modbus_tcp_backend = {
     _MODBUS_BACKEND_TYPE_TCP,
     _MODBUS_TCP_HEADER_LENGTH,
@@ -786,7 +794,7 @@ const modbus_backend_t _modbus_tcp_pi_backend = {
     _modbus_tcp_close,
     _modbus_tcp_flush,
     _modbus_tcp_select,
-    _modbus_tcp_free
+    _modbus_tcp_pi_free
 };

 modbus_t* modbus_new_tcp(const char *ip, int port)
@@ -858,8 +866,6 @@ modbus_t* modbus_new_tcp_pi(const char *node, const char *service)
 {
     modbus_t *ctx;
     modbus_tcp_pi_t *ctx_tcp_pi;
-    size_t dest_size;
-    size_t ret_size;

     ctx = (modbus_t *)malloc(sizeof(modbus_t));
     if (ctx == NULL) {
@@ -880,49 +886,34 @@ modbus_t* modbus_new_tcp_pi(const char *node, const char *service)
     }
     ctx_tcp_pi = (modbus_tcp_pi_t *)ctx->backend_data;

+    /* Ensure this variable is initialised for proper error handling below */
+    ctx_tcp_pi->service = NULL;
+
     if (node == NULL) {
         /* The node argument can be empty to indicate any hosts */
-        ctx_tcp_pi->node[0] = 0;
-    } else {
-        dest_size = sizeof(char) * _MODBUS_TCP_PI_NODE_LENGTH;
-        ret_size = strlcpy(ctx_tcp_pi->node, node, dest_size);
-        if (ret_size == 0) {
-            fprintf(stderr, "The node string is empty\n");
-            modbus_free(ctx);
-            errno = EINVAL;
-            return NULL;
-        }
+        node = "";
+    }
+    ctx_tcp_pi->node = strdup(node);
+    if (ctx_tcp_pi->node == NULL) {
+        modbus_free(ctx);
+        errno = ENOMEM;
+        return NULL;
+    }

-        if (ret_size >= dest_size) {
-            fprintf(stderr, "The node string has been truncated\n");
+    if (service != NULL && service[0] != '\0') {
+        ctx_tcp_pi->service = strdup(service);
+        if (ctx_tcp_pi->service == NULL) {
             modbus_free(ctx);
-            errno = EINVAL;
+            errno = ENOMEM;
             return NULL;
         }
-    }
-
-    if (service != NULL) {
-        dest_size = sizeof(char) * _MODBUS_TCP_PI_SERVICE_LENGTH;
-        ret_size = strlcpy(ctx_tcp_pi->service, service, dest_size);
     } else {
-        /* Empty service is not allowed, error caught below. */
-        ret_size = 0;
-    }
-
-    if (ret_size == 0) {
         fprintf(stderr, "The service string is empty\n");
         modbus_free(ctx);
         errno = EINVAL;
         return NULL;
     }

-    if (ret_size >= dest_size) {
-        fprintf(stderr, "The service string has been truncated\n");
-        modbus_free(ctx);
-        errno = EINVAL;
-        return NULL;
-    }
-
     ctx_tcp_pi->t_id = 0;

     return ctx;

If you do not like the two extra memory allocations:

My other idea would be to use flexible array members. This means something like that (patch also completely untested):

diff --git a/src/modbus-tcp-private.h b/src/modbus-tcp-private.h
index 164c8c7..9a54c13 100644
--- a/src/modbus-tcp-private.h
+++ b/src/modbus-tcp-private.h
@@ -35,10 +35,8 @@ typedef struct _modbus_tcp_pi {
     uint16_t t_id;
     /* TCP port */
     int port;
-    /* Node */
-    char node[_MODBUS_TCP_PI_NODE_LENGTH];
-    /* Service */
-    char service[_MODBUS_TCP_PI_SERVICE_LENGTH];
+    /* Node and service concatenated into one string */
+    char node_and_service[];
 } modbus_tcp_pi_t;

 #endif /* MODBUS_TCP_PRIVATE_H */
diff --git a/src/modbus-tcp.c b/src/modbus-tcp.c
index 5164273..3593127 100644
--- a/src/modbus-tcp.c
+++ b/src/modbus-tcp.c
@@ -358,6 +358,8 @@ static int _modbus_tcp_pi_connect(modbus_t *ctx)
     struct addrinfo *ai_list;
     struct addrinfo *ai_ptr;
     struct addrinfo ai_hints;
+    const char *node;
+    const char *service;
     modbus_tcp_pi_t *ctx_tcp_pi = ctx->backend_data;

 #ifdef OS_WIN32
@@ -366,6 +368,9 @@ static int _modbus_tcp_pi_connect(modbus_t *ctx)
     }
 #endif

+    node = ctx_tcp_pi->node_and_service;
+    service = ctx_tcp_pi->node_and_service + strlen(node) + 1;
+
     memset(&ai_hints, 0, sizeof(ai_hints));
 #ifdef AI_ADDRCONFIG
     ai_hints.ai_flags |= AI_ADDRCONFIG;
@@ -377,8 +382,7 @@ static int _modbus_tcp_pi_connect(modbus_t *ctx)
     ai_hints.ai_next = NULL;

     ai_list = NULL;
-    rc = getaddrinfo(ctx_tcp_pi->node, ctx_tcp_pi->service,
-                     &ai_hints, &ai_list);
+    rc = getaddrinfo(node, service, &ai_hints, &ai_list);
     if (rc != 0) {
         if (ctx->debug) {
             fprintf(stderr, "Error returned by getaddrinfo: %s\n", gai_strerror(rc));
@@ -564,6 +568,8 @@ int modbus_tcp_pi_listen(modbus_t *ctx, int nb_connection)
     }
 #endif

+    // TODO: Handle changed struct
+
     if (ctx_tcp_pi->node[0] == 0) {
         node = NULL; /* == any */
     } else {
@@ -858,10 +864,27 @@ modbus_t* modbus_new_tcp_pi(const char *node, const char *service)
 {
     modbus_t *ctx;
     modbus_tcp_pi_t *ctx_tcp_pi;
-    size_t dest_size;
-    size_t ret_size;
+    size_t node_size;
+    size_t service_size;

-    ctx = (modbus_t *)malloc(sizeof(modbus_t));
+    if (node == NULL) {
+        /* The node argument can be empty to indicate any hosts */
+        node_size = 0;
+        node = "";
+    } else {
+        node_size = strlen(node_size);
+    }
+
+    if (service != NULL && service[0] != '\0') {
+        service_size = strlen(service);
+    } else {
+        fprintf(stderr, "The service string is empty\n");
+        modbus_free(ctx);
+        errno = EINVAL;
+        return NULL;
+    }
+
+    ctx = (modbus_t *)malloc(sizeof(modbus_t) + node_size + 1 + service_size + 1);
     if (ctx == NULL) {
         return NULL;
     }
@@ -880,48 +903,8 @@ modbus_t* modbus_new_tcp_pi(const char *node, const char *service)
     }
     ctx_tcp_pi = (modbus_tcp_pi_t *)ctx->backend_data;

-    if (node == NULL) {
-        /* The node argument can be empty to indicate any hosts */
-        ctx_tcp_pi->node[0] = 0;
-    } else {
-        dest_size = sizeof(char) * _MODBUS_TCP_PI_NODE_LENGTH;
-        ret_size = strlcpy(ctx_tcp_pi->node, node, dest_size);
-        if (ret_size == 0) {
-            fprintf(stderr, "The node string is empty\n");
-            modbus_free(ctx);
-            errno = EINVAL;
-            return NULL;
-        }
-
-        if (ret_size >= dest_size) {
-            fprintf(stderr, "The node string has been truncated\n");
-            modbus_free(ctx);
-            errno = EINVAL;
-            return NULL;
-        }
-    }
-
-    if (service != NULL) {
-        dest_size = sizeof(char) * _MODBUS_TCP_PI_SERVICE_LENGTH;
-        ret_size = strlcpy(ctx_tcp_pi->service, service, dest_size);
-    } else {
-        /* Empty service is not allowed, error caught below. */
-        ret_size = 0;
-    }
-
-    if (ret_size == 0) {
-        fprintf(stderr, "The service string is empty\n");
-        modbus_free(ctx);
-        errno = EINVAL;
-        return NULL;
-    }
-
-    if (ret_size >= dest_size) {
-        fprintf(stderr, "The service string has been truncated\n");
-        modbus_free(ctx);
-        errno = EINVAL;
-        return NULL;
-    }
+    strcpy(ctx_tcp_pi->node_and_service, node);
+    strcpy(&ctx_tcp_pi->node_and_service[node_size + 1], service);

     ctx_tcp_pi->t_id = 0;

Steps to reproduce the behavior (commands or source code)

Source code is linked above, I guess. I don't have any own code using libmodbus, so....?

libmodbus output with debug mode enabled

Uhm... command not found, I guess? See pkg-config output from above.

Copyright

If you do not worry about copyright issues with my untested sketch patches above, then just ignore this section.

In case you worry: Pick one of the following:

stephane commented 2 years ago

LGTM. Could you provide a PR, please?

psychon commented 2 years ago

LGTM.

Which of the two proposed variants do you mean? Or are both fine?

Could you provide a PR, please?

Sorry, nope, I cannot. I would gladly provide a LGPL licensed patch, but you do not accept such contributions. In fact, I fear that if I actually work too much on this, the license would actually prevent anyone else from doing the work in a way that you would deem acceptable (because that would then easily end up as a derived work of my patch and that makes license stuff more complicated).

stephane commented 1 year ago

I choose the proposal with 2 mallocs, way easier to understand and maintain ;) Thank you @psychon

psychon commented 1 year ago

Thanks!