-- KittenOS N.E.O Kernel: "Tell Mettaton I said hi." -- Copyright (C) 2018-2021 by KittenOS NEO contributors -- -- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -- THIS SOFTWARE. -- 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 and 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 () return function (x, key) local k, v = next(t, key) if k then return k, wrapMeta(v) end end, 9, nil end, __ipairs = function () return function (x, key) key = key + 1 if t[key] then return key, wrapMeta(t[key]) end end, 9, 0 end, __len = function () return #t 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_%-%+%,%.%#%~%@%'%;%[%]%(%)%&%%%$%! %=%{%}%^\x80-\xFF]+$") 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, 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 rawequal(mt, uniqueNEOProtectionObject) then return "NEO-Protected Object" end return mt end, rawset = function (t, i, v) local mt = getmetatable(t) if rawequal(mt, uniqueNEOProtectionObject) then error("NEO-Protected Object") end return rawset(t, i, v) end, rawget = rawget, rawlen = rawlen, rawequal = rawequal, checkArg = checkArg } 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") return not not accesses[accessName] 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.load = function (a, b, c, d, ...) if rawequal(d, nil) then d = pe end return load(a, b, c, d, ...) end pe.neo = setmetatable({}, baseProcNeoMT) pe._G = 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.": Component -- "s.": 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.": This access is actually useless on it's own - it is given by x. -- "k.": Kernel -- "k.root": _ENV (holy grail), and indirectly security request control (which is basically equivalent to this) -- "k.computer": computer -- "r.": Registration Of Service... -- "x.": 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, secret) -- 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 if not secret then -- Announce registration distEvent(nil, "k.registration", uid) end 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 emergencyFunction("starting:", pkg) 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