Skip to content

prefix= is filtered for [/] but the other m4-passing %option values aren't #728

@MarkLee131

Description

@MarkLee131

src/parse.y:209-210 rejects [ or ] in the prefix= value:

|  TOK_PREFIX '=' NAME
    { ctrl.prefix = xstrdup(nmstr);
      if (strchr(ctrl.prefix, '[') || strchr(ctrl.prefix, ']'))
          flexerror(_("Prefix must not contain [ or ]")); }

But the same rule isn't applied to the other %option values that flow into m4 input later, even though they're all interpolated the same way. From src/filter.c:275:

fprintf (to_c, "m4_define( [[M4_YY_OUTFILE_NAME]],[[%s]])m4_dnl\n",
         env.outfilename != NULL ? env.outfilename : "<stdout>");

env.outfilename came from xstrdup(nmstr) at src/parse.y:202 with no [/] filter. Same shape for header-file= (parse.y:213-214 → filter.c:265-267), and for yydecl= / yyclass= / extra-type= / pre-action= / post-action= / yyterminate= which reach m4 via out_str(...) and out_m4_define(...) calls in src/main.c:1279-1560.

The OPTION-state quoted-string rule (src/scan.l:480, "\""[^""\n]*"\"") accepts everything except " and newline, so [ ] ( ) are all allowed in those values.

PR #81 (commit 7528bc0, merged 2016-09-25, "Fix escaping of [[ and ]] in strings") already wired up the ESCAPED_QSTART / ESCAPED_QEND macros (defined at src/scan.l:49-50, with the actual escape strings at src/main.c:126-127) for action and code-block contexts. The existing call sites are at src/scan.l:101, :270, :304, :1035, :1045. The <OPTION>-state quoted-string rule at src/scan.l:480 was never given the same treatment, so %option outfile="…" (and the other m4-passing options) flow into m4 input unescaped.

Reproducer

%option outfile="ok_]]m4_syscmd(touch PWNED)m4_dnl[[.c"
%%
.   ;
%%
$ flex poc.l
gm4:stdin:N: ERROR: end of file in argument list
$ ls PWNED
PWNED

The injection works because flex passes -P to m4 (src/main.c initialize_output_filters line ~315), and m4 -P only renames builtins with the m4_ prefix; it does NOT disable them. So m4_syscmd, m4_esyscmd, m4_include are all available to inject into.

Verified the same payload across all %option values that flow into m4. Tested against upstream master @ 13c8018 (locally built), and against Apple's flex 2.6.4 Apple(flex-35) shipped in Xcode CLT for comparison:

%option upstream master Apple flex-35
outfile=
header-file=
yyclass= (with c++)
extra-type= (with reentrant)
yydecl=
pre-action=
post-action=
yyterminate=
tables-file= ✗ (doesn't go through m4)
prefix= ✗ (existing filter)

8 of 8 reachable vectors reproduce on upstream; the Apple fork only sees the four filename-shaped ones (outfile=, header-file=, yyclass=, extra-type=), suggesting the fork already adjusted some option-emit paths. The two filename-shaped options that reproduce in both versions are the ones most likely to slip past PR review.

Suggested fix

Two reasonable shapes:

  • Cheap: extend the existing prefix= filter to all of outfile= header-file= yydecl= yyclass= extra-type= pre-action= post-action= yyterminate=. Reject any [ or ]. Mostly a no-op for real users, since none of those values normally contain brackets.
  • Same shape as PR Fix escaping of [[ and ]] in strings #81: route those values through a small helper that substitutes [[ and ]] with escaped_qstart / escaped_qend before each fprintf(to_*, "...[[%s]]...", env.X) site in filter.c / main.c. That keeps bracketed values working for anyone who wants them, and reuses the escape strings PR Fix escaping of [[ and ]] in strings #81 already vetted.

Happy to PR either approach, your call.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions