Add ability to trace only select syscalls.

On android, the raw_syscalls tracepoints are used to trace syscalls.
These however include *all* syscalls, though most users are probably
only interested in a subset.

This change adds the ability to specify the set of syscall events you
want to track and ignore the rest. It works using ftrace filters and
uses proper muxing to handle multiple configs. If no filter is provided,
the behavior is the exact same as it was before.

When muxing, the union of all necessary syscalls will be enabled. Since
the individual probers don't filter, this means that concurrent traces
can give more syscalls than you requested, but never less.

Test: ran perfetto_unittests
Change-Id: I1478cbd28625f3b4c5c2d050df8b6fed72db993c
diff --git a/protos/perfetto/config/ftrace/ftrace_config.proto b/protos/perfetto/config/ftrace/ftrace_config.proto
index 4b25317..bd81d57 100644
--- a/protos/perfetto/config/ftrace/ftrace_config.proto
+++ b/protos/perfetto/config/ftrace/ftrace_config.proto
@@ -18,7 +18,7 @@
 
 package perfetto.protos;
 
-// Next id: 18.
+// Next id: 19.
 message FtraceConfig {
   repeated string ftrace_events = 1;
   repeated string atrace_categories = 2;
@@ -92,4 +92,10 @@
   // expand to events that aren't of interest to the tracing user.
   // Introduced in: Android T.
   optional bool disable_generic_events = 16;
+
+  // The list of syscalls that should be recorded by sys_{enter,exit} ftrace
+  // events. When empty, all syscalls are recorded. If neither sys_{enter,exit}
+  // are enabled, this setting has no effect. Example: ["sys_read", "sys_open"].
+  // Introduced in: Android U.
+  repeated string syscall_events = 18;
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index a6dcdce..1e4484d 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -466,7 +466,7 @@
 
 // Begin of protos/perfetto/config/ftrace/ftrace_config.proto
 
-// Next id: 18.
+// Next id: 19.
 message FtraceConfig {
   repeated string ftrace_events = 1;
   repeated string atrace_categories = 2;
@@ -540,6 +540,12 @@
   // expand to events that aren't of interest to the tracing user.
   // Introduced in: Android T.
   optional bool disable_generic_events = 16;
+
+  // The list of syscalls that should be recorded by sys_{enter,exit} ftrace
+  // events. When empty, all syscalls are recorded. If neither sys_{enter,exit}
+  // are enabled, this setting has no effect. Example: ["sys_read", "sys_open"].
+  // Introduced in: Android U.
+  repeated string syscall_events = 18;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 42c7f85..90c0915 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -466,7 +466,7 @@
 
 // Begin of protos/perfetto/config/ftrace/ftrace_config.proto
 
-// Next id: 18.
+// Next id: 19.
 message FtraceConfig {
   repeated string ftrace_events = 1;
   repeated string atrace_categories = 2;
@@ -540,6 +540,12 @@
   // expand to events that aren't of interest to the tracing user.
   // Introduced in: Android T.
   optional bool disable_generic_events = 16;
+
+  // The list of syscalls that should be recorded by sys_{enter,exit} ftrace
+  // events. When empty, all syscalls are recorded. If neither sys_{enter,exit}
+  // are enabled, this setting has no effect. Example: ["sys_read", "sys_open"].
+  // Introduced in: Android U.
+  repeated string syscall_events = 18;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
diff --git a/src/kernel_utils/syscall_table.h b/src/kernel_utils/syscall_table.h
index 6889282..3e88e0a 100644
--- a/src/kernel_utils/syscall_table.h
+++ b/src/kernel_utils/syscall_table.h
@@ -40,6 +40,10 @@
  public:
   explicit SyscallTable(Architecture arch);
 
+  // Use for testing.
+  SyscallTable(const char* const* table, size_t count)
+      : syscall_count_(count), syscall_table_(table) {}
+
   // Return the architecture enum for the given uname machine string.
   static Architecture ArchFromString(base::StringView machine);
 
diff --git a/src/traced/probes/ftrace/BUILD.gn b/src/traced/probes/ftrace/BUILD.gn
index c3e18ef..d19674f 100644
--- a/src/traced/probes/ftrace/BUILD.gn
+++ b/src/traced/probes/ftrace/BUILD.gn
@@ -120,6 +120,7 @@
     "../../../android_internal:lazy_library_loader",
     "../../../base",
     "../../../kallsyms",
+    "../../../kernel_utils:syscall_table",
     "../../../protozero",
     "format_parser",
   ]
diff --git a/src/traced/probes/ftrace/cpu_reader_benchmark.cc b/src/traced/probes/ftrace/cpu_reader_benchmark.cc
index f6f7038..e8f6891 100644
--- a/src/traced/probes/ftrace/cpu_reader_benchmark.cc
+++ b/src/traced/probes/ftrace/cpu_reader_benchmark.cc
@@ -317,6 +317,7 @@
   auto page = PageFromXxd(test_case->data);
 
   FtraceDataSourceConfig ds_config{EventFilter{},
+                                   EventFilter{},
                                    DisabledCompactSchedConfigForTesting(),
                                    {},
                                    {},
diff --git a/src/traced/probes/ftrace/cpu_reader_fuzzer.cc b/src/traced/probes/ftrace/cpu_reader_fuzzer.cc
index 84dfda0..8cb4353 100644
--- a/src/traced/probes/ftrace/cpu_reader_fuzzer.cc
+++ b/src/traced/probes/ftrace/cpu_reader_fuzzer.cc
@@ -55,6 +55,7 @@
 
   FtraceMetadata metadata{};
   FtraceDataSourceConfig ds_config{EventFilter{},
+                                   EventFilter{},
                                    DisabledCompactSchedConfigForTesting(),
                                    {},
                                    {},
diff --git a/src/traced/probes/ftrace/cpu_reader_unittest.cc b/src/traced/probes/ftrace/cpu_reader_unittest.cc
index fcc428a..2b16ff2 100644
--- a/src/traced/probes/ftrace/cpu_reader_unittest.cc
+++ b/src/traced/probes/ftrace/cpu_reader_unittest.cc
@@ -65,6 +65,7 @@
 
 FtraceDataSourceConfig EmptyConfig() {
   return FtraceDataSourceConfig{EventFilter{},
+                                EventFilter{},
                                 DisabledCompactSchedConfigForTesting(),
                                 {},
                                 {},
@@ -862,6 +863,7 @@
   auto page = PageFromXxd(test_case->data);
 
   FtraceDataSourceConfig ds_config{EventFilter{},
+                                   EventFilter{},
                                    EnabledCompactSchedConfigForTesting(),
                                    {},
                                    {},
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 69c4176..ddd429f 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -38,6 +38,12 @@
 constexpr int kDefaultPerCpuBufferSizeKb = 2 * 1024;  // 2mb
 constexpr int kMaxPerCpuBufferSizeKb = 64 * 1024;     // 64mb
 
+// A fake "syscall id" that indicates all syscalls should be recorded. This
+// allows us to distinguish between the case where `syscall_events` is empty
+// because raw_syscalls aren't enabled, or the case where it is and we want to
+// record all events.
+constexpr size_t kAllSyscallsId = kMaxSyscalls + 1;
+
 // trace_clocks in preference order.
 // If this list is changed, the FtraceClocks enum in ftrace_event_bundle.proto
 // and FtraceConfigMuxer::SetupClock() should be also changed accordingly.
@@ -449,6 +455,73 @@
   return events;
 }
 
+bool FtraceConfigMuxer::FilterHasGroup(const EventFilter& filter,
+                                       const std::string& group) {
+  const std::vector<const Event*>* events = table_->GetEventsByGroup(group);
+  if (!events) {
+    return false;
+  }
+
+  for (const Event* event : *events) {
+    if (filter.IsEventEnabled(event->ftrace_event_id)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+EventFilter FtraceConfigMuxer::BuildSyscallFilter(
+    const EventFilter& ftrace_filter,
+    const FtraceConfig& request) {
+  EventFilter output;
+
+  if (!FilterHasGroup(ftrace_filter, "raw_syscalls")) {
+    return output;
+  }
+
+  if (request.syscall_events().empty()) {
+    output.AddEnabledEvent(kAllSyscallsId);
+    return output;
+  }
+
+  for (const std::string& syscall : request.syscall_events()) {
+    base::Optional<size_t> id = syscalls_.GetByName(syscall);
+    if (!id.has_value()) {
+      PERFETTO_ELOG("Can't enable %s, syscall not known", syscall.c_str());
+      continue;
+    }
+    output.AddEnabledEvent(*id);
+  }
+
+  return output;
+}
+
+bool FtraceConfigMuxer::SetSyscallEventFilter(
+    const EventFilter& extra_syscalls) {
+  EventFilter syscall_filter;
+
+  syscall_filter.EnableEventsFrom(extra_syscalls);
+  for (const auto& id_config : ds_configs_) {
+    const perfetto::FtraceDataSourceConfig& config = id_config.second;
+    syscall_filter.EnableEventsFrom(config.syscall_filter);
+  }
+
+  std::set<size_t> filter_set = syscall_filter.GetEnabledEvents();
+  if (syscall_filter.IsEventEnabled(kAllSyscallsId)) {
+    filter_set.clear();
+  }
+
+  if (current_state_.syscall_filter != filter_set) {
+    if (!ftrace_->SetSyscallFilter(filter_set)) {
+      return false;
+    }
+
+    current_state_.syscall_filter = filter_set;
+  }
+
+  return true;
+}
+
 // Post-conditions:
 // 1. result >= 1 (should have at least one page per CPU)
 // 2. result * 4 < kMaxTotalBufferSizeKb
@@ -473,9 +546,11 @@
 FtraceConfigMuxer::FtraceConfigMuxer(
     FtraceProcfs* ftrace,
     ProtoTranslationTable* table,
+    SyscallTable syscalls,
     std::map<std::string, std::vector<GroupAndName>> vendor_events)
     : ftrace_(ftrace),
       table_(table),
+      syscalls_(std::move(syscalls)),
       current_state_(),
       ds_configs_(),
       vendor_events_(vendor_events) {}
@@ -568,6 +643,12 @@
     }
   }
 
+  EventFilter syscall_filter = BuildSyscallFilter(filter, request);
+  if (!SetSyscallEventFilter(syscall_filter)) {
+    PERFETTO_ELOG("Failed to set raw_syscall ftrace filter in SetupConfig");
+    return 0;
+  }
+
   auto compact_sched =
       CreateCompactSchedConfig(request, table_->compact_sched_format());
 
@@ -576,7 +657,8 @@
   FtraceConfigId id = ++last_id_;
   ds_configs_.emplace(
       std::piecewise_construct, std::forward_as_tuple(id),
-      std::forward_as_tuple(std::move(filter), compact_sched, std::move(apps),
+      std::forward_as_tuple(std::move(filter), std::move(syscall_filter),
+                            compact_sched, std::move(apps),
                             std::move(categories), request.symbolize_ksyms()));
   return id;
 }
@@ -631,6 +713,10 @@
       (current_state_.atrace_apps.size() != expected_apps.size()) ||
       (current_state_.atrace_categories.size() != expected_categories.size());
 
+  if (!SetSyscallEventFilter({})) {
+    PERFETTO_ELOG("Failed to set raw_syscall ftrace filter in RemoveConfig");
+  }
+
   // Disable any events that are currently enabled, but are not in any configs
   // anymore.
   std::set<size_t> event_ids = current_state_.ftrace_events.GetEnabledEvents();
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index c5a9323..190ed63 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -20,6 +20,7 @@
 #include <map>
 #include <set>
 
+#include "src/kernel_utils/syscall_table.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
 #include "src/traced/probes/ftrace/ftrace_controller.h"
@@ -40,11 +41,13 @@
 // that data source's config.
 struct FtraceDataSourceConfig {
   FtraceDataSourceConfig(EventFilter _event_filter,
+                         EventFilter _syscall_filter,
                          CompactSchedConfig _compact_sched,
                          std::vector<std::string> _atrace_apps,
                          std::vector<std::string> _atrace_categories,
                          bool _symbolize_ksyms)
       : event_filter(std::move(_event_filter)),
+        syscall_filter(std::move(_syscall_filter)),
         compact_sched(_compact_sched),
         atrace_apps(std::move(_atrace_apps)),
         atrace_categories(std::move(_atrace_categories)),
@@ -54,6 +57,10 @@
   // x is enabled for this data source.
   EventFilter event_filter;
 
+  // Specifies the syscalls (by id) that are enabled for this data source. An
+  // empty filter implies all events are enabled.
+  EventFilter syscall_filter;
+
   // Configuration of the optional compact encoding of scheduling events.
   const CompactSchedConfig compact_sched;
 
@@ -84,6 +91,7 @@
   FtraceConfigMuxer(
       FtraceProcfs* ftrace,
       ProtoTranslationTable* table,
+      SyscallTable syscalls,
       std::map<std::string, std::vector<GroupAndName>> vendor_events);
   virtual ~FtraceConfigMuxer();
 
@@ -132,6 +140,10 @@
     return &current_state_.ftrace_events;
   }
 
+  const std::set<size_t>& GetSyscallFilterForTesting() const {
+    return current_state_.syscall_filter;
+  }
+
  private:
   static bool StartAtrace(const std::vector<std::string>& apps,
                           const std::vector<std::string>& categories,
@@ -139,6 +151,8 @@
 
   struct FtraceState {
     EventFilter ftrace_events;
+    std::set<size_t> syscall_filter;
+
     // Used only in Android for ATRACE_EVENT/os.Trace() userspace
     std::vector<std::string> atrace_apps;
     std::vector<std::string> atrace_categories;
@@ -162,11 +176,30 @@
   std::set<GroupAndName> GetFtraceEvents(const FtraceConfig& request,
                                          const ProtoTranslationTable*);
 
+  // Returns true if the event filter has at least one event from group.
+  bool FilterHasGroup(const EventFilter& filter, const std::string& group);
+
+  // Configs have three states:
+  // 1. The config does not include raw_syscall ftrace events (empty filter).
+  // 2. The config has at least one raw_syscall ftrace events, then either:
+  //   a. The syscall_events is left empty (match all events).
+  //   b. The syscall_events is non-empty (match only those events).
+  EventFilter BuildSyscallFilter(const EventFilter& ftrace_filter,
+                                 const FtraceConfig& request);
+
+  // Updates the ftrace syscall filters such that they satisfy all ds_configs_
+  // and the extra_syscalls provided here. The filter is set to be the union of
+  // all configs meaning no config will lose events, but concurrent configs can
+  // see additional events. You may provide a syscall filter during SetUpConfig
+  // so the filter can be updated before ds_configs_.
+  bool SetSyscallEventFilter(const EventFilter& extra_syscalls);
+
   FtraceConfigId GetNextId();
 
   FtraceConfigId last_id_ = 1;
   FtraceProcfs* ftrace_;
   ProtoTranslationTable* table_;
+  SyscallTable syscalls_;
 
   FtraceState current_state_;
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 4012763..78ea2a6 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -45,6 +45,13 @@
 constexpr int kFakeSchedSwitchEventId = 1;
 constexpr int kCgroupMkdirEventId = 12;
 constexpr int kFakePrintEventId = 20;
+constexpr int kSysEnterId = 329;
+
+constexpr size_t kFakeSyscallCount = 2;
+constexpr const char* kFakeSyscalls[] = {
+    "sys_open",
+    "sys_read",
+};
 
 class MockFtraceProcfs : public FtraceProcfs {
  public:
@@ -123,6 +130,10 @@
             InvalidCompactSchedEventFormatForTesting()));
   }
 
+  SyscallTable GetSyscallTable() {
+    return SyscallTable(kFakeSyscalls, kFakeSyscallCount);
+  }
+
   std::unique_ptr<ProtoTranslationTable> CreateFakeTable(
       CompactSchedEventFormat compact_format =
           InvalidCompactSchedEventFormatForTesting()) {
@@ -184,6 +195,14 @@
       events.push_back(event);
     }
 
+    {
+      Event event = {};
+      event.name = "sys_enter";
+      event.group = "raw_syscalls";
+      event.ftrace_event_id = kSysEnterId;
+      events.push_back(event);
+    }
+
     return std::unique_ptr<ProtoTranslationTable>(new ProtoTranslationTable(
         &table_procfs_, events, std::move(common_fields),
         ProtoTranslationTable::DefaultPageHeaderSpecForTesting(),
@@ -208,13 +227,92 @@
   EXPECT_EQ(ComputeCpuBufferSizeInPages(42), 10u);
 }
 
+TEST_F(FtraceConfigMuxerTest, GenericSyscallFiltering) {
+  auto fake_table = CreateFakeTable();
+  NiceMock<MockFtraceProcfs> ftrace;
+
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  *config.add_syscall_events() = "sys_open";
+  *config.add_syscall_events() = "sys_read";
+
+  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
+
+  EXPECT_CALL(ftrace, WriteToFile(_, _)).WillRepeatedly(Return(true));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_enter/filter",
+                                  "id == 0 || id == 1"));
+  EXPECT_CALL(ftrace, WriteToFile("/root/events/raw_syscalls/sys_exit/filter",
+                                  "id == 0 || id == 1"));
+
+  FtraceConfigId id = model.SetupConfig(config);
+  ASSERT_TRUE(model.ActivateConfig(id));
+
+  const std::set<size_t>& filter = model.GetSyscallFilterForTesting();
+  ASSERT_THAT(filter, UnorderedElementsAre(0, 1));
+}
+
+TEST_F(FtraceConfigMuxerTest, UnknownSyscallFilter) {
+  auto fake_table = CreateFakeTable();
+  NiceMock<MockFtraceProcfs> ftrace;
+  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
+
+  FtraceConfig config = CreateFtraceConfig({"raw_syscalls/sys_enter"});
+  config.add_syscall_events("sys_open");
+  config.add_syscall_events("sys_not_a_call");
+
+  // Unknown syscall is ignored.
+  ASSERT_TRUE(model.SetupConfig(config));
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+}
+
+TEST_F(FtraceConfigMuxerTest, SyscallFilterMuxing) {
+  auto fake_table = CreateFakeTable();
+  NiceMock<MockFtraceProcfs> ftrace;
+  FtraceConfigMuxer model(&ftrace, fake_table.get(), GetSyscallTable(), {});
+
+  FtraceConfig empty_config = CreateFtraceConfig({});
+
+  FtraceConfig syscall_config = empty_config;
+  syscall_config.add_ftrace_events("raw_syscalls/sys_enter");
+
+  FtraceConfig syscall_open_config = syscall_config;
+  syscall_open_config.add_syscall_events("sys_open");
+
+  FtraceConfig syscall_read_config = syscall_config;
+  syscall_read_config.add_syscall_events("sys_read");
+
+  // Expect no filter for non-syscall config.
+  model.SetupConfig(empty_config);
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Expect no filter for syscall config with no specified events.
+  FtraceConfigId syscall_id = model.SetupConfig(syscall_config);
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // Still expect no filter to satisfy this and the above.
+  FtraceConfigId syscall_open_id = model.SetupConfig(syscall_open_config);
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
+
+  // After removing the generic syscall trace, only the one with filter is left.
+  ASSERT_TRUE(model.RemoveConfig(syscall_id));
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0));
+
+  // With sys_read and sys_open traced separately, filter includes both.
+  FtraceConfigId syscall_read_id = model.SetupConfig(syscall_read_config);
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre(0, 1));
+
+  // After removing configs with filters, filter is reset to empty.
+  ASSERT_TRUE(model.RemoveConfig(syscall_open_id));
+  ASSERT_TRUE(model.RemoveConfig(syscall_read_id));
+  ASSERT_THAT(model.GetSyscallFilterForTesting(), UnorderedElementsAre());
+}
+
 TEST_F(FtraceConfigMuxerTest, AddGenericEvent) {
   auto mock_table = GetMockTable();
   MockFtraceProcfs ftrace;
 
   FtraceConfig config = CreateFtraceConfig({"power/cpu_frequency"});
 
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), {});
+  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
 
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
@@ -261,7 +359,7 @@
 
   FtraceConfig config = CreateFtraceConfig({"group_one/foo", "group_two/foo"});
 
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), {});
+  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
 
   static constexpr int kEventId1 = 1;
   Event event1;
@@ -315,7 +413,7 @@
   EXPECT_CALL(ftrace,
               WriteToFile("/root/events/sched/sched_new_event/enable", "1"));
 
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), {});
+  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
   std::set<std::string> n = {"sched_switch", "sched_new_event"};
   ON_CALL(ftrace, GetEventNamesForGroup("events/sched"))
       .WillByDefault(Return(n));
@@ -363,7 +461,7 @@
 
   FtraceConfig config = CreateFtraceConfig({"group_one/*", "group_two/*"});
 
-  FtraceConfigMuxer model(&ftrace, mock_table.get(), {});
+  FtraceConfigMuxer model(&ftrace, mock_table.get(), GetSyscallTable(), {});
 
   std::set<std::string> event_names = {"foo"};
   ON_CALL(ftrace, GetEventNamesForGroup("events/group_one"))
@@ -412,7 +510,7 @@
 
   FtraceConfig config = CreateFtraceConfig({"sched_switch", "foo"});
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
@@ -460,7 +558,7 @@
 
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   // If someone is using ftrace already don't stomp on what they are doing.
   EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
@@ -476,7 +574,7 @@
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   *config.add_atrace_categories() = "sched";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
       .WillOnce(Return('0'));
@@ -518,7 +616,7 @@
   *config.add_atrace_apps() = "com.google.android.gms.persistent";
   *config.add_atrace_apps() = "com.google.android.gms";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
       .WillOnce(Return('0'));
@@ -563,7 +661,7 @@
   *config_c.add_atrace_apps() = "app_c";
   *config_c.add_atrace_categories() = "cat_c";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_a",
@@ -634,7 +732,7 @@
   *config_c.add_atrace_categories() = "cat_1";
   *config_c.add_atrace_categories() = "cat_3";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(
       atrace,
@@ -695,7 +793,7 @@
   *config_b.add_atrace_apps() = "app_1";
   *config_b.add_atrace_categories() = "cat_1";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_1",
@@ -732,7 +830,7 @@
   FtraceConfig config_d = CreateFtraceConfig({"sched/sched_cpu_hotplug"});
   *config_d.add_atrace_categories() = "d";
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   FtraceConfigId id_a = model.SetupConfig(config_a);
   ASSERT_TRUE(id_a);
@@ -784,7 +882,7 @@
   EXPECT_CALL(ftrace, ReadOneCharFromFile("/root/tracing_on"))
       .WillRepeatedly(Return('0'));
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   EXPECT_CALL(atrace, RunAtrace(ElementsAreArray({"atrace", "--async_start",
                                                   "--only_userspace", "cat_1",
@@ -807,7 +905,7 @@
   MockFtraceProcfs ftrace;
   FtraceConfig config;
 
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
   namespace pb0 = protos::pbzero;
 
   EXPECT_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
@@ -840,7 +938,7 @@
 
 TEST_F(FtraceConfigMuxerTest, GetFtraceEvents) {
   MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
   std::set<GroupAndName> events =
@@ -852,7 +950,7 @@
 
 TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtrace) {
   MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   FtraceConfig config = CreateFtraceConfig({});
   *config.add_atrace_categories() = "sched";
@@ -866,7 +964,7 @@
 
 TEST_F(FtraceConfigMuxerTest, GetFtraceEventsAtraceCategories) {
   MockFtraceProcfs ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   FtraceConfig config = CreateFtraceConfig({});
   *config.add_atrace_categories() = "sched";
@@ -890,7 +988,7 @@
   MockFtraceProcfs ftrace;
   FtraceConfig config =
       CreateFtraceConfig({"sched/sched_switch", "cgroup/cgroup_mkdir"});
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   ON_CALL(ftrace, ReadFileIntoString("/root/trace_clock"))
       .WillByDefault(Return("[local] global boot"));
@@ -951,7 +1049,7 @@
 
   NiceMock<MockFtraceProcfs> ftrace;
   table_ = CreateFakeTable(valid_compact_format);
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   // First data source - request compact encoding.
   FtraceConfig config_enabled = CreateFtraceConfig({"sched/sched_switch"});
@@ -982,7 +1080,7 @@
 
 TEST_F(FtraceConfigMuxerTest, CompactSchedConfigWithInvalidFormat) {
   NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   // Request compact encoding.
   FtraceConfig config = CreateFtraceConfig({"sched/sched_switch"});
@@ -1003,7 +1101,7 @@
 
 TEST_F(FtraceConfigMuxerTest, SkipGenericEventsOption) {
   NiceMock<MockFtraceProcfs> ftrace;
-  FtraceConfigMuxer model(&ftrace, table_.get(), {});
+  FtraceConfigMuxer model(&ftrace, table_.get(), GetSyscallTable(), {});
 
   static constexpr int kFtraceGenericEventId = 42;
   ON_CALL(table_procfs_, ReadEventFormat("sched", "generic"))
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 5cb1de1..82ec64b 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -154,8 +154,11 @@
   auto vendor_evts =
       vendor_tracepoints::DiscoverVendorTracepoints(&hal, ftrace_procfs.get());
 
-  std::unique_ptr<FtraceConfigMuxer> model = std::unique_ptr<FtraceConfigMuxer>(
-      new FtraceConfigMuxer(ftrace_procfs.get(), table.get(), vendor_evts));
+  auto syscalls = SyscallTable::FromCurrentArch();
+
+  std::unique_ptr<FtraceConfigMuxer> model =
+      std::unique_ptr<FtraceConfigMuxer>(new FtraceConfigMuxer(
+          ftrace_procfs.get(), table.get(), std::move(syscalls), vendor_evts));
   return std::unique_ptr<FtraceController>(
       new FtraceController(std::move(ftrace_procfs), std::move(table),
                            std::move(model), runner, observer));
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index a3a02cc..b521274 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -94,7 +94,7 @@
 std::unique_ptr<FtraceConfigMuxer> FakeModel(FtraceProcfs* ftrace,
                                              ProtoTranslationTable* table) {
   return std::unique_ptr<FtraceConfigMuxer>(
-      new FtraceConfigMuxer(ftrace, table, {}));
+      new FtraceConfigMuxer(ftrace, table, SyscallTable(kUnknown), {}));
 }
 
 class MockFtraceProcfs : public FtraceProcfs {
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 55b81b4..fd38b05 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -107,6 +107,28 @@
 FtraceProcfs::FtraceProcfs(const std::string& root) : root_(root) {}
 FtraceProcfs::~FtraceProcfs() = default;
 
+bool FtraceProcfs::SetSyscallFilter(const std::set<size_t>& filter) {
+  std::vector<std::string> parts;
+  for (size_t id : filter) {
+    base::StackString<16> m("id == %zu", id);
+    parts.push_back(m.ToStdString());
+  }
+
+  std::string filter_str = "0";
+  if (!parts.empty()) {
+    filter_str = base::Join(parts, " || ");
+  }
+
+  for (const std::string& event : {"sys_enter", "sys_exit"}) {
+    std::string path = root_ + "events/raw_syscalls/" + event + "/filter";
+    if (!WriteToFile(path, filter_str)) {
+      PERFETTO_ELOG("Failed to write file: %s", path.c_str());
+      return false;
+    }
+  }
+  return true;
+}
+
 bool FtraceProcfs::EnableEvent(const std::string& group,
                                const std::string& name) {
   std::string path = root_ + "events/" + group + "/" + name + "/enable";
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index b7ddf99..05807cf 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -42,6 +42,9 @@
   explicit FtraceProcfs(const std::string& root);
   virtual ~FtraceProcfs();
 
+  // Set the filter for syscall events. If empty, clear the filter.
+  bool SetSyscallFilter(const std::set<size_t>& filter);
+
   // Enable the event under with the given |group| and |name|.
   bool EnableEvent(const std::string& group, const std::string& name);