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)

Parameters:

  • str (String)

    The assembly source to be scanned.

  • arch (Symbol)

    Target architecture, decides which syscall names are recognized.

  • filename (String?) (defaults to: nil)

    Only used for error messages, defaults to <inline>.



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)

Returns The architecture this scanner was created with.

Returns:

  • (Symbol)

    The architecture this scanner was created with.



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

def arch
  @arch
end

#syscalls{Symbol => Integer} (readonly)

Returns Syscall name to number, for the architecture this scanner was created with.

Returns:

  • ({Symbol => Integer})

    Syscall name to number, for the architecture this scanner was created with.



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

Parameters:

  • tok (Token)

    The token the message refers to.

  • msg (String)

    The message to be shown next to the source location.

Returns:

  • (String)

    The location and message, the offending source line, and a row of carets underlining the token.



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)
  <<-EOS
#{@filename}:#{tok.line + 1}:#{tok.col + 1} #{msg}
#{line}
#{(' ' * calculate_spaces(@lines[tok.line][0...tok.col])) + ('^' * tok.str.size)}
  EOS
end

#scanArray<Token>

Scans the whole string into tokens.

The result is memoized, so repeated calls are free.

Returns:

  • (Array<Token>)

    All tokens in source order. Unrecognized text becomes :unknown tokens rather than raising, see #validate!.



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.

Returns:

  • (Array<Token>)

    The :unknown tokens, empty when the source is valid.



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.

Returns:

  • (self)

    The scanner itself, so calls can be chained.

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