Blob Blame History Raw
From 11c0d687a081fe64501e21c95def7f893611d029 Mon Sep 17 00:00:00 2001
From: Norbert Pocs <npocs@redhat.com>
Date: Wed, 16 Nov 2022 10:40:38 +0100
Subject: [PATCH 1/5] Add a placehohlder for non-expanded identities

Expanding a string twice could lead to unwanted behaviour.
This solution creates a ssh_list (`opts.identites_non_exp`) to store the strings
before expansion and by using ssh_apply it moves the string to the
`opts.identities`. This way the expanded strings are separated.

Signed-off-by: Norbert Pocs <npocs@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
---
 include/libssh/session.h |  1 +
 src/options.c            | 86 +++++++++++++++++++++++++---------------
 src/session.c            | 23 +++++++++--
 3 files changed, 75 insertions(+), 35 deletions(-)

diff --git a/include/libssh/session.h b/include/libssh/session.h
index d3e5787c..e22b0d67 100644
--- a/include/libssh/session.h
+++ b/include/libssh/session.h
@@ -209,6 +209,7 @@ struct ssh_session_struct {
 #endif
     struct {
         struct ssh_list *identity;
+        struct ssh_list *identity_non_exp;
         char *username;
         char *host;
         char *bindaddr; /* bind the client to an ip addr */
diff --git a/src/options.c b/src/options.c
index 56e09c65..bb085384 100644
--- a/src/options.c
+++ b/src/options.c
@@ -52,7 +52,7 @@
  * @brief Duplicate the options of a session structure.
  *
  * If you make several sessions with the same options this is useful. You
- * cannot use twice the same option structure in ssh_session_connect.
+ * cannot use twice the same option structure in ssh_connect.
  *
  * @param src           The session to use to copy the options.
  *
@@ -61,13 +61,14 @@
  *
  * @returns             0 on success, -1 on error with errno set.
  *
- * @see ssh_session_connect()
+ * @see ssh_connect()
  * @see ssh_free()
  */
 int ssh_options_copy(ssh_session src, ssh_session *dest)
 {
     ssh_session new;
     struct ssh_iterator *it = NULL;
+    struct ssh_list *list = NULL;
     char *id = NULL;
     int i;

@@ -105,14 +106,15 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
     }

     /* Remove the default identities */
-    for (id = ssh_list_pop_head(char *, new->opts.identity);
+    for (id = ssh_list_pop_head(char *, new->opts.identity_non_exp);
          id != NULL;
-         id = ssh_list_pop_head(char *, new->opts.identity)) {
+         id = ssh_list_pop_head(char *, new->opts.identity_non_exp)) {
         SAFE_FREE(id);
     }
     /* Copy the new identities from the source list */
-    if (src->opts.identity != NULL) {
-        it = ssh_list_get_iterator(src->opts.identity);
+    list = new->opts.identity_non_exp;
+    it = ssh_list_get_iterator(src->opts.identity_non_exp);
+    for (i = 0; i < 2; i++) {
         while (it) {
             int rc;

@@ -122,7 +124,7 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
                 return -1;
             }

-            rc = ssh_list_append(new->opts.identity, id);
+            rc = ssh_list_append(list, id);
             if (rc < 0) {
                 free(id);
                 ssh_free(new);
@@ -130,6 +132,10 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
             }
             it = it->next;
         }
+
+        /* copy the identity list if there is any already */
+        list = new->opts.identity;
+        it = ssh_list_get_iterator(src->opts.identity);
     }

     if (src->opts.sshdir != NULL) {
@@ -331,7 +337,7 @@ int ssh_options_set_algo(ssh_session session,
  *                Add a new identity file (const char *, format string) to
  *                the identity list.\n
  *                \n
- *                By default identity, id_dsa and id_rsa are checked.\n
+ *                By default id_rsa, id_ecdsa and id_ed25519 files are used.\n
  *                \n
  *                The identity used to authenticate with public key will be
  *                prepended to the list.
@@ -700,7 +706,11 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
             if (q == NULL) {
                 return -1;
             }
-            rc = ssh_list_prepend(session->opts.identity, q);
+            if (session->opts.exp_flags & SSH_OPT_EXP_FLAG_IDENTITY) {
+                rc = ssh_list_append(session->opts.identity_non_exp, q);
+            } else {
+                rc = ssh_list_prepend(session->opts.identity_non_exp, q);
+            }
             if (rc < 0) {
                 free(q);
                 return -1;
@@ -1202,7 +1212,7 @@ int ssh_options_get_port(ssh_session session, unsigned int* port_target) {
  *              - SSH_OPTIONS_IDENTITY:
  *                Get the first identity file name (const char *).\n
  *                \n
- *                By default identity, id_dsa and id_rsa are checked.
+ *                By default id_rsa, id_ecdsa and id_ed25519 files are used.
  *
  *              - SSH_OPTIONS_PROXYCOMMAND:
  *                Get the proxycommand necessary to log into the
@@ -1246,7 +1256,11 @@ int ssh_options_get(ssh_session session, enum ssh_options_e type, char** value)
             break;
         }
         case SSH_OPTIONS_IDENTITY: {
-            struct ssh_iterator *it = ssh_list_get_iterator(session->opts.identity);
+            struct ssh_iterator *it;
+            it = ssh_list_get_iterator(session->opts.identity);
+            if (it == NULL) {
+                it = ssh_list_get_iterator(session->opts.identity_non_exp);
+            }
             if (it == NULL) {
                 return SSH_ERROR;
             }
@@ -1541,7 +1555,6 @@ out:

 int ssh_options_apply(ssh_session session)
 {
-    struct ssh_iterator *it;
     char *tmp;
     int rc;

@@ -1586,15 +1599,17 @@ int ssh_options_apply(ssh_session session)
         size_t plen = strlen(session->opts.ProxyCommand) +
                       5 /* strlen("exec ") */;

-        p = malloc(plen + 1 /* \0 */);
-        if (p == NULL) {
-            return -1;
-        }
+        if (strncmp(session->opts.ProxyCommand, "exec ", 5) != 0) {
+            p = malloc(plen + 1 /* \0 */);
+            if (p == NULL) {
+                return -1;
+            }

-        rc = snprintf(p, plen + 1, "exec %s", session->opts.ProxyCommand);
-        if ((size_t)rc != plen) {
-            free(p);
-            return -1;
+            rc = snprintf(p, plen + 1, "exec %s", session->opts.ProxyCommand);
+            if ((size_t)rc != plen) {
+                free(p);
+                return -1;
+            }
         }

         tmp = ssh_path_expand_escape(session, p);
@@ -1606,24 +1621,33 @@ int ssh_options_apply(ssh_session session)
         session->opts.ProxyCommand = tmp;
     }

-    for (it = ssh_list_get_iterator(session->opts.identity);
-         it != NULL;
-         it = it->next) {
-        char *id = (char *) it->data;
-        if (strncmp(id, "pkcs11:", 6) == 0) {
+    for (tmp = ssh_list_pop_head(char *, session->opts.identity_non_exp);
+         tmp != NULL;
+         tmp = ssh_list_pop_head(char *, session->opts.identity_non_exp)) {
+        char *id = tmp;
+        if (strncmp(id, "pkcs11:", 6) != 0) {
             /* PKCS#11 URIs are using percent-encoding so we can not mix
              * it with ssh expansion of ssh escape characters.
-             * Skip these identities now, before we will have PKCS#11 support
              */
-             continue;
+            tmp = ssh_path_expand_escape(session, id);
+            if (tmp == NULL) {
+                return -1;
+            }
+            free(id);
         }
-        tmp = ssh_path_expand_escape(session, id);
-        if (tmp == NULL) {
+
+        /* use append to keep the order at first call and use prepend
+         * to put anything that comes on the nth calls to the beginning */
+        if (session->opts.exp_flags & SSH_OPT_EXP_FLAG_IDENTITY) {
+            rc = ssh_list_prepend(session->opts.identity, tmp);
+        } else {
+            rc = ssh_list_append(session->opts.identity, tmp);
+        }
+        if (rc != SSH_OK) {
             return -1;
         }
-        free(id);
-        it->data = tmp;
     }
+    session->opts.exp_flags |= SSH_OPT_EXP_FLAG_IDENTITY;

     return 0;
 }
diff --git a/src/session.c b/src/session.c
index 64e54957..34a492e4 100644
--- a/src/session.c
+++ b/src/session.c
@@ -118,13 +118,17 @@ ssh_session ssh_new(void)
     if (session->opts.identity == NULL) {
         goto err;
     }
+    session->opts.identity_non_exp = ssh_list_new();
+    if (session->opts.identity_non_exp == NULL) {
+        goto err;
+    }

     id = strdup("%d/id_ed25519");
     if (id == NULL) {
         goto err;
     }

-    rc = ssh_list_append(session->opts.identity, id);
+    rc = ssh_list_append(session->opts.identity_non_exp, id);
     if (rc == SSH_ERROR) {
         goto err;
     }
@@ -134,7 +138,7 @@ ssh_session ssh_new(void)
     if (id == NULL) {
         goto err;
     }
-    rc = ssh_list_append(session->opts.identity, id);
+    rc = ssh_list_append(session->opts.identity_non_exp, id);
     if (rc == SSH_ERROR) {
         goto err;
     }
@@ -144,7 +148,7 @@ ssh_session ssh_new(void)
     if (id == NULL) {
         goto err;
     }
-    rc = ssh_list_append(session->opts.identity, id);
+    rc = ssh_list_append(session->opts.identity_non_exp, id);
     if (rc == SSH_ERROR) {
         goto err;
     }
@@ -154,7 +158,7 @@ ssh_session ssh_new(void)
     if (id == NULL) {
         goto err;
     }
-    rc = ssh_list_append(session->opts.identity, id);
+    rc = ssh_list_append(session->opts.identity_non_exp, id);
     if (rc == SSH_ERROR) {
         goto err;
     }
@@ -284,6 +288,17 @@ void ssh_free(ssh_session session)
       ssh_list_free(session->opts.identity);
   }

+  if (session->opts.identity_non_exp) {
+      char *id;
+
+      for (id = ssh_list_pop_head(char *, session->opts.identity_non_exp);
+           id != NULL;
+           id = ssh_list_pop_head(char *, session->opts.identity_non_exp)) {
+          SAFE_FREE(id);
+      }
+      ssh_list_free(session->opts.identity_non_exp);
+  }
+
     while ((b = ssh_list_pop_head(struct ssh_buffer_struct *,
                                   session->out_queue)) != NULL) {
         SSH_BUFFER_FREE(b);
--
2.38.1


From 4cb84b99fdb1ffd26c0241f5809e4f67ddd407c6 Mon Sep 17 00:00:00 2001
From: Norbert Pocs <npocs@redhat.com>
Date: Wed, 16 Nov 2022 11:03:30 +0100
Subject: [PATCH 2/5] tests: Use opts.identites_non_exp not opts.identities

The configuration of identities are first saved to `opts.identities_non_exp`,
then moved to `opts.identities` after calling ssh_options_apply and expanding
the identity strings. These tests are testing against the proper configuration

Signed-off-by: Norbert Pocs <npocs@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
---
 tests/client/torture_auth_pkcs11.c |  2 +-
 tests/unittests/torture_config.c   |  3 ++-
 tests/unittests/torture_options.c  | 14 +++++++-------
 3 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/tests/client/torture_auth_pkcs11.c b/tests/client/torture_auth_pkcs11.c
index ee97bff4..e75fea0e 100644
--- a/tests/client/torture_auth_pkcs11.c
+++ b/tests/client/torture_auth_pkcs11.c
@@ -196,7 +196,7 @@ static void torture_auth_autopubkey(void **state, const char *obj_name, const ch

     rc = ssh_options_set(session, SSH_OPTIONS_IDENTITY, priv_uri);
     assert_int_equal(rc, SSH_OK);
-    assert_string_equal(session->opts.identity->root->data, priv_uri);
+    assert_string_equal(session->opts.identity_non_exp->root->data, priv_uri);

     rc = ssh_connect(session);
     assert_int_equal(rc, SSH_OK);
diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c
index 354adc2f..100e68f6 100644
--- a/tests/unittests/torture_config.c
+++ b/tests/unittests/torture_config.c
@@ -2078,7 +2078,8 @@ static void torture_config_identity(void **state)

     _parse_config(session, NULL, LIBSSH_TESTCONFIG_STRING13, SSH_OK);

-    it = ssh_list_get_iterator(session->opts.identity);
+    /* The identities are first added to this temporary list before expanding */
+    it = ssh_list_get_iterator(session->opts.identity_non_exp);
     assert_non_null(it);
     id = it->data;
     /* The identities are prepended to the list so we start with second one */
diff --git a/tests/unittests/torture_options.c b/tests/unittests/torture_options.c
index dc4df383..3be2de8a 100644
--- a/tests/unittests/torture_options.c
+++ b/tests/unittests/torture_options.c
@@ -406,12 +406,12 @@ static void torture_options_set_identity(void **state) {

     rc = ssh_options_set(session, SSH_OPTIONS_ADD_IDENTITY, "identity1");
     assert_true(rc == 0);
-    assert_string_equal(session->opts.identity->root->data, "identity1");
+    assert_string_equal(session->opts.identity_non_exp->root->data, "identity1");

     rc = ssh_options_set(session, SSH_OPTIONS_IDENTITY, "identity2");
     assert_true(rc == 0);
-    assert_string_equal(session->opts.identity->root->data, "identity2");
-    assert_string_equal(session->opts.identity->root->next->data, "identity1");
+    assert_string_equal(session->opts.identity_non_exp->root->data, "identity2");
+    assert_string_equal(session->opts.identity_non_exp->root->next->data, "identity1");
 }

 static void torture_options_get_identity(void **state) {
@@ -429,7 +429,7 @@ static void torture_options_get_identity(void **state) {

     rc = ssh_options_set(session, SSH_OPTIONS_IDENTITY, "identity2");
     assert_int_equal(rc, SSH_OK);
-    assert_string_equal(session->opts.identity->root->data, "identity2");
+    assert_string_equal(session->opts.identity_non_exp->root->data, "identity2");
     rc = ssh_options_get(session, SSH_OPTIONS_IDENTITY, &identity);
     assert_int_equal(rc, SSH_OK);
     assert_non_null(identity);
@@ -867,9 +867,9 @@ static void torture_options_copy(void **state)
     assert_non_null(new);

     /* Check the identities match */
-    it = ssh_list_get_iterator(session->opts.identity);
+    it = ssh_list_get_iterator(session->opts.identity_non_exp);
     assert_non_null(it);
-    it2 = ssh_list_get_iterator(new->opts.identity);
+    it2 = ssh_list_get_iterator(new->opts.identity_non_exp);
     assert_non_null(it2);
     while (it != NULL && it2 != NULL) {
         assert_string_equal(it->data, it2->data);
@@ -956,7 +956,7 @@ static void torture_options_getopt(void **state)
                         "aes128-ctr");
     assert_string_equal(session->opts.wanted_methods[SSH_CRYPT_S_C],
                         "aes128-ctr");
-    assert_string_equal(session->opts.identity->root->data, "id_rsa");
+    assert_string_equal(session->opts.identity_non_exp->root->data, "id_rsa");
 #ifdef WITH_ZLIB
     assert_string_equal(session->opts.wanted_methods[SSH_COMP_C_S],
                         "zlib@openssh.com,zlib,none");
--
2.38.1


From cd30217c9032419ebcf722c0bfc6b5ebfa3518d0 Mon Sep 17 00:00:00 2001
From: Norbert Pocs <npocs@redhat.com>
Date: Wed, 16 Nov 2022 16:51:02 +0100
Subject: [PATCH 3/5] Add flags for escape expand operation

Calling `ssh_options_apply` more times can result in an unwanted behaviour of
expanding the escape characters more times. Adding flags to check if the
expansion was already done on the current string variables.

Signed-off-by: Norbert Pocs <npocs@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
---
 include/libssh/session.h |  7 ++++
 src/options.c            | 91 ++++++++++++++++++++++++----------------
 src/session.c            |  2 +
 3 files changed, 63 insertions(+), 37 deletions(-)

diff --git a/include/libssh/session.h b/include/libssh/session.h
index e22b0d67..cf219c2a 100644
--- a/include/libssh/session.h
+++ b/include/libssh/session.h
@@ -93,6 +93,12 @@ enum ssh_pending_call_e {
 #define SSH_OPT_FLAG_KBDINT_AUTH 0x4
 #define SSH_OPT_FLAG_GSSAPI_AUTH 0x8

+/* Escape expansion of different variables */
+#define SSH_OPT_EXP_FLAG_KNOWNHOSTS 0x1
+#define SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS 0x2
+#define SSH_OPT_EXP_FLAG_PROXYCOMMAND 0x4
+#define SSH_OPT_EXP_FLAG_IDENTITY 0x8
+
 /* extensions flags */
 /* negotiation enabled */
 #define SSH_EXT_NEGOTIATION     0x01
@@ -232,6 +238,7 @@ struct ssh_session_struct {
         char *gss_client_identity;
         int gss_delegate_creds;
         int flags;
+        int exp_flags;
         int nodelay;
         bool config_processed;
         uint8_t options_seen[SOC_MAX];
diff --git a/src/options.c b/src/options.c
index bb085384..c566244b 100644
--- a/src/options.c
+++ b/src/options.c
@@ -730,6 +730,7 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
                     ssh_set_error_oom(session);
                     return -1;
                 }
+                session->opts.exp_flags &= ~SSH_OPT_EXP_FLAG_KNOWNHOSTS;
             }
             break;
         case SSH_OPTIONS_GLOBAL_KNOWNHOSTS:
@@ -751,6 +752,7 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
                     ssh_set_error_oom(session);
                     return -1;
                 }
+                session->opts.exp_flags &= ~SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS;
             }
             break;
         case SSH_OPTIONS_TIMEOUT:
@@ -1014,6 +1016,7 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
                         return -1;
                     }
                     session->opts.ProxyCommand = q;
+                    session->opts.exp_flags &= ~SSH_OPT_EXP_FLAG_PROXYCOMMAND;
                 }
             }
             break;
@@ -1572,53 +1575,67 @@ int ssh_options_apply(ssh_session session)
         }
     }

-    if (session->opts.knownhosts == NULL) {
-        tmp = ssh_path_expand_escape(session, "%d/known_hosts");
-    } else {
-        tmp = ssh_path_expand_escape(session, session->opts.knownhosts);
-    }
-    if (tmp == NULL) {
-        return -1;
+    if ((session->opts.exp_flags & SSH_OPT_EXP_FLAG_KNOWNHOSTS) == 0) {
+        if (session->opts.knownhosts == NULL) {
+            tmp = ssh_path_expand_escape(session, "%d/known_hosts");
+        } else {
+            tmp = ssh_path_expand_escape(session, session->opts.knownhosts);
+        }
+        if (tmp == NULL) {
+            return -1;
+        }
+        free(session->opts.knownhosts);
+        session->opts.knownhosts = tmp;
+        session->opts.exp_flags |= SSH_OPT_EXP_FLAG_KNOWNHOSTS;
     }
-    free(session->opts.knownhosts);
-    session->opts.knownhosts = tmp;

-    if (session->opts.global_knownhosts == NULL) {
-        tmp = strdup("/etc/ssh/ssh_known_hosts");
-    } else {
-        tmp = ssh_path_expand_escape(session, session->opts.global_knownhosts);
-    }
-    if (tmp == NULL) {
-        return -1;
+    if ((session->opts.exp_flags & SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS) == 0) {
+        if (session->opts.global_knownhosts == NULL) {
+            tmp = strdup("/etc/ssh/ssh_known_hosts");
+        } else {
+            tmp = ssh_path_expand_escape(session,
+                                         session->opts.global_knownhosts);
+        }
+        if (tmp == NULL) {
+            return -1;
+        }
+        free(session->opts.global_knownhosts);
+        session->opts.global_knownhosts = tmp;
+        session->opts.exp_flags |= SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS;
     }
-    free(session->opts.global_knownhosts);
-    session->opts.global_knownhosts = tmp;

-    if (session->opts.ProxyCommand != NULL) {
-        char *p = NULL;
-        size_t plen = strlen(session->opts.ProxyCommand) +
-                      5 /* strlen("exec ") */;

-        if (strncmp(session->opts.ProxyCommand, "exec ", 5) != 0) {
-            p = malloc(plen + 1 /* \0 */);
-            if (p == NULL) {
-                return -1;
-            }
+    if ((session->opts.exp_flags & SSH_OPT_EXP_FLAG_PROXYCOMMAND) == 0) {
+        if (session->opts.ProxyCommand != NULL) {
+            char *p = NULL;
+            size_t plen = strlen(session->opts.ProxyCommand) +
+                          5 /* strlen("exec ") */;
+
+            if (strncmp(session->opts.ProxyCommand, "exec ", 5) != 0) {
+                p = malloc(plen + 1 /* \0 */);
+                if (p == NULL) {
+                    return -1;
+                }

-            rc = snprintf(p, plen + 1, "exec %s", session->opts.ProxyCommand);
-            if ((size_t)rc != plen) {
+                rc = snprintf(p, plen + 1, "exec %s", session->opts.ProxyCommand);
+                if ((size_t)rc != plen) {
+                    free(p);
+                    return -1;
+                }
+                tmp = ssh_path_expand_escape(session, p);
                 free(p);
-                return -1;
+            } else {
+                tmp = ssh_path_expand_escape(session,
+                                             session->opts.ProxyCommand);
             }
-        }

-        tmp = ssh_path_expand_escape(session, p);
-        free(p);
-        if (tmp == NULL) {
-            return -1;
+            if (tmp == NULL) {
+                return -1;
+            }
+            free(session->opts.ProxyCommand);
+            session->opts.ProxyCommand = tmp;
+            session->opts.exp_flags |= SSH_OPT_EXP_FLAG_PROXYCOMMAND;
         }
-        free(session->opts.ProxyCommand);
-        session->opts.ProxyCommand = tmp;
     }

     for (tmp = ssh_list_pop_head(char *, session->opts.identity_non_exp);
diff --git a/src/session.c b/src/session.c
index 34a492e4..06f6a26f 100644
--- a/src/session.c
+++ b/src/session.c
@@ -114,6 +114,8 @@ ssh_session ssh_new(void)
                           SSH_OPT_FLAG_KBDINT_AUTH |
                           SSH_OPT_FLAG_GSSAPI_AUTH;

+    session->opts.exp_flags = 0;
+
     session->opts.identity = ssh_list_new();
     if (session->opts.identity == NULL) {
         goto err;
--
2.38.1


From ed58082f9706f2ab3bdeca24f632356b9bc325e6 Mon Sep 17 00:00:00 2001
From: Norbert Pocs <npocs@redhat.com>
Date: Wed, 16 Nov 2022 17:17:14 +0100
Subject: [PATCH 4/5] torture_options.c: Add identity test for ssh_options_copy

Test if the ssh_options_apply is called on session before ssh_options_copy,
then `opts.identity` ssh_list will be copied

Signed-off-by: Norbert Pocs <npocs@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
---
 tests/unittests/torture_options.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/tests/unittests/torture_options.c b/tests/unittests/torture_options.c
index 3be2de8a..907cc8df 100644
--- a/tests/unittests/torture_options.c
+++ b/tests/unittests/torture_options.c
@@ -918,6 +918,34 @@ static void torture_options_copy(void **state)
                         sizeof(session->opts.options_seen));

     ssh_free(new);
+
+    /* test if ssh_options_apply was called before ssh_options_copy
+     * the opts.identity list gets copied (percent expanded list) */
+    rv = ssh_options_apply(session);
+    assert_ssh_return_code(session, rv);
+
+    rv = ssh_options_copy(session, &new);
+    assert_ssh_return_code(session, rv);
+    assert_non_null(new);
+
+    it = ssh_list_get_iterator(session->opts.identity_non_exp);
+    assert_null(it);
+    it2 = ssh_list_get_iterator(new->opts.identity_non_exp);
+    assert_null(it2);
+
+    it = ssh_list_get_iterator(session->opts.identity);
+    assert_non_null(it);
+    it2 = ssh_list_get_iterator(new->opts.identity);
+    assert_non_null(it2);
+    while (it != NULL && it2 != NULL) {
+        assert_string_equal(it->data, it2->data);
+        it = it->next;
+        it2 = it2->next;
+    }
+    assert_null(it);
+    assert_null(it2);
+
+    ssh_free(new);
 }

 #define EXECUTABLE_NAME "test-exec"
--
2.38.1


From 89dd4a927b946d4df5c48073ca25cd843e0acde0 Mon Sep 17 00:00:00 2001
From: Norbert Pocs <npocs@redhat.com>
Date: Wed, 16 Nov 2022 17:18:49 +0100
Subject: [PATCH 5/5] torture_options.c: Add test for ssh_options_apply

Test that ssh_options_apply can be called multiple times without expanding
escape characters more than once. If the options are updated after calling
ssh_options_apply keep the last options.

Signed-off-by: Norbert Pocs <npocs@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
---
 tests/unittests/torture_options.c | 165 ++++++++++++++++++++++++++++++
 1 file changed, 165 insertions(+)

diff --git a/tests/unittests/torture_options.c b/tests/unittests/torture_options.c
index 907cc8df..ea63b45e 100644
--- a/tests/unittests/torture_options.c
+++ b/tests/unittests/torture_options.c
@@ -1332,6 +1332,170 @@ static void torture_options_caret_sign(void **state)
     free(awaited);
 }

+static void torture_options_apply (void **state) {
+    ssh_session session = *state;
+    struct ssh_list *awaited_list = NULL;
+    struct ssh_iterator *it1 = NULL, *it2 = NULL;
+    char *id = NULL;
+    int rc;
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_KNOWNHOSTS,
+                         "%%d/.ssh/known_hosts");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_GLOBAL_KNOWNHOSTS,
+                         "/etc/%%u/libssh/known_hosts");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_PROXYCOMMAND,
+                         "exec echo \"Hello libssh %%d!\"");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_ADD_IDENTITY,
+                         "%%d/do_not_expand");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_apply(session);
+    assert_ssh_return_code(session, rc);
+
+    /* check that the values got expanded */
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_KNOWNHOSTS);
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS);
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_PROXYCOMMAND);
+    assert_true(ssh_list_count(session->opts.identity_non_exp) == 0);
+    assert_true(ssh_list_count(session->opts.identity) > 0);
+
+    /* should not change anything calling it again */
+    rc = ssh_options_apply(session);
+    assert_ssh_return_code(session, rc);
+
+    /* check that the expansion was done only once */
+    assert_string_equal(session->opts.knownhosts, "%d/.ssh/known_hosts");
+    assert_string_equal(session->opts.global_knownhosts,
+                        "/etc/%u/libssh/known_hosts");
+    /* no exec should be added if there already is one */
+    assert_string_equal(session->opts.ProxyCommand,
+                        "exec echo \"Hello libssh %d!\"");
+    assert_string_equal(session->opts.identity->root->data,
+                        "%d/do_not_expand");
+
+    /* apply should keep the freshest setting */
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_KNOWNHOSTS,
+                         "hello there");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_GLOBAL_KNOWNHOSTS,
+                         "lorem ipsum");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_PROXYCOMMAND,
+                         "mission_impossible");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_ADD_IDENTITY,
+                         "007");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_ADD_IDENTITY,
+                         "3");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_ADD_IDENTITY,
+                         "2");
+    assert_ssh_return_code(session, rc);
+
+    rc = ssh_options_set(session,
+                         SSH_OPTIONS_ADD_IDENTITY,
+                         "1");
+    assert_ssh_return_code(session, rc);
+
+    /* check that flags show need of escape expansion */
+    assert_false(session->opts.exp_flags & SSH_OPT_EXP_FLAG_KNOWNHOSTS);
+    assert_false(session->opts.exp_flags & SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS);
+    assert_false(session->opts.exp_flags & SSH_OPT_EXP_FLAG_PROXYCOMMAND);
+    assert_false(ssh_list_count(session->opts.identity_non_exp) == 0);
+
+    rc = ssh_options_apply(session);
+    assert_ssh_return_code(session, rc);
+
+    /* check that the values got expanded */
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_KNOWNHOSTS);
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_GLOBAL_KNOWNHOSTS);
+    assert_true(session->opts.exp_flags & SSH_OPT_EXP_FLAG_PROXYCOMMAND);
+    assert_true(ssh_list_count(session->opts.identity_non_exp) == 0);
+
+    assert_string_equal(session->opts.knownhosts, "hello there");
+    assert_string_equal(session->opts.global_knownhosts, "lorem ipsum");
+    /* check that the "exec " was added at the beginning */
+    assert_string_equal(session->opts.ProxyCommand, "exec mission_impossible");
+    assert_string_equal(session->opts.identity->root->data, "1");
+
+    /* check the order of the identity files after double expansion */
+    awaited_list = ssh_list_new();
+    /* append the new data in order */
+    id = strdup("1");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+    id = strdup("2");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+    id = strdup("3");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+    id = strdup("007");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+    id = strdup("%d/do_not_expand");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+    /* append the defaults; this list is copied from ssh_new@src/session.c */
+    id = ssh_path_expand_escape(session, "%d/id_ed25519");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+#ifdef HAVE_ECC
+    id = ssh_path_expand_escape(session, "%d/id_ecdsa");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+#endif
+    id = ssh_path_expand_escape(session, "%d/id_rsa");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+#ifdef HAVE_DSA
+    id = ssh_path_expand_escape(session, "%d/id_dsa");
+    rc = ssh_list_append(awaited_list, id);
+    assert_int_equal(rc, SSH_OK);
+#endif
+
+    assert_int_equal(ssh_list_count(awaited_list),
+                     ssh_list_count(session->opts.identity));
+
+    it1 = ssh_list_get_iterator(awaited_list);
+    assert_non_null(it1);
+    it2 = ssh_list_get_iterator(session->opts.identity);
+    assert_non_null(it2);
+    while (it1 != NULL && it2 != NULL) {
+        assert_string_equal(it1->data, it2->data);
+
+        free((void*)it1->data);
+        it1 = it1->next;
+        it2 = it2->next;
+    }
+    assert_null(it1);
+    assert_null(it2);
+
+    ssh_list_free(awaited_list);
+}
+
 #ifdef WITH_SERVER
 const char template[] = "temp_dir_XXXXXX";

@@ -2132,6 +2296,7 @@ int torture_run_tests(void) {
                                         setup, teardown),
         cmocka_unit_test_setup_teardown(torture_options_caret_sign,
                                         setup, teardown),
+        cmocka_unit_test_setup_teardown(torture_options_apply, setup, teardown),
     };

 #ifdef WITH_SERVER
--
2.38.1