OC-KittenOS/code/init.lua

643 lines
17 KiB
Lua

-- KittenOS N.E.O Kernel: "Tell Mettaton I said hi."
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- NOTE: local is considered unnecessary in kernel since 21 March
-- In case of OpenComputers configuration abnormality
readBufSize = 2048
-- A function used for logging, usable by programs.
emergencyFunction = function (...)
computer.pushSignal("_kosneo_syslog", "kernel", ...)
if ocemu.log then
pcall(ocemu.log, ...)
end
end
-- Comment this out if you don't want programs to have
-- access to ocemu's logger.
ocemu = (component.list("ocemu", true)()) or (component.list("sandbox", true)())
if ocemu then
ocemu = component.proxy(ocemu)
end
-- It is a really bad idea to remove this.
-- If the code inside this block even executes, then removing it is a security risk.
if load(string.dump(function()end)) then
emergencyFunction("detected bytecode access, preventing (only remove this block if you trust every app ever on your KittenOS NEO system)")
local oldLoad = load
load = function (c, n, m, ...)
return oldLoad(c, n, "t", ...)
end
end
primaryDisk = component.proxy(computer.getBootAddress())
-- {{time, func, arg1...}...}
timers = {}
libraries = {}
setmetatable(libraries, {__mode = "v"})
-- proc.co = coroutine.create(appfunc)
-- proc.pkg = "pkg"
-- proc.access = {["perm"] = true, ...}
-- proc.denied = {["perm"] = true, ...}
-- proc.deathCBs = {function(), ...}
-- very slightly adjusted total CPU time
-- proc.cpuUsage
processes = {}
-- Maps registration-accesses to function(pkg, pid)
accesses = {}
lastPID = 0
-- Kernel global "idle time" counter, useful for accurate performance data
idleTime = 0
-- This function is critical to wide text support.
function unicode.safeTextFormat(s, ptr)
local res = ""
if not ptr then ptr = 1 end
local aptr = 1
for i = 1, unicode.len(s) do
local ch = unicode.sub(s, i, i)
local ex = unicode.charWidth(ch)
if i < ptr then
aptr = aptr + ex
end
for j = 2, ex do
ch = ch .. " "
end
res = res .. ch
end
return res, aptr
end
-- The issue with the above function, of course, is that in practice the GPU is a weird mess.
-- So this undoes the above transformation for feeding to gpu.set.
-- (In practice if safeTextFormat supports RTL, and that's a big "if", then this will not undo that.
-- The point is that this converts it into gpu.set format.)
function unicode.undoSafeTextFormat(s)
local res = ""
local ignoreNext = false
for i = 1, unicode.len(s) do
if not ignoreNext then
local ch = unicode.sub(s, i, i)
if unicode.charWidth(ch) ~= 1 then
if unicode.sub(s, i + 1, i + 1) ~= " " then
ch = " "
else
ignoreNext = true
end
end
res = res .. ch
else
ignoreNext = false
end
end
return res
end
function loadfile(s, e)
local h, er = primaryDisk.open(s)
if h then
local ch = ""
local c = primaryDisk.read(h, readBufSize)
while c do
ch = ch .. c
c = primaryDisk.read(h, readBufSize)
end
primaryDisk.close(h)
return load(ch, "=" .. s, "t", e)
end
return nil, tostring(er)
end
uniqueNEOProtectionObject = {}
wrapMetaCache = {}
setmetatable(wrapMetaCache, {__mode = "v"})
function wrapMeta(t)
if type(t) == "table" then
if wrapMetaCache[t] then
return wrapMetaCache[t]
end
local t2 = {}
wrapMetaCache[t] = t2
setmetatable(t2, {
__index = function (a, k) return wrapMeta(t[k]) end,
__newindex = error,
-- WTF
__call = function (_, ...)
return t(...)
end,
__pairs = function (a)
return function (x, key)
local k, v = next(t, k)
if k then return k, wrapMeta(v) end
end, 9, nil
end,
__ipairs = function (a)
return function (x, key)
key = key + 1
if t[key] then
return key, wrapMeta(t[key])
end
end, 9, 0
end,
__metatable = uniqueNEOProtectionObject
-- Don't protect this table - it'll make things worse
})
return t2
else
return t
end
end
function ensureType(a, t)
if type(a) ~= t then error("Invalid parameter, expected a " .. t) end
if t == "table" then
if getmetatable(a) then error("Invalid parameter, has metatable") end
end
end
function ensurePathComponent(s)
if not string.match(s, "^[a-zA-Z0-9_%-%+%,%.%#%~%@%'%;%[%]%(%)%&%%%$%! %=%{%}%^]+$") then error("chars disallowed: " .. s) end
if s == "." then error("single dot disallowed") end
if s == ".." then error("double dot disallowed") end
end
function ensurePath(s, r)
string.gsub(s, "[^/]+", ensurePathComponent)
if s:sub(1, r:len()) ~= r then error("base disallowed") end
if s:match("//") then error("// disallowed") end
end
-- Use with extreme care.
-- (A process killing itself will actually survive until the next yield... before any of the death events have run.)
function termProc(pid, reason)
if processes[pid] then
-- Immediately prepare for GC, it's possible this is out of memory.
-- If out of memory, then to reduce risk of memory leak by error, memory needs to be freed ASAP.
-- Start by getting rid of all process data.
local dcbs = processes[pid].deathCBs
local pkg = processes[pid].pkg
local usage = processes[pid].cpuUsage
processes[pid] = nil
-- This gets rid of a few more bits of data.
for _, v in ipairs(dcbs) do
v()
end
-- This finishes off that.
dcbs = nil
if reason then
emergencyFunction("d1 " .. pkg .. "/" .. pid)
emergencyFunction("d2 " .. reason)
end
-- And this is why it's important, because this generates timers.
-- The important targets of these timers will delete even more data.
distEvent(nil, "k.procdie", pkg, pid, reason, usage)
end
end
function execEvent(k, ...)
if processes[k] then
local v = processes[k]
local timerA = computer.uptime()
local r, reason = coroutine.resume(v.co, ...)
-- Mostly reliable accounting
v.cpuUsage = v.cpuUsage + (computer.uptime() - timerA)
reason = ((not r) and tostring(reason)) or nil
local dead = (not not reason) or coroutine.status(v.co) == "dead"
if dead then
termProc(k, reason)
return not not reason
end
end
end
function distEvent(pid, s, ...)
local ev = {...}
if pid then
local v = processes[pid]
if not v then
return
end
if not (s:sub(1, 2) == "k." or v.access["s." .. s] or v.access["k.root"]) then
return
end
-- Schedule the timer to carry the event.
table.insert(timers, {0, execEvent, pid, s, table.unpack(ev)})
else
for k, v in pairs(processes) do
distEvent(k, s, ...)
end
end
end
function lister(pfx)
return function ()
local n = primaryDisk.list(pfx)
local n2 = {}
for k, v in ipairs(n) do
if v:sub(#v - 3) == ".lua" then
table.insert(n2, v:sub(1, #v - 4))
end
end
return n2
end
end
function loadLibraryInner(library)
ensureType(library, "string")
library = "libs/" .. library .. ".lua"
ensurePath(library, "libs/")
if libraries[library] then return libraries[library] end
emergencyFunction("loading " .. library)
local l, r = loadfile(library, baseProcEnv())
if l then
local ok, al = pcall(l)
if ok then
al = wrapMeta(al)
libraries[library] = al
return al
else
return nil, al
end
end
return nil, r
end
wrapMath = wrapMeta(math)
wrapTable = wrapMeta(table)
wrapString = wrapMeta(string)
wrapUnicode = wrapMeta(unicode)
wrapCoroutine = wrapMeta(coroutine)
-- inject stuff into os
os.totalMemory = computer.totalMemory
os.freeMemory = computer.freeMemory
os.energy = computer.energy
os.maxEnergy = computer.maxEnergy
os.uptime = computer.uptime
os.address = computer.address
wrapOs = wrapMeta(os)
wrapDebug = wrapMeta(debug)
wrapBit32 = wrapMeta(bit32)
wrapUtf8 = wrapMeta(utf8)
baseProcEnvCore = {
_VERSION = _VERSION,
math = wrapMath,
table = wrapTable,
string = wrapString,
unicode = wrapUnicode,
coroutine = wrapCoroutine,
os = wrapOs,
debug = wrapDebug,
bit32 = wrapBit32,
utf8 = wrapUtf8,
require = loadLibraryInner,
assert = assert, ipairs = ipairs,
load = load,
next = function (t, k)
local mt = getmetatable(t)
if mt == uniqueNEOProtectionObject then error("NEO-Protected Object") end
return next(t, k)
end,
pairs = pairs, pcall = pcall,
xpcall = xpcall, select = select,
type = type, error = error,
tonumber = tonumber, tostring = tostring,
setmetatable = setmetatable, getmetatable = function (n)
local mt = getmetatable(n)
if mt == uniqueNEOProtectionObject then return "NEO-Protected Object" end
return mt
end,
rawset = function (t, i, v)
local mt = getmetatable(t)
if mt == uniqueNEOProtectionObject then error("NEO-Protected Object") end
return rawset(t, i, v)
end, rawget = rawget, rawlen = rawlen, rawequal = rawequal,
}
baseProcNeo = {
emergency = emergencyFunction,
readBufSize = readBufSize,
wrapMeta = wrapMeta,
listProcs = function ()
local n = {}
for k, v in pairs(processes) do
table.insert(n, {k, v.pkg, v.cpuUsage})
end
return n
end,
listApps = lister("apps/"),
listLibs = lister("libs/"),
usAccessExists = function (accessName)
ensureType(accessName, "string")
if accesses[accessName] then
return true
end
end,
totalIdleTime = function () return idleTime end,
ensurePath = ensurePath,
ensurePathComponent = ensurePathComponent,
ensureType = ensureType
}
baseProcEnvMT = {
__index = baseProcEnvCore,
__metatable = uniqueNEOProtectionObject
}
baseProcNeoMT = {
__index = baseProcNeo,
__metatable = uniqueNEOProtectionObject
}
function baseProcEnv()
local pe = setmetatable({}, baseProcEnvMT)
pe.neo = setmetatable({}, baseProcNeoMT)
pe._G = pe
pe._ENV = pe
return pe
end
-- These two are hooks for k.root level applications to change policy.
-- Only a k.root application is allowed to do this for obvious reasons.
function securityPolicy(pid, proc, perm, req)
-- Important safety measure : only sys-* gets anything at first
req(proc.pkg:sub(1, 4) == "sys-")
end
function runProgramPolicy(ipkg, pkg, pid, ...)
-- VERY specific injunction here:
-- non "sys-" apps NEVER start "sys-" apps
-- This is part of the "default security policy" below:
-- sys- has all access
-- anything else has none
if ipkg:sub(1, 4) == "sys-" then
if pkg:sub(1, 4) ~= "sys-" then
return nil, "non-sys app trying to start sys app"
end
end
return true
end
function retrieveAccess(perm, pkg, pid)
-- Return the access lib and the death callback.
-- Access categories are sorted into:
-- "c.<hw>": Component
-- "s.<event>": Signal receiver (with responsibilities for Security Request watchers)
-- "s.k.<...>": Kernel stuff
-- "s.k.procnew" : New process (pkg, pid, ppkg, ppid)
-- "s.k.procdie" : Process dead (pkg, pid, reason, usage)
-- "s.k.registration" : Registration of service alert ("x." .. etc)
-- "s.k.deregistration" : Registration of service alert ("x." .. etc)
-- "s.k.securityresponse" : Response from security policy (accessId, accessObj)
-- "s.h.<...>": Incoming HW messages
-- "s.x.<endpoint>": This access is actually useless on it's own - it is given by x.<endpoint>
-- "k.<x>": Kernel
-- "k.root": _ENV (holy grail), and indirectly security request control (which is basically equivalent to this)
-- "k.computer": computer
-- "r.<endpoint>": Registration Of Service...
-- "x.<endpoint>": Access Of Service (handled by r. & accesses table)
if accesses[perm] then
return accesses[perm](pkg, pid)
end
if perm == "k.root" then
return _ENV
end
if perm == "k.computer" then
return wrapMeta(computer)
end
if perm == "k.kill" then
return function(npid)
ensureType(npid, "number")
termProc(npid, "Killed by " .. pkg .. "/" .. pid)
end
end
if perm:sub(1, 2) == "s." then
-- This is more of a "return success". Signal access is determined by the access/denied maps.
return true
end
if perm:sub(1, 2) == "c." then
-- Allows for simple "Control any of these connected to the system" APIs,
-- for things the OS shouldn't be poking it's nose in.
local primary = nil
local temporary = nil
local t = perm:sub(3)
if t == "filesystem" then
primary = wrapMeta(primaryDisk)
temporary = wrapMeta(component.proxy(computer.tmpAddress()))
end
return {
list = function ()
local i = component.list(t, true)
return function ()
local ii = i()
if not ii then return nil end
return wrapMeta(component.proxy(ii))
end
end,
primary = primary,
temporary = temporary
}
end
if perm:sub(1, 2) == "r." then
local uid = "x" .. perm:sub(2)
local sid = "s.x" .. perm:sub(2)
if accesses[uid] then return nil end
accesses[uid] = function (pkg, pid)
return nil
end
return function (f)
-- Registration function
ensureType(f, "function")
local accessObjectCache = {}
accesses[uid] = function(pkg, pid)
-- Basically, a per registration per process cache.
-- This is a consistent yet flexible behavior.
if accessObjectCache[pid] then
return accessObjectCache[pid]
end
processes[pid].access[sid] = true
local ok, a = pcall(f, pkg, pid, function (...)
distEvent(pid, uid, ...)
end)
if ok then
accessObjectCache[pid] = a
return a, function ()
accessObjectCache[pid] = nil
end
end
-- returns nil and fails
end
-- Announce registration
distEvent(nil, "k.registration", uid)
end, function ()
-- Registration becomes null (access is held but other processes cannot retrieve object)
if accesses[uid] then
distEvent(nil, "k.deregistration", uid)
end
accesses[uid] = nil
end
end
end
function start(pkg, ppkg, ppid, ...)
local proc = {}
local pid = lastPID
lastPID = lastPID + 1
local function startFromUser(ipkg, ...)
ensureType(ipkg, "string")
local ok, n = pcall(ensurePathComponent, ipkg .. ".lua")
if not ok then return nil, n end
local k, r = runProgramPolicy(ipkg, pkg, pid, ...)
if k then
return start(ipkg, pkg, pid, ...)
else
return k, r
end
end
local function osExecuteCore(handler, ...)
local pid, err = startFromUser(...)
while pid do
local sig = {coroutine.yield()}
handler(table.unpack(sig))
if sig[1] == "k.procdie" then
if sig[3] == pid then
return 0, sig[4]
end
end
end
return -1, err
end
local requestAccessAsync = function (perm)
ensureType(perm, "string")
-- Safety-checked, prepare security event.
local req = function (res)
if processes[pid] then
local n = nil
local n2 = nil
if res then
proc.access[perm] = true
proc.denied[perm] = nil
n, n2 = retrieveAccess(perm, pkg, pid)
if n2 then
table.insert(processes[pid].deathCBs, n2)
end
else
proc.denied[perm] = true
end
distEvent(pid, "k.securityresponse", perm, n)
end
end
-- outer security policy:
if proc.access["k.root"] or proc.access[perm] or proc.denied[perm] then
-- Use cached result to prevent possible unintentional security service spam
req(proc.access["k.root"] or not proc.denied[perm])
return
end
-- Denied goes to on to prevent spam
proc.denied[perm] = true
securityPolicy(pid, proc, perm, req)
end
local env = baseProcEnv()
env.neo.pid = pid
env.neo.pkg = pkg
env.neo.executeAsync = startFromUser
env.neo.execute = function (...)
return osExecuteCore(function () end, ...)
end
env.neo.executeExt = osExecuteCore
env.neo.requestAccessAsync = requestAccessAsync
env.neo.requestAccess = function (perm, handler)
requestAccessAsync(perm)
handler = handler or function() end
while true do
local n = {coroutine.yield()}
handler(table.unpack(n))
if n[1] == "k.securityresponse" then
-- Security response - if it involves the permission, then take it
if n[2] == perm then return n[3] end
end
end
end
env.neo.requireAccess = function (perm, reason)
-- Allows for hooking
local res = env.neo.requestAccess(perm)
if not res then error(pkg .. " needed " .. perm .. " for " .. (reason or "some reason")) end
return res
end
env.neo.scheduleTimer = function (time)
ensureType(time, "number")
local tag = {}
table.insert(timers, {time, execEvent, pid, "k.timer", tag, time})
return tag
end
local appfunc, r = loadfile("apps/" .. pkg .. ".lua", env)
if not appfunc then
return nil, r
end
proc.co = coroutine.create(function (...) local r = {xpcall(appfunc, debug.traceback, ...)} if not r[1] then error(table.unpack(r, 2)) end return table.unpack(r, 2) end)
proc.pkg = pkg
proc.access = {}
proc.denied = {}
-- You are dead. Not big surprise.
proc.deathCBs = {function () pcall(function () env.neo.dead = true end) end}
proc.cpuUsage = 0
-- Note the target process doesn't get the procnew (the dist occurs before it's creation)
pcall(distEvent, nil, "k.procnew", pkg, pid, ppkg, ppid)
processes[pid] = proc
table.insert(timers, {0, execEvent, pid, ppkg, ppid, ...})
return pid
end
-- Kernel Scheduling Loop --
if not start("sys-init") then error("Could not start sys-init") end
while true do
local tmr = nil
for i = 1, 16 do
tmr = nil
local now = computer.uptime()
local didAnything = false -- for early exit
local k = 1
while timers[k] do
local v = timers[k]
if v[1] <= now then
table.remove(timers, k)
if v[2](table.unpack(v, 3)) then
didAnything = false -- to break
tmr = 0.05
break
end
didAnything = true
else
if not tmr then
tmr = v[1]
else
tmr = math.min(tmr, v[1])
end
k = k + 1
end
end
if not didAnything then break end
end
now = computer.uptime() -- the above probably took a while
local dist = tmr and math.max(0.05, tmr - now)
local signal = {computer.pullSignal(dist)}
idleTime = idleTime + (computer.uptime() - now)
if signal[1] then
distEvent(nil, "h." .. signal[1], select(2, table.unpack(signal)))
end
end