Switch ssh_config parsing to use argv_split()
authordjm <djm@openbsd.org>
Tue, 8 Jun 2021 07:07:15 +0000 (07:07 +0000)
committerdjm <djm@openbsd.org>
Tue, 8 Jun 2021 07:07:15 +0000 (07:07 +0000)
This fixes a couple of problems with the previous tokeniser,
strdelim()

1. strdelim() is permissive wrt accepting '=' characters. This is
   intended to allow it to tokenise "Option=value" but because it
   cannot keep state, it will incorrectly split "Opt=val=val2".
2. strdelim() has rudimentry handling of quoted strings, but it
   is incomplete and inconsistent. E.g. it doesn't handle escaped
   quotes inside a quoted string.
3. It has no support for stopping on a (unquoted) comment. Because
   of this readconf.c r1.343 added chopping of lines at '#', but
   this caused a regression because these characters may legitimately
   appear inside quoted strings.

The new tokeniser is stricter is a number of cases, including #1 above
but previously it was also possible for some directives to appear
without arguments. AFAIK these were nonsensical in all cases, and the
new tokeniser refuses to accept them.

The new code handles quotes much better, permitting quoted space as
well as escaped closing quotes. Finally, comment handling should be
fixed - the tokeniser will terminate only on unquoted # characters.

feedback & ok markus@

tested in snaps for the last five or so days - thanks Theo and those who
caught bugs

usr.bin/ssh/readconf.c
usr.bin/ssh/ssh.c

index 96c2848..c68c26a 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.c,v 1.355 2021/06/08 07:02:46 dtucker Exp $ */
+/* $OpenBSD: readconf.c,v 1.356 2021/06/08 07:07:15 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -589,25 +589,33 @@ match_cfg_line(Options *options, char **condition, struct passwd *pw,
        debug2("checking match for '%s' host %s originally %s",
            cp, host, original_host);
        while ((oattrib = attrib = strdelim(&cp)) && *attrib != '\0') {
-               criteria = NULL;
+               /* Terminate on comment */
+               if (*attrib == '#') {
+                       cp = NULL; /* mark all arguments consumed */
+                       break;
+               }
+               arg = criteria = NULL;
                this_result = 1;
                if ((negate = attrib[0] == '!'))
                        attrib++;
-               /* criteria "all" and "canonical" have no argument */
+               /* Criterion "all" has no argument and must appear alone */
                if (strcasecmp(attrib, "all") == 0) {
-                       if (attributes > 1 ||
-                           ((arg = strdelim(&cp)) != NULL && *arg != '\0')) {
+                       if (attributes > 1 || ((arg = strdelim(&cp)) != NULL &&
+                           *arg != '\0' && *arg != '#')) {
                                error("%.200s line %d: '%s' cannot be combined "
                                    "with other Match attributes",
                                    filename, linenum, oattrib);
                                result = -1;
                                goto out;
                        }
+                       if (arg != NULL && *arg == '#')
+                               cp = NULL; /* mark all arguments consumed */
                        if (result)
                                result = negate ? 0 : 1;
                        goto out;
                }
                attributes++;
+               /* criteria "final" and "canonical" have no argument */
                if (strcasecmp(attrib, "canonical") == 0 ||
                    strcasecmp(attrib, "final") == 0) {
                        /*
@@ -626,7 +634,8 @@ match_cfg_line(Options *options, char **condition, struct passwd *pw,
                        continue;
                }
                /* All other criteria require an argument */
-               if ((arg = strdelim(&cp)) == NULL || *arg == '\0') {
+               if ((arg = strdelim(&cp)) == NULL ||
+                   *arg == '\0' || *arg == '#') {
                        error("Missing Match criteria for %s", attrib);
                        result = -1;
                        goto out;
@@ -901,7 +910,7 @@ process_config_line_depth(Options *options, struct passwd *pw, const char *host,
     const char *original_host, char *line, const char *filename,
     int linenum, int *activep, int flags, int *want_final_pass, int depth)
 {
-       char *s, **charptr, *endofnumber, *keyword, *arg, *arg2, *p, ch;
+       char *str, **charptr, *endofnumber, *keyword, *arg, *arg2, *p, ch;
        char **cpptr, ***cppptr, fwdarg[256];
        u_int i, *uintptr, uvalue, max_entries = 0;
        int r, oactive, negated, opcode, *intptr, value, value2, cmdline = 0;
@@ -915,6 +924,9 @@ process_config_line_depth(Options *options, struct passwd *pw, const char *host,
        struct allowed_cname *cname;
        glob_t gl;
        const char *errstr;
+       char **oav = NULL, **av;
+       int oac = 0, ac;
+       int ret = -1;
 
        if (activep == NULL) { /* We are processing a command line directive */
                cmdline = 1;
@@ -930,46 +942,62 @@ process_config_line_depth(Options *options, struct passwd *pw, const char *host,
                line[len] = '\0';
        }
 
-       s = line;
+       str = line;
        /* Get the keyword. (Each line is supposed to begin with a keyword). */
-       if ((keyword = strdelim(&s)) == NULL)
+       if ((keyword = strdelim(&str)) == NULL)
                return 0;
        /* Ignore leading whitespace. */
        if (*keyword == '\0')
-               keyword = strdelim(&s);
+               keyword = strdelim(&str);
        if (keyword == NULL || !*keyword || *keyword == '\n' || *keyword == '#')
                return 0;
        /* Match lowercase keyword */
        lowercase(keyword);
 
+       /* Prepare to parse remainder of line */
+       if (str != NULL)
+               str += strspn(str, WHITESPACE);
+       if (str == NULL || *str == '\0') {
+               error("%s line %d: no argument after keyword \"%s\"",
+                   filename, linenum, keyword);
+               return -1;
+       }
        opcode = parse_token(keyword, filename, linenum,
            options->ignored_unknown);
+       if (argv_split(str, &oac, &oav, 1) != 0) {
+               error("%s line %d: invalid quotes", filename, linenum);
+               return -1;
+       }
+       ac = oac;
+       av = oav;
 
        switch (opcode) {
        case oBadOption:
                /* don't panic, but count bad options */
-               return -1;
+               goto out;
        case oIgnore:
-               return 0;
+               argv_consume(&ac);
+               break;
        case oIgnoredUnknownOption:
                debug("%s line %d: Ignored unknown option \"%s\"",
                    filename, linenum, keyword);
-               return 0;
+               argv_consume(&ac);
+               break;
        case oConnectTimeout:
                intptr = &options->connection_timeout;
 parse_time:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%s line %d: missing time value.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (strcmp(arg, "none") == 0)
                        value = -1;
                else if ((value = convtime(arg)) == -1) {
                        error("%s line %d: invalid time value.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1)
                        *intptr = value;
@@ -978,11 +1006,11 @@ parse_time:
        case oForwardAgent:
                intptr = &options->forward_agent;
 
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%s line %d: missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
 
                value = -1;
@@ -1010,12 +1038,12 @@ parse_time:
  parse_flag:
                multistate_ptr = multistate_flag;
  parse_multistate:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if ((value = parse_multistate_value(arg, filename, linenum,
                    multistate_ptr)) == -1) {
                        error("%s line %d: unsupported option \"%s\".",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1)
                        *intptr = value;
@@ -1105,11 +1133,11 @@ parse_time:
                goto parse_int;
 
        case oRekeyLimit:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.", filename,
                            linenum);
-                       return -1;
+                       goto out;
                }
                if (strcmp(arg, "default") == 0) {
                        val64 = 0;
@@ -1117,19 +1145,19 @@ parse_time:
                        if (scan_scaled(arg, &val64) == -1) {
                                error("%.200s line %d: Bad number '%s': %s",
                                    filename, linenum, arg, strerror(errno));
-                               return -1;
+                               goto out;
                        }
                        if (val64 != 0 && val64 < 16) {
                                error("%.200s line %d: RekeyLimit too small",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                }
                if (*activep && options->rekey_limit == -1)
                        options->rekey_limit = val64;
-               if (s != NULL) { /* optional rekey interval present */
-                       if (strcmp(s, "none") == 0) {
-                               (void)strdelim(&s);     /* discard */
+               if (ac != 0) { /* optional rekey interval present */
+                       if (strcmp(av[0], "none") == 0) {
+                               (void)argv_next(&ac, &av);      /* discard */
                                break;
                        }
                        intptr = &options->rekey_interval;
@@ -1138,11 +1166,11 @@ parse_time:
                break;
 
        case oIdentityFile:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep) {
                        intptr = &options->num_identity_files;
@@ -1150,7 +1178,7 @@ parse_time:
                                error("%.200s line %d: Too many identity files "
                                    "specified (max %d).", filename, linenum,
                                    SSH_MAX_IDENTITY_FILES);
-                               return -1;
+                               goto out;
                        }
                        add_identity_file(options, NULL,
                            arg, flags & SSHCONF_USERCONF);
@@ -1158,11 +1186,11 @@ parse_time:
                break;
 
        case oCertificateFile:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep) {
                        intptr = &options->num_certificate_files;
@@ -1171,7 +1199,7 @@ parse_time:
                                    "files specified (max %d).",
                                    filename, linenum,
                                    SSH_MAX_CERTIFICATE_FILES);
-                               return -1;
+                               goto out;
                        }
                        add_certificate_file(options, arg,
                            flags & SSHCONF_USERCONF);
@@ -1185,11 +1213,11 @@ parse_time:
        case oUser:
                charptr = &options->user;
 parse_string:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && *charptr == NULL)
                        *charptr = xstrdup(arg);
@@ -1200,17 +1228,34 @@ parse_string:
                uintptr = &options->num_system_hostfiles;
                max_entries = SSH_MAX_HOSTS_FILES;
 parse_char_array:
-               if (*activep && *uintptr == 0) {
-                       while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+               i = 0;
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0') {
+                               error("%s line %d: keyword %s empty argument",
+                                   filename, linenum, keyword);
+                               goto out;
+                       }
+                       /* Allow "none" only in first position */
+                       if (strcasecmp(arg, "none") == 0) {
+                               if (i > 0 || ac > 0) {
+                                       error("%s line %d: keyword %s \"none\" "
+                                           "argument must appear alone.",
+                                           filename, linenum, keyword);
+                                       goto out;
+                               }
+                       }
+                       i++;
+                       if (*activep && *uintptr == 0) {
                                if ((*uintptr) >= max_entries) {
-                                       error("%s line %d: too many known "
-                                           "hosts files.", filename, linenum);
-                                       return -1;
+                                       error("%s line %d: too many %s "
+                                           "entries.", filename, linenum,
+                                           keyword);
+                                       goto out;
                                }
                                cpptr[(*uintptr)++] = xstrdup(arg);
                        }
                }
-               return 0;
+               break;
 
        case oUserKnownHostsFile:
                cpptr = (char **)&options->user_hostfiles;
@@ -1256,42 +1301,45 @@ parse_char_array:
                if (options->jump_host != NULL)
                        charptr = &options->jump_host; /* Skip below */
 parse_command:
-               if (s == NULL) {
+               if (str == NULL) {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
-               len = strspn(s, WHITESPACE "=");
+               len = strspn(str, WHITESPACE "=");
                if (*activep && *charptr == NULL)
-                       *charptr = xstrdup(s + len);
-               return 0;
+                       *charptr = xstrdup(str + len);
+               argv_consume(&ac);
+               break;
 
        case oProxyJump:
-               if (s == NULL) {
+               if (str == NULL) {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
-               len = strspn(s, WHITESPACE "=");
-               if (parse_jump(s + len, options, *activep) == -1) {
+               len = strspn(str, WHITESPACE "=");
+               /* XXX use argv? */
+               if (parse_jump(str + len, options, *activep) == -1) {
                        error("%.200s line %d: Invalid ProxyJump \"%s\"",
-                           filename, linenum, s + len);
-                       return -1;
+                           filename, linenum, str + len);
+                       goto out;
                }
-               return 0;
+               argv_consume(&ac);
+               break;
 
        case oPort:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                value = a2port(arg);
                if (value <= 0) {
                        error("%.200s line %d: Bad port '%s'.",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                if (*activep && options->port == -1)
                        options->port = value;
@@ -1300,63 +1348,63 @@ parse_command:
        case oConnectionAttempts:
                intptr = &options->connection_attempts;
 parse_int:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if ((errstr = atoi_err(arg, &value)) != NULL) {
                        error("%s line %d: integer value %s.",
                            filename, linenum, errstr);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1)
                        *intptr = value;
                break;
 
        case oCiphers:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*arg != '-' &&
                    !ciphers_valid(*arg == '+' || *arg == '^' ? arg + 1 : arg)){
                        error("%.200s line %d: Bad SSH2 cipher spec '%s'.",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*activep && options->ciphers == NULL)
                        options->ciphers = xstrdup(arg);
                break;
 
        case oMacs:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*arg != '-' &&
                    !mac_valid(*arg == '+' || *arg == '^' ? arg + 1 : arg)) {
                        error("%.200s line %d: Bad SSH2 MAC spec '%s'.",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*activep && options->macs == NULL)
                        options->macs = xstrdup(arg);
                break;
 
        case oKexAlgorithms:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*arg != '-' &&
                    !kex_names_valid(*arg == '+' || *arg == '^' ?
                    arg + 1 : arg)) {
                        error("%.200s line %d: Bad SSH2 KexAlgorithms '%s'.",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*activep && options->kex_algorithms == NULL)
                        options->kex_algorithms = xstrdup(arg);
@@ -1365,18 +1413,18 @@ parse_int:
        case oHostKeyAlgorithms:
                charptr = &options->hostkeyalgorithms;
 parse_pubkey_algos:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*arg != '-' &&
                    !sshkey_names_valid2(*arg == '+' || *arg == '^' ?
                    arg + 1 : arg, 1)) {
                        error("%s line %d: Bad key types '%s'.",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*activep && *charptr == NULL)
                        *charptr = xstrdup(arg);
@@ -1388,12 +1436,12 @@ parse_pubkey_algos:
 
        case oLogLevel:
                log_level_ptr = &options->log_level;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                value = log_level_number(arg);
                if (value == SYSLOG_LEVEL_NOT_SET) {
                        error("%.200s line %d: unsupported log level '%s'",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*activep && *log_level_ptr == SYSLOG_LEVEL_NOT_SET)
                        *log_level_ptr = (LogLevel) value;
@@ -1401,12 +1449,12 @@ parse_pubkey_algos:
 
        case oLogFacility:
                log_facility_ptr = &options->log_facility;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                value = log_facility_number(arg);
                if (value == SYSLOG_FACILITY_NOT_SET) {
                        error("%.200s line %d: unsupported log facility '%s'",
                            filename, linenum, arg ? arg : "<NONE>");
-                       return -1;
+                       goto out;
                }
                if (*log_facility_ptr == -1)
                        *log_facility_ptr = (SyslogFacility) value;
@@ -1415,37 +1463,53 @@ parse_pubkey_algos:
        case oLogVerbose:
                cppptr = &options->log_verbose;
                uintptr = &options->num_log_verbose;
-               if (*activep && *uintptr == 0) {
-                       while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+               i = 0;
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0') {
+                               error("%s line %d: keyword %s empty argument",
+                                   filename, linenum, keyword);
+                               goto out;
+                       }
+                       /* Allow "none" only in first position */
+                       if (strcasecmp(arg, "none") == 0) {
+                               if (i > 0 || ac > 0) {
+                                       error("%s line %d: keyword %s \"none\" "
+                                           "argument must appear alone.",
+                                           filename, linenum, keyword);
+                                       goto out;
+                               }
+                       }
+                       i++;
+                       if (*activep && *uintptr == 0) {
                                *cppptr = xrecallocarray(*cppptr, *uintptr,
                                    *uintptr + 1, sizeof(**cppptr));
                                (*cppptr)[(*uintptr)++] = xstrdup(arg);
                        }
                }
-               return 0;
+               break;
 
        case oLocalForward:
        case oRemoteForward:
        case oDynamicForward:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
 
                remotefwd = (opcode == oRemoteForward);
                dynamicfwd = (opcode == oDynamicForward);
 
                if (!dynamicfwd) {
-                       arg2 = strdelim(&s);
+                       arg2 = argv_next(&ac, &av);
                        if (arg2 == NULL || *arg2 == '\0') {
                                if (remotefwd)
                                        dynamicfwd = 1;
                                else {
                                        error("%.200s line %d: Missing target "
                                            "argument.", filename, linenum);
-                                       return -1;
+                                       goto out;
                                }
                        } else {
                                /* construct a string for parse_forward */
@@ -1459,7 +1523,7 @@ parse_pubkey_algos:
                if (parse_forward(&fwd, fwdarg, dynamicfwd, remotefwd) == 0) {
                        error("%.200s line %d: Bad forwarding specification.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
 
                if (*activep) {
@@ -1474,7 +1538,7 @@ parse_pubkey_algos:
        case oPermitRemoteOpen:
                uintptr = &options->num_permitted_remote_opens;
                cppptr = &options->permitted_remote_opens;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0')
                        fatal("%s line %d: missing %s specification",
                            filename, linenum, lookup_opcode_name(opcode));
@@ -1487,7 +1551,7 @@ parse_pubkey_algos:
                        }
                        break;
                }
-               for (; arg != NULL && *arg != '\0'; arg = strdelim(&s)) {
+               while ((arg = argv_next(&ac, &av)) != NULL) {
                        arg2 = xstrdup(arg);
                        ch = '\0';
                        p = hpdelim2(&arg, &ch);
@@ -1524,13 +1588,20 @@ parse_pubkey_algos:
                if (cmdline) {
                        error("Host directive not supported as a command-line "
                            "option");
-                       return -1;
+                       goto out;
                }
                *activep = 0;
                arg2 = NULL;
-               while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
-                       if ((flags & SSHCONF_NEVERMATCH) != 0)
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0') {
+                               error("%s line %d: keyword %s empty argument",
+                                   filename, linenum, keyword);
+                               goto out;
+                       }
+                       if ((flags & SSHCONF_NEVERMATCH) != 0) {
+                               argv_consume(&ac);
                                break;
+                       }
                        negated = *arg == '!';
                        if (negated)
                                arg++;
@@ -1541,6 +1612,7 @@ parse_pubkey_algos:
                                            "for %.100s", filename, linenum,
                                            arg);
                                        *activep = 0;
+                                       argv_consume(&ac);
                                        break;
                                }
                                if (!*activep)
@@ -1551,33 +1623,39 @@ parse_pubkey_algos:
                if (*activep)
                        debug("%.200s line %d: Applying options for %.100s",
                            filename, linenum, arg2);
-               /* Avoid garbage check below, as strdelim is done. */
-               return 0;
+               break;
 
        case oMatch:
                if (cmdline) {
                        error("Host directive not supported as a command-line "
                            "option");
-                       return -1;
+                       goto out;
                }
-               value = match_cfg_line(options, &s, pw, host, original_host,
+               value = match_cfg_line(options, &str, pw, host, original_host,
                    flags & SSHCONF_FINAL, want_final_pass,
                    filename, linenum);
                if (value < 0) {
                        error("%.200s line %d: Bad Match condition", filename,
                            linenum);
-                       return -1;
+                       goto out;
                }
                *activep = (flags & SSHCONF_NEVERMATCH) ? 0 : value;
+               /*
+                * If match_cfg_line() didn't consume all its arguments then
+                * arrange for the extra arguments check below to fail.
+                */
+
+               if (str == NULL || *str == '\0')
+                       argv_consume(&ac);
                break;
 
        case oEscapeChar:
                intptr = &options->escape_char;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (strcmp(arg, "none") == 0)
                        value = SSH_ESCAPECHAR_NONE;
@@ -1589,7 +1667,7 @@ parse_pubkey_algos:
                else {
                        error("%.200s line %d: Bad escape character.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1)
                        *intptr = value;
@@ -1617,11 +1695,11 @@ parse_pubkey_algos:
                goto parse_int;
 
        case oSendEnv:
-               while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
-                       if (strchr(arg, '=') != NULL) {
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0' || strchr(arg, '=') != NULL) {
                                error("%s line %d: Invalid environment name.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        if (!*activep)
                                continue;
@@ -1634,7 +1712,7 @@ parse_pubkey_algos:
                                if (options->num_send_env >= INT_MAX) {
                                        error("%s line %d: too many send env.",
                                            filename, linenum);
-                                       return -1;
+                                       goto out;
                                }
                                options->send_env = xrecallocarray(
                                    options->send_env, options->num_send_env,
@@ -1648,11 +1726,11 @@ parse_pubkey_algos:
 
        case oSetEnv:
                value = options->num_setenv;
-               while ((arg = strdelimw(&s)) != NULL && *arg != '\0') {
+               while ((arg = argv_next(&ac, &av)) != NULL) {
                        if (strchr(arg, '=') == NULL) {
                                error("%s line %d: Invalid SetEnv.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        if (!*activep || value != 0)
                                continue;
@@ -1660,7 +1738,7 @@ parse_pubkey_algos:
                        if (options->num_setenv >= INT_MAX) {
                                error("%s line %d: too many SetEnv.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        options->setenv = xrecallocarray(
                            options->setenv, options->num_setenv,
@@ -1681,11 +1759,11 @@ parse_pubkey_algos:
        case oControlPersist:
                /* no/false/yes/true, or a time spec */
                intptr = &options->control_persist;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing ControlPersist"
                            " argument.", filename, linenum);
-                       return -1;
+                       goto out;
                }
                value = 0;
                value2 = 0;     /* timeout */
@@ -1698,7 +1776,7 @@ parse_pubkey_algos:
                else {
                        error("%.200s line %d: Bad ControlPersist argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1) {
                        *intptr = value;
@@ -1716,17 +1794,17 @@ parse_pubkey_algos:
                goto parse_multistate;
 
        case oTunnelDevice:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                value = a2tun(arg, &value2);
                if (value == SSH_TUNID_ERR) {
                        error("%.200s line %d: Bad tun device.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && options->tun_local == -1) {
                        options->tun_local = value;
@@ -1754,10 +1832,15 @@ parse_pubkey_algos:
                if (cmdline) {
                        error("Include directive not supported as a "
                            "command-line option");
-                       return -1;
+                       goto out;
                }
                value = 0;
-               while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0') {
+                               error("%s line %d: keyword %s empty argument",
+                                   filename, linenum, keyword);
+                               goto out;
+                       }
                        /*
                         * Ensure all paths are anchored. User configuration
                         * files may begin with '~/' but system configurations
@@ -1768,7 +1851,7 @@ parse_pubkey_algos:
                        if (*arg == '~' && (flags & SSHCONF_USERCONF) == 0) {
                                error("%.200s line %d: bad include path %s.",
                                    filename, linenum, arg);
-                               return -1;
+                               goto out;
                        }
                        if (!path_absolute(arg) && *arg != '~') {
                                xasprintf(&arg2, "%s/%s",
@@ -1786,7 +1869,7 @@ parse_pubkey_algos:
                        } else if (r != 0) {
                                error("%.200s line %d: glob failed for %s.",
                                    filename, linenum, arg2);
-                               return -1;
+                               goto out;
                        }
                        free(arg2);
                        oactive = *activep;
@@ -1805,7 +1888,7 @@ parse_pubkey_algos:
                                            "%.100s: %.100s", gl.gl_pathv[i],
                                            strerror(errno));
                                        globfree(&gl);
-                                       return -1;
+                                       goto out;
                                }
                                /*
                                 * don't let Match in includes clobber the
@@ -1818,23 +1901,23 @@ parse_pubkey_algos:
                        globfree(&gl);
                }
                if (value != 0)
-                       return value;
+                       ret = value;
                break;
 
        case oIPQoS:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if ((value = parse_ipqos(arg)) == -1) {
                        error("%s line %d: Bad IPQoS value: %s",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (arg == NULL)
                        value2 = value;
                else if ((value2 = parse_ipqos(arg)) == -1) {
                        error("%s line %d: Bad IPQoS value: %s",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                if (*activep && options->ip_qos_interactive == -1) {
                        options->ip_qos_interactive = value;
@@ -1857,11 +1940,27 @@ parse_pubkey_algos:
 
        case oCanonicalDomains:
                value = options->num_canonical_domains != 0;
-               while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+               i = 0;
+               while ((arg = argv_next(&ac, &av)) != NULL) {
+                       if (*arg == '\0') {
+                               error("%s line %d: keyword %s empty argument",
+                                   filename, linenum, keyword);
+                               goto out;
+                       }
+                       /* Allow "none" only in first position */
+                       if (strcasecmp(arg, "none") == 0) {
+                               if (i > 0 || ac > 0) {
+                                       error("%s line %d: keyword %s \"none\" "
+                                           "argument must appear alone.",
+                                           filename, linenum, keyword);
+                                       goto out;
+                               }
+                       }
+                       i++;
                        if (!valid_domain(arg, 1, &errstr)) {
                                error("%s line %d: %s", filename, linenum,
                                    errstr);
-                               return -1;
+                               goto out;
                        }
                        if (!*activep || value)
                                continue;
@@ -1869,7 +1968,7 @@ parse_pubkey_algos:
                            MAX_CANON_DOMAINS) {
                                error("%s line %d: too many hostname suffixes.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        options->canonical_domains[
                            options->num_canonical_domains++] = xstrdup(arg);
@@ -1878,7 +1977,7 @@ parse_pubkey_algos:
 
        case oCanonicalizePermittedCNAMEs:
                value = options->num_permitted_cnames != 0;
-               while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+               while ((arg = argv_next(&ac, &av)) != NULL) {
                        /* Either '*' for everything or 'list:list' */
                        if (strcmp(arg, "*") == 0)
                                arg2 = arg;
@@ -1889,7 +1988,7 @@ parse_pubkey_algos:
                                        error("%s line %d: "
                                            "Invalid permitted CNAME \"%s\"",
                                            filename, linenum, arg);
-                                       return -1;
+                                       goto out;
                                }
                                *arg2 = '\0';
                                arg2++;
@@ -1900,7 +1999,7 @@ parse_pubkey_algos:
                            MAX_CANON_DOMAINS) {
                                error("%s line %d: too many permitted CNAMEs.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        cname = options->permitted_cnames +
                            options->num_permitted_cnames++;
@@ -1923,17 +2022,17 @@ parse_pubkey_algos:
                goto parse_flag;
 
        case oStreamLocalBindMask:
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing StreamLocalBindMask "
                            "argument.", filename, linenum);
-                       return -1;
+                       goto out;
                }
                /* Parse mode in octal format */
                value = strtol(arg, &endofnumber, 8);
                if (arg == endofnumber || value < 0 || value > 0777) {
                        error("%.200s line %d: Bad mask.", filename, linenum);
-                       return -1;
+                       goto out;
                }
                options->fwd_opts.streamlocal_bind_mask = (mode_t)value;
                break;
@@ -1948,16 +2047,16 @@ parse_pubkey_algos:
 
        case oFingerprintHash:
                intptr = &options->fingerprint_hash;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if ((value = ssh_digest_alg_by_name(arg)) == -1) {
                        error("%.200s line %d: Invalid hash algorithm \"%s\".",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                if (*activep && *intptr == -1)
                        *intptr = value;
@@ -1977,8 +2076,8 @@ parse_pubkey_algos:
                goto parse_pubkey_algos;
 
        case oAddKeysToAgent:
-               arg = strdelim(&s);
-               arg2 = strdelim(&s);
+               arg = argv_next(&ac, &av);
+               arg2 = argv_next(&ac, &av);
                value = parse_multistate_value(arg, filename, linenum,
                    multistate_yesnoaskconfirm);
                value2 = 0; /* unlimited lifespan by default */
@@ -1988,20 +2087,20 @@ parse_pubkey_algos:
                            value2 > INT_MAX) {
                                error("%s line %d: invalid time value.",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                } else if (value == -1 && arg2 == NULL) {
                        if ((value2 = convtime(arg)) == -1 ||
                            value2 > INT_MAX) {
                                error("%s line %d: unsupported option",
                                    filename, linenum);
-                               return -1;
+                               goto out;
                        }
                        value = 1; /* yes */
                } else if (value == -1 || arg2 != NULL) {
                        error("%s line %d: unsupported option",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
                if (*activep && options->add_keys_to_agent == -1) {
                        options->add_keys_to_agent = value;
@@ -2011,18 +2110,18 @@ parse_pubkey_algos:
 
        case oIdentityAgent:
                charptr = &options->identity_agent;
-               arg = strdelim(&s);
+               arg = argv_next(&ac, &av);
                if (!arg || *arg == '\0') {
                        error("%.200s line %d: Missing argument.",
                            filename, linenum);
-                       return -1;
+                       goto out;
                }
   parse_agent_path:
                /* Extra validation if the string represents an env var. */
                if ((arg2 = dollar_expand(&r, arg)) == NULL || r) {
                        error("%.200s line %d: Invalid environment expansion "
                            "%s.", filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                free(arg2);
                /* check for legacy environment format */
@@ -2030,7 +2129,7 @@ parse_pubkey_algos:
                    !valid_env_name(arg + 1)) {
                        error("%.200s line %d: Invalid environment name %s.",
                            filename, linenum, arg);
-                       return -1;
+                       goto out;
                }
                if (*activep && *charptr == NULL)
                        *charptr = xstrdup(arg);
@@ -2039,25 +2138,33 @@ parse_pubkey_algos:
        case oDeprecated:
                debug("%s line %d: Deprecated option \"%s\"",
                    filename, linenum, keyword);
-               return 0;
+               argv_consume(&ac);
+               break;
 
        case oUnsupported:
                error("%s line %d: Unsupported option \"%s\"",
                    filename, linenum, keyword);
-               return 0;
+               argv_consume(&ac);
+               break;
 
        default:
                error("%s line %d: Unimplemented opcode %d",
                    filename, linenum, opcode);
+               goto out;
        }
 
        /* Check that there is no garbage at end of line. */
-       if ((arg = strdelim(&s)) != NULL && *arg != '\0') {
-               error("%.200s line %d: garbage at end of line; \"%.200s\".",
-                   filename, linenum, arg);
-               return -1;
+       if (ac > 0) {
+               error("%.200s line %d: keyword %s extra arguments "
+                   "at end of line", filename, linenum, keyword);
+               goto out;
        }
-       return 0;
+
+       /* success */
+       ret = 0;
+ out:
+       argv_free(oav, oac);
+       return ret;
 }
 
 /*
@@ -2083,7 +2190,7 @@ read_config_file_depth(const char *filename, struct passwd *pw,
     int flags, int *activep, int *want_final_pass, int depth)
 {
        FILE *f;
-       char *cp, *line = NULL;
+       char *line = NULL;
        size_t linesize = 0;
        int linenum;
        int bad_options = 0;
@@ -2119,8 +2226,6 @@ read_config_file_depth(const char *filename, struct passwd *pw,
                 * NB - preserve newlines, they are needed to reproduce
                 * line numbers later for error messages.
                 */
-               if ((cp = strchr(line, '#')) != NULL)
-                       *cp = '\0';
                if (process_config_line_depth(options, pw, host, original_host,
                    line, filename, linenum, activep, flags, want_final_pass,
                    depth) != 0)
@@ -2791,7 +2896,10 @@ parse_forward(struct Forward *fwd, const char *fwdspec, int dynamicfwd, int remo
        if (fwd->connect_host != NULL &&
            strlen(fwd->connect_host) >= NI_MAXHOST)
                goto fail_free;
-       /* XXX - if connecting to a remote socket, max sun len may not match this host */
+       /*
+        * XXX - if connecting to a remote socket, max sun len may not
+        * match this host
+        */
        if (fwd->connect_path != NULL &&
            strlen(fwd->connect_path) >= PATH_MAX_SUN)
                goto fail_free;
@@ -2826,6 +2934,12 @@ parse_jump(const char *s, Options *o, int active)
        active &= o->proxy_command == NULL && o->jump_host == NULL;
 
        orig = sdup = xstrdup(s);
+
+       /* Remove comment and trailing whitespace */
+       if ((cp = strchr(orig, '#')) != NULL)
+               *cp = '\0';
+       rtrim(orig);
+
        first = active;
        do {
                if (strcasecmp(s, "none") == 0)
@@ -2998,6 +3112,8 @@ dump_cfg_strarray_oneline(OpCodes code, u_int count, char **vals)
        u_int i;
 
        printf("%s", lookup_opcode_name(code));
+       if (count == 0)
+               printf(" none");
        for (i = 0; i < count; i++)
                printf(" %s",  vals[i]);
        printf("\n");
index 35c0327..4704435 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: ssh.c,v 1.558 2021/06/04 05:02:40 djm Exp $ */
+/* $OpenBSD: ssh.c,v 1.559 2021/06/08 07:07:15 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -488,6 +488,8 @@ resolve_canonicalize(char **hostp, int port)
        }
        /* Attempt each supplied suffix */
        for (i = 0; i < options.num_canonical_domains; i++) {
+               if (strcasecmp(options.canonical_domains[i], "none") == 0)
+                       break;
                xasprintf(&fullhost, "%s.%s.", *hostp,
                    options.canonical_domains[i]);
                debug3_f("attempting \"%s\" => \"%s\"", *hostp, fullhost);
@@ -1314,8 +1316,11 @@ main(int ac, char **av)
 
        /* reinit */
        log_init(argv0, options.log_level, options.log_facility, !use_syslog);
-       for (j = 0; j < options.num_log_verbose; j++)
+       for (j = 0; j < options.num_log_verbose; j++) {
+               if (strcasecmp(options.log_verbose[j], "none") == 0)
+                       break;
                log_verbose_add(options.log_verbose[j]);
+       }
 
        if (options.request_tty == REQUEST_TTY_YES ||
            options.request_tty == REQUEST_TTY_FORCE)