|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import ast |
| 4 | +import keyword |
4 | 5 | import marshal |
5 | 6 | import re |
6 | 7 | import struct |
@@ -44,6 +45,63 @@ def is_std_module(module_name: str) -> bool: |
44 | 45 | return in_stdlib(module_name) or module_name in BUILTIN_MODULE_NAMES |
45 | 46 |
|
46 | 47 |
|
| 48 | +def extract_identifier_from_ast_node( |
| 49 | + node: ast.expr | str, fallback_prefix: str = "_malformed" |
| 50 | +) -> str: |
| 51 | + """ |
| 52 | + Extract a valid Python identifier from an AST node. |
| 53 | +
|
| 54 | + For malformed pickle files where STACK_GLOBAL receives complex AST nodes |
| 55 | + instead of strings, this function attempts to extract a meaningful identifier |
| 56 | + or generates a safe fallback. |
| 57 | +
|
| 58 | + Args: |
| 59 | + node: An AST expression node or string |
| 60 | + fallback_prefix: Prefix for generated fallback identifiers |
| 61 | +
|
| 62 | + Returns: |
| 63 | + A valid Python identifier string |
| 64 | + """ |
| 65 | + # If already a string, validate and return or fix it |
| 66 | + if isinstance(node, str): |
| 67 | + if node.isidentifier() and not keyword.iskeyword(node): |
| 68 | + return node |
| 69 | + # For invalid string identifiers, create a safe fallback |
| 70 | + node_hash = abs(hash(node)) % 100000 |
| 71 | + return f"{fallback_prefix}_str_{node_hash}" |
| 72 | + |
| 73 | + # Handle ast.Name - extract the id attribute |
| 74 | + if isinstance(node, ast.Name): |
| 75 | + return node.id |
| 76 | + |
| 77 | + # Handle ast.Attribute - return the attribute name |
| 78 | + if isinstance(node, ast.Attribute): |
| 79 | + return node.attr |
| 80 | + |
| 81 | + # Handle ast.Call - extract identifier from the function being called |
| 82 | + if isinstance(node, ast.Call): |
| 83 | + return extract_identifier_from_ast_node(node.func, fallback_prefix) |
| 84 | + |
| 85 | + # Handle ast.Constant - extract value if it's a valid identifier string |
| 86 | + if isinstance(node, ast.Constant): |
| 87 | + value = node.value |
| 88 | + if isinstance(value, str): |
| 89 | + if value.isidentifier() and not keyword.iskeyword(value): |
| 90 | + return value |
| 91 | + # For invalid constant strings, create a deterministic fallback |
| 92 | + value_hash = abs(hash(value)) % 100000 |
| 93 | + return f"{fallback_prefix}_const_{value_hash}" |
| 94 | + # For non-string constants (int, float, etc.), create a type-based fallback |
| 95 | + type_name = type(value).__name__ |
| 96 | + value_hash = abs(hash(value)) % 100000 |
| 97 | + return f"{fallback_prefix}_{type_name}_{value_hash}" |
| 98 | + |
| 99 | + # For any other complex node types, generate a safe placeholder |
| 100 | + node_type = type(node).__name__.lower() |
| 101 | + node_hash = abs(hash(id(node))) % 100000 |
| 102 | + return f"{fallback_prefix}_{node_type}_{node_hash}" |
| 103 | + |
| 104 | + |
47 | 105 | class MarkObject: |
48 | 106 | pass |
49 | 107 |
|
@@ -1060,20 +1118,45 @@ class StackGlobal(NoOp): |
1060 | 1118 | def run(self, interpreter: Interpreter): |
1061 | 1119 | attr = interpreter.stack.pop() |
1062 | 1120 | module = interpreter.stack.pop() |
| 1121 | + |
| 1122 | + # Extract values from ast.Constant nodes |
1063 | 1123 | if isinstance(module, ast.Constant): |
1064 | 1124 | module = module.value |
1065 | 1125 | if isinstance(attr, ast.Constant): |
1066 | 1126 | attr = attr.value |
1067 | 1127 |
|
1068 | | - # normalize module and attr to strings |
1069 | | - if not isinstance(module, str) or not isinstance(attr, str): |
| 1128 | + # Normalize module and attr to strings, extracting meaningful identifiers from AST nodes |
| 1129 | + module_needs_extraction = not isinstance(module, str) |
| 1130 | + attr_needs_extraction = not isinstance(attr, str) |
| 1131 | + |
| 1132 | + if module_needs_extraction or attr_needs_extraction: |
1070 | 1133 | sys.stdout.write( |
1071 | 1134 | f"Warning: malformed pickle file. STACK_GLOBAL called with invalid types. " |
1072 | 1135 | f"'Module' is {type(module).__name__} ({module!r}), 'Attr' is {type(attr).__name__} ({attr!r}). " |
1073 | | - f"Expected str; casting to string to continue analysis.\n" |
| 1136 | + f"Expected str; extracting identifiers to continue analysis.\n" |
1074 | 1137 | ) |
1075 | | - module = str(module) |
1076 | | - attr = str(attr) |
| 1138 | + |
| 1139 | + if module_needs_extraction: |
| 1140 | + module = extract_identifier_from_ast_node( |
| 1141 | + module, fallback_prefix="_malformed_module" |
| 1142 | + ) |
| 1143 | + if attr_needs_extraction: |
| 1144 | + attr = extract_identifier_from_ast_node(attr, fallback_prefix="_malformed_attr") |
| 1145 | + |
| 1146 | + # Final validation: ensure both are valid identifier strings |
| 1147 | + if not isinstance(module, str) or not isinstance(attr, str): |
| 1148 | + raise TypeError( |
| 1149 | + f"Failed to extract valid identifiers from STACK_GLOBAL arguments. " |
| 1150 | + f"Module: {type(module).__name__}, Attr: {type(attr).__name__}" |
| 1151 | + ) |
| 1152 | + |
| 1153 | + if not module.isidentifier() or not attr.isidentifier(): |
| 1154 | + raise ValueError( |
| 1155 | + f"Extracted identifiers are not valid Python identifiers. " |
| 1156 | + f"Module: {module!r}, Attr: {attr!r}" |
| 1157 | + ) |
| 1158 | + |
| 1159 | + # Continue with normal processing |
1077 | 1160 | if module in ("__builtin__", "__builtins__", "builtins"): |
1078 | 1161 | # no need to emit an import for builtins! |
1079 | 1162 | pass |
@@ -1133,6 +1216,10 @@ class BinPut(Opcode): |
1133 | 1216 | def run(self, interpreter: Interpreter): |
1134 | 1217 | interpreter.memory[self.arg] = interpreter.stack[-1] |
1135 | 1218 |
|
| 1219 | + def encode_body(self): |
| 1220 | + assert self.arg <= 255, "BINPUT only supports 1-byte memo indexing" |
| 1221 | + return bytes([self.arg]) |
| 1222 | + |
1136 | 1223 |
|
1137 | 1224 | class LongBinPut(BinPut): |
1138 | 1225 | name = "LONG_BINPUT" |
@@ -1422,6 +1509,10 @@ def run(self, interpreter: Interpreter): |
1422 | 1509 | else: |
1423 | 1510 | interpreter.stack.append(interpreter.memory[self.arg]) |
1424 | 1511 |
|
| 1512 | + def encode_body(self): |
| 1513 | + assert self.arg <= 255, "BINGET only supports 1-byte memo indexing" |
| 1514 | + return bytes([self.arg]) |
| 1515 | + |
1425 | 1516 |
|
1426 | 1517 | class LongBinGet(Opcode): |
1427 | 1518 | name = "LONG_BINGET" |
|
0 commit comments