puppetlabs/puppet · error · SecurityError
Illegal method definition of method '%{method_name}' in sour
Error message
Illegal method definition of method '%{method_name}' in source %{source_ref} on line %{line} in legacy function. See %{url} for more information What it means
Raised as a SecurityError by RubyLegacyFunctionInstantiator when Puppet parses the Ruby source of a legacy 3.x function (a file under lib/puppet/parser/functions/ that calls newfunction) and finds a `def` or `defs` node. The instantiator walks the Ripper AST of the function body; method definitions are forbidden because legacy function bodies are eval'ed into a shared scope and a def would leak methods into the Puppet::Parser::Functions namespace or the compiler process. The only permitted Ruby 'call' shape is the newfunction() invocation itself (:fcall/:call).
Source
Thrown at lib/puppet/pops/loader/ruby_legacy_function_instantiator.rb:98
ripped.each { |x| walk(x, source_ref, result) }
true
end
private_class_method :assert_code
def self.walk(x, source_ref, result)
return unless x.is_a?(Array)
first = x[0]
case first
when :fcall, :call
# Ripper returns a :fcall for a function call in a module (want to know there is a call to newfunction()).
# And it returns :call for a qualified named call
identity_part = find_identity(x)
result << :found_newfunction if identity_part.is_a?(Array) && identity_part[1] == 'newfunction'
when :def, :defs
# There should not be any calls to def in a 3x function
mname, mline = extract_name_line(find_identity(x))
raise SecurityError, _("Illegal method definition of method '%{method_name}' in source %{source_ref} on line %{line} in legacy function. See %{url} for more information") % {
method_name: mname,
source_ref: source_ref,
line: mline,
url: "https://puppet.com/docs/puppet/latest/functions_refactor_legacy.html"
}
end
x.each { |v| walk(v, source_ref, result) }
end
private_class_method :walk
def self.find_identity(rast)
rast.find { |x| x.is_a?(Array) && x[0] == :@ident }
end
private_class_method :find_identity
# Extracts the method name and line number from the Ripper Rast for an id entry.
# The expected input (a result from Ripper :@ident entry) is an array with:
# [0] == :def (or :defs for self.def)View on GitHub (pinned to e227c27540)
Solutions
- Refactor the function to the modern 4.x API: Puppet::Functions.create_function(:mymodule::myfunc) — method definitions on the function class are legal there.
- If staying on 3.x, replace the `def` with a lambda assigned to a local variable (helper = ->(x) { ... }) inside the newfunction block.
- Move the helper methods into a separate Ruby class/module under lib/puppet_x/<org>/ and require + call it from the function body instead of defining it inline.
- Remove dead code: sometimes the def is a leftover from a copy-paste and is never called — delete it.
Example fix
// before (lib/puppet/parser/functions/myfunc.rb)
newfunction(:myfunc) do |args|
def split_it(str)
str.split(',')
end
split_it(args[0])
end
// after (lib/puppet/functions/mymodule/myfunc.rb)
Puppet::Functions.create_function(:'mymodule::myfunc') do
dispatch :myfunc do
required_param 'String', :str
return_type 'Array'
end
def myfunc(str)
str.split(',')
end
end Defensive patterns
Strategy: validation
Validate before calling
# Scan legacy 3.x function sources for method definitions before Puppet loads them
require 'ripper'
def contains_method_def?(ruby_source)
ast = Ripper.sexp(ruby_source)
return false unless ast
walk = lambda do |node|
case node
when Array
return true if node[0].is_a?(Symbol) && %i[def defs].include?(node[0])
node.any? { |child| walk.call(child) }
else
false
end
end
walk.call(ast)
end
Dir['lib/puppet/parser/functions/*.rb'].each do |f|
abort "#{f}: illegal def in legacy function" if contains_method_def?(File.read(f))
end Prevention
- Prefer the 4.x function API (Puppet::Functions.create_function) for all new Ruby functions — method definitions are legal there.
- In legacy functions, express helpers as lambdas (helper = ->(x) { ... }) instead of def.
- Keep shared helpers in lib/puppet_x/<org>/<util>.rb and require them from function files.
- Run a CI grep for /^\s*def\s/ under lib/puppet/parser/functions/ to catch regressions.
When it happens
Trigger: Loading a 3.x function whose block contains `def helper(...)` or `def self.helper(...)` — e.g. lib/puppet/parser/functions/myfunc.rb with `newfunction(:myfunc) do |args| def split_it(x) ... end ... end`. The walk() finds the :def node, extracts method name and line via find_identity/extract_name_line, and raises before the function is ever usable.
Common situations: Copying a 3.x function from an old module or a Puppet 3 cookbook into a modern module; refactoring a 4.x function back to the legacy API; vendors shipping functions with helper methods written as defs; upgrading Puppet versions where this security check got stricter.
Related errors
- Puppet #{Puppet.version} requires Ruby #{Puppet::OLDEST_RECO
- Invalid entry at %{error_location}: '%{file_text}'
- %{mount} is already mounted at %{name} at %{error_location}
- Fileset paths must be fully qualified: %{path}
- Fileset paths must exist
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/b71ee92fd4caf2b9.
Report an issue: GitHub.