13
(base) MacBook-Pro-2:pdftex zmx$ latex
This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017) (preloaded format=latex)
 restricted \write18 enabled.
**\s^EH
entering extended mode
LaTeX2e <2017-04-15>
Babel <3.10> and hyphenation patterns for 84 language(s) loaded.
! Undefined control sequence.
<*> \s
      ^^EH
? 1
! Text line contains an invalid character.
<*> \s^^E
         H
? Q
OK, entering \batchmode

In the example above, ^E is ctrl+e on MacOS. Obviously it is an invalid character, so that when I typed in "1", it outputted an error message "Text line contains an invalid character". I then typed in "Q", it seems it didn't start running, but required further input instead.

My question is why it require further input after "Q"?

Actually I'm debugging pdftex for my project. On pdftex 1.40.18 if you typed in:

\s^EH

1

Q

V

Then there will be a segment fault. enter image description here

If anyone is interested in this bug, feel free to discuss with me.

3
  • 5
    Doesn't happen with TeX Live 2020 on Linux. Update your TeX distribution. – Henri Menke Jun 27 '20 at 5:55
  • 1
    @HenriMenke Just because ^^E is no longer invalid when LaTeX starts up. – egreg Jun 27 '20 at 8:32
  • @egreg Yes, indeed but if I understand the question correctly OP is debugging a document and upgrading should fix it. – Henri Menke Jun 27 '20 at 9:02
15
  1. Original poster: can you please email me at karl at tug.org? You've discovered a bug in the original TeX and DEK will surely want to transfer a hefty balance to you in the Bank of San Seriffe :).

  2. the "further input after Q" and "segmentation fault" have the same underlying cause: TeX's data was getting into an inconsistent state in this unusual sequence of interactions.

  3. I've committed a fix (thanks to David Fuchs) to TL svn, r55767. It applies to all TeX variants except LuaTeX, which will have to be fixed independently. After a little more testing, I expect Akira will push out new binaries in his w32tex distribution, for anyone who can use that and feels like trying. The TL binaries will not be updated for this.

  4. The culprit was that your input caused module 83, <Get user's advice...>, to be invoked when interaction = batchmode. But this is never supposed to happen. Havoc ensues.

  5. The "H" (or any text) is needed to cause the crash in web2c because that crash happens due to trying to write the text to a closed \write stream. (selector was decremented from 16, no_print, to 15, which corresponds to \write15. Like I said, havoc.)

  6. For the record, I could reproduce the bug by running tex -ini <invalid.in, where invalid.in is a text file containing the four lines:

    \catcode`\^=7 \catcode`\^^?=15 \s^^?E
    1
    q
    v
    

Glad you found it.

7
+500

As of TeX version 3.141592653 and Metafont version 2.71828182, the bug has been fixed. The rest of this answer applies only to previous versions.


Note: It just occurred to me that this bug is present in Metafont as well. See the end of this answer.

For the sake of documentation, here’s exactly what makes the bug happen, since it took me a little while to figure out.

The key players are the two integer variables selector and interaction. Let’s focus on interaction first, since it’s the simpler of the two. It is supposed to control whether TeX stops to interact with the user, and it has four possible values:

  • When interaction = error_stop_mode = 3, TeX stops if an error occurs (§82, §530), or if \pausing is set to a positive value (§363), or if \read is used to get input from the terminal (§484), or if interrupt is nonzero at certain points [after a token list has been scanned (§324), after a line of input has been read (§343), and during the processing of ligatures (§753, §911)].

  • When interaction = scroll_mode = 2, TeX does not stop when a nonfatal error occurs, unless the problem is that a file can’t be found, in which case TeX will still prompt the user for a new file name (§530).

  • When interaction = nonstop_mode = 1, TeX does not stop unless a fatal error occurs, or it gets into a situation that requires input from the user—namely, if an \end command is not present in a file (§360), if a \read command requests input from the terminal (§484), or if a file cannot be found (§530). (These situations are treated as fatal, although they would not be if interaction is scroll_mode or error_stop_mode.)

  • When interaction = batch_mode = 0, TeX’s behavior is as when interaction is nonstop_mode, except that output to the terminal is omitted (§75, §90, §92, §1328). This is important.

Note that the level of user interaction increases as the value of interaction increases. Initially, interaction is set to error_stop_mode (§74).

The selector variable controls where TeX’s various text printing routines send their output. In TeX82, it has twenty-two possible values, ranging from 0 to 21. When 0 ≤ selector ≤ 15, it represents one of the files opened with \openout. The values of selector above 15 have the following meanings:

  • When selector = no_print = 16, printing goes nowhere.

  • When selector = term_only = 17, printing goes only to the terminal.

  • When selector = log_only = 18, printing goes only to the transcript file.

  • When selector = term_and_log = 19, printing goes to the terminal and to the transcript file.

  • When selector = pseudo = 20, characters are “printed” to a buffer for use by the show_context procedure, in a process called “pseudoprinting” (§315). This setting is not relevant to us.

  • When selector = new_string = 21, characters get appended to the string memory (if there’s any space left). This setting is not relevant to us either.

Initially, selector is term_only (§55, §1332), since no transcript file has been opened.

The values of selector and interaction are mostly independent. However, as you may expect, when interaction is batch_mode, selector should not be term_only or term_and_log. In one case, it is set to term_only unconditionally (though temporarily, since the previous value is saved in §534) in §535, regardless of interaction. But the general idea is that selector will be term_only or term_and_log if and only if interaction > batch_mode; in particular, selector will be term_only or term_and_log when interaction = error_stop_mode.

When TeX wishes to read a line from the terminal, it calls term_input (usually via prompt_input; see §71). This routine makes clever use of the numeric relation between selector’s possible values, in order to echo the input line if appropriate. The program assumes that selector must be either term_only or term_and_log upon entry to term_input (no other values would make sense). Hence term_input can decrement selector and unconditionally print the line the user input; if selector was term_only, it becomes no_print, which is correct because the line will have been echoed already (owing to the nature of terminals), and if selector was term_and_log, it becomes log_only, which is correct because the line must be written to the transcript file.

Let’s look at the error routine now. Its top level looks like this (§82):

procedure error;
  label continue, exit;
  var …;
  begin
    …
    if interaction = error_stop_mode then
      ⟨Get the user’s advice and return⟩;
    …
exit:
  end;

And the outline of ⟨Get the user’s advice…⟩ looks like this:

loop
  begin continue:
    clear_for_error_prompt;
    prompt_input("? ");
    if last = first then
      return;
    c ← buffer[first]
    if c ≥ "a" then
      c ← c + "A" − "a"; {convert to uppercase}
    ⟨Interpret code c and return if done⟩;
    ⟨Print the menu of available options⟩;
  end

The section ⟨Print the menu…⟩ is what it sounds like, except that the option to type E to edit the input file is not listed if no input file is open, and the option to type a number to delete tokens is not listed if deletions_allowed is false (in order to thwart more than two levels of recursion in error).

The interesting part of ⟨Interpret code c…⟩ is a big case statement, switching on the value of c. (The uninteresting part is actually ⟨Print the menu…⟩. I moved it to make the overall flow of the loop clearer.) In the following descriptions, transfers of control are in bold.

  1. If c is a decimal digit, and if it’s OK to delete tokens, then the number of tokens specified by the user are deleted and control goes to continue.

  2. If c is "E", then (in TeX82) the user is told what line of what file to edit and TeX terminates.

  3. If c is "H", then the help information is printed and control goes to continue.

  4. If c is "I", then a line of input is read from the terminal as the next thing for TeX to process, and control goes to exit via the return macro.

  5. If c is "Q", then interaction becomes batch_mode, selector gets decremented (to suppress terminal output), and control goes to exit.

  6. If c is "R", then interaction becomes nonstop_mode and control goes to exit.

  7. If c is "S", then interaction becomes scroll_mode and control goes to exit.

  8. If c is "X", then interaction becomes scroll_mode and TeX terminates.

  9. Otherwise, nothing happens; control falls through to ⟨Print the menu…⟩ and we go back to the top of the loop.

There is also a case for c = "D", if code for debugging isn’t commented out. Control goes to continue afterwards.

[Something interesting to note about cases 5, 6, 7: Each change of interaction is accompanied by a message saying OK, entering , followed by the new mode; e.g., when you type S, TeX says OK, entering scrollmode. Then the program does print("..."), so that the message ends up being OK, entering scrollmode.... In case 5, however, selector is decremented before the ellipses, so it ends up going either to the transcript file if selector was term_and_log or to nowhere if selector was term_only; the ... will not appear on the terminal. Knuth acknowledges this in the answer to his sixth exercise for TeX: The Program he published in TUGboat (exercises here, answers here).]

The process of deletion is pretty simple. First, the values of certain global variables (cur_tokcur_cmdcur_chr, and align_state) are saved. Then OK_to_interrupt is set to false—this is another measure to stop unwanted recursion, since error might be called if an interrupt occurs and OK_to_interrupt is true. Next, c is set to the number typed in by the user. The following loop is executed:

while c > 0 do
  begin
    get_token; {one-level recursive call of error is possible}
    decr(c);
  end

Hence tokens are deleted by simply reading and ignoring tokens. The get_token procedure can be regarded, for our purposes, as identical to get_next. The recursion can happen because get_next might cause error to be called. Most of the erroneous situations that can arise in get_next eventually terminate the program; they are fatal errors. But there’s one direct call to error, which happens when an invalid character is read (§346). The deletions_allowed variable is set to false before the call, and to true afterwards.

So what’s the problem? Let’s consider what happens when you start up plain TeX and enter the troublesome input. (I'm using plain TeX because ^^? is already made illegal.) First \s^^?E is typed in response to the ** prompt. Because the first character of input is \ (= escape), TeX treats it as regular code (i.e., it doesn’t assume you wanted to \input a file named \s^^?​E; see §1337). The \s is read and TeX tries to expand a control sequence named s. Since \s has no definition, the expand routine calls error (§370).

At this point, interaction is error_stop_mode and selector is term_only. (This is why the error has to happen on the first line of input; otherwise the transcript file is opened and selector changes.) The loop in §83 begins. Then you type 1 (this is case 1 listed above) and §88 starts to be executed, and get_next is called by get_token. The invalid character ^^? (ASCII code 127 = '177 = "7F; see Appendix C of The TeXbook) is read and control moves to §346. The error routine is called again.

The values of interaction and selector have not changed, so the error dialog is entered as before. Now you type Q. The code in §86 runs; interaction becomes batch_mode, and selector gets decremented to no_print. Control returns from error back to get_next, which skips over the invalid character and reads the E left in the input. Then we get back to error; remember that we’re in case 1, so control goes up to continue and the dialog loop begins again.

At this point, interaction is batch_mode and selector is no_print = 16, and we are at the top of the loop in §83, which should be executed only if interaction = error_stop_mode. All the pieces of the puzzle are now in place. The prompt_input macro first attempts to print ? ; nothing is displayed, because of the value of selector. Then prompt_input calls term_input, which does input_ln(term_in, true); this is why TeX waits for input, even though it’s supposed to be in batch mode. The reason there must be text following the invalid character is that otherwise TeX will encounter the end of input (in get_next, §360) and report a fatal error [*** (job aborted, no legal \end found)]. The fatal_error procedure (§93) calls normalize_selector (§92), which is intended to avoid situations just like what I'm describing!

Next term_input decrements selector; its value becomes 15. If you typed anything in response to the invisible ? , then term_input will attempt to print it, by calling print on each character in buffer, which will end up calling print_char. (Simple exercise: Why can’t term_input call print_char directly?) The value of selector isn’t one of the six important ones enumerated above, so print_char tries to print to write_file[selector]. The elements of write_file are of type alpha_file, and none of them are actual open streams, so what happens now is system-dependent. In Web2C, the result is that putc will be called with a null pointer as its second argument (see fixwrites.c), which causes a segmentation fault. ∎


Now that we know what goes wrong, how might it be fixed? In TeX Live and in the recent tune-up, §83 was changed to have a test at the start of the loop, so that it now looks like

loop
  begin continue:
    if interaction ≠ error_stop_mode then
      return;
    clear_for_error_prompt;
    prompt_input("? ");
    if last = first then
      return;
    c ← buffer[first]
    if c ≥ "a" then
      c ← c + "A" − "a" {convert to uppercase}
    ⟨Interpret code c and return if done⟩;
  end

(See this July 6 commit. Here I have not lifted ⟨Print the menu…⟩ out of ⟨Interpret code c…⟩ as I did before.)

After studying the original code, I’ve come up with the following alternative solution. First, we change error’s top level (§82) so that

if interaction = error_stop_mode then
  ⟨Get the user's advice and return⟩;

is

while interaction = error_stop_mode do
  ⟨Get the user's advice and return⟩;

Then we change §83 to read

begin
  clear_for_error_prompt;
  prompt_input("? ");
  if last = first then
    return;
  c ← buffer[first];
  if c ≥ "a" then
    c ← c + "A" − "a"; {convert to uppercase}
  ⟨Interpret code c and return if done⟩;
continue:
end

There are also other, more drastic options. We could make the same changes, but remove the continue label from §83 and change ⟨Interpret code c…⟩ into something like

if (c ≥ "0") ∧ (c ≤ "9") ∧ deletions_allowed then
  ⟨Delete c − "0" tokens⟩
else
  if (c = "E") ∧ (base_ptr > 0) then
    …
  else
    case c of
      debug "D"
        begin
          debug_help;
        end;
      gubed
      "H":
        ⟨Print the help information⟩;
      "I":
        ⟨Introduce new material from the terminal and return⟩;
      "Q", "R", "S":
        ⟨Change the interaction level and return⟩;
      "X":
        begin
          interaction ← scroll_mode;
          jump_out;
        end;
    othercases
      ⟨Print the menu of available options⟩
    endcases

where goto continue has been removed from the deletion code, from the debugging code, and from the help-displaying code. In my opinion, this is worse, because it’s not as obvious that the menu might be printed even if c is "E" or a digit.

Other places besides error could be changed as well. We could make term_input or prompt_input explicitly validate the assumption that selector ∈ {term_only, term_and_log}. For example, prompt_input(#) might be made to expand into

begin
  if (selector ≠ term_only) ∧ (selector ≠ term_and_log) then
    confusion("selector");
  wake_up_terminal();
  print(#);
  term_input;
end

Of course, that would be helpful only if any more bugs of this sort exist in the program.


Addendum: Metafont and TeX share a lot of programming, and in fact their versions of the error routine are nearly identical. So it isn’t too surprising that this bug can happen in both programs. It’s not as bad in Metafont, though; no segmentation fault can occur. The problematic first line this time is \1:=^Ax, where ^A is control+a. (Any invalid character will do, but it must be typed directly, since Metafont doesn't have an equivalent to TeX's ^^ syntax.) You’ll get the error

Improper `:=' will be changed to `='.

The rest of the interaction proceeds as before. You type 1, Metafont decries the invalid character, then you type q, and Metafont waits for input after supposedly entering batch mode.

There are of course other ways to cause the bug. You could say \1;^Ax, but you’d have to delete two tokens instead of just one.

Most of the exposition above about TeX applies to Metafont, although many of the section numbers are different. The selector shenanigans don’t happen, since Metafont expects it to be between 0 and 5 and does nothing if it isn’t.

1
  • 1
    The tombstone end-of-proof symbol. Nice! – Davislor Sep 17 '20 at 2:27
5

I can reproduce the behavior with tex with the following test document

\tracingall
\catcode`\^^E=15
\s^^EH

If I run tex test, I get

This is TeX, Version 3.14159265 (TeX Live 2020) (preloaded format=tex)
(./testinv.tex
{vertical mode: \tracingstats}
{\tracingpages}
{\tracingoutput}
{\tracinglostchars}
{\tracingmacros}
{\tracingparagraphs}
{\tracingrestores}
{\showboxbreadth}
{\showboxdepth}
{\catcode}
{undefined}
! Undefined control sequence.
l.3 \s
      ^^EH
? 1
! Text line contains an invalid character.
l.3 \s^^E
         H
? q
OK, entering \batchmode

>

The last line denotes the shell prompt to which I only get if I press the return key.

Why is somebody telling that the behavior cannot be reproduced in TL 2020? Good question. Until 2018, LaTeX made several characters invalid, including ^^E, in order to catch faulty input. The situation changed when UTF-8 was made the default input encoding and now ^^E is no longer assigned category code 15 in the format.

If I hit r instead, I get

This is TeX, Version 3.14159265 (TeX Live 2020) (preloaded format=tex)
(./testinv.tex
{vertical mode: \tracingstats}
{\tracingpages}
{\tracingoutput}
{\tracinglostchars}
{\tracingmacros}
{\tracingparagraphs}
{\tracingrestores}
{\showboxbreadth}
{\showboxdepth}
{\catcode}
{undefined}
! Undefined control sequence.
l.3 \s
      ^^EH
? 1
! Text line contains an invalid character.
l.3 \s^^E
         H
? r
OK, entering \nonstopmode...
l.3 \s^^EH

?

which hints that the invalid character is indeed ignored and the 1 instruction (ignore one token) has not yet been performed and TeX is still waiting for user input.

Moral of the story: an invalid character is, well, invalid and is not taken into account when tokens are being deleted during error recovery.

On the other hand, the

? q
OK, entering \batchmode

line should mean what it says (but it doesn't, just like when r is hit). This might be a genuine bug.


A shorter test file is

\s^^?H

The log file when doing the operations 1 and q at the prompt, but hitting return once more is

This is TeX, Version 3.14159265 (TeX Live 2020) (preloaded format=tex 2020.4.17)  27 JUN 2020 10:34
**test
(./test.tex
! Undefined control sequence.
l.1 \s
      ^^?H
? 1
! Text line contains an invalid character.
l.1 \s^^?
         H
? q
OK, entering \batchmode...
l.1 \s^^?H
          
? )
! Emergency stop.
<*> test
           
*** (job aborted, no legal \end found)

No pages of output.
1
  • 1
    Thank a lot. The answer is really helpful. And do you know why "H" is essential for this segfault? If I delete "H" the segfault will disappear. – 潇洒张 Jun 27 '20 at 17:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.