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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions