aboutsummarylogtreecommitdiffstats
path: root/linux-pax-flags.rb
blob: d5c00c9483a0a3fbb0e2a349709466d9390f384d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/env ruby

require 'getoptlong'
require 'readline'
require 'singleton'
require 'yaml'

# Monkey-path the Array class.
class Array
  # ["foo", {"foo" => 1}].cleanup => [{"foo" => 1}]
  # If the key in a Hash element of an Array is also present as an element of
  # the Array, delete the latter.
  def cleanup
    array = self.dup
    self.grep(Hash).map(&:keys).flatten.each do |x|
      array.delete x
    end
    array
  end
end

# Class handles configuration parameters.
class FlagsConfig < Hash
  # This is a singleton class.
  include Singleton

  # Merges a Hash or YAML file (containing a Hash) with itself.
  def load config
    if config.class == Hash
      merge! config
      return
    end

    unless config.nil?
      merge_yaml! config
    end
  end

  # Merge Config Hash with Hash in YAML file.
  def merge_yaml! path
    merge!(load_file path) do |key, old, new|
      (old + new).uniq.cleanup if old.is_a? Array and new.is_a? Array
    end
  end

  # Load YAML file and work around tabs not working for identation.
  def load_file path
    YAML.load open(path).read.gsub(/\t/, '   ')
  rescue Psych::SyntaxError => e
    print path, ':', e.message.split(':').last, "\n"
    exit 1
  end
end

# A method to print a beautiful usage message.
def usage
  $stderr.puts <<EOF
#{File.basename($0)} [options] [filters]

  OPTIONS

    -c, --config     Override default configuration paths. Requires one
                     argument. Can contain globs (escape them in some shells
                     (zsh for example)). 
    -h, --help       This help.
    -p, --prepend    Do not change anything.
    -y, --yes        Non-interactive mode. Assume yes on questions.
    -x, --xattr      Sets the PaX flags through setfattr, underlying
                     filesystems need xattr support.

  FILTERS

    Only change flags for paths, which contain one of these filters as a string.

EOF
  exit 1
end

# This iterates each config entry (which matches the filters). It yields flags,
# entry, pattern and path of the config entry to the block code.
def each_entry config, filters
  config.each do |flags, entries|
    entries.each do |entry|
      # Distinguish easy (String) and complex (Hash) config entries.
      if entry.is_a? String
        pattern = entry
      elsif entry.is_a? Hash
        pattern = entry.keys.first
      end

      # Skip this entry, if its path pattern does not contain one of the
      # filters.
      # TODO Do this for every matching path.
      unless filters.empty?
        temp_filters = filters.dup
        temp_filters.keep_if do |filter|
          pattern.downcase.include? filter.downcase
        end
        next if temp_filters.empty?
      end

      # If this runs with sudo, the ~ (for the users home path) have to point to
      # the user who runs it, not to root.
      unless ENV['SUDO_USER'].nil?
        paths = File.expand_path pattern.gsub('~', '~' + ENV['SUDO_USER'])
      else
        paths = File.expand_path pattern
      end

      # Now yield for every matching path.
      Dir.glob(paths).each do |path|
        yield flags, entry, pattern, path
      end
    end
  end
end

# Trap SIGINT (ctrl+c)
trap(:INT) { exit 1 }

# Define the possible options.
options = GetoptLong.new(
  ['--config',  '-c', GetoptLong::REQUIRED_ARGUMENT],
  ['--help',    '-h', GetoptLong::NO_ARGUMENT],
  ['--prepend', '-p', GetoptLong::NO_ARGUMENT],
  ['--xattr',   '-x', GetoptLong::NO_ARGUMENT],
  ['--yes',     '-y', GetoptLong::NO_ARGUMENT],
)

# Initialize option variables.
new_configs = []
prepend = false
yes = false
xattr = false

# Set option variables.
begin
  options.each do |option, argument|
    case option
      when '--config'
        new_configs = Dir.glob argument
      when '--help'
        usage
      when '--prepend'
        prepend = true
      when '--xattr'
        xattr = true
      when '--yes'
        yes = true
    end
  end
rescue GetoptLong::InvalidOption => e
  usage
end

# Whatever is left over is a filter.
filters = ARGV

# Exit if we are not running with root privileges.
if Process.uid != 0
  $stderr << "Root privileges needed.\n"
  exit 1
end

# Either default config paths or overridden ones.
config_paths = if new_configs.empty?
  ['/etc/pax-flags/*.conf', '/usr/share/linux-pax-flags/*.conf']
else
  new_configs
end

# Initialize the singleton config object...
config = FlagsConfig.instance

# ... and load every config file.
config_paths.each do |path|
  Dir.glob(path).each do |file|
    config.load file
  end
end

# Helper text for simple entries.
puts <<EOF
Some programs do not work properly without deactivating some of the PaX
features. Please close all instances of them if you want to change the
configuration for the following binaries.
EOF

# Show every simple entry.
each_entry config, filters do |flags, entry, pattern, path|
  puts ' * ' + path if File.exists? path and entry.is_a? String
end

# Let us sum up the complex entries...
autopaths = []
each_entry config, filters do |flags, entry, pattern, path|
  if File.exists? path and entry.is_a? Hash
    autopaths.push path if not (entry.nil? and entry[path]['skip'])
  end
end

# ... to decide, if we need to print them.
unless autopaths.empty?
  puts <<EOF

For the following programs there are also changes neccessary but you do not have
to close or restart instances of them manually.
EOF

  autopaths.each do |path|
    puts ' * ' + path
  end
end

puts
puts 'Continue writing PaX headers? [Y/n]'

$stdout.flush

unless yes
  a = Readline.readline.chomp.downcase
  exit 1 if a.downcase != 'y' unless a.empty?
end

# Iterate each entry to actually set the flags.
each_entry config, filters do |flags, entry, pattern, path|
  if File.exists? path
    e = entry[pattern]
    actions = %w(status start stop)
    start_again = false

    # Get action commands from entries config.
    status = e['status']
    start  = e['start']
    stop   = e['stop']

    # If the type attribute is set to systemd, we set the action command
    # variables again but to systemd defaults.
    if e['type'] == 'systemd'
      name = e['systemd_name'] || File.basename(path)
      actions.each do |action|
        eval "#{action} = \"systemctl #{action} #{name}.service\""
      end
    end

    # If the entry is complex, stop it if it is running.
    if entry.is_a? Hash
      if status and system(status + '> /dev/null')
        system stop unless prepend
        start_again = true if start
      end
    end

    if xattr
      # setfattr seems to be picky about the order of the flags,
      # rearrange it beforehand
      xflags = flags[/[Pp]/] + flags[/[Ee]/] + flags[/[Mm]/] +
               flags[/[Rr]/] + flags[/[Ss]/]
      print xflags, ' ', path, "\n"
    else
      print flags, ' ', path, "\n"
    end

    # Set the flags and notify the user.
    unless prepend
      if xattr
        `setfattr -n user.pax.flags -v #{xflags} "#{path}"`
      else
        header = 'c'
        header = 'C' if e['header'] == 'create'
        `paxctl -#{header}#{flags} "#{path}"`
      end
    end

    # Start the complex entries service again, if it is neccessary.
    system start unless prepend if start_again
  end
end