Properly escape ANSI color codes in shell scripts

What does this MR do?

Fixes ANSI color code handling in shell scripts by properly escaping commands in trace section output. Adds ANSI color formatting to GPG signing disabled messages to improve visibility.

Why was this MR needed?

Fixes gitlab-org/step-runner#414 where ANSI color codes in GPG signing disabled messages were being incorrectly parsed by pilot runners, causing job failures with errors like 31m******: command not found.

The root cause was that commands containing special characters (including ANSI escape sequences) were not being properly escaped for POSIX shell execution in trace section output.

Changes

  • Trace Section Writer: Properly escape commands in trace section start/end output to handle special characters correctly and ANSI Color
  • Fix related unit tests

What's the best way to test this MR?

config.toml
concurrent = 1
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "Local GitLab Runner for tests and debugging"
  url = "https://gitlab.com"
  id = 0
  token = "glrt-REDACTED"
  token_obtained_at = "0001-01-01T00:00:00Z"
  token_expires_at = "0001-01-01T00:00:00Z"
  executor = "docker"

  [runners.feature_flags]
    FF_USE_FASTZIP = true
    FF_USE_NEW_BASH_EVAL_STRATEGY = true
    FF_SCRIPT_SECTIONS = true
    FF_EXPORT_HIGH_CARDINALITY_METRICS = true
    FF_USE_ADAPTIVE_REQUEST_CONCURRENCY = false
    FF_SCRIPT_TO_STEP_MIGRATION = true
    FF_DEBUG_TRACE = true

  [runners.cache]
    MaxUploadedArchiveSize = 0
  [runners.docker]
    host = "unix:///Users/ratchade/.colima/default/docker.sock"
    image = "alpine"
    pull_policy = "if-not-present"
    helper_image = "registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper-dev:alpine-latest-arm64-943917bd" #MR generated helper
    privileged = false
    disable_entrypoint_overwrite = true
    oom_kill_disable = false
    disable_cache = false
    shm_size = 0
    volumes = ["/cache"]
gitlab-ci.yaml
hello:
  variables:
    TEST: "$CI_PIPELINE_IID-TEST"
    FF_SCRIPT_TO_STEP_MIGRATION: "true"
  image: alpine
  before_script:
    # - sleep 120s
    - |
      # checking GPG signing support
      if [ -f "$GPG_KEY_PATH" ]; then
        export GPG_KEY=$(cat ${GPG_KEY_PATH})
        export GPG_PASSPHRASE=$(cat ${GPG_PASSPHRASE_PATH})
      else
        echo -e "\033[0;31m****** GPG signing disabled ******\033[0m"
      fi
    - |
      # --- TestEscapeForAnsiC tests ---

      # no escaping needed
      echo -e "hello world"
      printf '%b\n' "hello world"

      # backslash
      echo -e "path\to\file"
      printf '%b\n' "path\to\file"

      # single quote
      echo -e "it's working"
      printf '%b\n' "it's working"

      # newline
      echo -e "line1\nline2"
      printf '%b\n' "line1\nline2"

      # tab
      echo -e "col1\tcol2"
      printf '%b\n' "col1\tcol2"

      # carriage return
      echo -e "text\rmore"
      printf '%b\n' "text\rmore"

      # mixed special chars
      echo -e "echo 'hello'\nworld\test"
      printf '%b\n' "echo 'hello'\nworld\test"

      # command with quotes
      echo -e "echo \"hello\" 'world'"
      printf '%b\n' "echo \"hello\" 'world'"

      # ANSI escape sequence - ESC character
      echo -e "\x1b[1;32mGreen\x1b[0m"
      printf '%b\n' "\x1b[1;32mGreen\x1b[0m"

      # terminal clear screen
      echo -e "\x1b[2J\x1b[H"
      printf '%b\n' "\x1b[2J\x1b[H"

      # null byte
      echo -e "text\x00more"
      printf '%b\n' "text\x00more"

      # DEL character
      echo -e "text\x7fmore"
      printf '%b\n' "text\x7fmore"

      # non-ASCII characters (café - raw UTF-8 bytes)
      echo -e "caf\xc3\xa9"
      printf '%b\n' "caf\xc3\xa9"

      # mixed control and printable
      echo -e "Hello\x1b[31mRed\x1b[0mWorld"
      printf '%b\n' "Hello\x1b[31mRed\x1b[0mWorld"

      # literal backslash-033 red foreground
      echo -e "\033[31m"
      printf '%b\n' "\033[31m"

      # literal backslash-033 reset
      echo -e "\033[0m"
      printf '%b\n' "\033[0m"

      # literal backslash-033 bold blue
      echo -e "\033[1;34m"
      printf '%b\n' "\033[1;34m"

      # literal backslash-033 green background
      echo -e "\033[42m"
      printf '%b\n' "\033[42m"

      # literal backslash-x1b hex escape
      echo -e "\x1bHello"
      printf '%b\n' "\x1bHello"

      # literal backslash-e bash escape
      echo -e "\eHello"
      printf '%b\n' "\eHello"

      # literal backslash-u unicode escape
      echo -e "\u001b"
      printf '%b\n' "\u001b"

      # literal backslash-X uppercase hex
      echo -e "\X1b"
      printf '%b\n' "\X1b"

      # literal backslash-E uppercase bash escape
      echo -e "\E"
      printf '%b\n' "\E"

      # consecutive backslashes four
      echo -e "\\\\\\\\"
      printf '%b\n' "\\\\\\\\"

      # triple backslash
      echo -e "\\\\\\"
      printf '%b\n' "\\\\\\"

      # trailing backslash
      echo -e "test\\"
      printf '%b\n' "test\\"

      # backslash before non-printf char
      echo -e "\z"
      printf '%b\n' "\z"

      # backslash before uppercase non-printf char
      echo -e "\Z"
      printf '%b\n' "\Z"

      # multiple printf-sensitive sequences
      echo -e "\n\t\r\a"
      printf '%b\n' "\n\t\r\a"

      # backslash before b and f and v
      echo -e "\b\f\v"
      printf '%b\n' "\b\f\v"

      # real ESC char with dollar sign
      echo -e "\x1b[31m\$HOME\x1b[0m"
      printf '%b\n' "\x1b[31m\$HOME\x1b[0m"

      # literal escape string with double quotes
      echo -e "echo \"\033[31mred\033[0m\""
      printf '%b\n' "echo \"\033[31mred\033[0m\""

      # dollar and literal backslash-n
      echo -e "\$VAR\ntext"
      printf '%b\n' "\$VAR\ntext"

      # all shell metacharacters together
      echo -e "\$\"'\\"
      printf '%b\n' "\$\"'\\"

      # complex command with literal ANSI
      echo -e "printf \"\033[31m%s\033[0m\n\" \"\$msg\""
      printf '%b\n' "printf \"\033[31m%s\033[0m\n\" \"\$msg\""

      # backtick is escaped for double-quoted context
      echo -e "echo \`date\`"
      printf '%b\n' "echo \`date\`"

      # backticks inside single quotes
      echo -e "echo '\`whoami\`'"
      printf '%b\n' "echo '\`whoami\`'"

      # real ESC bold and dim combined
      echo -e "\x1b[1m\x1b[2mtext\x1b[0m"
      printf '%b\n' "\x1b[1m\x1b[2mtext\x1b[0m"

      # real ESC 256-color foreground
      echo -e "\x1b[38;5;196mred\x1b[0m"
      printf '%b\n' "\x1b[38;5;196mred\x1b[0m"

      # real ESC RGB truecolor
      echo -e "\x1b[38;2;255;0;0mred\x1b[0m"
      printf '%b\n' "\x1b[38;2;255;0;0mred\x1b[0m"

      # mixed real ESC and shell metacharacters
      echo -e "\x1b[31m\$USER said \"hello\" at \`date\`\x1b[0m"
      printf '%b\n' "\x1b[31m\$USER said \"hello\" at \`date\`\x1b[0m"

      # literal octal sequences with surrounding text
      echo -e "Status: \033[32mOK\033[0m - done"
      printf '%b\n' "Status: \033[32mOK\033[0m - done"

      # --- TestEscapeForAnsiC_SecurityFeatures tests ---

      # prevent screen clear
      echo -e "\x1b[2J"
      printf '%b\n' "\x1b[2J"

      # prevent cursor positioning
      echo -e "\x1b[10;20H"
      printf '%b\n' "\x1b[10;20H"

      # prevent color manipulation
      echo -e "\x1b[31m\x1b[42m"
      printf '%b\n' "\x1b[31m\x1b[42m"

      # prevent line deletion
      echo -e "\x1b[2K"
      printf '%b\n' "\x1b[2K"

      # prevent scrolling region manipulation
      echo -e "\x1b[1;24r"
      printf '%b\n' "\x1b[1;24r"

      # prevent title change via OSC
      echo -e "\x1b]0;pwned\x07"
      printf '%b\n' "\x1b]0;pwned\x07"

      # prevent 256-color abuse
      echo -e "\x1b[38;5;0m\x1b[48;5;0m"
      printf '%b\n' "\x1b[38;5;0m\x1b[48;5;0m"

      # prevent cursor save and restore
      echo -e "\x1b[s\x1b[10;1H\x1b[2Kfake log\x1b[u"
      printf '%b\n' "\x1b[s\x1b[10;1H\x1b[2Kfake log\x1b[u"
  script:
    - echo $TEST > artifact.txt
  artifacts:
    paths:
      - artifact.txt
    expire_in: 1 week

Job passes and artifact is uploaded.

What are the relevant issue numbers?

Closes step-runner#414 (closed)

Edited by Romuald Atchadé

Merge request reports

Loading