Xây Dựng Môi Trường an Toàn Cho Plugin Lua
Hệ thống plugin tùy chỉnh do Blizzard mở ra đã trở thành một yếu tố then chốt trong thành công của World of Warcraft. So với các tựa game trực tuyến tại Trung Quốc hay Hàn Quốc, hiện chưa có sản phẩm nào đạt được mức độ tự do trong tùy biến giao diện người dùng như hệ thống này.
Ban đầu Blizzard chỉ định tạo ra giao diện tùy chỉnh cho người dùng và bên thứ ba. Nhưng dần dần hệ thống plugin dựa trên XML và Lua đã vượt ra khỏi khuôn khổ giao diện, trở thành công cụ phát triển đa năng.
Trong suốt sự nghiệp làm việc với ngành game online, tôi nhận thấy việc phát triển UI luôn tiêu tốn rất nhiều nguồn lực. Giai đoạn bảo trì sản phẩm thường phải đầu tư nhân lực lớn vào việc tối ưu hóa giao diện. Một hệ thống plugin mở không chỉ mang lại lợi ích cho người chơi mà còn giúp nhà phát triển tiết kiệm chi phí phát triển.
Tuy nhiên, hệ thống plugin cũng tồn tại mặt trái về an ninh. Sự tự do vượt bậc dễ bị lợi dụng để phát triển phần mềm gián điệp hoặc robot tự động. Vấn đề này xin phép không đề cập ở đây, thay vào đó chúng ta tập trung vào một khía cạnh quan trọng khác: bảo vệ dữ liệu nhạy cảm khỏi bị truy cập trái phép thông qua plugin.
Lua cung cấp các thư viện bên thứ ba như giải pháp hoàn hảo cho vấn đề này. Lua Rings là một ví dụ tiêu biểu, cho phép tạo nhiều state Lua làm môi trường Sandbox. Tuy nhiên việc giao tiếp giữa các state có thể gây ra vấn đề hiệu năng nếu mức độ tương tác cao.
Nếu không dùng thư viện bên ngoài, chúng ta có thể tận dụng chính cơ chế ngôn ngữ Lua. Hàm setfenv giúp thiết lập môi trường thực thi cho từng hàm cụ thể, giới hạn phạm vi truy cập dữ liệu. Tuy nhiên cần thận trọng với các “lỗ hổng” như hàm getfenv - công cụ tiềm năng để phá vỡ hàng rào bảo vệ.
Khi cung cấp các API cơ bản (pairs, print,…) cho plugin, không nên đơn giản sao chép vào môi trường plugin. Thay vào đó, phương pháp tối ưu là sử dụng metatable tạo cơ chế tự động đóng gói. Mỗi lần gọi API được phép, hệ thống sẽ tạo closure thực thi trong môi trường bị giới hạn.
Một thách thức khác là cách phơi bày các đối tượng nội bộ cho plugin. Việc giao trực tiếp đối tượng Lua cho plugin rất nguy hiểm vì người dùng có thể dùng vòng lặp for để khám phá toàn bộ cấu trúc bên trong. Giải pháp hợp lý là xây dựng proxy thông qua metatable kiểm soát quyền truy cập. Tôi khuyên nên thiết kế cơ chế thuộc tính riêng (sử dụng __index và __newindex), chỉ cho phép truy cập thuộc tính được phép thay vì gọi trực tiếp phương thức đối tượng.
Vấn đề cuối cùng là ngăn chặn việc dùng getmetatable để truy cập metatable gốc. Giải pháp đơn giản là tạo metatable “ảo” bằng cách thiết lập trường __metatable. Khi đó, getmetatable sẽ trả về giá trị được định nghĩa sẵn thay vì metatable thật sự.
Để xây dựng proxy an toàn, chúng ta có thể dùng bảng trống kết hợp metatable được bảo vệ. Lưu ý không lưu trữ đối tượng thật trong proxy, mà nên tạo bảng yếu (weak table) riêng để ánh xạ. Một lựa chọn khác là dùng hàm newproxy - dù không được tài liệu hóa nhưng vẫn rất hữu ích. Nếu tương lai Blizzard bỏ hàm này, ta hoàn toàn có thể tự xây dựng cơ chế tương tự trong C.
Khi làm việc với userdata, bảo vệ metatable sẽ an toàn hơn so với table. Có người thắc mắc tại sao newproxy lại gán cùng metatable cho các userdata 0-byte khác nhau? Thực ra chúng ta chỉ cần tính duy nhất của đối tượng. Dù dùng table trống hay userdata, điều quan trọng là giữ đối tượng thật ở nơi plugin không thể tiếp cận.
Trong trường hợp hệ thống cần truyền đối tượng nội bộ, ví dụ khi thiết lập quan hệ cây như: a.parent = b Hệ thống thực tế sẽ gọi: a:setParent(b) Trong đó b là tham số kiểu đối tượng tùy chỉnh. Cả a và b đều phải được thay thế bằng proxy khi đưa vào môi trường plugin.
Vấn đề nằm ở chỗ Lua không hỗ trợ cơ chế chuyển đổi ngầm như C++. Hai giải pháp khả thi:
- Kiểm tra giá trị phải có phải là proxy không, nếu đúng thì chuyển sang đối tượng thật. Phương pháp này trực quan nhưng yêu cầu expose khái niệm proxy.
- Định nghĩa thuộc tính “plug” nội bộ trả về đối tượng thật, đồng thời chặn plugin truy cập thuộc tính này. Khi đó: proxya.parent = proxyb sẽ được chuyển thành: a:setParent(b:getPlug())