diff --git a/backends/igvm.c b/backends/igvm.c index 3490d8cb27973..317b555e8dde3 100644 --- a/backends/igvm.c +++ b/backends/igvm.c @@ -26,6 +26,9 @@ #include #include +#ifdef CONFIG_FDT +#include +#endif /* * Some directives are specific to particular confidential computing platforms. @@ -100,6 +103,10 @@ static int qigvm_directive_snp_id_block(QIgvm *ctx, const uint8_t *header_data, static int qigvm_initialization_guest_policy(QIgvm *ctx, const uint8_t *header_data, Error **errp); +#ifdef CONFIG_FDT +static int qigvm_directive_device_tree(QIgvm *ctx, const uint8_t *header_data, + Error **errp); +#endif struct QIGVMHandler { uint32_t type; @@ -130,6 +137,10 @@ static struct QIGVMHandler handlers[] = { qigvm_initialization_guest_policy }, { IGVM_VHT_MADT, IGVM_HEADER_SECTION_DIRECTIVE, qigvm_directive_madt }, +#ifdef CONFIG_FDT + { IGVM_VHT_DEVICE_TREE, IGVM_HEADER_SECTION_DIRECTIVE, + qigvm_directive_device_tree }, +#endif }; static int qigvm_handler(QIgvm *ctx, uint32_t type, Error **errp) @@ -790,6 +801,49 @@ static int qigvm_directive_snp_id_block(QIgvm *ctx, const uint8_t *header_data, return 0; } +#ifdef CONFIG_FDT +static int qigvm_directive_device_tree(QIgvm *ctx, const uint8_t *header_data, + Error **errp) +{ + const IGVM_VHS_PARAMETER *param = (const IGVM_VHS_PARAMETER *)header_data; + g_autofree void *fdt_packed = NULL; + QIgvmParameterData *param_entry; + uint32_t fdt_size; + + param_entry = qigvm_find_param_entry(ctx, param->parameter_area_index); + if (param_entry == NULL) { + error_setg(errp, "IGVM: parameter area index %u not found", + param->parameter_area_index); + return -1; + } + + if (ctx->machine_state->fdt == NULL) { + error_setg(errp, "IGVM: device tree not available"); + return -1; + } + + fdt_size = fdt_totalsize(ctx->machine_state->fdt); + fdt_packed = g_memdup2(ctx->machine_state->fdt, fdt_size); + + if (fdt_pack(fdt_packed)) { + error_setg(errp, "IGVM: failed to pack device tree"); + return -1; + } + + fdt_size = fdt_totalsize(fdt_packed); + if (fdt_size > param_entry->size) { + error_setg(errp, + "IGVM: device tree size exceeds parameter area" + " defined in IGVM file"); + return -1; + } + + memcpy(param_entry->data, fdt_packed, fdt_size); + + return 0; +} +#endif + static int qigvm_initialization_guest_policy(QIgvm *ctx, const uint8_t *header_data, Error **errp) { diff --git a/hw/core/machine.c b/hw/core/machine.c index 0aa77a57e9566..22ec5bb4adb78 100644 --- a/hw/core/machine.c +++ b/hw/core/machine.c @@ -1218,6 +1218,7 @@ static void machine_initfn(Object *obj) ms->kernel_cmdline = g_strdup(""); ms->ram_size = mc->default_ram_size; ms->maxram_size = mc->default_ram_size; + ms->device_plane = 0; if (mc->nvdimm_supported) { ms->nvdimms_state = g_new0(NVDIMMState, 1); @@ -1253,6 +1254,12 @@ static void machine_initfn(Object *obj) "ACPI Serial Port Console Redirection " "Table (spcr)"); + /* Default Device Plane */ + object_property_add_uint8_ptr(obj, "device-plane", &ms->device_plane, + OBJ_PROP_FLAG_READWRITE); + object_property_set_description(obj, "device-plane", + "Default plane to receive device IRQs"); + /* default to mc->default_cpus */ ms->smp.cpus = mc->default_cpus; ms->smp.max_cpus = mc->default_cpus; @@ -1761,6 +1768,15 @@ void qdev_machine_creation_done(void) register_global_state(); } +uint8_t qdev_default_plane(void) +{ + if (current_machine != NULL) { + return current_machine->device_plane; + } else { + return 0; + } +} + static const TypeInfo machine_info = { .name = TYPE_MACHINE, .parent = TYPE_OBJECT, diff --git a/hw/core/qdev.c b/hw/core/qdev.c index e48616b2c6f24..73d18fc0d6398 100644 --- a/hw/core/qdev.c +++ b/hw/core/qdev.c @@ -662,6 +662,28 @@ static bool device_get_hotplugged(Object *obj, Error **errp) return dev->hotplugged; } +static void device_get_plane(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + DeviceState *dev = DEVICE(obj); + uint8_t value = dev->plane; + + visit_type_uint8(v, name, &value, errp); +} + +static void device_set_plane(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + DeviceState *dev = DEVICE(obj); + uint8_t value; + + if (!visit_type_uint8(v, name, &value, errp)) { + return; + } + + dev->plane = value; +} + static void device_initfn(Object *obj) { DeviceState *dev = DEVICE(obj); @@ -674,6 +696,7 @@ static void device_initfn(Object *obj) dev->instance_id_alias = -1; dev->realized = false; dev->allow_unplug_during_migration = false; + dev->plane = qdev_default_plane(); QLIST_INIT(&dev->gpios); QLIST_INIT(&dev->clocks); @@ -796,6 +819,9 @@ static void device_class_init(ObjectClass *class, const void *data) device_get_hotplugged, NULL); object_class_property_add_link(class, "parent_bus", TYPE_BUS, offsetof(DeviceState, parent_bus), NULL, 0); + object_class_property_add(class, "plane", "uint8", + device_get_plane, device_set_plane, + NULL, NULL); } static void do_legacy_reset(Object *obj, ResetType type) diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig index 12473acaa7344..2b78ba575ba68 100644 --- a/hw/i386/Kconfig +++ b/hw/i386/Kconfig @@ -17,6 +17,8 @@ config TDX config PC bool + select DEVICE_TREE + depends on FDT imply APPLESMC imply HYPERV imply ISA_IPMI_KCS diff --git a/hw/i386/meson.build b/hw/i386/meson.build index 63ae57baa511e..bd0eddf90cff1 100644 --- a/hw/i386/meson.build +++ b/hw/i386/meson.build @@ -28,6 +28,7 @@ i386_ss.add(when: 'CONFIG_SGX', if_true: files('sgx-epc.c','sgx.c'), i386_ss.add(when: 'CONFIG_ACPI', if_true: files('acpi-common.c')) i386_ss.add(when: 'CONFIG_PC', if_true: files( 'x86-common.c', + 'x86_dt_common.c', 'pc.c', 'pc_sysfw.c', 'acpi-build.c', diff --git a/hw/i386/microvm-dt.c b/hw/i386/microvm-dt.c index 45fbb5bbd150f..6f70caac11a2d 100644 --- a/hw/i386/microvm-dt.c +++ b/hw/i386/microvm-dt.c @@ -73,6 +73,8 @@ static void dt_add_virtio(MicrovmMachineState *mms, VirtIOMMIOProxy *mmio) return; } + VirtIODevice *vdev = virtio_bus_get_device(mmio_virtio_bus); + uint8_t plane = object_property_get_int(OBJECT(vdev), "plane", &error_fatal); hwaddr base = dev->mmio[0].addr; hwaddr size = 512; unsigned index = (base - VIRTIO_MMIO_BASE) / size; @@ -83,13 +85,15 @@ static void dt_add_virtio(MicrovmMachineState *mms, VirtIOMMIOProxy *mmio) qemu_fdt_setprop_string(ms->fdt, nodename, "compatible", "virtio,mmio"); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); qemu_fdt_setprop(ms->fdt, nodename, "dma-coherent", NULL, 0); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); dt_add_microvm_irq(mms, nodename, irq); g_free(nodename); } -static void dt_add_xhci(MicrovmMachineState *mms) +static void dt_add_xhci(MicrovmMachineState *mms, DeviceState *dev) { const char compat[] = "generic-xhci"; + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); MachineState *ms = MACHINE(mms); uint32_t irq = MICROVM_XHCI_IRQ; hwaddr base = MICROVM_XHCI_BASE; @@ -101,12 +105,14 @@ static void dt_add_xhci(MicrovmMachineState *mms) qemu_fdt_setprop(ms->fdt, nodename, "compatible", compat, sizeof(compat)); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); qemu_fdt_setprop(ms->fdt, nodename, "dma-coherent", NULL, 0); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); dt_add_microvm_irq(mms, nodename, irq); g_free(nodename); } -static void dt_add_pcie(MicrovmMachineState *mms) +static void dt_add_pcie(MicrovmMachineState *mms, DeviceState *dev) { + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); MachineState *ms = MACHINE(mms); hwaddr base = PCIE_MMIO_BASE; int nr_pcie_buses; @@ -121,6 +127,7 @@ static void dt_add_pcie(MicrovmMachineState *mms) qemu_fdt_setprop_cell(ms->fdt, nodename, "#size-cells", 2); qemu_fdt_setprop_cell(ms->fdt, nodename, "linux,pci-domain", 0); qemu_fdt_setprop(ms->fdt, nodename, "dma-coherent", NULL, 0); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, PCIE_ECAM_BASE, 2, PCIE_ECAM_SIZE); @@ -154,6 +161,7 @@ static void dt_add_pcie(MicrovmMachineState *mms) static void dt_add_ioapic(MicrovmMachineState *mms, SysBusDevice *dev) { + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); MachineState *ms = MACHINE(mms); hwaddr base = dev->mmio[0].addr; char *nodename; @@ -181,6 +189,7 @@ static void dt_add_ioapic(MicrovmMachineState *mms, SysBusDevice *dev) qemu_fdt_setprop_cell(ms->fdt, nodename, "#address-cells", 0x2); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, 0x1000); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); ph = qemu_fdt_alloc_phandle(ms->fdt); qemu_fdt_setprop_cell(ms->fdt, nodename, "phandle", ph); @@ -195,6 +204,7 @@ static void dt_add_isa_serial(MicrovmMachineState *mms, ISADevice *dev) const char compat[] = "ns16550"; uint32_t irq = object_property_get_int(OBJECT(dev), "irq", &error_fatal); hwaddr base = object_property_get_int(OBJECT(dev), "iobase", &error_fatal); + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); MachineState *ms = MACHINE(mms); hwaddr size = 8; char *nodename; @@ -203,6 +213,7 @@ static void dt_add_isa_serial(MicrovmMachineState *mms, ISADevice *dev) qemu_fdt_add_subnode(ms->fdt, nodename); qemu_fdt_setprop(ms->fdt, nodename, "compatible", compat, sizeof(compat)); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); dt_add_microvm_irq(mms, nodename, irq); if (base == 0x3f8 /* com1 */) { @@ -217,6 +228,7 @@ static void dt_add_isa_rtc(MicrovmMachineState *mms, ISADevice *dev) const char compat[] = "motorola,mc146818"; uint32_t irq = object_property_get_uint(OBJECT(dev), "irq", &error_fatal); hwaddr base = object_property_get_uint(OBJECT(dev), "iobase", &error_fatal); + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); MachineState *ms = MACHINE(mms); hwaddr size = 8; char *nodename; @@ -225,6 +237,7 @@ static void dt_add_isa_rtc(MicrovmMachineState *mms, ISADevice *dev) qemu_fdt_add_subnode(ms->fdt, nodename); qemu_fdt_setprop(ms->fdt, nodename, "compatible", compat, sizeof(compat)); qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); dt_add_microvm_irq(mms, nodename, irq); g_free(nodename); } @@ -291,14 +304,14 @@ static void dt_setup_sys_bus(MicrovmMachineState *mms) /* xhci */ obj = object_dynamic_cast(OBJECT(dev), TYPE_XHCI_SYSBUS); if (obj) { - dt_add_xhci(mms); + dt_add_xhci(mms, DEVICE(obj)); continue; } /* pcie */ obj = object_dynamic_cast(OBJECT(dev), TYPE_GPEX_HOST); if (obj) { - dt_add_pcie(mms); + dt_add_pcie(mms, DEVICE(obj)); continue; } diff --git a/hw/i386/pc.c b/hw/i386/pc.c index d061b304fbbd7..5300d2d08cdd3 100644 --- a/hw/i386/pc.c +++ b/hw/i386/pc.c @@ -67,6 +67,7 @@ #include "trace.h" #include "sev.h" #include CONFIG_DEVICES +#include "x86_dt_common.h" #ifdef CONFIG_XEN_EMU #include "hw/xen/xen-legacy-backend.h" @@ -522,6 +523,7 @@ void pc_machine_done(Notifier *notifier, void *data) PCMachineState *pcms = container_of(notifier, PCMachineState, machine_done); X86MachineState *x86ms = X86_MACHINE(pcms); + MachineState *machine = MACHINE(pcms); cxl_hook_up_pxb_registers(pcms->pcibus, &pcms->cxl_devices_state, &error_fatal); @@ -546,6 +548,9 @@ void pc_machine_done(Notifier *notifier, void *data) } pc_cmos_init_late(pcms); + if (x86ms->igvm) { + dt_setup_x86(machine); + } } /* setup pci memory address space mapping into system address space */ diff --git a/hw/i386/x86_dt_common.c b/hw/i386/x86_dt_common.c new file mode 100644 index 0000000000000..c4f3b7081fadc --- /dev/null +++ b/hw/i386/x86_dt_common.c @@ -0,0 +1,190 @@ +/* + * x86 device tree generation for device plane filtering + * + * Generate a flattened device tree describing devices that belong to + * non-guest planes (e.g. SVSM firmware plane). The resulting FDT is + * passed to the firmware via IGVM so it can discover virtio-mmio + * assigned to its plane. + * + * The device tree is intentionally incomplete: CPUs and memory are + * not included as firmware obtains that information through other + * channels. + * + * Based on hw/i386/microvm-dt.c + * + * Copyright (c) Red Hat, Inc. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#include "qemu/osdep.h" +#include "qemu/cutils.h" +#include "qemu/error-report.h" +#include "qapi/error.h" +#include "system/device_tree.h" +#include "hw/char/serial-isa.h" +#include "hw/core/sysbus.h" +#include "hw/nvram/fw_cfg.h" +#include "hw/virtio/virtio-mmio.h" +#include "qom/object.h" + +#include "fw_cfg.h" +#include "x86_dt_common.h" +#include + +/* TODO: This should be exported from hw/misc/debugexit.c */ +#define TYPE_ISA_DEBUG_EXIT_DEVICE "isa-debug-exit" + + +static bool debug; + +static void dt_add_virtio(MachineState *ms, VirtIOMMIOProxy *mmio) +{ + SysBusDevice *dev = SYS_BUS_DEVICE(mmio); + VirtioBusState *mmio_virtio_bus = &mmio->bus; + BusState *mmio_bus = &mmio_virtio_bus->parent_obj; + VirtIODevice *vdev; + hwaddr base = dev->mmio[0].addr; + hwaddr size = 512; + char *nodename; + uint8_t plane; + + if (QTAILQ_EMPTY(&mmio_bus->children)) { + return; + } + + /* Get the plane property of the inner device */ + vdev = virtio_bus_get_device(mmio_virtio_bus); + plane = object_property_get_int(OBJECT(vdev), "plane", &error_fatal); + + nodename = g_strdup_printf("/virtio_mmio@%" PRIx64, base); + qemu_fdt_add_subnode(ms->fdt, nodename); + qemu_fdt_setprop_string(ms->fdt, nodename, "compatible", "virtio,mmio"); + + qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); + qemu_fdt_setprop(ms->fdt, nodename, "dma-coherent", NULL, 0); + g_free(nodename); +} + +static void dt_add_fw_cfg(MachineState *ms) +{ + hwaddr base = FW_CFG_IO_BASE; + hwaddr size = FW_CFG_CTL_SIZE; + char *nodename; + + nodename = g_strdup_printf("/fw-cfg@%" PRIx64, base); + qemu_fdt_add_subnode(ms->fdt, nodename); + qemu_fdt_setprop_string(ms->fdt, nodename, "compatible", "qemu,fw-cfg-io"); + qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + g_free(nodename); +} + +static void dt_add_debug_exit(MachineState *ms, ISADevice *dev) +{ + hwaddr base = object_property_get_int(OBJECT(dev), "iobase", &error_fatal); + hwaddr size = object_property_get_int(OBJECT(dev), "iosize", &error_fatal); + char *nodename; + + nodename = g_strdup_printf("/debug-exit@%" PRIx64, base); + qemu_fdt_add_subnode(ms->fdt, nodename); + qemu_fdt_setprop_string(ms->fdt, nodename, "compatible", "qemu,debug-exit"); + qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + g_free(nodename); +} + +static void dt_add_isa_serial(MachineState *ms, ISADevice *dev) +{ + const char compat[] = "ns16550"; + hwaddr base = object_property_get_int(OBJECT(dev), "iobase", &error_fatal); + uint8_t plane = object_property_get_int(OBJECT(dev), "plane", &error_fatal); + + hwaddr size = 8; + char *nodename; + + nodename = g_strdup_printf("/serial@%" PRIx64, base); + qemu_fdt_add_subnode(ms->fdt, nodename); + qemu_fdt_setprop(ms->fdt, nodename, "compatible", compat, sizeof(compat)); + qemu_fdt_setprop_sized_cells(ms->fdt, nodename, "reg", 2, base, 2, size); + qemu_fdt_setprop_cell(ms->fdt, nodename, "plane", plane); + + g_free(nodename); +} + +static void dt_setup_isa_bus(MachineState *ms, BusState *bus) +{ + BusChild *kid; + Object *obj; + + QTAILQ_FOREACH(kid, &bus->children, sibling) { + DeviceState *dev = kid->child; + + /* serial */ + obj = object_dynamic_cast(OBJECT(dev), TYPE_ISA_SERIAL); + if (obj) { + dt_add_isa_serial(ms, ISA_DEVICE(obj)); + continue; + } + + /* debug-exit */ + obj = object_dynamic_cast(OBJECT(dev), TYPE_ISA_DEBUG_EXIT_DEVICE); + if (obj) { + dt_add_debug_exit(ms, ISA_DEVICE(obj)); + continue; + } + + if (debug) { + fprintf(stderr, "%s: unhandled: %s type: %s\n", __func__, + dev->canonical_path, + object_get_typename(OBJECT(dev))); + } + } +} + +static void dt_setup_sys_bus(MachineState *ms) +{ + ISABus *isa_bus; + BusState *bus; + BusChild *kid; + Object *obj; + + /* PC Architecture always contains an ISA bus */ + isa_bus = ISA_BUS(object_resolve_path_type("", TYPE_ISA_BUS, NULL)); + dt_setup_isa_bus(ms, BUS(isa_bus)); + + bus = sysbus_get_default(); + + QTAILQ_FOREACH(kid, &bus->children, sibling) { + DeviceState *dev = kid->child; + + /* virtio */ + obj = object_dynamic_cast(OBJECT(dev), TYPE_VIRTIO_MMIO); + if (obj) { + dt_add_virtio(ms, VIRTIO_MMIO(obj)); + continue; + } + + if (debug) { + fprintf(stderr, "%s: unhandled: %s type: %s\n", __func__, + dev->canonical_path, + object_get_typename(OBJECT(dev))); + } + } +} + +void dt_setup_x86(MachineState *ms) +{ + int fdt_size = 0; + + g_free(ms->fdt); + ms->fdt = create_device_tree(&fdt_size); + + /* root node */ + qemu_fdt_setprop_string(ms->fdt, "/", "compatible", "svsm"); + qemu_fdt_setprop_cell(ms->fdt, "/", "#address-cells", 0x2); + qemu_fdt_setprop_cell(ms->fdt, "/", "#size-cells", 0x2); + qemu_fdt_setprop_cell(ms->fdt, "/", "device-plane", ms->device_plane); + + dt_setup_sys_bus(ms); + dt_add_fw_cfg(ms); + +} diff --git a/hw/i386/x86_dt_common.h b/hw/i386/x86_dt_common.h new file mode 100644 index 0000000000000..ac128ab979786 --- /dev/null +++ b/hw/i386/x86_dt_common.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef HW_I386_X86_DT_COMMON_H +#define HW_I386_X86_DT_COMMON_H + +#include "hw/core/boards.h" + +void dt_setup_x86(MachineState *ms); + +#endif diff --git a/include/hw/core/boards.h b/include/hw/core/boards.h index b8dad0a1074dd..d2d1336939ed9 100644 --- a/include/hw/core/boards.h +++ b/include/hw/core/boards.h @@ -447,6 +447,9 @@ struct MachineState { * Set to false by default for all regular use. */ bool new_accel_vmfd_on_reset; + + /* Default plane to receive device IRQs */ + uint8_t device_plane; }; /* diff --git a/include/hw/core/qdev.h b/include/hw/core/qdev.h index f99a8979ccb19..28d2efcbe455b 100644 --- a/include/hw/core/qdev.h +++ b/include/hw/core/qdev.h @@ -295,6 +295,10 @@ struct DeviceState { * Used to prevent re-entrancy confusing things. */ MemReentrancyGuard mem_reentrancy_guard; + /** + * @plane: Plane the device is assigned to. + */ + uint8_t plane; }; typedef struct DeviceListener DeviceListener; @@ -560,6 +564,7 @@ void qdev_simple_device_unplug_cb(HotplugHandler *hotplug_dev, DeviceState *dev, Error **errp); void qdev_machine_creation_done(void); bool qdev_machine_modified(void); +uint8_t qdev_default_plane(void); /** * qdev_add_unplug_blocker: Add an unplug blocker to a device diff --git a/tests/unit/test-qdev-global-props.c b/tests/unit/test-qdev-global-props.c index 8ea362cbb9021..2aca5bda22b92 100644 --- a/tests/unit/test-qdev-global-props.c +++ b/tests/unit/test-qdev-global-props.c @@ -71,6 +71,11 @@ static const TypeInfo subclass_type = { .parent = TYPE_STATIC_PROPS, }; +uint8_t qdev_default_plane(void) +{ + return 0; +} + /* * Initialize a fake machine, being prepared for future tests. * diff --git a/tests/unit/test-qdev.c b/tests/unit/test-qdev.c index 20eae38e03f4b..6e3127b41afdd 100644 --- a/tests/unit/test-qdev.c +++ b/tests/unit/test-qdev.c @@ -26,6 +26,11 @@ static const Property my_dev_props[] = { qdev_prop_uint32, uint32_t), }; +uint8_t qdev_default_plane(void) +{ + return 0; +} + static void my_dev_class_init(ObjectClass *oc, const void *data) { DeviceClass *dc = DEVICE_CLASS(oc);