Friday, May 22, 2009

WPF / VS2010 Font rendering: blurryville

I downloaded and installed VS2010 Beta 1 to see how it's looking. It's not pleasant though - it makes me feel like I'm looking at the screen through a layer of coke-bottle plastic:

Compared to the old rendering, where my preferred fixed-width code / terminal font, Dina, is correctly chosen, as opposed to falling back to what looks like Courier New:

The new rendering hurts my eyes (they keep trying to focus more, but it doesn't help!) and makes VS2010 Beta unusable, as far as I'm concerned.

The "recommended solution" for text clarity issues in WPF is "use the largest font size possible". That's a bit of a cop-out for the working programmer, where displaying as much code on-screen as is comfortably legible is usually the best option.

FWIW, I registered a feedback item on this.

Tuesday, April 28, 2009

Sharing a printer, the hard way: writing your own spooler

I have an ultra-cheap HP Color Laserjet 1600 printer, the kind that costs less than a full set of replacement toner cartridges, and the ones that are shipped in the box are only 20% full. From the driver publisher info it appears the I/O protocol logic of the printer was provided by a company called Zenographics, which appears to now be either a part of or owned by Marvell. It works well enough when you have it directly connected to a machine via the USB cable, but sharing it is a nightmare.

In particular, the drivers are very unstable when targeting shared printers. Especially problematic is printing from Vista to XP, or XP to XP, or XP to Vista. With some combinations, the print spooler crashes; with others, it simply hangs, and the spooler service needs to be restarted to get the document printing at all. The folks on the HP forums seem to imply (a) that this is by design, and (b) sharing the printer effectively is impossible. I find this hard to swallow; since the printer works when directly connected, there is no valid reason why printing across the network should not also work, since the physical printer doesn't come into the equation. Bits are bits; if the driver can't consistently send some bits across the network, either there's some atrocious QA going on inside Zenographics, or worse, malice.

I had despaired of ever getting it working nicely as a small workgroup printer, but I saw a faint glimmer of hope when setting up my home NAS, based on Solaris. CUPS is available for Solaris, and there's a Foomatic printer driver available for this specific model as well.

It turns out that I couldn't get CUPS configured with this printer and driver setup. CUPS uses a web page interface for configuration, but it was desperately slow, and every time I tried to print a test page, it informed me that the printer had just gone offline. I could see all the background processes it had started up to print, and the printer device at /dev/printers/0 was indeed opened for writing, but it never got anywhere.

But that didn't matter. The foo2hp driver provided the only real tools I needed: a PostScript to ZjStream (the printer protocol) filter, which simply reads the raw PostScript and outputs the raw print data. Windows ships with a generic PostScript driver; I use the PScript5-based "HP Color Laserjet PS". A simple shell pipeline from source to printer device would do.

To share the printer, I needed some kind of queue for buffering. I decided on a simple filesystem-based approach, which seems to me to be in the Unix tradition. I created a share on my Solaris box specifically for files to be printed. Windows clients can use the aforementioned generic printer driver with a FILE: target, and place the output in this shared directory. The spooler script I wrote polls this directory periodically, and attempts to print whatever it finds.

The printing logic is pretty simple, at its core:

foo2hp2600-wrapper -p 9 input.ps > /dev/printers/0

The actual code is a mite more sophisticated, of course; it does logging, moves documents to be printed into a directory categorized by date and time, etc. And in future it can be extended to other document types than PS, e.g. printing PDFs or photos simply by copying them to a directory.

Another advantage of this approach is that I can shell out to ps2pdf to create an archival version of the printed document. The PDF is usually far smaller than the PS and is viewable from Windows without installing a PostScript viewer. This archival version is useful if I ever need to print it again. Consider online flight checkins, for example - some websites (such as EasyJet) prevent printing the boarding pass more than three times, which is a little inconvenient if you're having printer trouble.

On the downside, printer options that can't be expressed directly in the PostScript with the generic driver can't be configured. For example, the way I have things configured I'm limited to printing monochrome on A4 by default. In practice this isn't really a problem, since the vast majority of print jobs fit this template; I could create other spooler directories for different defaults.

I expect that with enough research, I'll be able to get CUPS configured correctly, or to get Samba to look like a PostScript printer that dumps the raw network data into my spooler directory. However, I'm pretty happy with my hack for now...

Sunday, March 22, 2009

ZFS/Solaris as a NAS

I've finally got a semblance of a Solaris system up and running with a nice fat ZFS storage pool. It was neither trivial nor pleasant, however.

Here's the executive summary of what I learned:

  • Don't use OpenSolaris 2008.11; use at least NexentaCore 2 instead; others may work.
  • Don't use an Intel or Realtek network adapter; if you must, use Realtek with an older version of the gani driver.
  • Don't use Solaris's built-in CIFS implementation, smb/server, aka hereafter "cifs" (to contrast with "samba").
  • Don't let ZFS to default to 128KB max block size unless all your files are big (greater than about 2MB) or tiny (less than about 64KB).

OpenSolaris 2008.11, the version I tried, is not production ready unless you have extremely specific hardware and quite narrow requirements. I finally ended up using NexentaCore Unstable v2.0 Beta 2.

The prime requirements for a working NAS are:

  1. Lots of attachment points for disks
  2. Good physical network connectivity
  3. Good network protocol implementation
  4. Good filesystem implementation
There is no one of these that I didn't have issues with.

The first one, attachment points, was a nice-to-know gotcha that it took me a while to figure out. My motherboard (an old Abit AW9D-MAX) has 7 internal SATA attachment points, as well as 1 PATA channel for two extra PATA devices. This being insufficient, I procured an extra PCIe two-port SATA adapter. I was up and running fine with 5 disks attached (1 boot drive and 4 drives in a raidz ZFS pool - effectively RAID 5 without the need for NVRAM), and in preparation for creating my second raidz vdev (virtual device in ZFS lingo), I started attaching more drives, one by one so that I could label them (and thereby know which one failed when it fails - which it will, eventually). When I had 7 drives attached, the OS occasionally failed to boot, but would reliably hang upon execution of the 'format' command (the handiest way of figuring out the drive device name under Solaris).

The gotcha, however, is that the BIOS defaults to IDE emulation mode, which, for this particular BIOS, supports a maximum of 6 devices. It packs the first 2 hard drives into pretend-PATA (complete with "master" and "slave") even if they aren't actually PATA, and then labels the next 4 drives as SATA 1 through 4. I had to change the BIOS's mode to AHCI to get it to support more drives. Luckily, my boot drive is an old PATA one, so it didn't need compatibility to stay in the same place (master on the first PATA channel).

Physical network connectivity was a far, far harder task, and one that still isn't quite complete. My motherboard has two physical 1Gb Ethernet adapters built in. Unfortunately, OpenSolaris defaults to a driver it calls rge for the chipset (RTL8111/8168B PCIe), but this driver has big issues, that I suspect are related somehow to duplex operations. Big transfers using big buffers using cifs (the Windows-compatible protocol Solaris ships with) will work with reasonable performance (30MB/sec or so), but streaming video (small reads that expect low latency) performs abysmally. Even worse, at non-deterministically random times (as far as I could ascertain), the rge driver would fall into a mode where transfers were extremely slow, as in less than 1MB/sec slow. I took to keeping a ssh session open to the Solaris machine and simply echoing '.' back and forth (echo '.' loop piped to ssh cat -), and mysteriously enough, so long as there was small back and forth traffic, transfer performance rocketed back up again.

This being unsatisfactory, I tried to use the gani driver. I never could get gani-2.6.3 to work with my chipset, however. I didn't know if it was a problem with the driver, a conflict with my chipset, or my poor configuration skills with Solaris. All I know is that I tried every technique I could find, up to and including patching driver_aliases and running sys-unconfig to start things off from a fresh basis.

So, I bought an Intel 1000/PRO Gigabit ethernet adapter. Folks online, in response to reports of rge not working correctly, seemed to say that the Intel adapter "just worked", but I should have dug deeper... The driver Solaris uses for this is called e1000g. The version of this driver shipping in OpenSolaris 2008.11 doesn't work. It drops packets. Simply pinging the Solaris machine from the outside shows packet loss exceeding 5%. With a ssh session open to Solaris, simply holding down '.' on the keyboard and watching the character getting echoed in the terminal is sufficient to demonstrate the hiccups: the pauses are visible and disturbing. It actually affects typing in ssh, almost as if one was at the end of a dodgy Internet connection, perhaps some half-baked cafe wifi link in Marrakesh. A recursive diff of about 100MB of data across about 1000 files using cifs took a whole 45 minutes. A trivial test using dd to copy a 640KB blob of data using a 64KB buffer size took about 15 seconds and dd reported transfer rate of 45KB/sec.

It was this network connectivity that prompted me to start trying out other distributions. NexentaCore 2 was first in line. It defaults to the gani driver for the Realtek adapter, but an earlier version, 2.6.2. This works, but only at 100Mbps; try as I might, I can't convince it to go higher. (Windows running on the same hardware, same patch cable and same switch, never, ever dropped down to 100Mbps - something I can't say about the other Windows machines I have here.) Alos with significant importance, NexentaCore doesn't ship with a GUI (so no need to disable it), and does ship with apt-get and a reasonable selection of packages, including my preferred Linux/Unix editor, joe (very similar to old Borland DOS IDE text editors). I detest vi - I've never used a program that gleefully punished more heavily every transgression of the user.

Anyhow, that's how I got one reasonably-working 100Mbps connection out of three 1Gbps adapters, four driver versions and two operating system installs.

Next, the network protocol. Solaris defaults to using something that is colloquially called cifs, although the usual identifiers used when e.g. enabling and disabling with svcadm are smb/server and smb/client. I can't say much about smb/client - I'm sure it works well enough, but I don't have much use for it - but smb/server barely works at all. It's implemented as a kernel module, which unfortunately means it has odd and painful limitations compared to how user-level programs view the file system. In particular, only one ZFS filesystem can be navigated from a cifs share, significantly harming the usefulness of creating lots of ZFS filesystems. A ZFS file system mounted inside an existing filesystem, where the mountpoint is visible from a cifs share, will show up as an empty directory to clients, whereas it appears correctly mounted locally on the Solaris box.

Another limitation is symbolic links. By apparent design, cifs prohibits what Samba calls "wide links": symbolic links that resolve to locations that are outside the share's hard-linked subtree. Such symbolic links look much like dead links from Windows, i.e. the text little files they are implemented as. Samba defaults to "wide links" on for performance reasons if nothing else.

A final limitation is hard links created from Windows using CreateHardLink. Cygwin 'ln' ultimately uses this API. For whatever reason (and I didn't dig too deeply, like investigating Wireshark traces), Cygwin determines that 'ln' isn't supported on cifs shares and falls back to copying instead. Cygwin 'ln' works correctly on Windows shares, however, and it also works correctly over Samba.

The verdict: cifs isn't worth it. Needs more time to bake. Use Samba instead.

Finally, ZFS itself. ZFS is the big draw, and the reason I chose to use Solaris. Userspace implementations are available in Linux via FUSE, and also in FreeBSD, but I had bad performance experiences of NTFS-3G/FUSE in Linux, and FreeBSD's implementation sounded dangerously non-production ready. ZFS largely works as advertised. The primary limitation is the inflexibility in removing drives from pools. Raidz vdevs can't have drives removed at all. A word of warning, though: ZFS has what I can only consider a bad bug for files in the 128KB..2MB range. If you have a file of about 129KB, depending on how it was created, it ought to be using up about 100% more space than it should be. In some largeish directories of files I was seeing wastage of about 35% (as measured by 'du' versus 'du --apparent-size'), whereas NTFS on Windows had wastage in the region of 5%. Paring back the ZFS default block size (dynamically settable on ZFS - yay! - but only affects subsequent file operations) to 8KB or 16KB improves things immensely, but still not quite as space-efficient as NTFS.

In conclusion, ZFS/Solaris as a NAS can work well with very carefully chosen hardware and select software configuration. If you get it working better than I describe herein, be very careful what you touch, and whatever you do, don't upgrade your zpools until you're sure that whatever step forward you take is better than what you had previously.

Friday, March 06, 2009

OpenSolaris, ZFS, Dvorak and VI

Been experimenting with OpenSolaris to try out ZFS. OpenSolaris is not a pleasant experience even after using an average Linux.

Its termcap and terminfo databases don't include complete information for xterm as implemented by the very gnome-terminal it defaults to, much less gnome-terminal itself; nor does it include any entries for rxvt derivatives (so I have to export TERM=xterm upon ssh in from Cygwin). Thus, there is much fun with Del, Home, End, PgUp, PgDn etc. brokenness: Ctrl+A/E instead of Home/End, Ctrl+D instead of Del, f/b in less rather than PgUp/PgDn, etc. The terminfo DB is spartan even by comparison with Cygwin, and termcap rather, eh, antique:

solaris$ find /usr/share/lib/terminfo/ | wc
    1716    1716   58553
cygwin$ find /usr/share/terminfo/ | wc
   2544    2544   79257

solaris$ grep 1982 /usr/share/lib/termcap
# From research!ikeya!rob Tue Aug 31 23:41 EDT 1982
# From jwb Wed Mar 31 13:25:09 1982 remote from ihuxp
# Extensive changes to c108 by arpavax:eric Feb 1982

Also, there is apparently no text-mode Dvorak layout shipped with OpenSolaris. I managed to patch something rudimentary together reverse engineering the contents of /usr/share/lib/keytables and applying with loadkeys. Within X, xmodmap can load an appropriate translation, but getting that working before gdm comes up for the login and password screen is tedious. Getting a vnc server session running gdm is easier.

Finally, I have to resort to using vi to edit basic files. Vi is a line editor masquerading as a text editor. Now I feel like I'm living in the 80s. The early 80s.

Wednesday, February 04, 2009

SecuROM and SysInternals tools

If you're like me, you like visibility into the workings of your OS, which processes are running, what modules are loaded and from where and whether they are signed or not, what files are open, what files are being touched and what registry entries are being frobbed.

I use the SysInternals tools for getting increased visibility into these things. In particular, I use Process Explorer (procexp) alongside Task Manager (the two aren't complete overlaps for functionality), with File Monitor (filemon) and Registry Monitor (regmon) on XP and Process Monitor (procmon) on Vista. Filemon and regmon are more focused to their specific tasks and have easier to configure filters than procmon, so I usually prefer them, but they are not compatible with Vista.

Any time I see an unexpected filesystem locking error, such as a failure in Windows Explorer to delete a folder, I can search for the culprit in procexp's handle view. Often the culprit is Windows Explorer itself - perhaps caused by a shell extension like Winzip or Winrar - and I can force closed the handle right there from the procexp interface itself.

I have a set of "verification columns" configured procexp that makes visible the Company, Description and Verified Signer columns for processes and modules (DLLs). With Options | Verify Image Signatures turned on, this makes for easy checking for suspicious DLLs and kernel drivers (modules loaded into the System process) loaded in the system.

Procmon supports a whole lot more analysis than the easier to use tools like filemon and regmon, but the most important distinct feature is probably the stack trace on every file and registry operation. Double-clicking an event in the procmon view pops up a dialog that has a Stack tab, wherein the stack trace is displayed. When Windows symbols are correctly configured, procmon will call out to windbg.dll and show symbolic information such as function name and offset for each code address in the stack trace, up to and including downloading the symbols PDB if necessary.

Since I don't think I've done it already on this blog, I'll outline the sequence of steps required to get symbols correctly configured.

  1. Download and install WinDbg. WinDbg is a useful tool in its own right for low-level Windows debugging, and particularly for .NET debugging when using the SOS command extension DLL.
  2. Choose a location - i.e. an empty directory - for the symbol cache on your system. Once chosen, create an environment variable called _NT_SYMBOL_PATH with the value "SRV*[symbol-cache-dir]*http://msdl.microsoft.com/download/symbols" (without quotes), but replace [symbol-cache-dir] with the path to your selected directory.
  3. Now, the SysInternals tools can be configured with these settings. Procmon and Procexp both have Configure Symbols dialogs in their Options menus. Set the DbgHelp.dll path to the DbgHelp.dll from the installation location of WinDbg, and set the "Symbol paths" textbox to the value of the _NT_SYMBOL_PATH environment variable created earlier.

However, the reason I wrote this post is not merely to laud the SysInternals tools and the work of Mark Russinovich. The fact is that SecuROM, a DRM solution used in many games these days, has panic attacks when it sees evidence of SysInternals tools being used. I don't really understand what SecuROM's justification for its conniptions could be, since monitoring the process with the tools could only help in an initial cracking attempt, yet game cracks appear to come out no slower than they ever did. All it seems to do is annoy honest users - and in this case, developers. Not only does SecuROM want you to exit the applications in question, but it also wants you to reboot the entire system, ignoring the fact that you might have concurrent long-lived tasks that don't merit interruption by a mere game

Well, I couldn't let that stand. I created a tool to enable me to simply exit the SysInternals utilities in question, rather than having to reboot the machine. The key to SecuROM's detection of the tools is in the communication mechanism between the SysInternals tools and their kernel driver counterparts, in particular the named devices in the Windows Object Manager namespace, visible using the WinObj tool. It turns out that merely by changing the ACL (access control list) on the device object to a single deny-all ACE (access control entry) is sufficient to fool SecuROM from thinking the device is no longer loaded - though SecuROM also looks for top-level windows with particular window classes, so the utilities do have to be exited as well.

So, I wrote a simple Delphi console application, called cacls_driver, to modify the ACL of a driver device. A key set of libraries that made this easy was the JEDI Win32 API library, in particular the units JwaWinType, JwaWinBase, JwaWinNT, JwaNative and JwaSddl. I've appended the tool's source code to this entry.

Using the tool is simple enough. The most important thing to know is the name of the driver one is changing the permissions of. For the versions of filemon and regmon I have installed, the correct driver names are filemon701 and regmon701 respectively. I figured this out by running the aforementioned WinObj and looking in the 'GLOBAL??' folder for devices with names starting with filemon and regmon. For the original Crysis, SecuROM even complained about Process Explorer, so I had to include procexp110 (IIRC) as well. SecuROM support, on the other hand, recommended that I upgrade Process Explorer, which of course used an incrementally different device name (procexp111) and thereby avoided the SecuROM sanction.

Here's an example of the tool in use hiding the regmon and filemon driver links ($ is my command-line prompt, and # are comments):

# This simply shows the current ACL
$ cacls_driver -s filemon701 regmon701
filemon701 :: D:(A;;CCRC;;;WD)(A;;CCSDRCWDWO;;;SY)(A;;CCSDRCWDWO;;;BA)(A;;CCRC;;;RC)
regmon701 :: D:(A;;CCRC;;;WD)(A;;CCSDRCWDWO;;;SY)(A;;CCSDRCWDWO;;;BA)(A;;CCRC;;;RC)

# This disables the ACL. There's no output from this command.
$ cacls_driver -d filemon701 regmon701

# Verifying that the ACL has been set.
$ cacls_driver -s filemon701 regmon701
filemon701 :: D:
regmon701 :: D:

These ACLs having been applied - and ensuring the tools in question aren't still running - I'm free to run my games without the odious requirement for a reboot.

Source code follows...

program cacls_driver;

{$APPTYPE CONSOLE}

uses
  SysUtils, JwaWinType, JwaWinBase, JwaWinNT, JwaNative, JwaSddl, Generics.Collections;

type
  TOperation = (opNone, opApply, opShow);

  TOptions = class
  private
    FOperation: TOperation;
    FDrivers: TList<string>;
    FDacl: string;
  public
    constructor Create;
    destructor Destroy; override;
    property Operation: TOperation read FOperation write FOperation;
    property Drivers: TList<string> read FDrivers;
    property Dacl: string read FDacl write FDacl;
  end;

function ParseOptions: TOptions;
const
  // http://msdn.microsoft.com/en-us/library/cc230374.aspx
  // These were the defaults on my system (WinXP SP3)
  // SID tokens (last two letters in ACEs):
  // SY = local system; BA = builtin administrators; WD = everyone; 
  // RC = restricted code
  // Permissions:
  // CC = create child; RC = read control; SD = delete; WD = write DAC;
  // WO = write owner
  DefaultDacl = 'D:(A;;CCRC;;;WD)(A;;CCSDRCWDWO;;;SY)(A;;CCSDRCWDWO;;;BA)(A;;CCRC;;;RC)';
  NullDacl = 'D:';
var
  i: Integer;
  opt: string;

  procedure Usage;
  begin
    Writeln(Format('usage: %s <command> <driver...>', [ParamStr(0)]));
    Writeln('Modify discretionary ACL for the given global namespace driver symlinks.');
    Writeln('  -d       Disable access (deny-all ACL)');
    Writeln('  -e       Enable access (apply default ACL: ', DefaultDacl, ')');
    Writeln('  -s       Show ACL for the given drivers');
    Writeln('  -a <acl> Apply an explicit ACL');
  end;
  
  procedure SetOpOnly(Op: TOperation);
  begin
    if Result.Operation <> opNone then
      raise Exception.Create('only one of -s, -d or -e allowed');
    Result.Operation := Op;
  end;

  procedure SetOp(Op: TOperation; const ADacl: string);
  begin
    SetOpOnly(Op);
    Result.Dacl := ADacl;
  end;
  
  procedure SetOpArg(Op: TOperation);
  begin
    Inc(i);
    if i > ParamCount then
      raise Exception.Create('missing argument to option ' + ParamStr(i - 1));
    SetOp(Op, ParamStr(i));
  end;
  
begin
  Result := TOptions.Create;
  try
    i := 1;
    if ParamCount = 0 then
    begin
      Usage;
      Exit;
    end;
    
    while i <= ParamCount do
    begin
      opt := ParamStr(i);
      case opt[1] of
        '/', '-':
        begin
          if Length(opt) > 2 then
            raise Exception.Create('invalid option');
          case opt[2] of
            's': SetOpOnly(opShow);
            'd': SetOp(opApply, NullDacl);
            'e': SetOp(opApply, DefaultDacl);
            'a': SetOpArg(opApply);
            '?', 'h': Usage;
          else
            raise Exception.Create('invalid option');
          end;
        end;
      else
        Result.Drivers.Add(opt);
      end;
      Inc(i);
    end;
  except
    Result.Free;
    raise;
  end;
end;

{ TOptions }

constructor TOptions.Create;
begin
  FDrivers := TList<string>.Create;
end;

destructor TOptions.Destroy;
begin
  FDrivers.Free;
  inherited;
end;

procedure EnablePrivilege(Process: THandle; const Name: string);
var
  privs: TTokenPrivileges;
begin
  FillChar(privs, SizeOf(privs), 0);
  privs.PrivilegeCount := 1;
  privs.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
  Win32Check(LookupPrivilegeValue(nil, PChar(Name),
    privs.Privileges[0].Luid));
  Win32Check(
    AdjustTokenPrivileges(Process, False, @privs, SizeOf(privs), nil, nil));
end;

procedure ElevatePrivileges;
var
  process: THandle;
begin
  Win32Check(OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, process));
  try
    EnablePrivilege(process, SE_SECURITY_NAME);
    EnablePrivilege(process, SE_DEBUG_NAME);
  finally
    CloseHandle(process);
  end;
end;

function NtErrorMessage(Status: TNTStatus): string;
var
  buf: PChar;
  h: THandle;
begin
  h := LoadLibrary('ntdll.dll');
  try
    FormatMessage(
      FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM or
        FORMAT_MESSAGE_FROM_HMODULE,
      Pointer(h),
      Status,
      0,
      PChar(@buf),
      0,
      nil);
    try
      Result := TrimRight(buf);
    finally
      LocalFree(Cardinal(buf));
    end;
  finally
    CloseHandle(h);
  end;
end;

procedure NtCheck(Status: TNTStatus);
begin
  if Status <> 0 then
    raise EOSError.Create(NtErrorMessage(Status));
end;

procedure WithDriver(const Name: string; Mask: TAccessMask; const Proc: TProc<THandle>);
var
  path: string;
  driverName: TUnicodeString;
  driverAttr: TObjectAttributes;
  handle: THandle;
begin
  path := '\GLOBAL??\' + Name;
  RtlInitUnicodeString(@driverName, PChar(path));
  try
    FillChar(driverAttr, SizeOf(driverAttr), 0);
    driverAttr.Length := SizeOf(driverAttr);
    driverAttr.ObjectName := @driverName;
    
    NtCheck(NtOpenSymbolicLinkObject(@handle, Mask, @driverAttr));
  finally
    RtlFreeUnicodeString(@driverName);
  end;
  try
    Proc(handle);
  finally
    NtClose(handle);
  end;
end;

function GetKernelObjectDacl(Handle: THandle): string;
var
  len: Cardinal;
  desc: PSecurityDescriptor;
  buf: PChar;
begin
  GetKernelObjectSecurity(handle, DACL_SECURITY_INFORMATION, nil, 0, len);
  desc := AllocMem(len);
  try
    Win32Check(
      GetKernelObjectSecurity(handle, DACL_SECURITY_INFORMATION, desc, len, len));

    Win32Check(ConvertSecurityDescriptorToStringSecurityDescriptor(desc,
      SDDL_REVISION_1, DACL_SECURITY_INFORMATION, buf, nil));
    try
      Result := buf;
    finally
      LocalFree(Cardinal(buf));
    end;
  finally
    FreeMem(desc);
  end;
end;

procedure SetKernelObjectDacl(Handle: THandle; const Dacl: string);
var
  secDesc: PSecurityDescriptor;
begin
  Win32Check(ConvertStringSecurityDescriptorToSecurityDescriptor(PChar(Dacl), 
    SDDL_REVISION_1, secDesc, nil));
  try
    Win32Check(SetKernelObjectSecurity(Handle, DACL_SECURITY_INFORMATION, secDesc));
  finally
    LocalFree(Cardinal(secDesc));
  end;
end;

procedure ApplyToDriverHandle(Drivers: TList<string>; const Proc: TProc<string,THandle>);
var
  driver: string;
begin
  ElevatePrivileges;
  
  for driver in Drivers do
    WithDriver(driver, MAXIMUM_ALLOWED,
      procedure(Handle: THandle)
      begin
        try
          Proc(driver, Handle);
        except
          on e: Exception do
            Writeln(Format('failed on "%s": %s', [driver, e.Message]));
        end;
      end);
end;

procedure DoShow(Drivers: TList<string>);
begin
  ApplyToDriverHandle(Drivers,
    procedure(Driver: string; Handle: THandle)
    begin
      Writeln(Format('%s :: %s', [driver, GetKernelObjectDacl(Handle)]));
    end);
end;

procedure DoApply(Drivers: TList<string>; const Dacl: string);
begin
  ApplyToDriverHandle(Drivers,
    procedure(Driver: string; Handle: THandle)
    begin
      SetKernelObjectDacl(Handle, Dacl);
    end);
end;

begin
  try
    with ParseOptions do
      try
        case Operation of
          opShow: DoShow(Drivers);
          opApply: DoApply(Drivers, Dacl);
        end;
      finally
        Free;
      end;
  except
    on E: EOSError do
      Writeln('error: ', e.Message);
    on E: Exception do
      Writeln(e.Message);
  end;
end.