diff --git a/compiler/analysis/typechecker.c b/compiler/analysis/typechecker.c index 4ced9fbe8..9f15aee89 100644 --- a/compiler/analysis/typechecker.c +++ b/compiler/analysis/typechecker.c @@ -154,6 +154,26 @@ int scope_name_in_whitelist(SymbolTable* table, const char* name) { return name_list_contains(table->seal_whitelist, name); } +static void scope_apply_visibility_directives(SymbolTable* table, ASTNode* block) { + if (!table || !block) return; + for (int i = 0; i < block->child_count; i++) { + ASTNode* child = block->children[i]; + if (!child) continue; + if (child->type == AST_HIDE_DIRECTIVE) { + for (int j = 0; j < child->child_count; j++) { + ASTNode* id = child->children[j]; + if (id && id->value) scope_hide_name(table, id->value); + } + } else if (child->type == AST_SEAL_DIRECTIVE) { + table->is_sealed = 1; + for (int j = 0; j < child->child_count; j++) { + ASTNode* id = child->children[j]; + if (id && id->value) scope_seal_except(table, id->value); + } + } + } +} + // Returns 1 if `name` was blocked by a `hide` or `seal except` directive // somewhere in the scope chain AND a real binding for it exists farther // up (above the blocking scope). Used to give a better error message than @@ -533,6 +553,15 @@ static Symbol* lookup_symbol_in(SymbolTable* table, const char* name, const char if (symbol && !table->parent) return own_extern_symbol(symbol, merged_from, name); if (symbol) return symbol; + // Leaving this scope, whether through a DSL receiver or the parent chain: + // enforce hide / seal directives before resolving an outward name. + if (scope_name_is_hidden(table, name)) { + return NULL; + } + if (table->is_sealed && !scope_name_in_whitelist(table, name)) { + return NULL; + } + // Issue #333 DSL block receiver fallback: when this scope is the // body of a `receiver.method(args) { ... }` trailing closure, // `dsl_receiver` names the receiver namespace or struct type. @@ -558,22 +587,16 @@ static Symbol* lookup_symbol_in(SymbolTable* table, const char* name, const char snprintf(rewritten, sizeof(rewritten), "%.250s_%.250s", table->dsl_receiver, name); for (SymbolTable* t = table; t; t = t->parent) { + if (t != table && + (scope_name_is_hidden(t, name) || + (t->is_sealed && !scope_name_in_whitelist(t, name)))) { + return NULL; + } Symbol* s = lookup_symbol_local(t, rewritten); if (s) return s; } } - // Crossing the scope boundary upward: enforce hide / seal directives. - // - `hide foo` blocks any name in the hidden_names list. - // - `seal except a, b` blocks anything that isn't in the whitelist. - // Either way, return NULL so the caller sees an undefined identifier. - if (scope_name_is_hidden(table, name)) { - return NULL; - } - if (table->is_sealed && !scope_name_in_whitelist(table, name)) { - return NULL; - } - if (table->parent) { return lookup_symbol_in(table->parent, name, merged_from); } @@ -15414,22 +15437,7 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { // PRE-PASS: collect hide / seal directives BEFORE processing any // other statements, so they're scope-level (position within the // block doesn't matter) and apply to every other statement here. - for (int i = 0; i < stmt->child_count; i++) { - ASTNode* child = stmt->children[i]; - if (!child) continue; - if (child->type == AST_HIDE_DIRECTIVE) { - for (int j = 0; j < child->child_count; j++) { - ASTNode* id = child->children[j]; - if (id && id->value) scope_hide_name(block_table, id->value); - } - } else if (child->type == AST_SEAL_DIRECTIVE) { - block_table->is_sealed = 1; - for (int j = 0; j < child->child_count; j++) { - ASTNode* id = child->children[j]; - if (id && id->value) scope_seal_except(block_table, id->value); - } - } - } + scope_apply_visibility_directives(block_table, stmt); for (int i = 0; i < stmt->child_count; i++) { ASTNode* child = stmt->children[i]; @@ -17436,6 +17444,12 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { } } + for (int i = 0; i < expr->child_count; i++) { + ASTNode* child = expr->children[i]; + if (child && child->type == AST_BLOCK) + scope_apply_visibility_directives(closure_scope, child); + } + // Register closure parameters in the child scope for (int i = 0; i < expr->child_count; i++) { ASTNode* child = expr->children[i]; diff --git a/new_changelogs/20261029T075358Z-security-hide-seal-dsl-receiver.md b/new_changelogs/20261029T075358Z-security-hide-seal-dsl-receiver.md new file mode 100644 index 000000000..a4f7b11e9 --- /dev/null +++ b/new_changelogs/20261029T075358Z-security-hide-seal-dsl-receiver.md @@ -0,0 +1,3 @@ +- **`hide` and `seal except` now cover DSL receiver fallback.** A trailing + builder block could previously resolve a blocked bare name through its + receiver-prefixed method before lexical visibility was enforced. diff --git a/tests/integration/hide_seal_directives/test_hide_reject.sh b/tests/integration/hide_seal_directives/test_hide_reject.sh index b0eed4a38..fb4a8c07b 100755 --- a/tests/integration/hide_seal_directives/test_hide_reject.sh +++ b/tests/integration/hide_seal_directives/test_hide_reject.sh @@ -270,6 +270,68 @@ main() { EOF run_accept "seal except lets a whitelisted prefix through nested blocks" $AE_TMP/ae_seal_qualified_whitelisted.ae +# Cases 16-19: DSL receiver fallback is still an outward lookup. A hidden or +# sealed-out bare name must not reach the receiver-prefixed function, while a +# name admitted by the seal remains available through that fallback. +mkdir -p $AE_TMP/dsl_helper +cat > $AE_TMP/dsl_helper/module.ae << 'EOF' +exports(run, script) + +builder run(label: string) {} + +script(_ctx: ptr, name: string) {} +EOF + +cat > $AE_TMP/ae_hide_dsl_receiver.ae << 'EOF' +import dsl_helper +main() { + dsl_helper.run("direct") { + hide script + script("blocked") + } +} +EOF +run_reject "hide blocks DSL receiver fallback" $AE_TMP/ae_hide_dsl_receiver.ae + +cat > $AE_TMP/ae_seal_dsl_receiver_nested.ae << 'EOF' +import dsl_helper +main() { + dsl_helper.run("nested") { + seal except println + { + script("blocked") + } + } +} +EOF +run_reject "seal except blocks nested DSL receiver fallback" $AE_TMP/ae_seal_dsl_receiver_nested.ae + +cat > $AE_TMP/ae_seal_dsl_receiver_allowed.ae << 'EOF' +import dsl_helper +main() { + dsl_helper.run("allowed") { + seal except script + script("ok") + } +} +EOF +run_accept "seal except admits whitelisted DSL receiver fallback" $AE_TMP/ae_seal_dsl_receiver_allowed.ae + +cat > $AE_TMP/ae_hide_dsl_receiver_outer.ae << 'EOF' +import dsl_helper +main() { + { + hide script + { + dsl_helper.run("outer") { + script("blocked") + } + } + } +} +EOF +run_reject "hide blocks DSL receiver fallback across nested scopes" $AE_TMP/ae_hide_dsl_receiver_outer.ae + # Cleanup rm -f $AE_TMP/ae_hide_*.ae $AE_TMP/ae_seal_*.ae $AE_TMP/ae_hide_out