Class: SeccompTools::Asm::Scanner

Inherits:
Object
  • Object
show all
Defined in:
lib/seccomp-tools/asm/scanner.rb

Overview

Converts text to array of tokens.

Maintains columns and rows to have informative error messages.

Internally used by the seccomp asm parser.

Constant Summary collapse

KEYWORDS =

Keywords with special meanings in our assembly. Keywords are all case-insensitive.

%w[a x if else return mem args args_h data len sys_number arch instruction_pointer].freeze
KEYWORD_MATCHER =

Regexp for matching keywords.

/\A\b(#{KEYWORDS.join('|')})\b/i
ACTIONS =

Action strings can be used in a return statement. Actions must be in upper case. See Const::BPF::ACTION.

Const::BPF::ACTION.keys.map(&:to_s)
ACTION_MATCHER =

Regexp for matching actions.

/\A\b(#{ACTIONS.join('|')})\b/
AUDIT_ARCHES =

Special constants for checking the current architecture. See Const::Audit::ARCH. These constants are case-insensitive.

Const::Audit::ARCH.keys
AUDIT_ARCH_MATCHER =

Regexp for matching arch values.

/\A\b(#{AUDIT_ARCHES.join('|')})\b/i
COMPARE =

Comparisons.

%w[== != >= <= > <].freeze
COMPARE_MATCHER =

Regexp for matching comparisons.

/\A(#{COMPARE.join('|')})/
ALU_OP =

All valid arithmetic operators.

%w[+ - * / | ^ << >>].freeze
ALU_OP_MATCHER =

Regexp for matching ALU operators.

/\A(#{ALU_OP.map { |o| ::Regexp.escape(o) }.join('|')})/
ARCHES =

Supported architectures

SeccompTools::Syscall::ABI.keys.map(&:to_s)
TAB_WIDTH =

Let tab on terminal be 4 spaces wide.

4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(str, arch, filename: nil) ⇒ Scanner

Instantiates a SeccompTools::Asm::Scanner object.

Examples:

Scanner.new('return ALLOW', :amd64)


58
59
60
61
62
63
64
65
66
67
# File 'lib/seccomp-tools/asm/scanner.rb', line 58

def initialize(str, arch, filename: nil)
  @filename = filename || '<inline>'
  @str = str
  @arch = arch
  @syscalls =
    begin; Const::Syscall.const_get(arch.to_s.upcase); rescue NameError; []; end
  @syscall_all = ARCHES.each_with_object({}) do |ar, memo|
    memo.merge!(Const::Syscall.const_get(ar.to_s.upcase))
  end.keys
end

Instance Attribute Details

#archSymbol (readonly)



21
22
23
# File 'lib/seccomp-tools/asm/scanner.rb', line 21

def arch
  @arch
end

#syscalls{Symbol => Integer} (readonly)



18
19
20
# File 'lib/seccomp-tools/asm/scanner.rb', line 18

def syscalls
  @syscalls
end

Instance Method Details

#format_error(tok, msg) ⇒ String

Formats an error message that points at a token in the source.

Examples:

scanner.format_error(tok, 'unknown token "foo"')
#=> <<-EOS
#   <inline>:1:1 unknown token "foo"
#   foo
#   ^^^
# EOS


176
177
178
179
180
181
182
183
184
185
186
# File 'lib/seccomp-tools/asm/scanner.rb', line 176

def format_error(tok, msg)
  @lines = @str.lines unless defined?(@lines)
  line = @lines[tok.line]
  line = line[0..-2] if line.end_with?("\n")
  line = line.gsub("\t", ' ' * TAB_WIDTH)
  "\#{@filename}:\#{tok.line + 1}:\#{tok.col + 1} \#{msg}\n\#{line}\n\#{(' ' * calculate_spaces(@lines[tok.line][0...tok.col])) + ('^' * tok.str.size)}\n  EOS\nend\n"

#scanArray<Token>

Scans the whole string into tokens.

The result is memoized, so repeated calls are free.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/seccomp-tools/asm/scanner.rb', line 95

def scan
  return @tokens if defined?(@tokens)

  @tokens = []
  row = 0
  col = 0
  str = @str
  add_token = ->(sym, s, c = col) { @tokens.push(Token.new(sym, s, row, c)) }
  # define a helper because it's commonly used - add a token with matched string, bump col with string size
  bump_vars = lambda {
    col += ::Regexp.last_match(0).size
    str = ::Regexp.last_match.post_match
  }
  add_token_def = lambda do |sym|
    add_token.call(sym, ::Regexp.last_match(0))
    bump_vars.call
  end
  syscalls = @syscalls.keys.map { |s| ::Regexp.escape(s) }.join('|')
  syscall_matcher = ::Regexp.compile("\\A\\b(#{syscalls})\\b")
  syscall_all_matcher = ::Regexp.compile("\\A(#{ARCHES.join('|')})\\.(#{@syscall_all.join('|')})\\b")
  until str.empty?
    case str
    when /\A\n+/
      # Don't push newline as the first token
      add_token.call(:NEWLINE, ::Regexp.last_match(0)) unless @tokens.empty?
      row += ::Regexp.last_match(0).size
      col = 0
      str = ::Regexp.last_match.post_match
    when /\A\s+/, /\A#.*/ then bump_vars.call
    when /\A(\w+):/
      add_token.call(:SYMBOL, ::Regexp.last_match(1))
      bump_vars.call
    when /\A(goto|jmp|jump)\s+(\w+)\b/i
      add_token.call(:GOTO, ::Regexp.last_match(1), col + ::Regexp.last_match.begin(1))
      add_token.call(:GOTO_SYMBOL, ::Regexp.last_match(2), col + ::Regexp.last_match.begin(2))
      bump_vars.call
    when KEYWORD_MATCHER then add_token_def.call(::Regexp.last_match(0).upcase.to_sym)
    when ACTION_MATCHER then add_token_def.call(:ACTION)
    when AUDIT_ARCH_MATCHER then add_token_def.call(:ARCH_VAL)
    when syscall_matcher, syscall_all_matcher then add_token_def.call(:SYSCALL)
    when /\A-?0x[0-9a-f]+\b/ then add_token_def.call(:HEX_INT)
    when /\A-?[0-9]+\b/ then add_token_def.call(:INT)
    when ALU_OP_MATCHER then add_token_def.call(:ALU_OP)
    when COMPARE_MATCHER then add_token_def.call(:COMPARE)
    # '&' is in both compare and ALU op category, handle it here
    when /\A(\(|\)|=|\[|\]|&|!)/ then add_token_def.call(::Regexp.last_match(0))
    when /\A\?\s*(?<jt>\w+)\s*:\s*(?<jf>\w+)/
      i[jt jf].each do |s|
        add_token.call(:GOTO_SYMBOL, ::Regexp.last_match(s), col + ::Regexp.last_match.begin(s))
      end
      bump_vars.call
    when /\A([^\s]+)(\s?)/
      # unrecognized token - match until \s
      last = ::Regexp.last_match(1)
      add_token.call(:unknown, last)
      col += last.size
      str = ::Regexp.last_match(2) + ::Regexp.last_match.post_match
    end
  end
  @tokens
end

#validateArray<Token>

Same as #validate! but returns the array of errors instead of raising an exception.



85
86
87
# File 'lib/seccomp-tools/asm/scanner.rb', line 85

def validate
  scan.select { |t| t.sym == :unknown }
end

#validate!self

Scans the whole string and raises errors when there are unrecognized tokens.

Raises:



74
75
76
77
78
79
# File 'lib/seccomp-tools/asm/scanner.rb', line 74

def validate!
  errors = validate
  return self if errors.empty?

  raise UnrecognizedTokenError, errors.map { |e| format_error(e, "unknown token #{e.str.inspect}") }.join("\n")
end