Copyright updates:
[exim.git] / src / src / acl.c
index 7284831a6ef881952d4a1c184329b7d83d6157a7..c1d60bbd965f18798a61797da8fcdc0be37ad49b 100644 (file)
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
+/* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for handling Access Control Lists (ACLs) */
@@ -112,7 +113,8 @@ enum { ACLC_ACL,
 /* ACL conditions/modifiers: "delay", "control", "continue", "endpass",
 "message", "log_message", "log_reject_target", "logwrite", "queue" and "set" are
 modifiers that look like conditions but always return TRUE. They are used for
-their side effects. */
+their side effects.  Do not invent new modifier names that result in one name
+being the prefix of another; the binary-search in the list will go wrong. */
 
 typedef struct condition_def {
   uschar       *name;
@@ -366,7 +368,7 @@ enum {
   CONTROL_NO_MULTILINE,
   CONTROL_NO_PIPELINING,
 
-  CONTROL_QUEUE_ONLY,
+  CONTROL_QUEUE,
   CONTROL_SUBMISSION,
   CONTROL_SUPPRESS_LOCAL_FIXUPS,
 #ifdef SUPPORT_I18N
@@ -502,8 +504,8 @@ static control_def controls_list[] = {
          ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START
   },
 
-[CONTROL_QUEUE_ONLY] =
-  { US"queue_only",              FALSE,
+[CONTROL_QUEUE] =
+  { US"queue",                 TRUE,
          (unsigned)
          ~(ACL_BIT_MAIL | ACL_BIT_RCPT |
            ACL_BIT_PREDATA | ACL_BIT_DATA |
@@ -511,7 +513,6 @@ static control_def controls_list[] = {
            ACL_BIT_NOTSMTP | ACL_BIT_MIME)
   },
 
-
 [CONTROL_SUBMISSION] =
   { US"submission",              TRUE,
          (unsigned)
@@ -732,7 +733,7 @@ uschar * s;
 
 *error = NULL;
 
-while ((s = (*func)()) != NULL)
+while ((s = (*func)()))
   {
   int v, c;
   BOOL negated = FALSE;
@@ -742,8 +743,7 @@ while ((s = (*func)()) != NULL)
   /* Conditions (but not verbs) are allowed to be negated by an initial
   exclamation mark. */
 
-  while (isspace(*s)) s++;
-  if (*s == '!')
+  if (Uskip_whitespace(&s) == '!')
     {
     negated = TRUE;
     s++;
@@ -859,7 +859,7 @@ while ((s = (*func)()) != NULL)
        }
       cond->u.varname = string_copyn(s, 18);
       s = endptr;
-      while (isspace(*s)) s++;
+      Uskip_whitespace(&s);
       }
     else
 #endif
@@ -895,7 +895,7 @@ while ((s = (*func)()) != NULL)
 
     cond->u.varname = string_copyn(s + 4, endptr - s - 4);
     s = endptr;
-    while (isspace(*s)) s++;
+    Uskip_whitespace(&s);
     }
 
   /* For "set", we are now positioned for the data. For the others, only
@@ -909,7 +909,7 @@ while ((s = (*func)()) != NULL)
         conditions[c].is_modifier ? US"modifier" : US"condition");
       return NULL;
       }
-    while (isspace(*s)) s++;
+    Uskip_whitespace(&s);
     cond->arg = string_copy(s);
     }
   }
@@ -1022,8 +1022,8 @@ for (p = q; *p; p = q)
   if (!*hptr)
     {
     /* The header_line struct itself is not tainted, though it points to
-    tainted data. */
-    header_line *h = store_get(sizeof(header_line), FALSE);
+    possibly tainted data. */
+    header_line * h = store_get(sizeof(header_line), FALSE);
     h->text = hdr;
     h->next = NULL;
     h->type = newtype;
@@ -1601,7 +1601,7 @@ an error if options are given for items that don't expect them.
 
 uschar *slash = Ustrchr(arg, '/');
 const uschar *list = arg;
-uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size);
+uschar *ss = string_nextinlist(&list, &sep, NULL, 0);
 verify_type_t * vp;
 
 if (!ss) goto BAD_VERIFY;
@@ -2113,7 +2113,9 @@ return ERROR;
 *        Check argument for control= modifier    *
 *************************************************/
 
-/* Called from acl_check_condition() below
+/* Called from acl_check_condition() below.
+To handle the case "queue_only" we accept an _ in the
+initial / option-switch position.
 
 Arguments:
   arg         the argument string for control=
@@ -2129,10 +2131,11 @@ decode_control(const uschar *arg, const uschar **pptr, int where, uschar **log_m
 {
 int idx, len;
 control_def * d;
+uschar c;
 
 if (  (idx = find_control(arg, controls_list, nelem(controls_list))) < 0
-   || (  arg[len = Ustrlen((d = controls_list+idx)->name)] != 0
-      && (!d->has_option || arg[len] != '/')
+   || (  (c = arg[len = Ustrlen((d = controls_list+idx)->name)]) != 0
+      && (!d->has_option || c != '/' && c != '_')
    )  )
   {
   *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
@@ -2258,7 +2261,7 @@ count = 1.0;
 
 /* Parse the other options. */
 
-while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)))
+while ((ss = string_nextinlist(&arg, &sep, NULL, 0)))
   {
   if (strcmpic(ss, US"leaky") == 0) leaky = TRUE;
   else if (strcmpic(ss, US"strict") == 0) strict = TRUE;
@@ -3018,193 +3021,196 @@ for (; cb; cb = cb->next)
       switch(control_type)
        {
        case CONTROL_AUTH_UNADVERTISED:
-       f.allow_auth_unadvertised = TRUE;
-       break;
+         f.allow_auth_unadvertised = TRUE;
+         break;
 
-       #ifdef EXPERIMENTAL_BRIGHTMAIL
+#ifdef EXPERIMENTAL_BRIGHTMAIL
        case CONTROL_BMI_RUN:
-       bmi_run = 1;
-       break;
-       #endif
+         bmi_run = 1;
+         break;
+#endif
 
 #ifndef DISABLE_DKIM
        case CONTROL_DKIM_VERIFY:
-       f.dkim_disable_verify = TRUE;
+         f.dkim_disable_verify = TRUE;
 # ifdef SUPPORT_DMARC
-       /* Since DKIM was blocked, skip DMARC too */
-       f.dmarc_disable_verify = TRUE;
-       f.dmarc_enable_forensic = FALSE;
+         /* Since DKIM was blocked, skip DMARC too */
+         f.dmarc_disable_verify = TRUE;
+         f.dmarc_enable_forensic = FALSE;
 # endif
        break;
 #endif
 
 #ifdef SUPPORT_DMARC
        case CONTROL_DMARC_VERIFY:
-       f.dmarc_disable_verify = TRUE;
-       break;
+         f.dmarc_disable_verify = TRUE;
+         break;
 
        case CONTROL_DMARC_FORENSIC:
-       f.dmarc_enable_forensic = TRUE;
-       break;
+         f.dmarc_enable_forensic = TRUE;
+         break;
 #endif
 
        case CONTROL_DSCP:
-       if (*p == '/')
-         {
-         int fd, af, level, optname, value;
-         /* If we are acting on stdin, the setsockopt may fail if stdin is not
-         a socket; we can accept that, we'll just debug-log failures anyway. */
-         fd = fileno(smtp_in);
-         af = ip_get_address_family(fd);
-         if (af < 0)
+         if (*p == '/')
            {
-           HDEBUG(D_acl)
-             debug_printf_indent("smtp input is probably not a socket [%s], not setting DSCP\n",
-                 strerror(errno));
-           break;
-           }
-         if (dscp_lookup(p+1, af, &level, &optname, &value))
-           {
-           if (setsockopt(fd, level, optname, &value, sizeof(value)) < 0)
+           int fd, af, level, optname, value;
+           /* If we are acting on stdin, the setsockopt may fail if stdin is not
+           a socket; we can accept that, we'll just debug-log failures anyway. */
+           fd = fileno(smtp_in);
+           if ((af = ip_get_address_family(fd)) < 0)
              {
-             HDEBUG(D_acl) debug_printf_indent("failed to set input DSCP[%s]: %s\n",
-                 p+1, strerror(errno));
+             HDEBUG(D_acl)
+               debug_printf_indent("smtp input is probably not a socket [%s], not setting DSCP\n",
+                   strerror(errno));
+             break;
              }
+           if (dscp_lookup(p+1, af, &level, &optname, &value))
+             if (setsockopt(fd, level, optname, &value, sizeof(value)) < 0)
+               {
+               HDEBUG(D_acl) debug_printf_indent("failed to set input DSCP[%s]: %s\n",
+                   p+1, strerror(errno));
+               }
+             else
+               {
+               HDEBUG(D_acl) debug_printf_indent("set input DSCP to \"%s\"\n", p+1);
+               }
            else
              {
-             HDEBUG(D_acl) debug_printf_indent("set input DSCP to \"%s\"\n", p+1);
+             *log_msgptr = string_sprintf("unrecognised DSCP value in \"control=%s\"", arg);
+             return ERROR;
              }
            }
          else
            {
-           *log_msgptr = string_sprintf("unrecognised DSCP value in \"control=%s\"", arg);
+           *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
            return ERROR;
            }
-         }
-       else
-         {
-         *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
-         return ERROR;
-         }
-       break;
+         break;
 
        case CONTROL_ERROR:
-       return ERROR;
+         return ERROR;
 
        case CONTROL_CASEFUL_LOCAL_PART:
-       deliver_localpart = addr->cc_local_part;
-       break;
+         deliver_localpart = addr->cc_local_part;
+         break;
 
        case CONTROL_CASELOWER_LOCAL_PART:
-       deliver_localpart = addr->lc_local_part;
-       break;
+         deliver_localpart = addr->lc_local_part;
+         break;
 
        case CONTROL_ENFORCE_SYNC:
-       smtp_enforce_sync = TRUE;
-       break;
+         smtp_enforce_sync = TRUE;
+         break;
 
        case CONTROL_NO_ENFORCE_SYNC:
-       smtp_enforce_sync = FALSE;
-       break;
+         smtp_enforce_sync = FALSE;
+         break;
 
-       #ifdef WITH_CONTENT_SCAN
+#ifdef WITH_CONTENT_SCAN
        case CONTROL_NO_MBOX_UNSPOOL:
-       f.no_mbox_unspool = TRUE;
-       break;
-       #endif
+         f.no_mbox_unspool = TRUE;
+         break;
+#endif
 
        case CONTROL_NO_MULTILINE:
-       f.no_multiline_responses = TRUE;
-       break;
+         f.no_multiline_responses = TRUE;
+         break;
 
        case CONTROL_NO_PIPELINING:
-       f.pipelining_enable = FALSE;
-       break;
+         f.pipelining_enable = FALSE;
+         break;
 
        case CONTROL_NO_DELAY_FLUSH:
-       f.disable_delay_flush = TRUE;
-       break;
+         f.disable_delay_flush = TRUE;
+         break;
 
        case CONTROL_NO_CALLOUT_FLUSH:
-       f.disable_callout_flush = TRUE;
-       break;
+         f.disable_callout_flush = TRUE;
+         break;
 
        case CONTROL_FAKEREJECT:
-       cancel_cutthrough_connection(TRUE, US"fakereject");
-       case CONTROL_FAKEDEFER:
-       fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL;
-       if (*p == '/')
-         {
-         const uschar *pp = p + 1;
-         while (*pp) pp++;
-         fake_response_text = expand_string(string_copyn(p+1, pp-p-1));
-         p = pp;
-         }
-        else
-         {
-         /* Explicitly reset to default string */
-         fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s).";
-         }
-       break;
+         cancel_cutthrough_connection(TRUE, US"fakereject");
+         case CONTROL_FAKEDEFER:
+         fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL;
+         if (*p == '/')
+           {
+           const uschar *pp = p + 1;
+           while (*pp) pp++;
+           fake_response_text = expand_string(string_copyn(p+1, pp-p-1));
+           p = pp;
+           }
+          else /* Explicitly reset to default string */
+           fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s).";
+         break;
 
        case CONTROL_FREEZE:
-       f.deliver_freeze = TRUE;
-       deliver_frozen_at = time(NULL);
-       freeze_tell = freeze_tell_config;       /* Reset to configured value */
-       if (Ustrncmp(p, "/no_tell", 8) == 0)
-         {
-         p += 8;
-         freeze_tell = NULL;
-         }
-       if (*p != 0)
-         {
-         *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
-         return ERROR;
-         }
-       cancel_cutthrough_connection(TRUE, US"item frozen");
-       break;
+         f.deliver_freeze = TRUE;
+         deliver_frozen_at = time(NULL);
+         freeze_tell = freeze_tell_config;       /* Reset to configured value */
+         if (Ustrncmp(p, "/no_tell", 8) == 0)
+           {
+           p += 8;
+           freeze_tell = NULL;
+           }
+         if (*p)
+           {
+           *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+           return ERROR;
+           }
+         cancel_cutthrough_connection(TRUE, US"item frozen");
+         break;
 
-       case CONTROL_QUEUE_ONLY:
-       f.queue_only_policy = TRUE;
-       cancel_cutthrough_connection(TRUE, US"queueing forced");
-       break;
+       case CONTROL_QUEUE:
+         f.queue_only_policy = TRUE;
+         if (Ustrcmp(p, "_only") == 0)
+           p += 5;
+         else while (*p == '/')
+           if (Ustrncmp(p, "/only", 5) == 0)
+             { p += 5; f.queue_smtp = FALSE; }
+           else if (Ustrncmp(p, "/first_pass_route", 17) == 0)
+             { p += 17; f.queue_smtp = TRUE; }
+           else
+             break;
+         cancel_cutthrough_connection(TRUE, US"queueing forced");
+         break;
 
        case CONTROL_SUBMISSION:
-       originator_name = US"";
-       f.submission_mode = TRUE;
-       while (*p == '/')
-         {
-         if (Ustrncmp(p, "/sender_retain", 14) == 0)
-           {
-           p += 14;
-           f.active_local_sender_retain = TRUE;
-           f.active_local_from_check = FALSE;
-           }
-         else if (Ustrncmp(p, "/domain=", 8) == 0)
+         originator_name = US"";
+         f.submission_mode = TRUE;
+         while (*p == '/')
            {
-           const uschar *pp = p + 8;
-           while (*pp && *pp != '/') pp++;
-           submission_domain = string_copyn(p+8, pp-p-8);
-           p = pp;
+           if (Ustrncmp(p, "/sender_retain", 14) == 0)
+             {
+             p += 14;
+             f.active_local_sender_retain = TRUE;
+             f.active_local_from_check = FALSE;
+             }
+           else if (Ustrncmp(p, "/domain=", 8) == 0)
+             {
+             const uschar *pp = p + 8;
+             while (*pp && *pp != '/') pp++;
+             submission_domain = string_copyn(p+8, pp-p-8);
+             p = pp;
+             }
+           /* The name= option must be last, because it swallows the rest of
+           the string. */
+           else if (Ustrncmp(p, "/name=", 6) == 0)
+             {
+             const uschar *pp = p + 6;
+             while (*pp) pp++;
+             submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6,
+               big_buffer, big_buffer_size));
+             p = pp;
+             }
+           else break;
            }
-         /* The name= option must be last, because it swallows the rest of
-         the string. */
-         else if (Ustrncmp(p, "/name=", 6) == 0)
+         if (*p)
            {
-           const uschar *pp = p + 6;
-           while (*pp) pp++;
-           submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6,
-             big_buffer, big_buffer_size));
-           p = pp;
+           *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+           return ERROR;
            }
-         else break;
-         }
-       if (*p != 0)
-         {
-         *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
-         return ERROR;
-         }
-       break;
+         break;
 
        case CONTROL_DEBUG:
          {
@@ -3239,99 +3245,99 @@ for (; cb; cb = cb->next)
              debug_logging_stop();
            else
              debug_logging_activate(debug_tag, debug_opts);
+         break;
          }
-       break;
 
        case CONTROL_SUPPRESS_LOCAL_FIXUPS:
-       f.suppress_local_fixups = TRUE;
-       break;
+         f.suppress_local_fixups = TRUE;
+         break;
 
        case CONTROL_CUTTHROUGH_DELIVERY:
-       {
-       uschar * ignored = NULL;
+         {
+         uschar * ignored = NULL;
 #ifndef DISABLE_PRDR
-       if (prdr_requested)
+         if (prdr_requested)
 #else
-       if (0)
+         if (0)
 #endif
-         /* Too hard to think about for now.  We might in future cutthrough
-         the case where both sides handle prdr and this-node prdr acl
-         is "accept" */
-         ignored = US"PRDR active";
-       else
-         {
-         if (f.deliver_freeze)
-           ignored = US"frozen";
-         else if (f.queue_only_policy)
-           ignored = US"queue-only";
-         else if (fake_response == FAIL)
-           ignored = US"fakereject";
+           /* Too hard to think about for now.  We might in future cutthrough
+           the case where both sides handle prdr and this-node prdr acl
+           is "accept" */
+           ignored = US"PRDR active";
          else
            {
-           if (rcpt_count == 1)
+           if (f.deliver_freeze)
+             ignored = US"frozen";
+           else if (f.queue_only_policy)
+             ignored = US"queue-only";
+           else if (fake_response == FAIL)
+             ignored = US"fakereject";
+           else
              {
-             cutthrough.delivery = TRUE;       /* control accepted */
-             while (*p == '/')
+             if (rcpt_count == 1)
                {
-               const uschar * pp = p+1;
-               if (Ustrncmp(pp, "defer=", 6) == 0)
+               cutthrough.delivery = TRUE;     /* control accepted */
+               while (*p == '/')
                  {
-                 pp += 6;
-                 if (Ustrncmp(pp, "pass", 4) == 0) cutthrough.defer_pass = TRUE;
-                 /* else if (Ustrncmp(pp, "spool") == 0) ;     default */
+                 const uschar * pp = p+1;
+                 if (Ustrncmp(pp, "defer=", 6) == 0)
+                   {
+                   pp += 6;
+                   if (Ustrncmp(pp, "pass", 4) == 0) cutthrough.defer_pass = TRUE;
+                   /* else if (Ustrncmp(pp, "spool") == 0) ;   default */
+                   }
+                 else
+                   while (*pp && *pp != '/') pp++;
+                 p = pp;
                  }
-               else
-                 while (*pp && *pp != '/') pp++;
-               p = pp;
                }
+             else
+               ignored = US"nonfirst rcpt";
              }
-           else
-             ignored = US"nonfirst rcpt";
            }
+         DEBUG(D_acl) if (ignored)
+           debug_printf(" cutthrough request ignored on %s item\n", ignored);
          }
-       DEBUG(D_acl) if (ignored)
-         debug_printf(" cutthrough request ignored on %s item\n", ignored);
-       }
        break;
 
 #ifdef SUPPORT_I18N
        case CONTROL_UTF8_DOWNCONVERT:
-       if (*p == '/')
-         {
-         if (p[1] == '1')
+         if (*p == '/')
            {
-           message_utf8_downconvert = 1;
-           addr->prop.utf8_downcvt = TRUE;
-           addr->prop.utf8_downcvt_maybe = FALSE;
-           p += 2;
-           break;
+           if (p[1] == '1')
+             {
+             message_utf8_downconvert = 1;
+             addr->prop.utf8_downcvt = TRUE;
+             addr->prop.utf8_downcvt_maybe = FALSE;
+             p += 2;
+             break;
+             }
+           if (p[1] == '0')
+             {
+             message_utf8_downconvert = 0;
+             addr->prop.utf8_downcvt = FALSE;
+             addr->prop.utf8_downcvt_maybe = FALSE;
+             p += 2;
+             break;
+             }
+           if (p[1] == '-' && p[2] == '1')
+             {
+             message_utf8_downconvert = -1;
+             addr->prop.utf8_downcvt = FALSE;
+             addr->prop.utf8_downcvt_maybe = TRUE;
+             p += 3;
+             break;
+             }
+           *log_msgptr = US"bad option value for control=utf8_downconvert";
            }
-         if (p[1] == '0')
+         else
            {
-           message_utf8_downconvert = 0;
-           addr->prop.utf8_downcvt = FALSE;
+           message_utf8_downconvert = 1;
+           addr->prop.utf8_downcvt = TRUE;
            addr->prop.utf8_downcvt_maybe = FALSE;
-           p += 2;
            break;
            }
-         if (p[1] == '-' && p[2] == '1')
-           {
-           message_utf8_downconvert = -1;
-           addr->prop.utf8_downcvt = FALSE;
-           addr->prop.utf8_downcvt_maybe = TRUE;
-           p += 3;
-           break;
-           }
-         *log_msgptr = US"bad option value for control=utf8_downconvert";
-         }
-       else
-         {
-         message_utf8_downconvert = 1;
-         addr->prop.utf8_downcvt = TRUE;
-         addr->prop.utf8_downcvt_maybe = FALSE;
-         break;
-         }
-       return ERROR;
+         return ERROR;
 #endif
 
        }
@@ -3472,13 +3478,13 @@ for (; cb; cb = cb->next)
       {
       uschar *endcipher = NULL;
       uschar *cipher = Ustrchr(tls_in.cipher, ':');
-      if (cipher == NULL) cipher = tls_in.cipher; else
+      if (!cipher) cipher = tls_in.cipher; else
         {
         endcipher = Ustrchr(++cipher, ':');
-        if (endcipher != NULL) *endcipher = 0;
+        if (endcipher) *endcipher = 0;
         }
       rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-      if (endcipher != NULL) *endcipher = ':';
+      if (endcipher) *endcipher = ':';
       }
     break;
 
@@ -3491,8 +3497,7 @@ for (; cb; cb = cb->next)
 
     case ACLC_HOSTS:
     rc = verify_check_this_host(&arg, sender_host_cache, NULL,
-      (sender_host_address == NULL)? US"" : sender_host_address,
-      CUSS &host_data);
+      sender_host_address ? sender_host_address : US"", CUSS &host_data);
     if (rc == DEFER) *log_msgptr = search_error_message;
     if (host_data) host_data = string_copy_perm(host_data, TRUE);
     break;
@@ -3590,6 +3595,12 @@ for (; cb; cb = cb->next)
     #endif
 
     case ACLC_QUEUE:
+    if (is_tainted(arg))
+      {
+      *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
+                                   arg);
+      return ERROR;
+      }
     if (Ustrchr(arg, '/'))
       {
       *log_msgptr = string_sprintf(
@@ -3831,16 +3842,16 @@ uschar *yield;
 
 for(;;)
   {
-  while (isspace(*acl_text)) acl_text++;   /* Leading spaces/empty lines */
-  if (*acl_text == 0) return NULL;         /* No more data */
-  yield = acl_text;                        /* Potential data line */
+  Uskip_whitespace(&acl_text);         /* Leading spaces/empty lines */
+  if (!*acl_text) return NULL;         /* No more data */
+  yield = acl_text;                    /* Potential data line */
 
   while (*acl_text && *acl_text != '\n') acl_text++;
 
   /* If we hit the end before a newline, we have the whole logical line. If
   it's a comment, there's no more data to be given. Otherwise, yield it. */
 
-  if (*acl_text == 0) return (*yield == '#')? NULL : yield;
+  if (!*acl_text) return *yield == '#' ? NULL : yield;
 
   /* After reaching a newline, end this loop if the physical line does not
   start with '#'. If it does, it's a comment, and the loop continues. */