Skip to content

REDIRECTION_TOKENS mutated by alias/macro create+list (4 locations) -- compounding cross-instance state corruption #1649

@pramodavansaber

Description

@pramodavansaber

Hi cmd2 team — found a module-level state mutation bug in cmd2/cmd2.py while running adversarial test generation via tailtest. One root cause, four copy-pasted source locations, compounding effect across calls.

Bug

Four functions assign constants.REDIRECTION_TOKENS by reference then .extend() it with terminators:

Function Line
_alias_create 3821
_alias_list 3901
_macro_create 4068
_macro_list 4191

Each is shaped like:

tokens_to_unquote = constants.REDIRECTION_TOKENS  # reference, not copy
tokens_to_unquote.extend(self.statement_parser.terminators)

Every call to these four functions permanently appends terminators to the module-level constants.REDIRECTION_TOKENS. Over multiple calls the list grows without bound:

After 0 calls: ['|', '>', '>>']
After 1 call:  ['|', '>', '>>', ';']
After 5 calls: ['|', '>', '>>', ';', ';', ';', ';', ';', ';', ';', ';']

Impact

  1. Compounding mutation: Each alias/macro create/list call pollutes the global list.
  2. Cross-instance corruption: Two Cmd() instances in the same process share the corrupted state. Instance A's alias operations affect instance B's redirection parsing.
  3. Unbounded growth in long-running sessions: Each invocation adds duplicates.

Fix

In all 4 locations, change:

tokens_to_unquote = constants.REDIRECTION_TOKENS

to:

tokens_to_unquote = list(constants.REDIRECTION_TOKENS)

Reproduction

import pytest
from cmd2 import Cmd
from cmd2 import constants


def test_alias_create_does_not_mutate_redirection_tokens():
    """alias create should not mutate the module-level REDIRECTION_TOKENS list."""
    snapshot = list(constants.REDIRECTION_TOKENS)
    app = Cmd()
    app.onecmd_plus_hooks("alias create myalias echo hello")
    assert constants.REDIRECTION_TOKENS == snapshot, (
        f"BUG: REDIRECTION_TOKENS mutated by alias create. "
        f"Was {snapshot!r}, now {constants.REDIRECTION_TOKENS!r}"
    )


def test_repeated_alias_creates_compound_mutation():
    """Compound effect: 5 alias creates should not grow REDIRECTION_TOKENS."""
    snapshot = list(constants.REDIRECTION_TOKENS)
    app = Cmd()
    for i in range(5):
        app.onecmd_plus_hooks(f"alias create alias_{i} echo {i}")
    assert constants.REDIRECTION_TOKENS == snapshot, (
        f"BUG: REDIRECTION_TOKENS grew over 5 alias creates: "
        f"{snapshot!r} -> {constants.REDIRECTION_TOKENS!r}"
    )


def test_two_instances_alias_create_independence():
    """Two Cmd instances should not share corrupted REDIRECTION_TOKENS state."""
    snapshot = list(constants.REDIRECTION_TOKENS)
    a = Cmd()
    b = Cmd()
    a.onecmd_plus_hooks("alias create x echo x")
    state_after_a = list(constants.REDIRECTION_TOKENS)
    assert state_after_a == snapshot, (
        f"BUG: instance A mutated module-level REDIRECTION_TOKENS: "
        f"{snapshot!r} -> {state_after_a!r}; instance B sees the corruption"
    )

Happy to open a PR — it's a 4-line change. Found via tailtest adversarial test generation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions